Kubernetes gives Pods their own IP addresses and a single DNS name for a setof Pods, and can load-balance across them.
K8s Service会为每个 Pod 都设置一个它自己的 IP,并为一组 Pod 提供一个统一的 DNS 域名,还可以提供在它们间做负载均衡的能力。这篇文章会对 kube-proxy 的 iptables 模式内部的机制做一个验证。大体上涉及的内容如下:
创建一个 Service,配置如下:
apiVersion: v1 |
创建后的 service 如下:
$ k get svc -o wide -A |
注意其中的 spring-test 和 kube-dns 两项,后面会用到。另外 service 对应的 podIP 如下:
$ k get ep |
K8s 会为 Service 创建一个 DNS域名,格式为
,例如我们创建的 spring-test
Service 则会有 spring-test.default.svc.cluster.local
[1] 域名。
我们首先进入 pod,看一下 /etc/resolv.conf
文件,关于域名解析的配置:
nameserver 10.1.0.10 |
这里的 10.1.0.10
是 kube-dns service 的 cluster IP
文件中配置了多个 search 域,因此我们写 spring-test
或 spring-test.default
或 spring-test.default.svc
都是可以解析的,另外注意解析后的 IP 也不是具体哪个 POD 的地址,而是为 Service 创建的虚拟地址ClusterIP。
[email protected]:/# nslookup spring-test |
ndots:5
指的是如果域名中的 .
大于等于 5 个,则不走 search 域,目的是减少常规域名的解析次数 [2]
DNS 里创建的记录解决了域名到 ClusterIP 的转换问题,发送到 ClusterIP 的请求,如何转发到对应的 POD 里呢?K8s Service 有几种实现方式,这里验证的是 iptables 的实现方式:kube-proxy 会监听 etcd 中关于 k8s 的事件,并动态地对 iptables 做配置,最终由 iptables 来完成转发。先看看跟这个 Service 相关的规则如下:
0. -A PREROUTING -j KUBE-SERVICES |
我们先用 iptables-save
打印出所有的规则,筛选出和 spring-test
service相关的规则,删除了一些 comment,并对名字做了简化。可以看到有这么几类:
KUBE-NODEPORTS
,这类规则用来将发送到 NodePort 的报文转到 KUBE-SVC-*
KUBE-SERVICES
:是识别目标地址为 ClusterIP( 10.1.68.7
),命中的报文转到 KUBE-SVC-*
做处理KUBE-SVC
的作用是做负载均衡,将请求分配到 KUBE-SEP
中KUBE-SEP
通过 DNAT 替换目标地址为 Pod IP,转发到具体的 POD 中另外经常看到 -j KUBE-MARK-MASQ
,它的作用是在请求里加上 mark,在 POSTROUTING
规则中做 SNAT,这点后面再细说。
我们开启 iptables 的 trace 模式 [3],并在其中一个 pod 发送一个请求,检查 TRACE 中规则的命中情况(由于输出特别多,这里挑选了重要的输出并做了精简):
0: nat:PREROUTING IN=cni0 OUT= SRC=10.244.1.7 DST=10.1.68.7 DPT=8080 |
PREROUTING
时,进入第 6 条进判定KUBE-SERVICES
判断目标地址为 10.1.68.7
且目标端口为 8080
,于是跳转进入 KUBE-SVC-S
链的判断KUBE-SVC-S
有多条规则,从日志看最终是从第 10 条退出,进入 KUBE-SEP-A
链KUBE-SEP-A
最终命中第 3 条规则退出,但此时会进行 DNAT 转换目标地址DST
目标地址已经变成 pod 地址 10.244.2.3
了类似的,如果我们是通过 NodePort 来访问 Service,则 Trace 日志如下:
0: nat:PREROUTING: IN=eth0 OUT= SRC=192.168.50.135 DST=192.168.50.238 DPT=31080 |
上一节我们比较关注 iptables 转发的内容,那么如何做负载均衡?这部分是比较纯粹的iptables 知识 [4]:
首先:iptables 对于规则的解析是严格顺序的,所以如果只是单纯列出两个条目,则会永远命中第一条:
-A KUBE-SVC-S -j KUBE-SEP-A |
于是,我们需要第一条规则在某些条件下不命中。这样 iptables 就有机会执行后面的规则。iptables 提供了两种方法,第一种是有随机数,也是上一节我们看到的:
-A KUBE-SVC-S -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-B |
这条规则在执行时,iptables 会随机生成一个数,并以 probability
的概率命中当前规则。换句话说,第一条命中的概率是 p
,则第二条规则就是 1-p
。如果有 3 个副本,则会类似下面这样的规则,大家可以计算下最后三个 Pod 是不是平均分配:
-A KUBE-SVC-S --mode random --probability 0.33333333349 -j KUBE-SEP-A |
另外一种模式是 round-robin,但是 kubernetes 的 iptables 模式不支持,这里就不细说了。猜想 kubernetes iptables 模式下不支持的原因是虽然单机 iptables 能支持round-robin,但多机模式下,无法做到全局的 round-robin。
前面我们提到 KUBE 系列的规则经常看到 -j KUBE-MARK-MASQ
,和它相关的规则有这些:
-A KUBE-MARK-MASQ -j MARK --set-xmark 0x4000/0x4000 |
首先 KUBE-MARK-MASQ
的作用是把报文打上 0x4000/0x4000
的标记,在 KUBE-POSTROUTING
时,如果报文中包含这个标记,会执行 -j MASQUERADE
操作,而这个操作的作用就是做源地址转换(SNAT)。那 SNAT 是什么,为什么要做 SNAT 呢?
这里引用 这篇文章里的图做说明:
如果没有 SNAT,被转发到 POD 的请求返回时,会尝试把请求直接返回给 Client,我们知道一个 TCP 连接的依据是(src_ip, src_port, dst_ip, dst_port),现在client 在等待 eIP/NP
返回的报文,等到的却是 pod IP
的返回,client 不认这个报文。换句话说,经过 proxy 的流量都正常情况下都应该原路返回才能工作。
在一些情况下可能希望关闭 SNAT,K8S 提供 externalTrafficPolicy: Local
的配置项,但流量的流转也会发生变化,这里不深入。
这篇文章和上一篇 Flannel 网络通信验证类似,都是尝试搭建环境,在学习 kube-proxy 工作机制的同时,对 kube-proxy 的产出iptables 做一些验证。文章中验证了这些内容:
/etc/resolv.conf
中搜索域的设置这篇文章的信息量不大,希望读者也撸起袖子,实打实地做一些验证,能让我们对kube-proxy 涉及的 iptables 的操作有更深刻的理解。
cluster.local
是可以改的,但是比较麻烦,参考: https://stackoverflow.com/a/66106716 ↩
参考 https://hansedong.github.io/2018/11/20/9/ ↩
https://www.opensourcerers.org/2016/05/27/how-to-trace-iptables-in-rhel7-centos7/ ↩
Turning IPTables into a TCP load balancer for fun and profit ↩