0%

k8s 中的网络入口 -- Service 资源管理

关于 Kubernetes 系列其他文章的传送门

  1. 《 部署 Kubernetes 也能如此简单吗? 》

  2. 《 初识 k8s,新时代的宠儿 》

  3. 《 深度学习 Pod 》

  4. 《 学会这 5 种 Pod 控制器,搞定发布、更新、回滚,从此告别人肉运维!! 》

  5. 《 k8s 中的网络入口 – Service 资源管理 》     您当前所在位置

  6. 《 k8s 中的 7 层调度 – Ingress 》

  7. 《 k8s 中的存储系统 – Volumes 》

  8. 《 k8s 中强大的配置中心 – ConfigMap 》

  9. 《 k8s 中的部署神器 – Statefulset 控制器 》

  10. 《 k8s 中的安全守护神 – 你不知道不代表它不在 》

  11. 《 k8s 中的图形接口 – Dashboard 》

  12. 《 k8s 中的网络插件 – flannel 和 Calico 》

  13. 《 k8s 中的调度器 – 亲和性、污点和容忍性 》

  14. 《 k8s 中对资源的监控 – 资源指标、Prometheus、弹性伸缩器 HPA 》

  15. 《 k8s 中的云原生应用管理利器 – Helm 》

  16. 《 理解 k8s 高可用,让你的集群稳如泰山 》



logo

一、Service 资源

为什么需要 Service 资源?

  • 动态的 Pod 对象会给客户端带来的困扰有:
    • Pod 资源对象存在生命周期且不可重现,必要时仅能创建一个新的替代者;
    • Pod 对象在其控制器进行应用规模伸缩时,同一应用程序的 Pod 对象会增加或减少;

所以 Service 资源为动态管理的 Pod 对象添加一个固定访问入口的抽象层:

  • Service 通过标签选择器关联至拥有相关标签的 Pod 对象;
  • 客户端向 Service 进行请求,而非目标 Pod 对象;

每一个 Service 对应于 k8s 实际上来讲,应该是被转换为每一个节点上的 iptables/IPVS。当客户端访问 Service 的地址时,Service 会根据调度算法,从众多的后端 Pod 中挑选一个 Pod 出来,并把对应的 Endpoint 的地址返回给客户端,或者直接把流量代理到 Endpoint 上,因此就能使客户端与 Service 的后端之间进行通信了。

Service 的后端可能是集群内的 Pod,也有可能是集群外的某个具体的服务器提供的服务。如果双方都是在同个节点中的 Pod,之后就会直接使用 Pod IP 进行通信了。

关于 kube-proxy

Service 随时有可能会被创建、删除、修改,也就意味着每个节点上的 iptables/IPVS 规则也可能会随时发成变动,这个变动如何及时的反应到每一个节点上呢?所以每个节点上都会运行一个守护进程 kube-proxy,这个进程可能运行为一个系统级进程,也可能运行为一个 Pod,如果是使用 kubeadm 创建的集群,会被创建为一个 Pod:

1
2
3
4
5
-> # kubectl get pods -n kube-system -o wide|grep proxy
kube-proxy-4mth4 1/1 Running 5 6d18h 192.168.50.11 node1.monster.com
kube-proxy-5c668 1/1 Running 5 6d18h 192.168.50.14 master.monster.com
kube-proxy-jgmvc 1/1 Running 5 6d18h 192.168.50.13 node3.monster.com
kube-proxy-ntxxn 1/1 Running 5 6d18h 192.168.50.12 node2.monster.com

可以看到在每一个节点上都运行了一个 proxy 的 Pod,这是由 ds 类型的控制器所控制,因此每个节点上只能运行 1 个。而且会容忍主节点的污点,所以主节点上也会运行一个。

kube-proxy 其实就是运行在每一个节点之上的守护进程或 Pod,它是 API Server 的客户端,并监听着 API Server 上每一个 Service 资源的变动,一旦 API Server 上某一个 Service 发生变动,会立即通知到监视此资源的所有节点上的 kube-proxy,而后由 kube-proxy 转换为本地的 iptables/IPVS 规则。

自 1.11 版的 k8s 开始默认使用 IPVS 模型,如果在部署 k8s 时,节点上没有装载 IPVS 模块,会自动把 IPVS 模型降级为 iptables 模型。

  • IPVS 与 iptables 规则的不同之处仅在于,IPVS 的请求流量的调度功能由 IPVS 实现,余下的其他功能仍由 iptables 来完成;
  • 另外,使用 IPVS 可以支持多种调度算法,比如 rr、wrr、lc、dh、sh 等,但是所有节点的所有 Service 都必须使用同一种算法。

定义 Service 资源

ports:定义 Service 自己的端口和对端的目标端口;

  • name:定义一个名字,可省略;
  • port:自己的端口;
  • targetPort:对端的端口;
  • protocol:定义使用哪种协议。默认 TCP
    clusterIP:定义 Service 自己的 IP;
    selector:使用标签选择器来关联对端是哪个;

示例:定义一个 Service 并关联至 pod

当前有 3 个 Pod,标签为 app:nginx

1
2
3
4
5
-> # kubectl get pods --show-labels              
NAME READY STATUS RESTARTS AGE LABELS
deploy-nginx-5745bb45d7-68w9g 1/1 Running 0 17s app=nginx,pod-template-hash=5745bb45d7
deploy-nginx-5745bb45d7-bfmn2 1/1 Running 0 17s app=nginx,pod-template-hash=5745bb45d7
deploy-nginx-5745bb45d7-hsq2f 1/1 Running 0 17s app=nginx,pod-template-hash=5745bb45d7

创建 Service,配置文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
~]# vim nginx-svc.yaml

apiVersion: v1 # API 分类为核心组的 v1
kind: Service
metadata:
name: nginx
namespace: default
spec:
clusterIP: # 指定集群 IP,可不指来自动分配,防止冲突
ports: # 自己的端口和对端的端口
- name: http
port: 80
targetPort: 80
selector:
app: nginx # 指定标签键为 app,且值为 nginx 的 Pod

查看刚刚创建的 Service,被自动绑定在对应的 Endpoint 上:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-> # kubectl describe svc nginx
Name: nginx
Namespace: default
Labels: <none>
Annotations: kubectl.kubernetes.io/last-applied-configuration:
{"apiVersion":"v1","kind":"Service","metadata":{"annotations":{},"name":"nginx","namespace":"default"},"spec":{"clusterIP":null,"ports":[{...
Selector: app=nginx
Type: ClusterIP
IP: 10.111.143.90
Port: http 80/TCP
TargetPort: 80/TCP
Endpoints: 10.244.1.28:80,10.244.2.28:80,10.244.3.31:80
Session Affinity: None
Events: <none>

这个 Endpoint 是自动创建的,查看 Endpoint 中的 IP 地址就是 Pod 的 IP:

1
2
3
-> # kubectl get endpoints 
NAME ENDPOINTS AGE
nginx 10.244.1.28:80,10.244.2.28:80,10.244.3.31:80 3m10s

查看此 Endpoint 的详细信息,因为 Endpoint 太简单了,所以一个没有 spec 字段的资源,但是有一个 subsets 字段,指定了绑定的 Pod:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
-> # kubectl get endpoints nginx -o yaml
apiVersion: v1
kind: Endpoints
metadata:
annotations:
endpoints.kubernetes.io/last-change-trigger-time: "2019-11-06T06:56:01Z"
creationTimestamp: "2019-11-06T06:56:01Z"
name: nginx
namespace: default
resourceVersion: "186201"
selfLink: /api/v1/namespaces/default/endpoints/nginx
uid: b88ad2e7-f062-40e3-9537-05c34bc579b1
subsets:
- addresses:
- ip: 10.244.1.28
nodeName: node1.monster.com
targetRef:
kind: Pod
name: deploy-nginx-5745bb45d7-hsq2f
namespace: default
resourceVersion: "185817"
uid: 732d19b8-fb96-419a-9da9-962aec21d248
- ip: 10.244.2.28
nodeName: node2.monster.com
targetRef:
kind: Pod
name: deploy-nginx-5745bb45d7-68w9g
namespace: default
resourceVersion: "185827"
uid: 3a376320-5bd2-43aa-9841-d5150ecb1c70
- ip: 10.244.3.31
nodeName: node3.monster.com
targetRef:
kind: Pod
name: deploy-nginx-5745bb45d7-bfmn2
namespace: default
resourceVersion: "185823"
uid: 8a8336cb-0f02-4049-b6af-1e60e8d29685
ports:
- name: http
port: 80
protocol: TCP

二、Service 的代理类型

定义 Service 如何代理到 Pod

ClusterIP (默认)

给一个 Service 分配一个当前集群上的,位于 Service 可用网段内的动态地址。这个地址使用私网地址,只在集群内可用。所以如果是 ClusterIP 类型,那么此 Service 就只能被集群内部的客户端所访问。

NodePort

对一个 Service 来说,它还是有 NodePort 的,但是会在集群的每一个节点上生成一个 iptables/IPVS 的 NAT 规则,这个规则可以把物理节点的某个端口,比如 3111,映射到 ClusterIP 的端口上。这样外部的客户端,通过去访问任一节点 IP 的 3111 端口,而后会被转发至集群内部的 ClusterIP 的端口上来,从而引入外部流量到 Service,然后 Service 把流量转发至 Pod。
nodeport

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
~]# vim nginx-svc.yaml

apiVersion: v1
kind: Service
metadata:
name: nginx
namespace: default
spec:
clusterIP:
ports:
- name: http
port: 80
nodePort: 30080 # 定义使用物理机上的哪个端口,可以不指定来默认分配,不过为了防止冲突,不建议手动指定
targetPort: 80
selector:
app: nginx
type: NodePort # 定义 Service 的代理类型

查看此 svc 的类型已经为 NodePort 了:

1
2
3
-> # kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
nginx NodePort 10.106.10.113 <none> 80:30080/TCP 4s

之后就可以在集群外部通过访问任意一个物理节点的 IP 加端口,来访问这个 Pod 了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
-> # curl 192.168.50.11:30080
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
body {
width: 35em;
margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif;
}
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>

注意:但是像 30080 这样的非标准端口,客户端是不知道怎么访问的,这时可以在外部使用一个负载均衡器来调度至此端口,且注意高可用。

ExternalName

如果某一个 Service 的后端不是集群内的资源时,就要使用此种 Service 类型。

类型为 ExternalName 的 Service 会将 Service 映射到其他的 DNS 名称上,而不是标签选择器:

1
2
3
4
5
6
7
8
apiVersion: v1
kind: Service
metadata:
name: my-service
namespace: prod
spec:
type: ExternalName
externalName: my.database.example.com

需要注意的是:这个外部 DNS 必须是 CoreDNS 能够解析的。如果是公网的 DNS,需要 CoreDNS 能够访问互联网,能从根域名查询到相关的 DNS 信息才可以正常访问。

三、其他补充

没有 selector 的 Service

另一种方式是创建 Service 时不定义标签选择器,然后通过手动创建一个和 Service 同名的 Endpoint,这个 Endpoint 则指向外部网络中的地址。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
~]# vim no-selector-svc.yaml

apiVersion: v1
kind: Service
metadata:
name: nginx
namespace: default
spec:
clusterIP:
ports:
- name: http
port: 80
targetPort: 80

由于此服务没有选择器,因此不会自动创建相应的 Endpoint 对象。可以通过手动添加 Endpoint 对象,将 Service 手动映射到运行该 Service 的网络地址和端口:

1
2
3
4
5
6
7
8
9
10
~]# vim mumnal-endpoint.yaml
apiVersion: v1
kind: Endpoints
metadata:
name: nginx # 名字要和 Service 的名字一致
subsets:
- addresses:
- ip: 192.0.2.42 # 此 IP 指向的是集群外部的地址
ports:
- port: 9376

headless(无头) Service

正常情况下,客户端在通过域名访问时,会先从 CoreDNS 中获取到域名对应的 IP 地址,这个 IP 是绑定在 Service 上的,然后由这个 Service 代理至后端的 Endpoint 绑定的 Pod 上。

但如果创建 Service 时,指定不使用 IP,那么这样的 Service 就叫 无头 Service(headless Service),定义方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
~]# vim headless-svc.yaml

apiVersion: v1
kind: Service
metadata:
name: nginx
namespace: default
spec:
clusterIP: None # 定义没有 clusterIP
ports:
- name: http
port: 80
nodePort: 30080
targetPort: 80
selector:
app: nginx
type: NodePort

当客户端访问这样的无头 Service 时,因为 Service 上没有 IP,那么 CoreDNS 就会直接把域名绑定到 Pod 的 IP 上。

Service 会话粘性

Service 也支持会话粘性的特性,可以支持把同一组客户端 IP 地址的请求始终绑定到同一个后端的 Pod 上,这在定义 Service 时使用一个属性即可:

  • sessionAffinity:定义会话粘性的属性:
    • none:默认没有会话粘性;
    • ClientIP:使用 IPVS 中的 HS 调度算法;

启用 IPVS 类型的 Service

先加载 IPVS 模块

注意:在每个节点执行以下操作

创建内核模块载入相关的脚本文件 /etc/sysconfig/modules/ipvs.modules,设定开机自动载入的内核模块。文件内容如下:

1
2
3
4
5
6
7
8
#!/bin/bash
ipvs_mods_dir="/usr/lib/modules/$(uname -r)/kernel/net/netfilter/ipvs"
for mod in $(ls $ipvs_mods_dir | grep -o "^[^.]*"); do
/sbin/modinfo -F filename $mod &> /dev/null
if [ $? -eq 0 ]; then
/sbin/modprobe $mod
fi
done

修改文件权限,并手动为当前系统加载内核模块:

1
2
~]# chmod +x /etc/sysconfig/modules/ipvs.modules 
~]# bash /etc/sysconfig/modules/ipvs.modules

修改 kube-proxy 的配置文件

如果要启用 IPVS 类型的 Service,接下来要去改 kube-proxy 的 Pod 配置规则:

使用 kubectl get cm -n kube-system 可以查看到集中式的配置文件。集中式配置文件的特定是:例如修改这里边的 kube-proxy 的配置文件后,所有 kube-proxy 的 Pod 容器都会发生改变。

使用 kubectl edit cm kube-proxy -n kube-system 编辑 kube-proxy 的配置文件:

1
2
3
mode: ""        # 把这一行配置

mode: "ipvs" # 改成这样即可

因为此前 kube-proxy 已经创建并运行,那么现在根据环境的不同,可能需要等待其重启或者重建后配置才会生效。

如果一直没有生效,可能需要手动将 Pod 删除,等待其重建,不过生产环境不建议这么做,最好从一开始就定义好使用 ipvs 而不是 iptables:

1
2
# 定义删除标签是 k8s-app=kube-proxy 的 Pod
kubectl delete pods -l k8s-app=kube-proxy -n kube-system

之后使用 ipvsadm -Ln 可以看到配置就代表 ipvs 的配置生效了。

定义调度规则

使用 kubectl edit cm kube-proxy -n kube-system 修改配置文件

1
2
3
schedeler: ""       # 把这一行配置

schedeler: "wrr" # 改为你想要的调度配置,比如 rr、wrr、dh 等,修改后所有的 Pod 都会生效