分析一下 kube-proxy 使用 iptables 作为负载均衡实现的规则,以及网络的细节。比如为什么 ping service 不通,为什么 iptables 会有性能问题。 添加 ipvs 简单分析。
pod 和 service 信息
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
nginx-deployment-54bcfc567b-2sqpc 1/1 Running 0 20h 10.0.0.4 k8s-master <none> <none>
nginx-deployment-54bcfc567b-6r7wm 1/1 Running 0 2d22h 10.0.1.2 k8s-worker <none> <none>
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
nginx-svc ClusterIP 10.96.138.71 <none> 8080/TCP 2d22h app=nginx
iptables 规则
kube-proxy 只在 nat 和 filter 表添加了规则。
nat 表
-A PREROUTING -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
-A OUTPUT -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
-A POSTROUTING -m comment --comment "kubernetes postrouting rules" -j KUBE-POSTROUTING
PREROUTING 和 OUTPUT 链都跳到了 KUBE-SERVICES 链:
-A KUBE-SERVICES -d 10.96.138.71/32 -p tcp -m comment --comment "default/nginx-svc cluster IP" -m tcp --dport 8080 -j KUBE-SVC-HL5LMXD5JFHQZ6LN
这条链的意思是目的网段10.96.138.71/32
,目的端口是8080
的 tcp 包跳转到链KUBE-SVC-HL5LMXD5JFHQZ6LN
,下面是这条链:
-A KUBE-SVC-HL5LMXD5JFHQZ6LN ! -s 10.0.0.0/16 -d 10.96.138.71/32 -p tcp -m comment --comment "default/nginx-svc cluster IP" -m tcp --dport 8080 -j KUBE-MARK-MASQ
-A KUBE-SVC-HL5LMXD5JFHQZ6LN -m comment --comment "default/nginx-svc -> 10.0.0.4:80" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-GS5MQAG4332XHLUZ
-A KUBE-SVC-HL5LMXD5JFHQZ6LN -m comment --comment "default/nginx-svc -> 10.0.1.2:80" -j KUBE-SEP-H5CTQI4CMO4ES5BQ
上面第一条规则:源ip非10.0.0.0/16
,并且目的ip是10.96.138.71/32
,目的端口是8080
的 tcp 包跳转到KUBE-MARK-MASQ
,
-A KUBE-MARK-MASQ -j MARK --set-xmark 0x4000/0x4000
数据包被打上标签以后因为没有 DROP 等动作,所以回到上面的第二条规则:有50%的几率命中这条链,如果没有命中,就到下一条链,这是 iptables 实现负载均衡的原理。
接下来就到KUBE-SEP-*
开头的链:
-A KUBE-SEP-GS5MQAG4332XHLUZ -s 10.0.0.4/32 -m comment --comment "default/nginx-svc" -j KUBE-MARK-MASQ
-A KUBE-SEP-GS5MQAG4332XHLUZ -p tcp -m comment --comment "default/nginx-svc" -m tcp -j DNAT --to-destination 10.0.0.4:80
-A KUBE-SEP-H5CTQI4CMO4ES5BQ -s 10.0.1.2/32 -m comment --comment "default/nginx-svc" -j KUBE-MARK-MASQ
-A KUBE-SEP-H5CTQI4CMO4ES5BQ -p tcp -m comment --comment "default/nginx-svc" -m tcp -j DNAT --to-destination 10.0.1.2:80
这两组规则的意思就是:如果源ip地址是对应的 pod ip地址,到KUBE-MARK-MASQ
(之前的是非 pod ip地址段打标签)打标签,然后做DNAT
到10.0.0.4
或者10.0.1.2
。
KUBE-POSTROUTING 链有下面的规则:
-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
第一条规则,如果数据包没有打上标签 RETURN 到上一个跳转的地方,第二条规则打标签,第三条规则做一个SNAT
动作,修改源ip为出口网络设备的ip。
nat表就这些,下面来看 filter 表。
filter 表
INPUT 链:
-A INPUT -m conntrack --ctstate NEW -m comment --comment "kubernetes load balancer firewall" -j KUBE-PROXY-FIREWALL
-A INPUT -m comment --comment "kubernetes health check service ports" -j KUBE-NODEPORTS
-A INPUT -m conntrack --ctstate NEW -m comment --comment "kubernetes externally-visible service portals" -j KUBE-EXTERNAL-SERVICES
-A INPUT -j KUBE-FIREWALL
前面三条跳转的链都是空链,只有第四条有一条规则:
-A KUBE-FIREWALL ! -s 127.0.0.0/8 -d 127.0.0.0/8 -m comment --comment "block incoming localnet connections" -m conntrack ! --ctstate RELATED,ESTABLISHED,DNAT -j DROP
丢弃本地的网络流量。
OUTPUT也是一样:
-A OUTPUT -m conntrack --ctstate NEW -m comment --comment "kubernetes load balancer firewall" -j KUBE-PROXY-FIREWALL
-A OUTPUT -m conntrack --ctstate NEW -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
-A OUTPUT -j KUBE-FIREWALL
FORWARD 链:
-A FORWARD -m conntrack --ctstate NEW -m comment --comment "kubernetes load balancer firewall" -j KUBE-PROXY-FIREWALL
-A FORWARD -m comment --comment "kubernetes forwarding rules" -j KUBE-FORWARD
-A FORWARD -m conntrack --ctstate NEW -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
-A FORWARD -m conntrack --ctstate NEW -m comment --comment "kubernetes externally-visible service portals" -j KUBE-EXTERNAL-SERVICES
第一条跳转到KUBE-PROXY-FIREWALL
是空链,第二条跳转到KUBE-FORWARD
,下面有三条:
-A KUBE-FORWARD -m conntrack --ctstate INVALID -j DROP
-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 rule" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
第一条状态是无效的,丢弃;第二条打标签,接受;第三条状态是RELATED 和 ESTABLISHD
的流量,接受。
总结一下,iptables 模式的 kube-proxy 主要的链:
-
KUBE-SERVICES链:访问集群内service的数据包入口,它会根据匹配到的service IP:port跳转到KUBE-SVC-XXX链。
-
KUBE-SVC-XXX链:对应service对象,基于random功能实现了流量的负载均衡。
-
KUBE-SEP-XXX链:通过DNAT将service IP:port替换成后端pod IP:port,从而将流量转发到相应的pod。
因为 clusterIP 对应的 ip 只是 iptables 的规则实现,所以不能 ping 通。(没有相应的网络设备)
规则差不多分析完了,再来看看实际请求走的规则流程,以‘在宿主机通过 service ip 访问其他机器的 pod ip’这个场景为例: 去程:
比如在节点A `curl 10.96.138.71:8080`
节点A:
# nat OUTPUT:
-A OUTPUT -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
-A KUBE-SERVICES -d 10.96.138.71/32 -p tcp -m comment --comment "default/nginx-svc cluster IP" -m tcp --dport 8080 -j KUBE-SVC-HL5LMXD5JFHQZ6LN
-A KUBE-SVC-HL5LMXD5JFHQZ6LN ! -s 10.0.0.0/16 -d 10.96.138.71/32 -p tcp -m comment --comment "default/nginx-svc cluster IP" -m tcp --dport 8080 -j KUBE-MARK-MASQ
-A KUBE-MARK-MASQ -j MARK --set-xmark 0x4000/0x4000
-A KUBE-SVC-HL5LMXD5JFHQZ6LN -m comment --comment "default/nginx-svc -> 10.0.1.2:80" -j KUBE-SEP-H5CTQI4CMO4ES5BQ
-A KUBE-SEP-H5CTQI4CMO4ES5BQ -p tcp -m comment --comment "default/nginx-svc" -m tcp -j DNAT --to-destination 10.0.1.2:80
# filter OUTPUT
-A OUTPUT -j KUBE-FIREWALL
# {src: localProcess, dst: 10.0.1.2:80}
# 根据路由规则:
10.0.1.0/24 via 10.0.1.0 dev flannel.1 onlink
通过 flannel.1 网络设备转发请求
# nat POSTROUTING
-A POSTROUTING -m comment --comment "kubernetes postrouting rules" -j KUBE-POSTROUTING
-A KUBE-POSTROUTING -j MARK --set-xmark 0x4000/0x0
-A KUBE-POSTROUTING -m comment --comment "kubernetes service traffic requiring SNAT" -j MASQUERADE --random-fully
# 到这里 {src:10.0.0.0:50959, dst:10.0.1.2:80},10.0.0.0 就是A节点 flannel.1 的ip。
# 通过 flannel 的网络来到节点B
# nat PREROUTING
-A PREROUTING -m comment --comment "kubernetes service portals" -j KUBE-SERVICES (注意:如果 KUBE-SERVICES 下面没有规则匹配的话,就按照 iptables 的顺序,走 FORWARD 链)
# 路由决策
10.0.1.0/24 dev cni0 proto kernel scope link src 10.0.1.1
# filter FORWARD
-A FORWARD -m comment --comment "kubernetes forwarding rules" -j KUBE-FORWARD
# nat POSTROUTING
-A POSTROUTING -m comment --comment "kubernetes postrouting rules" -j KUBE-POSTROUTING
回程:
# {src:10.0.1.2:80, dst: 10.0.0.0:50959}
# nat PREROUTING
-A PREROUTING -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
# 路由决策
10.0.0.0/24 via 10.0.0.0 dev flannel.1 onlink
# filter FORWARD
-A FORWARD -m comment --comment "kubernetes forwarding rules" -j KUBE-FORWARD
# nat POSTROUTING
-A POSTROUTING -m comment --comment "kubernetes postrouting rules" -j KUBE-POSTROUTING
# 通过 flannel.1 到节点A
# 请求变为 {src: 10.0.1.2:80, dst: localprocess}
# nat PREROUTING
-A PREROUTING -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
# 路由决策
10.0.0.0/24 dev cni0 proto kernel scope link src 10.0.0.1
# filter INPUT
-A INPUT -j KUBE-FIREWALL
最后能看到,iptables 主要在KUBE-SERVICE
这条链上添加规则,当 service 数量越来越多的时候,由于 iptables 规则顺序执行,性能会越来越差,所以后面用了 ipvs 代替这部分规则的实现。
ipvs
ipvs 简介
- Director:调度器,用于接受用户请求
- Real Server:真正处理用户请求
- CIP:client ip,客户端请求源ip
- VIP:virtual ip,调度器和客户端通信的ip
- RIP:real server ip, 后端主机的ip
ipvs 监听到达 INPUT 链的数据包,做 DNAT 动作,所以数据包的目的(VIP)必须是本机,k8s将这个ip绑定到虚拟网卡kube-ipvs0
上。
8: kube-ipvs0: <BROADCAST,NOARP> mtu 1500 qdisc noop state DOWN group default
link/ether 82:0f:08:b3:50:20 brd ff:ff:ff:ff:ff:ff
inet 10.96.0.10/32 scope global kube-ipvs0
valid_lft forever preferred_lft forever
inet 10.96.0.1/32 scope global kube-ipvs0
valid_lft forever preferred_lft forever
inet 10.99.181.130/32 scope global kube-ipvs0
valid_lft forever preferred_lft forever
# ipvsadm -ln
...
TCP 10.99.181.130:8080 rr
-> 10.0.0.2:80 Masq 1 0 0
-> 10.0.1.6:80 Masq 1 0 0
大概的数据包流程如下:
- cluster ip 绑定在 kube-ipvs0,内核识别 vip 是本机 ip
- 数据包到达 INPUT 链
- ipvs 修改数据包的目标ip(DNAT) 为 pod ip,将数据包发往 POSTROUTING
- 经过 POSTROUTING 链(SNAT),通过 flannel.1 发送出去
- pod 收到请求后,改变目的和源地址,返回给客户端
requirments
如果不满足条件,kube-proxy 会fall back到iptables。
modprobe -- ip_vs
modprobe -- ip_vs_rr
modprobe -- ip_vs_wrr
modprobe -- ip_vs_sh
modprobe -- nf_conntrack
安装 ipset
apt install ipset