分析一下 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地址段打标签)打标签,然后做DNAT10.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    

大概的数据包流程如下:

  1. cluster ip 绑定在 kube-ipvs0,内核识别 vip 是本机 ip
  2. 数据包到达 INPUT 链
  3. ipvs 修改数据包的目标ip(DNAT) 为 pod ip,将数据包发往 POSTROUTING
  4. 经过 POSTROUTING 链(SNAT),通过 flannel.1 发送出去
  5. 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