ClusterIP 是 kubernetes 的概念,是虚拟 IP,不是真实的 IP 地址。但是,我们通过 ClusterIP 和端口访问的时候,相应的访问是可以到达真正的后端 Pod。这一切的主要实现依赖于 Kube-Proxy。Kube-Proxy 是运行在 Kubernetes 每个节点上的一个网络代理组件。它会监听 service/endpoint/endpointslices/node 等资源,根据这些资源的情况维护相应节点上的网络规则,最终让发往 Service 的流量,可以负载均衡到正确的后端 Pod。
Kube-Proxy 支持多种模式实现上述的能力,相当于可以使用不同的底层能力来实现,
- iptables
- ipvs
- kernelspace
官方文档见:https://kubernetes.io/zh-cn/docs/reference/networking/virtual-ips/。
简单来说,如果使用 iptables 模式的话,kube-proxy 其实是在宿主机上设置一系列 iptables 规则。同时,kube-proxy 还需要在控制循环里不断地刷新这些规则来确保它们始终是正确的。当集群中有大量 Pod 的时候,成百上千条 iptables 规则不断地被刷新,会大量占用该宿主机的 CPU 资源,甚至会让宿主机“卡”在这个过程中。因此,对于大规模集群来说,iptables 模式会存在瓶颈。此时更加推荐 kube-proxy 使用 ipvs 模式。下面对 iptables 和 ipvs 两种模式的原理进行介绍。
iptables 实现
如果使用的是 iptables 的话,kube-proxy 则是通过配置一系列的 iptables 规则来实现负载均衡,主要是 filter 和 nat 两个表。
集群节点访问 ClusterIP
以集群主机节点访问 DNS service 为例进行讲解,
$ kubectl -n kube-system get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kube-dns ClusterIP 10.32.0.10 <none> 53/UDP,53/TCP,9153/TCP 98d
$ kubectl -n kube-system get pod -o wide | grep coredns
coredns-66f779496c-bjn9z 1/1 Running 0 98d 10.222.69.194 izbp1bndrgrf54r0gjzf1bz <none> <none>
coredns-66f779496c-tc5vb 1/1 Running 0 98d 10.222.69.193 izbp1bndrgrf54r0gjzf1bz <none> <none>
-
由于是在主机节点上访问 ClusterIP,因此访问的数据包会先经过 OUTPUT 链。OUTPUT 链上的规则,会将所有流量都转发到 KUBE-SERVICES 自定义链进行处理。
Chain OUTPUT (policy ACCEPT 288 packets, 17404 bytes) pkts bytes target prot opt in out source destination 21M 1291M KUBE-SERVICES all -- * * 0.0.0.0/0 0.0.0.0/0
-
KUBE-SERVICES 自定义链上设置了 3 条规则,这些规则都是关于 DNS service。
-
如果目的地址是 10.32.0.10,使用的是 tcp 协议,端口号是 53 的话,则转发给 KUBE-SVC-ERIFXISQEP7F7OF4 自定义链。
-
如果目的地址是 10.32.0.10,使用的是 tcp 协议,端口号是 9153 的话,则转发给 KUBE-SVC-JD5MR3NA4I4DYORP 自定义链。
-
如果目的地址是 10.32.0.10,使用的是 udp 协议,端口号是 53 的话,则转发给 KUBE-SVC-TCOU7JCQXEZGVUNU 自定义链。
Chain KUBE-SERVICES (2 references) pkts bytes target prot opt in out source destination 0 0 KUBE-SVC-ERIFXISQEP7F7OF4 tcp -- * * 0.0.0.0/0 10.32.0.10 tcp dpt:53 0 0 KUBE-SVC-JD5MR3NA4I4DYORP tcp -- * * 0.0.0.0/0 10.32.0.10 tcp dpt:9153 0 0 KUBE-SVC-TCOU7JCQXEZGVUNU udp -- * * 0.0.0.0/0 10.32.0.10 udp dpt:53 187 11160 KUBE-NODEPORTS all -- * * 0.0.0.0/0 0.0.0.0/0 ADDRTYPE match dst-type LOCAL
-
-
以 KUBE-SVC-ERIFXISQEP7F7OF4 自定义链为例继续阐述,其他两条 KUBE-SVC 链也都是类似的。
- 如果源地址不是集群内 Pod IP 的地址范围的话,则执行 KUBE-MARK-MASQ 自定义链中的规则。KUBE-MARK-MASQ 链的规则就是给数据包打上 0x4000 标签。
- 之后选择 KUBE-SEP-XALEUKC7I6NMGUZX 或 KUBE-SEP-I2G536MQTYUSDQGT 中的一条链进行执行。其中选中比率更是 0.5,这个是这么理解的,iptables 规则的匹配是从上往下逐条进行的,为了保证这 2 条规则被选中的概率都相同,如果第 1 条没有选中,那么就只剩下第 2 条了,所以第 2 条的 probability 设置 1 表示在第 1 条没中的时候,第 2 条必中。
Chain KUBE-SVC-ERIFXISQEP7F7OF4 (1 references) pkts bytes target prot opt in out source destination 2 120 KUBE-MARK-MASQ tcp -- * * !10.244.0.0/16 10.32.0.10 tcp dpt:53 0 0 KUBE-SEP-XALEUKC7I6NMGUZX all -- * * 0.0.0.0/0 0.0.0.0/0 statistic mode random probability 0.50000000000 2 120 KUBE-SEP-I2G536MQTYUSDQGT all -- * * 0.0.0.0/0 0.0.0.0/0 Chain KUBE-MARK-MASQ (11 references) pkts bytes target prot opt in out source destination 0 0 MARK all -- * * 0.0.0.0/0 0.0.0.0/0 MARK or 0x4000
-
这两条 KUBE-SEP 链中的规则是类似的,都是进行 DNAT。其中一条是将目的地址和端口改为 10.222.69.193:53,另一条则是改为 10.222.69.194:53。这两个地址则是 DNS Pod 实际的 IP 地址。
Chain KUBE-SEP-XALEUKC7I6NMGUZX (1 references) pkts bytes target prot opt in out source destination 0 0 KUBE-MARK-MASQ all -- * * 10.222.69.193 0.0.0.0/0 0 0 DNAT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp to:10.222.69.193:53 Chain KUBE-SEP-I2G536MQTYUSDQGT (1 references) pkts bytes target prot opt in out source destination 0 0 KUBE-MARK-MASQ all -- * * 10.222.69.194 0.0.0.0/0 2 120 DNAT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp to:10.222.69.194:53
-
OUTPUT 链上的规则处理完成之后,数据包还会经过 POSTROUTING 链的处理。POSTROUTING 链最终会使用 KUBE-POSTROUTING 自定义链上的规则进行处理。
Chain POSTROUTING (policy ACCEPT 2256 packets, 136K bytes) pkts bytes target prot opt in out source destination 21M 1291M KUBE-POSTROUTING all -- * * 0.0.0.0/0 0.0.0.0/0
-
KUBE-POSTROUTING 自定义链会先检查是否打上了 0x4000 的标签。如果打上了的话,则去掉 0x4000 的标签,然后进行源地址替换,也就是将该数据包的源地址替换为宿主机的 IP 地址。
Chain KUBE-POSTROUTING (1 references) pkts bytes target prot opt in out source destination 2256 136K RETURN all -- * * 0.0.0.0/0 0.0.0.0/0 mark match ! 0x4000/0x4000 0 0 MARK all -- * * 0.0.0.0/0 0.0.0.0/0 MARK xor 0x4000 0 0 MASQUERADE all -- * * 0.0.0.0/0 0.0.0.0/0 /* kubernetes service traffic requiring SNAT */
-
之后宿主机将这个数据包发送出去,此时数据包的源地址是宿主机 IP 地址,目的地址是 DNS Pod 地址,经过一系列的路由最终到达 Pod。
集群 Pod 访问 ClusterIP
集群中 Pod 访问 DNS Service 也是类似的。但是,由于 Pod 有自己的命名空间,访问的数据会先通过路由进入宿主机的命名空间。
- 在 PREROUTING 阶段就会进行上述的处理。
- 由于进行了 DNAT,目的地址变成了 DNS Pod 的 IP 地址,相应的数据包不会进入 INPUT 阶段,而是直接进入 FORWARD 阶段,接着再进入 POSTROUTING 阶段。
Chain PREROUTING (policy ACCEPT 25 packets, 1332 bytes)
pkts bytes target prot opt in out source destination
588K 29M KUBE-SERVICES all -- * * 0.0.0.0/0 0.0.0.0/0
IPVS 实现
IPVS 原理概述
IPVS 通过在 Netfilter 框架中的不同位置注册自己的处理函数来捕获数据包,并根据与 IPVS相关的信息表
对数据包进行处理,按照 IPVS 规则中定义的不同的包转发模式
,对数据包进行不同的转发处理。
-
Netfilter 为包过滤提供了 5 个 Hook 点。而 IPVS 将其相关处理函数挂载到 Netfilter 框架提供的
LOCAL_IN
、FORWARD
和LOCAL_OUT
这三个 Hook 上(内核 6.x,不同版本的内核,ipvs 的函数名称和注册 hook 的位置可能会不一样,比如有 hook 到POST_ROUTING
的)。其中 LOCAL_IN、LOCAL_OUT 上注册的 ip_vs_in 函数是负责 DNAT 的,也就是做 VIP 到 RIP 的转换。 -
IPVS 的规则由用户通过用户空间的管理工具
ipvsadm
写入。 -
IPVS 包含以下模式:
NAT
、IP tunneling
和Direct Routing
。由于只有 NAT 模式可以进行端口映射,因此 kube-proxy 中 ipvs 的实现使用了 NAT 模式,用来将 service IP 和 service port 映射到后端的 container ip 和container port。
IPVS 的 NAT 模式对数据包的流程如图所示。假设一个由外部网络发出的,访问局域网内服务的数据包会经历下面的流程(这个例子来自于:https://zhuanlan.zhihu.com/p/627514565):
- 外部网络 CIP
10.0.0.1:8888
发出访问 DS VIP10.0.0.2:80
的数据包。 - 请求到达 Director Server,此时报文的源 IP 为 CIP,目标 IP 为 VIP。由于 VIP 是本机上的 IP 地址,因此会进入 INPUT 链。
- 到了 INPUT 链之后就会进入 IPVS 注册的 hook 函数中进行处理。此时,会通过 IPVS 规则,找到该数据包实际要转发的 RS(Real Server)
192.168.1.1:8080
。找到 RS 信息后,DS 将数据包的目的地址和端口换成 IPVS 规则中配置的 RS 记录,并将这条连接添加到记录ESTABLISHED
类型连接的哈希表中,如果后续有相同源地址端口的数据包到来,负载均衡将根据哈希表记录统一转发给同一个 RS。之后再将数据包发至 POSTROUTING 链。 此时报文的源 IP 为 CIP,目标 IP 为 RIP。 - 然后 POSTROUTING 链通过选路,将数据包发送给 RS。
- 数据包到达 RS
192.168.1.1
,RS 处理完成后开始构建响应报文回给 Director Server。此时报文的源 IP 为 RIP,目标IP 为 CIP。之后 RS 通过默认网关192.168.1.254
将数据包发回 DS192.168.1.254
; - DS 收到 RS 返回的数据包后,对该数据包的源地址和端口做转换,更换成对应 VIP 和端口;
- DS 通过其与外部网络相连的网卡
10.0.0.2
将数据包发出去; - 外部网络 CIP
10.0.0.1
收到数据包; - 连接结束,DS 删除位于
ESTABLISHED
哈希表中的相关数据。
在使用 NAT 模式时可以看到会有以下这些隐式要求:
-
DS 提供至少两个 IP,一个连接外部网络(VIP),一个连接局域网(DIP)。VIP 需要是主机上的 IP,这样才能进入 INPUT 链,从而进行地址替换。
-
处于局域网内的 RS 必须以 DS 为默认网关。
kubernetes 模拟
对于 Kubernetes 来说,由于上述的隐性要求。如果想要使用 IPVS 实现地址映射,需要额外的操作:
- 需要创建一个 dummy 网卡,将 ClusterIP 注册到这个网卡上。这样收到的数据包才能进入 INPUT 链,从而继续接下去的操作。
- 在 POSTROUTING 阶段,需要有 SNAT 的能力。这样就可以解除 RS 必须以 DS 为默认网关的限制。
因此,kube-proxy 在使用 IPVS 实现的时候,还会使用 iptables 等能力,同时为了控制 iptables 规则的数量,还使用了 ipset。下面模仿 kube-proxy 获取到 Service 和 Endpoint 的信息后,使用 IPVS 模式在每个节点上创建负载均衡的例子。
# 创建 server
$ ipvsadm -A -t 10.32.0.10:53 -s rr
# 管理 Real Service
$ ipvsadm -a -t 10.32.0.10:53 -r 10.222.69.193:53 -m
$ ipvsadm -a -t 10.32.0.10:53 -r 10.222.69.194:53 -m
# 添加 SNAT
$ iptables -t nat -A POSTROUTING -m ipvs --vaddr 10.32.0.10 --vport 53 -j MASQUERADE
# 绑定VIP到一张 dummy 网卡
$ ip link add kube-ipvs0 type dummy
$ ip addr add 10.32.0.10/32 dev kube-ipvs0
# 开启 contrack
$ echo 1 > /proc/sys/net/ipv4/vs/conntrack
集群节点访问 ClusterIP
接下去我们以 kube-proxy 开启了 IPVS 模式为前提,梳理集群主机节点访问 DNS service 的过程。
$ kubectl -n kube-system get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kube-dns ClusterIP 10.32.0.10 <none> 53/UDP,53/TCP,9153/TCP 98d
$ kubectl -n kube-system get pod -o wide | grep coredns
coredns-66f779496c-bjn9z 1/1 Running 0 98d 10.222.69.194 izbp1bndrgrf54r0gjzf1bz <none> <none>
coredns-66f779496c-tc5vb 1/1 Running 0 98d 10.222.69.193 izbp1bndrgrf54r0gjzf1bz <none> <none>
# 查看 IPVS 的配置
$ ipvsadm -Ln
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
-> RemoteAddress:Port Forward Weight ActiveConn InActConn
TCP 10.32.0.10:53 rr
-> 10.222.69.193:53 Masq 1 0 0
-> 10.222.69.194:53 Masq 1 0 0
TCP 10.32.0.10:9153 rr
-> 10.222.69.193:9153 Masq 1 0 0
-> 10.222.69.194:9153 Masq 1 0 0
UDP 10.32.0.10:53 rr
-> 10.222.69.193:53 Masq 1 0 0
-> 10.222.69.194:53 Masq 1 0 0
# 查看是否创建了虚拟网卡,并配置了 IP 地址
$ ip addr
***
7: kube-ipvs0: <BROADCAST,NOARP> mtu 1500 qdisc noop state DOWN group default
link/ether 86:44:bc:c7:71:d3 brd ff:ff:ff:ff:ff:ff
inet 10.32.0.10/32 scope global kube-ipvs0
valid_lft forever preferred_lft forever
-
对于 IPVS 模式来说,kube-proxy 也会在 OUTPUT 链上注册若干规则。可以看到 OUTPUT 链上的规则,会将所有流量都转发到 KUBE-SERVICES 自定义链进行处理。
Chain OUTPUT (policy ACCEPT 33 packets, 1980 bytes) pkts bytes target prot opt in out source destination 21M 1292M KUBE-SERVICES all -- * * 0.0.0.0/0 0.0.0.0/0 /* kubernetes service portals */
-
查看 KUBE-SERVICES 自定义链上的情况。
- 第 2 条的意思是,如果 source 不是 10.244.0.0/16 网段,且目的地址位于 KUBE-CLUSTER-IP 中的话,则执行 KUBE-MARK-MASQ 中的规则,也就是给数据包打上 0x4000 的标签。
- 第 3 条的意思是
ADDRTYPE match dst-type LOCAL
的意思是,如果目的地址是本机地址(包括环回地址和在本机网络接口上配置的任何IP地址)的数据包,则执行 KUBE-NODE-PORT 自定义链中的情况。 - 第 4 条的意思是,如果目的地址位于 KUBE-CLUSTER-IP 中的话则接受该数据包。
Chain KUBE-SERVICES (2 references) pkts bytes target prot opt in out source destination 8 480 RETURN all -- * * 127.0.0.0/8 0.0.0.0/0 0 0 KUBE-MARK-MASQ all -- * * !10.244.0.0/16 0.0.0.0/0 match-set KUBE-CLUSTER-IP dst,dst 14 840 KUBE-NODE-PORT all -- * * 0.0.0.0/0 0.0.0.0/0 ADDRTYPE match dst-type LOCAL 0 0 ACCEPT all -- * * 0.0.0.0/0 0.0.0.0/0 match-set KUBE-CLUSTER-IP dst,dst Chain KUBE-MARK-MASQ (13 references) pkts bytes target prot opt in out source destination 0 0 MARK all -- * * 0.0.0.0/0 0.0.0.0/0 MARK or 0x4000
KUBE-CLUSTER-IP 中的地址如图所示,
$ ipset list KUBE-CLUSTER-IP Name: KUBE-CLUSTER-IP Type: hash:ip,port Revision: 5 Header: family inet hashsize 1024 maxelem 65536 Size in memory: 376 References: 3 Number of entries: 4 Members: 10.32.0.10,tcp:9153 10.32.0.1,tcp:443 10.32.0.10,udp:53 10.32.0.10,tcp:53
-
之后数据包进入 ipvs 进行 DNAT。
-
接着数据包来到 POSTROUTING 链,继续处理。可以看到 OUTPUT 链上的规则,会将所有流量都转发到 KUBE-POSTROUTING 自定义链进行处理。
Chain POSTROUTING (policy ACCEPT 2 packets, 120 bytes) pkts bytes target prot opt in out source destination 21M 1293M KUBE-POSTROUTING all -- * * 0.0.0.0/0 0.0.0.0/0 /* kubernetes postrouting rules */
-
KUBE-POSTROUTING 链中的规则如下,最终的效果是相当于对数据包进行源地址替换。
- 第 1 条表示如果目的地址和源地址均在 KUBE-LOOP-BACK 中的话,则进行源地址替换。
- 第 2 条表示如果没有打上标签的话,则返回。
- 第 3 条表示在打上标签的前提下,去掉这个标签。
- 第 4 条表示对数据包进行源地址替换。
Chain KUBE-POSTROUTING (1 references) pkts bytes target prot opt in out source destination 0 0 MASQUERADE all -- * * 0.0.0.0/0 0.0.0.0/0 match-set KUBE-LOOP-BACK dst,dst,src 2 120 RETURN all -- * * 0.0.0.0/0 0.0.0.0/0 mark match ! 0x4000/0x4000 0 0 MARK all -- * * 0.0.0.0/0 0.0.0.0/0 MARK xor 0x4000 0 0 MASQUERADE all -- * * 0.0.0.0/0 0.0.0.0/0 /* kubernetes service traffic requiring SNAT */
KUBE-LOOP-BACK 中的信息如下所示,
$ ipset list KUBE-LOOP-BACK Name: KUBE-LOOP-BACK Type: hash:ip,port,ip Revision: 5 Header: family inet hashsize 1024 maxelem 65536 Size in memory: 608 References: 1 Number of entries: 6 Members: 10.222.69.194,tcp:53,10.222.69.194 10.222.69.194,tcp:9153,10.222.69.194 10.222.69.193,tcp:53,10.222.69.193 10.222.69.194,udp:53,10.222.69.194 10.222.69.193,tcp:9153,10.222.69.193 10.222.69.193,udp:53,10.222.69.193
可以发现,在 IPVS 模式下,kube-proxy 通过 ipset 控制了 iptables 的规则数量(Pod 数量的增加只会增加 ipset list 中的 IP 数量,对于规则数来说是没有增加的)。同时 ipset 支持 hash 查找的方式,可将时间复杂度控制在 O(1)。
集群 Pod 访问 ClusterIP
对于集群中 Pod 访问 DNS service 也是类似的。但是,由于 Pod 有自己的命名空间,访问的数据会先通过路由进入宿主机的命名空间。
- 在 PREROUTING 阶段就会进行上述的处理。
- 之后就进入 IPVS 进行 DNAT。
- IPVS 的时候会直接调用到 POSTROUTING 阶段。
Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
589K 29M KUBE-SERVICES all -- * * 0.0.0.0/0 0.0.0.0/0 /* kubernetes service portals */
相关链接
虚拟 IP 和服务代理:https://kubernetes.io/zh-cn/docs/reference/networking/virtual-ips/
kubernetes的Kube-proxy的iptables转发规则:https://blog.csdn.net/qq_36183935/article/details/90734847
《一篇搞懂》系列之二——IPVS:https://zhuanlan.zhihu.com/p/627514565
ipvs的hook点位置以及hook点钩子函数剖析:https://blog.csdn.net/qq_43684922/article/details/128692789
Kubernetes IPVS 工作模式原理剖析:https://cloud.tencent.com/developer/article/2369702