编辑
2023-10-23
Kubernetes
00
请注意,本文编写于 200 天前,最后修改于 154 天前,其中某些信息可能已经过时。

目录

Kube-proxy
Quick Start
多副本 Service
Hairpin NAT
cluster-cidr
svc 的亲和性
NodePort
IPVS
参考资料

之前的文章介绍了 iptables ,接下来就可以介绍 service 是如何实现的,本文所需的前置知识是 iptables。

Pod是一个多变的事物,它可能被重建、消亡因此直接以Pod对外提供服务并不是一个很好的选择,为此k8s提供了service能够对外同一个稳定的IP作为提供服务的Pod的入口。本篇从 iptables 规则入手对 service 的工作原理做一个基本介绍。

Kube-proxy

在一个 k8s 集群,每个节点上都会有一个 kube-proxy 的Pod,它的作用是实现来 Service,能够对外提供一个 VIP (virtual IP)来实现对多个 Pod 负载均衡。kube-proxy的基本工作原理是依赖于 Watch 机制关注这 apiserver 是否有新的 service 被创建,如果有就在每个节点上新增一些 iptables 规则,于是该节点上的任一 Pod 访问 Service的时候都会经过 iptables 的规则被重定向到一个 Pod。

早期的kube-proxy还有一种称为userspace的模式,它完完全全就是一个代理的作用,任何通往 service 的流量会被本机上的 iptables 规则被重定向到 kube-proxy, 由 kube-proxy 负责与支撑 service 的 pod 进行通信,不过存在性能问题基本弃用。现在的 kube-proxy 默认都以 iptables 模式,不过缺陷是随着集群节点数量的增加,到上千节点以后 iptables 规则过多性能也会变差,于是又有了性能更好的 IPVS 模式的 kube-proxy,基于 eBPF 的CNI-cillium 也可以替换 kube-proxy,不过这些暂时都不是本文的目的。

Quick Start

我们使用一个nginx deplyoment作为实验目标,并且逐渐地增加副本数来查看 kube-proxy会如何去修改iptables的。示例yaml点击这里,结果如下:

shell
$ kubectl get svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 43h nginx ClusterIP 10.109.10.94 <none> 80/TCP 12m $ kubectl get pods -A -o wide # 省略了一些内容 NAME IP nginx-d46f5678b-nzxbg 10.0.254.139

可以想象的是,service 所提供的的是虚拟IP,为了能够让 pod 访问到service,必然要对 dst IP 做一些手脚。回顾一下nat表在报文出入的作用:

  1. 当报文进入到网卡需要经过 nat 表的 PREROUTING链,后续经过filter 与 POSTROUTING 出去。
  2. 当报文出去需要经过 nat表的 OUTPUT 链和 POSTROUTING 链。

切换到k8s的情境下,我们首先需要明确iptables是如何起作用的,这有张已经很完善的说明了整个过程,概括来说:

  1. k8s集群内物理机的非pod进程访问cluster IP 会经过 OUTPUT 链
  2. k8s集群内pod(包括宿主机上的pod通过svc访问到pod本身)访问svc都会进过PREROUTING。

所以首先来看最简单的情况,访问cluster ip。首先是 OUTPUT 链和 PREROUTING 链都会跳转到KUBE-SERVICES

shell
-A PREROUTING -m comment --comment "cali:6gwbT8clXdHdC1b1" -j cali-PREROUTING -A PREROUTING -m comment --comment "kubernetes service portals" -j KUBE-SERVICES # pod访问svc -A OUTPUT -m comment --comment "cali:tVnHkvAo15HuiPy0" -j cali-OUTPUT -A OUTPUT -m comment --comment "kubernetes service portals" -j KUBE-SERVICES # 针对非pod进程访问svc

接下来查看KUBE-SERVICES的规则,与我们实验的nginx相关的规则如下:

shell
# 如果 dst IP 是 nginx service,那么就跳转到 KUBE-SVC-4N57TFCL4MD7ZTDA 链处理 -A KUBE-SERVICES -d 10.109.10.94/32 -p tcp -m comment --comment "default/nginx: cluster IP" -m tcp --dport 80 -j KUBE-SVC-4N57TFCL4MD7ZTDA # KUBE-SVC-4N57TFCL4MD7ZTDA 啥也没,直接跳到 KUBE-SEP-X76RQKHDA4QWOROR 链处理 -A KUBE-SVC-4N57TFCL4MD7ZTDA -m comment --comment "default/nginx:" -j KUBE-SEP-X76RQKHDA4QWOROR # 这一步是关键了,进行了 DNAT,iptables 将任何发给Service的IP转为了某个POD的IP,显然我们此时只有一个 nginx(10.0.254.139) # KUBE-SEP 这里还少了一条规则,因为与cluster IP 无关就没放上来,看hairpin NAT小节 -A KUBE-SEP-X76RQKHDA4QWOROR -p tcp -m comment --comment "default/nginx:" -m tcp -j DNAT --to-destination 10.0.254.139:80

接下来就是根据目标POD的地址进行操作,进行路由查表。如果在路由表中找不到该pod,那么就需要进行转发,通信就变成了pod与pod之间通信。到这儿为止,PREROUTING的规则完毕。接下来看FILTER表的规则,如下:

shell
-A KUBE-FORWARD -m comment --comment "kubernetes forwarding rules" -m mark --mark 0x4000/0x4000 -j ACCEPT -A KUBE-FORWARD -m comment --comment "kubernetes forwarding conntrack pod source rule" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT -A KUBE-FORWARD -m comment --comment "kubernetes forwarding conntrack pod destination rule" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT

第一条规则有些让人摸不着头脑,这是因为 hairpin flow的情况kube-proxy会对这些报文打上一个0x4000/0x4000标记,因此filter表直接对这些报文放行。另外两条规则是调用了 conntrack 模块对nat的过程进行记录,让返回的报文的src IP 地址能够SNAT为service的地址,如果在pod内抓包可以看到实际报文的IP都是svc IP,从未发生变化,显然回来的报文已经被SNAT了。

最后来到了POSTROUTING链,nat表的POSTROUTING 链做的事情就是跳转到KUBE-POSTROUTING 链,它就三条规则:

shell
-A KUBE-POSTROUTING -m mark ! --mark 0x4000/0x4000 -j RETURN -A KUBE-POSTROUTING -j MARK --set-xmark 0x4000/0x0 -A KUBE-POSTROUTING -m comment --comment "kubernetes service traffic requiring SNAT" -j MASQUERADE --random-fully

第一条规则是对于没有被0x4000/0x4000 就之前返回了,第二个规则设置了一个标记,至于这个标记到底啥用的可以参考hairpin nat一小节,最后一条规则被MASQUERADE所处理,报文从宿主机SNAT以后出去了。

多副本 Service

部署为deployment的好处就是扩容方便了很多,使用如下命令对nginx进行扩容:

shell
kubectl scale deployment nginx --replicas=2

从 OUTPUT 链开始看,总体过程与前面单副本类似,如下:

shell
# 对于 nginx service 跳转到 KUBE-SVC-4N57TFCL4MD7ZTDA 链 -A KUBE-SERVICES -d 10.109.10.94/32 -p tcp -m comment --comment "default/nginx: cluster IP" -m tcp --dport 80 -j KUBE-SVC-4N57TFCL4MD7ZTDA # KUBE-SVC-4N57TFCL4MD7ZTDA 一共两条链 -A KUBE-SVC-4N57TFCL4MD7ZTDA -m comment --comment "default/nginx:" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-X76RQKHDA4QWOROR -A KUBE-SVC-4N57TFCL4MD7ZTDA -m comment --comment "default/nginx:" -j KUBE-SEP-VSSFB4ZLM3BVMCSQ # 随机的选择一个 nginx pod 转发 -A KUBE-SEP-X76RQKHDA4QWOROR -p tcp -m comment --comment "default/nginx:" -m tcp -j DNAT --to-destination 10.0.254.139:80 -A KUBE-SEP-VSSFB4ZLM3BVMCSQ -p tcp -m comment --comment "default/nginx:" -m tcp -j DNAT --to-destination 10.0.254.140:80

这引入了 iptables 的 statistic 模块,-m statistic --mode random --probability 0.50000000000 意思是KUBE-SEP-X76RQKHDA4QWOROR被选择到的概率为0.5,上面两条规则一起表示对于 nginx service 将会随机的选择一个 pod。对于statistic模块的说明可以参考文档

对于双副本每个pod被选择概率是对半开,那么更多的pod会咋样呢?类似的,总体逻辑保持不变,iptables的修改如下:

shell
-A KUBE-SVC-4N57TFCL4MD7ZTDA -m comment --comment "default/nginx:" -m statistic --mode random --probability 0.33333333349 -j KUBE-SEP-X76RQKHDA4QWOROR -A KUBE-SVC-4N57TFCL4MD7ZTDA -m comment --comment "default/nginx:" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-VSSFB4ZLM3BVMCSQ -A KUBE-SVC-4N57TFCL4MD7ZTDA -m comment --comment "default/nginx:" -j KUBE-SEP-TM7ETOLNVIZI7CAZ

当副本数增加到4个,总体的路子已经没啥内容了,无非是每个pod被转发到的概率的变化。

shell
-A KUBE-SVC-4N57TFCL4MD7ZTDA -m comment --comment "default/nginx:" -m statistic --mode random --probability 0.25000000000 -j KUBE-SEP-X76RQKHDA4QWOROR -A KUBE-SVC-4N57TFCL4MD7ZTDA -m comment --comment "default/nginx:" -m statistic --mode random --probability 0.33333333349 -j KUBE-SEP-VSSFB4ZLM3BVMCSQ -A KUBE-SVC-4N57TFCL4MD7ZTDA -m comment --comment "default/nginx:" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-TM7ETOLNVIZI7CAZ -A KUBE-SVC-4N57TFCL4MD7ZTDA -m comment --comment "default/nginx:" -j KUBE-SEP-N27B5LXKJT5AQKDV

Hairpin NAT

Hairpin这个名词第一次接触到是在 TCP/IP Volume1 里边,场景是一台内网主机对提供通过NAT提供服务,而内网的另外一台主机使用该服务的时候只知道它的域名,而不知道内网主机的内网IP。这就出现了两台内网主机之间需要通过NAT转发,听起来有些诡异,这个过程叫做 hairpin。那么,换到service的场景下就是一个pod访问通过service的虚拟 IP 访问它自己,对于这种情况,kube-proxy会设置 iptables 规则将src IP 通过SNAT设置为宿主机的IP地址

shell
# 跳转到 KUBE-SERVICES -A PREROUTING -m comment --comment "kubernetes service portals" -j KUBE-SERVICES # KUBE-SVC-4N57TFCL4MD7ZTDA -A KUBE-SERVICES -d 10.109.10.94/32 -p tcp -m comment --comment "default/nginx: cluster IP" -m tcp --dport 80 -j KUBE-SVC-4N57TFCL4MD7ZTDA # 跳转到 KUBE-SEP-X76RQKHDA4QWOROR -A KUBE-SVC-4N57TFCL4MD7ZTDA -m comment --comment "default/nginx:" -j KUBE-SEP-X76RQKHDA4QWOROR # 第一行作用是对于 src ip为(pod本身,也就是自己通过service 访问自己)10.0.254.139/32,那么跳转到 KUBE-MARK-MASQ -A KUBE-SEP-X76RQKHDA4QWOROR -s 10.0.254.139/32 -m comment --comment "default/nginx:" -j KUBE-MARK-MASQ # 0x4000/0x4000 打上一个标签,然后返回到KUBE-SEP-X76RQKHDA4QWOROR -A KUBE-MARK-MASQ -j MARK --set-xmark 0x4000/0x4000 # 回到 KUBE-SEP-X76RQKHDA4QWOROR 进行 DNAT -A KUBE-SEP-X76RQKHDA4QWOROR -p tcp -m comment --comment "default/nginx:" -m tcp -j DNAT --to-destination 10.0.254.139:80

PREROUTING链的规则到此为止,最后还是到了 POSTROUTING 规则,其中的一条规则直接跳到了KUBE-POSTROUTING:

shell
-A KUBE-POSTROUTING -m mark ! --mark 0x4000/0x4000 -j RETURN -A KUBE-POSTROUTING -j MARK --set-xmark 0x4000/0x0 -A KUBE-POSTROUTING -m comment --comment "kubernetes service traffic requiring SNAT" -j MASQUERADE --random-fully

因为PREROUTING链不能进行SNAT,所以只能在前面打个标签, POSTROUTING将标签设置为了0x4000/0x0,最后被 SNAT 处理于是报文的原地址被改成了宿主机的网卡地址。

cluster-cidr

对于kube-proxy的配置项cluster-cidr的作用理解十分模糊,引用文档的描述:

The CIDR range of pods in the cluster. When configured, traffic sent to a Service cluster IP from outside this range will be masqueraded and traffic sent from pods to an external LoadBalancer IP will be directed to the respective cluster IP instead. For dual-stack clusters, a comma-separated list is accepted with at least one CIDR per IP family (IPv4 and IPv6). This parameter is ignored if a config file is specified by --config.

这里让人有些摸不着头脑的是,为什么cluster ip 可以被外部网络访问,具体的使用场景可以参考这篇。简单来说就是以k8s集群内机器作为跳板和外部主机设置静态路由来访问cluster IP。当设置了cluster-cidr参数以后(一般与cni的ippool相同),每一台物理节点kube-proxy 都会在 nat 表插入一条形如以下的规则:

shell
-A KUBE-SERVICES ! -s 192.169.0.0/16 -d 10.109.186.214/32 -p tcp -m comment --comment "default/my-service: cluster IP" -m tcp --dport 80 -j KUBE-MARK-MASQ

192.169.0.0/16 就是集群的cluster-cidr,该规则表明任何来自己集群外的流量都会被SNAT(KUBE-MARK-MASQ 链所做的事情就是SNAT)。kube-proxy默认的将这个参数都设置为空,查看kube-proxy 的configmap就可以看到。因此可以说,如果service不需要对集群外提供cluster ip 服务,该参数没有实际的影响,并且在实际中service对外提供服务都以ingress,nodeport这两种形式。

svc 的亲和性

结合前面的描述,我们已经确认pod访问svc会被重定向到哪一个pod是随机。如果期望所访问的pod是同一个,可以在svc定义中设置.spec.sessionAffinityClientIP(默认值为None),有效时间为3小时。设置了该配置项以后,kube-proxy做的就是在iptables使用了recent模块。iptables 会插入两个部分的 iptable 规则,第一部分如下:

shell
-A KUBE-SVC-KEAUNL7HVWWSEZA6 -m comment --comment "default/my-service:" -m recent --rcheck --seconds 10800 --reap --name KUBE-SEP-BN6N2GMJFE7MZ6FH --mask 255.255.255.255 --rsource -j KUBE-SEP-BN6N2GMJFE7MZ6FH -A KUBE-SVC-KEAUNL7HVWWSEZA6 -m comment --comment "default/my-service:" -m recent --rcheck --seconds 10800 --reap --name KUBE-SEP-3JGQHZPW2LWEBRCU --mask 255.255.255.255 --rsource -j KUBE-SEP-3JGQHZPW2LWEBRCU -A KUBE-SVC-KEAUNL7HVWWSEZA6 -m comment --comment "default/my-service:" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-BN6N2GMJFE7MZ6FH -A KUBE-SVC-KEAUNL7HVWWSEZA6 -m comment --comment "default/my-service:" -j KUBE-SEP-3JGQHZPW2LWEBRC

关于recent模块参数可以参考这里。iptables规则在这里的作用是,当下一次dst ip为svc的packet到达以后,如果它在列表里并且存在时间没有超过10800秒(3h),那么就直接跳转到下一个链(DNAT)。不过recent模块的规则在这目前还没有将 ip 加入到list,这一工作在下边两条规则完成,如下:

shell
-A KUBE-SEP-3JGQHZPW2LWEBRCU -p tcp -m comment --comment "default/my-service:" -m recent --set --name KUBE-SEP-3JGQHZPW2LWEBRCU --mask 255.255.255.255 --rsource -m tcp -j DNAT --to-destination 192.169.254.129:80 -A KUBE-SEP-BN6N2GMJFE7MZ6FH -p tcp -m comment --comment "default/my-service:" -m recent --set --name KUBE-SEP-BN6N2GMJFE7MZ6FH --mask 255.255.255.255 --rsource -m tcp -j DNAT --to-destination 192.168.244.196:80

--name就是用于存放IP list 的文件名,位于/proc/net/xt_recent/下,--set的作用就是将ip加入到list里边。在宿主机上直接查看该文件下的内容,如下:

src=192.169.254.130 ttl: 64 last_seen: 4314280593 oldest_pkt: 14 4314266944, 4314279421, 4314279560, 4314279659, 4314279742, 4314279814, 4314279892, 4314279964, 4314280054, 4314280136, 4314280221, 4314280301, 4314280383, 4314280593

192.169.254.130就是用于访问svc的pod的IP。

NodePort

NodePort的作用是能够让client通过k8s集群的节点ip:port直接访问svc。至此为止,我们可以十分肯定的是,当通过宿主机ip:port访问,也会被一些iptables所处理。目的为svc的请求先到达k8s集群物理机,所以会从PREROUTING链开始进入,与NODEPORT相关的关键规则如下:

shell
# 任何目标地址为 local 的都转发到 KUBE-NODEPORTS -A -m comment --comment "kubernetes service nodeports; NOTE: this must be the last rule in this chain" -m addrtype --dst-type LOCAL -j KUBE-NODEPORTS # 打标记 -A KUBE-NODEPORTS -p tcp -m comment --comment "default/my-service:" -m tcp --dport 31911 -j KUBE-MARK-MASQ # 端口匹配31911就是svc的端口 -A KUBE-NODEPORTS -p tcp -m comment --comment "default/my-service:" -m tcp --dport 31911 -j KUBE-SVC-KEAUNL7HVWWSEZA6 # 随机选择一个 pod 进行转发 nodeport 流量,KUBE-SEP-xxx chain进行DNAT -A KUBE-SVC-KEAUNL7HVWWSEZA6 -m comment --comment "default/my-service:" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-BN6N2GMJFE7MZ6FH -A KUBE-SVC-KEAUNL7HVWWSEZA6 -m comment --comment "default/my-service:" -j KUBE-SEP-3JGQHZPW2LWEBRCU

NodePort为什么还需要KUBE-MARK-MASQ打标签呢?因为NodePort会让k8s集群的每一个物理节点作为svc的proxy,发到物理节点上的请求还需要被转发出去,这也就是意味着还需要经过POSTROUTING链,打标记的作用应该还是能够SNAT转发。

IPVS

IPVS (IP Virtual Server) 是内核层面的四层负载均衡,是LVS (Linux Virtual Server)的一部分。假设在一个5000个节点的大型集群中,每个service都有10个pod,2000个service就需要在每个节点上增加至少2w条iptables记录,这会很大程度增加内核负担。IPVS本身就是为了负载均衡而设计的,用来替代iptables实现Service再合适不过。将kube-proxy的configmap设置mode:ipvs就从iptables切换到ipvs模式。查看所有ipvs的规则:

shell
$ sudo ipvsadm -ln IP Virtual Server version 1.2.1 (size=4096) Prot LocalAddress:Port Scheduler Flags -> RemoteAddress:Port Forward Weight ActiveConn InActConn # 172.17.0.1 默认是docker0的地址 TCP 172.17.0.1:31033 rr -> 192.168.21.40:8443 Masq 1 0 0 -> 192.168.142.67:8443 Masq 1 0 0 # 省略了一些内容

上面的规则表示172.17.0.1:31033将会被NAT为192.168.21.40:8443192.168.142.67:8443 。IPVS只能提供负载均衡的能力,但是kube-proxy所提供的其他功能,node-port、亲和性等内容还是交给了iptables处理,具体地在如下的场景中:

  • kube-proxy start with --masquerade-all=true
  • Specify cluster CIDR in kube-proxy startup
  • Support Loadbalancer type service
  • Support NodePort type service

IPVS模式下的Kube-proxy不会在上面的场景中使用太多的iptables规则,而是使用了ipset相结合,提高了一定的效率,更多的内容可以参考文档

对ipvs模式下的nodeport做一些补充。重新回顾一下nodeport的作用,目的是让一台节点作为代理,让集群外节点可以使用svc。发往nodeport节点的报文,srcIP是client IP,dstIP是nodeport节点的IP。这个报文需要被nodeport节点进行nat,srcIP被sant为nodeport节点,因为svc只能在集群内的节点访问,dstIP需要被dnat为实际的podIP。

前者(DNAT)是由ipvs完成的,创建一个nodeport类型的svc以后,查看ipvs规则:

shell
$ k get svc -n kong NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE kong-controller-validation-webhook ClusterIP 10.102.183.31 <none> 443/TCP 19h kong-gateway-admin ClusterIP None <none> 8444/TCP 19h kong-gateway-manager NodePort 10.107.6.108 <none> 8002:30351/TCP,8445:31274/TCP 19h kong-gateway-proxy NodePort 10.102.172.169 <none> 80:30224/TCP,443:31512/TCP 19h $ sudo ipvsadm -L -n | grep 30224 TCP 10.246.205.113:30224 rr -> 172.16.94.193:8000 Masq 1 0 0 -> 172.16.142.84:8000 Masq 1 0 0

访问节点10.246.205.113:30224将会以RR的策略DNAT为两个目的pod。

后者(SNAT)是由iptables完成的,Kube-proxy会创建一个KUBE-NODE-PORT-TCP ipset,ipset内的都会被SNAT。

shell
$ sudo iptables -t nat -S | grep -i nodeport -N KUBE-NODEPORTS -A KUBE-NODE-PORT -p tcp -m comment --comment "Kubernetes nodeport TCP port for masquerade purpose" -m set --match-set KUBE-NODE-PORT-TCP dst -j KUBE-MARK-MASQ # 和前面描述的一样,打一个标记然后在 POSTROUTING 进行SNAT $ sudo iptables -t nat -S KUBE-MARK-MASQ -N KUBE-MARK-MASQ -A KUBE-MARK-MASQ -j MARK --set-xmark 0x4000/0x4000 # 查看ipset $ sudo ipset list KUBE-NODE-PORT-TCP Name: KUBE-NODE-PORT-TCP Type: bitmap:port Revision: 3 Header: range 0-65535 Size in memory: 8264 References: 1 Number of entries: 4 Members: 30224 # 这就是前面 svc 的端口 30351 31274 31512

参考资料

https://kubernetes.io/docs/tutorials/services/source-ip/

https://linux.cn/article-13373-1.html 描述了conntrack是如何记录

https://cloud.tencent.com/developer/article/1870244

https://serenafeng.github.io/2020/03/26/kube-proxy-in-iptables-mode/ 很详细介绍iptables规则,不过篇幅较长没认真看

https://zhuanlan.zhihu.com/p/361477382

https://kubernetes.io/blog/2018/07/09/ipvs-based-in-cluster-load-balancing-deep-dive/ ipvs的官方文档

本文作者:strickland

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!