0%

虚拟化之开始使用 k8s【2】

[TOC]

概述

kube-proxy 和 service

配置好网络之后,集群是什么情况呢?我们可以创建 pod,也能通过 ReplicationController 来创建特定副本的 pod(这是更推荐也是生产上要使用的方法,即使某个 rc 中只有一个 pod 实例)。可以从集群中获取每个 pod ip 地址,然后也能在集群内部直接通过 podIP:Port 来获取对应的服务。

但是还有一个问题:pod 是经常变化的,每次更新 ip 地址都可能会发生变化,如果直接访问容器 ip 的话,会有很大的问题。而且进行扩展的时候,rc 中会有新的 pod 创建出来,出现新的 ip 地址,我们需要一种更灵活的方式来访问 pod 的服务。

1
2
# 使用场景
podIp=`kcpg robotserver-pro | awk '{print $6}'` && curl "${podIp}:8050/ai_answer" -X POST

Service 和 cluster IP

针对这个问题,kubernetes 的解决方案是“服务”(service),每个服务都一个固定的虚拟 ip(这个 ip 也被称为 cluster IP),自动并且动态地绑定后面的 pod,所有的网络请求直接访问服务 ip,服务会自动向后端做转发。Service 除了提供稳定的对外访问方式之外,还能起到负载均衡(Load Balance)的功能,自动把请求流量分布到后端所有的服务上,服务可以做到对客户透明地进行水平扩展(scale)。

而实现 service 这一功能的关键,就是 kube-proxy。kube-proxy 运行在每个节点上,监听 API Server 中服务对象的变化,通过管理 iptables 来实现网络的转发。

NOTE: kube-proxy 要求 NODE 节点操作系统中要具备 /sys/module/br_netfilter 文件,而且还要设置 bridge-nf-call-iptables=1,如果不满足要求,那么 kube-proxy 只是将检查信息记录到日志中,kube-proxy 仍然会正常运行,但是这样通过 Kube-proxy 设置的某些 iptables 规则就不会工作。

kube-proxy 有两种实现 service 的方案:userspace 和 iptables

  • userspace 是在用户空间监听一个端口,所有的 service 都转发到这个端口,然后 kube-proxy 在内部应用层对其进行转发。因为是在用户空间进行转发,所以效率也不高
  • iptables 完全使用 iptables 来实现 service,是目前默认的方式,也是推荐的方式,效率很高(只有内核中 netfilter 一些损耗)。

现在还有一种叫做 ipvs 的实现方案。

这篇文章通过 iptables 模式运行 kube-proxy,后面的分析也是针对这个模式的,userspace 只是旧版本支持的模式,以后可能会放弃维护和支持。

kube-proxy 参数介绍

实例启动和测试

whoami 镜像

1
2
# https://github.com/hypriot/rpi-whoami
docker pull cizixs/whoami:v0.5

启动

为了方便测试,我们创建一个 rc,里面有三个 pod。这个 pod 运行的是 cizixs/whoami 容器,它是一个简单的 HTTP 服务器,监听在 3000 端口,访问它会返回容器的 hostname。

1
[root@localhost ~]# cat whoami-rc.yml

创建部署, kubectl create -f ./whoami-rc.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
apiVersion: v1
kind: ReplicationController
metadata:
name: whoami
spec:
replicas: 3
selector:
app: whoami
template:
metadata:
name: whoami
labels:
app: whoami
env: dev
spec:
containers:
- name: whoami
image: cizixs/whoami:v0.5
ports:
- containerPort: 3000
env:
- name: MESSAGE
value: viola

我们为每个 pod 设置了两个 label:app=whoamienv=dev,这两个标签很重要,也是后面服务进行绑定 pod 的关键。

为了使用 service,我们还要定义另外一个文件,并通过 kubectl create -f ./whoami-svc.yml 来创建出来对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
apiVersion: v1
kind: Service
metadata:
labels:
name: whoami
name: whoami
spec:
ports:
- port: 3000
targetPort: 3000
protocol: TCP
selector:
app: whoami
env: dev

其中 selector 告诉 kubernetes 这个 service 和后端哪些 pod 绑定在一起,这里包含的键值对会对所有 pod 的 labels 进行匹配,只要完全匹配,service 就会把 pod 作为后端。也就是说,service 和 rc 并不是对应的关系,一个 service 可能会使用多个 rc 管理的 pod 作为后端应用。

ports 字段指定服务的端口信息:

  • port:虚拟 ip 要绑定的 port,每个 service 会创建出来一个虚拟 ip,通过访问 vip:port 就能获取服务的内容。这个 port 可以用户随机选取,因为每个服务都有自己的 vip,也不用担心冲突的情况
  • targetPort:pod 中暴露出来的 port,这是运行的容器中具体暴露出来的端口,一定不能写错
  • protocol:提供服务的协议类型,可以是 TCP 或者 UDP

创建之后可以列出 service ,发现我们创建的 service 已经分配了一个虚拟 ip (10.108.161.211),这个虚拟 ip 地址是不会变化的(除非 service 被删除)。查看 service 的详情可以看到它的 endpoints 列出,对应了具体提供服务的 pod 地址和端口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|># kc get svc | grep whoami
whoami ClusterIP 10.108.161.211 <none> 3000/TCP 90s

# endpoints 中就是 pod 的ip + port,而这是易变的
|># kc describe svc whoami
Name: whoami
Namespace: test-ks
Labels: name=whoami
Annotations: <none>
Selector: app=whoami,env=dev
Type: ClusterIP
IP: 10.108.161.211
Port: <unset> 3000/TCP
TargetPort: 3000/TCP
Endpoints: 10.244.0.111:3000,10.244.0.112:3000,10.244.0.113:3000
Session Affinity: None
Events: <none>

默认的 service 类型是 ClusterIP,这个也可以从上面输出看出来。在这种情况下,只能从集群内部访问这个 IP,不能直接从集群外部访问服务。如果想对外提供服务,我们后面会讲解决方案。

测试一下,访问 service 服务的时候可以看到它会随机地访问后端的 pod,给出不同的返回:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 直接通过 pod ip + port 访问
|># curl "10.244.10.130:3000"
viola from whoami-hj7g8

# vip:port 访问 service,然后访问到 pod

root@localhost ~]# curl http://10.108.161.211:3000
viola from whoami-8fpqp
[root@localhost ~]# curl http://10.108.161.211:3000
viola from whoami-c0x6h
[root@localhost ~]# curl http://10.108.161.211:3000
viola from whoami-8fpqp
[root@localhost ~]# curl http://10.108.161.211:3000
viola from whoami-dc9ds

NOTE: 需要注意的是,服务分配的 cluster IP 是一个虚拟 ip,如果你尝试 ping 这个 IP 会发现它没有任何响应,这也是刚接触 kubernetes service 的人经常会犯的错误。实际上,这个虚拟 IP 只有和它的 port 一起的时候才有作用,直接访问它,或者想访问该 IP 的其他端口都是徒劳。

外部能够访问的服务

上面创建的服务只能在集群内部访问,这在生产环境中还不能直接使用。如果希望有一个能直接对外使用的服务,可以使用 NodePort 或者 LoadBalancer 类型的 Service。我们先说说 NodePort,它的意思是在所有 worker 节点上暴露一个端口,这样外部可以直接通过访问 nodeIP:Port 来访问应用。

我们先把刚才创建的服务删除:

1
2
3
4
5
[root@localhost ~]# kubectl delete rc whoami
replicationcontroller "whoami" deleted

[root@localhost ~]# kubectl delete svc whoami
service "whoami" deleted

启动

1
sh install2.sh ${ns}

nodePort 类型的服务会在所有的 worker 节点(运行了 kube-proxy)上统一暴露出端口对外提供服务,也就是说外部可以任意选择一个节点进行访问。比如我本地集群有三个节点:172.17.8.100172.17.8.101172.17.8.102

1
2
3
4
5
6
[root@localhost ~]# curl http://172.17.8.100:31647
viola from whoami-mc2fg
[root@localhost ~]# curl http://172.17.8.101:31647
viola from whoami-8zc3d
[root@localhost ~]# curl http://172.17.8.102:31647
viola from whoami-z6skj

有了 nodePort,用户可以通过外部的 Load Balance 或者路由器把流量转发到任意的节点,对外提供服务的同时,也可以做到负载均衡的效果。

nodePort 类型的服务并不影响原来虚拟 IP 的访问方式,内部节点依然可以通过 vip:port 的方式进行访问。

创建服务(原理解析)

在 Kubernetes 中创建一个新的 Service 对象需要两大模块同时协作,其中一个模块是控制器,它需要在每次客户端创建新的 Service 对象时,生成其他用于暴露一组 Pod 的 Kubernetes 对象,也就是 Endpoint 对象;另一个模块是 kube-proxy,它运行在 Kubernetes 集群中的每一个节点上,会根据 Service 和 Endpoint 的变动改变节点上 iptables 或者 ipvs 中保存的规则。

  • 控制器, 用于生成 endpoints 对象
  • kube-proxy

控制器

控制器模块其实总共有两个部分监听了 Service 变动的事件,其中一个是 ServiceController、另一个是 EndpointController,我们分别来看两者如何应对 Service 的变动。

EndpointController

EndpointController 本身并没有通过 Informer 监听 Endpoint 资源的变动,但是它却同时订阅了 Service 和 Pod 资源的增删事件,对于 Service 资源来讲,EndpointController 会通过以下的方式进行处理:

EndpointController 中的 syncService 方法是用于创建和删除 Endpoint 资源最重要的方法,在这个方法中我们会根据 Service 对象规格中的选择器 Selector 获取集群中存在的所有 Pod,并将 Service 和 Pod 上的端口进行映射生成一个 EndpointPort 结构体

查看 service 的endPoint

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
# 可以看到两个副本的 podIp
|># kcpg chatbot-client
chatbot-client-ks-deploy-64d5f6cc84-dpp78 1/1 Running 0 7d6h 172.0.7.197 alizjk-112191-prod-ks-kubelet-alg <none> <none>
chatbot-client-ks-deploy-64d5f6cc84-lx8ww 1/1 Running 0 7d6h 172.0.3.20 zjk-023112158-kubelet-ks-qc <none> <none>
|root@zjk-023112154-kubelet-ks-master ~


# 可以看到
# 1. service 的 type 和 clusterIP
# 2. endPoints,可以看到对应的就是上面两个 podIp
# 3. 这里有两组 endPoint, 一组是 http, 一组是 prof
|># kc describe service chatbot-client-ks-service
Name: chatbot-client-ks-service
Namespace: ks-prod
Labels: app=chatbot-client-ks
Annotations: kubectl.kubernetes.io/last-applied-configuration:
{"apiVersion":"v1","kind":"Service","metadata":{"annotations":{},"labels":{"app":"chatbot-client-ks"},"name":"chatbot-client-ks-service","...
Selector: app=chatbot-client-ks
Type: ClusterIP
IP: 172.96.243.64
Port: http 8080/TCP
TargetPort: 8080/TCP
Endpoints: 172.0.3.20:8080,172.0.7.197:8080
Port: prof 8081/TCP
TargetPort: 8081/TCP
Endpoints: 172.0.3.20:8081,172.0.7.197:8081
Session Affinity: None
Events: <none>

kube-proxy

在 Kubernetes 集群中的每一个节点都运行着一个 kube-proxy 进程,这个进程会负责监听 Kubernetes 主节点中 Service 的增加和删除事件并修改运行代理的配置,为节点内的客户端提供流量的转发和负载均衡等功能,但是当前 kube-proxy 的代理模式目前来看有三种:

iptable

iptables 作为一种代理模式,它同样实现了 OnServiceUpdate、OnEndpointsUpdate 等方法,这两个方法会分别调用相应的变更追踪对象。

主要功能就是根据 ServiceEndpoint 对象的变更生成一条一条的 iptables 规则.

当我们使用 iptables 的方式启动节点上的代理时,所有的流量都会先经过 PREROUTING 或者 OUTPUT 链,随后进入 Kubernetes 自定义的链入口 KUBE-SERVICES、单个 Service 对应的链 KUBE-SVC-XXXX 以及每个 Pod 对应的链 KUBE-SEP-XXXX,经过这些链的处理,最终才能够访问当一个服务的真实 IP 地址。

当我们使用 iptables 的方式启动节点上的代理时,所有的流量都会先经过 PREROUTING 或者 OUTPUT 链,随后进入 Kubernetes 自定义的链入口 KUBE-SERVICES、单个 Service 对应的链 KUBE-SVC-XXXX 以及每个 Pod 对应的链 KUBE-SEP-XXXX,经过这些链的处理,最终才能够访问当一个服务的真实 IP 地址。

ipvs

ipvs 就是用于解决在大量 Service 时,iptables 规则同步变得不可用的性能问题。与 iptables 比较像的是,ipvs 的实现虽然也基于 netfilter 的钩子函数,但是它却使用哈希表作为底层的数据结构并且工作在内核态,这也就是说 ipvs 在重定向流量和同步代理规则有着更好的性能。

总结

Kubernetes 中的 Service 将一组 Pod 以统一的形式对外暴露成一个服务,它利用运行在内核空间的 iptables 或者 ipvs 高效地转发来自节点内部和外部的流量。除此之外,作为非常重要的 Kubernetes 对象,Service 不仅在逻辑上提供了微服务的概念,还引入 LoadBalancer 类型的 Service 无缝对接云服务商提供的复杂资源。

参考

主要的参考

service 原理