带你了解 Kube-proxy 工作原理

kube-proxy介绍

为什么需要kube-proxy

我们知道容器的特点是快速创建、快速销毁,Kubernetes Pod 和容器一样只具有临时的生命周期,一个 Pod 随时有可能被终止或者漂移,随着集群的状态变化而变化,一旦Pod 变化,则该 Pod 提供的服务也就无法访问,如果直接访问 Pod 则无法实现服务的连续性和高可用性,因此显然不能使用 Pod 地址作为服务暴露端口。

解决这个问题的办法和传统数据中心解决无状态服务高可用的思路完全一样,通过负载均衡和 VIP 实现后端真实服务的自动转发、故障转移。

这个负载均衡在 Kubernetes 中称为 Service,VIP 即 Service ClusterIP,因此可以认为Kubernetes 的 Service 就是一个四层负载均衡,Kubernetes 对应的还有七层负载均衡 Ingress,本文仅介绍 Kubernetes Service。

这个 Service 就是由 kube-proxy 实现的,ClusterIP 不会因为 Pod 状态改变而变,需要注意的是 VIP 即 ClusterIP 是个假的 IP,这个 IP 在整个集群中根本不存在,当然也就无法通过IP协议栈无法路由,底层 underlay 设备更无法感知这个 IP 的存在,因此 ClusterIP 只能是单主机(Host Only)作用域可见,这个IP在其他节点以及集群外均无法访问。

Kubernetes 为了实现在集群所有的节点都能够访问 Service,kube-proxy 默认会在所有的 Node 节点都创建这个 VIP 并且实现负载,所以在部署 Kubernetes 后发现 kube-proxy 是一个 DaemonSet。

而 Service 负载之所以能够在 Node 节点上实现是因为无论 Kubernetes 使用哪个网络模型,均需要保证满足如下三个条件:

  1. 容器之间要求不需要任何NAT能直接通信;
  2. 容器与Node之间要求不需要任何NAT能直接通信;
  3. 容器看到自身的IP和外面看到它的IP必须是一样的,即不存在IP转化的问题。

至少第 2 点是必须满足的,有了如上几个假设,Kubernetes Service 才能在 Node 上实现,否则 Node 不通 Pod IP 也就实现不了了。

有人说既然 kube-proxy 是四层负载均衡,那 kube-proxy 应该可以使用 haproxy、nginx 等作为负载后端啊?

事实上确实没有问题,不过唯一需要考虑的就是性能问题,如上这些负载均衡功能都强大,但毕竟还是基于用户态转发或者反向代理实现的,性能必然不如在内核态直接转发处理好。

因此 kube-proxy 默认会优先选择基于内核态的负载作为后端实现机制,目前 kube-proxy 默认是通过 iptables 实现负载的,在此之前还有一种称为 userspace 模式,其实也是基于 iptables 实现,可以认为当前的 iptables 模式是对之前 userspace 模式的优化。

本节接下来将详细介绍kube-proxy iptables模式的实现原理。

kube-proxy iptables 模式实现原理

ClusterIP

首先创建了一个 ClusterIP 类型的 Service:

1
2
3
# kubectl get svc -l owner=int32bit
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes-bootcamp-v1 ClusterIP 10.106.224.41 <none> 8080/TCP 163m

其中 ClusterIP 为 10.106.224.41,我们可以验证这个IP在本地是不存在的:

1
2
3
4
5
6
7
8
root@ip-192-168-193-172:~# ping -c 2 -w 2 10.106.224.41
PING 10.106.224.41 (10.106.224.41) 56(84) bytes of data.

--- 10.106.224.41 ping statistics ---
2 packets transmitted, 0 received, 100% packet loss, time 1025ms

root@ip-192-168-193-172:~# ip a | grep 10.106.224.41
root@ip-192-168-193-172:~#

所以不要尝试去 ping ClusterIP,它不可能通的

此时在 Node 节点 192.168.193.172 上访问该 Service 服务,首先流量到达的是 OUTPUT 链,这里我们只关心 nat 表的 OUTPUT 链:

1
2
# iptables-save -t nat | grep -- '-A OUTPUT'
-A OUTPUT -m comment --comment "kubernetes service portals" -j KUBE-SERVICES

该链跳转到 KUBE-SERVICES 子链中:

1
2
3
4
# iptables-save -t nat | grep -- '-A KUBE-SERVICES'
...
-A KUBE-SERVICES ! -s 10.244.0.0/16 -d 10.106.224.41/32 -p tcp -m comment --comment "default/kubernetes-bootcamp-v1: cluster IP" -m tcp --dport 8080 -j KUBE-MARK-MASQ
-A KUBE-SERVICES -d 10.106.224.41/32 -p tcp -m comment --comment "default/kubernetes-bootcamp-v1: cluster IP" -m tcp --dport 8080 -j KUBE-SVC-RPP7DHNHMGOIIFDC

我们发现与之相关的有两条规则:

  • 第一条负责打标记 MARK 0x4000/0x4000,后面会用到这个标记。
  • 第二条规则跳到 KUBE-SVC-RPP7DHNHMGOIIFDC 子链。

其中 KUBE-SVC-RPP7DHNHMGOIIFDC 子链规则如下:

1
2
3
4
# iptables-save -t nat | grep -- '-A KUBE-SVC-RPP7DHNHMGOIIFDC'
-A KUBE-SVC-RPP7DHNHMGOIIFDC -m statistic --mode random --probability 0.33332999982 -j KUBE-SEP-FTIQ6MSD3LWO5HZX
-A KUBE-SVC-RPP7DHNHMGOIIFDC -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-SQBK6CVV7ZCKBTVI
-A KUBE-SVC-RPP7DHNHMGOIIFDC -j KUBE-SEP-IAZPHGLZVO2SWOVD

这几条规则看起来复杂,其实实现的功能很简单:

  • 1/3 的概率跳到子链 KUBE-SEP-FTIQ6MSD3LWO5HZX,
  • 剩下概率的 1/2,(1 - 1/3) * 1/2 == 1/3,即 1/3 的概率跳到子链 KUBE-SEP-SQBK6CVV7ZCKBTVI
  • 剩下 1/3 的概率跳到 KUBE-SEP-IAZPHGLZVO2SWOVD

我们查看其中一个子链 KUBE-SEP-FTIQ6马上到!3LWO5HZX规则:

1
2
3
# iptables-save -t nat | grep -- '-A KUBE-SEP-FTIQ6MSD3LWO5HZX'
...
-A KUBE-SEP-FTIQ6MSD3LWO5HZX -p tcp -m tcp -j DNAT --to-destination 10.244.1.2:8080

可见这条规则的目的是做了一次 DNAT,DNAT 目标为其中一个 Endpoint,即 Pod 服务。

由此可见子链 KUBE-SVC-RPP7DHNHMGOIIFDC 的功能就是按照概率均等的原则DNAT 到其中一个 Endpoint IP,即 Pod IP,假设为 10.244.1.2,

此时相当于:

1
2
3
4
5
192.168.193.172:xxxx -> 10.106.224.41:8080
|
| DNAT
V
192.168.193.172:xxxX -> 10.244.1.2:8080

接着来到 POSTROUTING 链:

1
2
3
4
# iptables-save -t nat | grep -- '-A POSTROUTING'
-A POSTROUTING -m comment --comment "kubernetes postrouting rules" -j KUBE-POSTROUTING
# iptables-save -t nat | grep -- '-A KUBE-POSTROUTING'
-A KUBE-POSTROUTING -m comment --comment "kubernetes service traffic requiring SNAT" -m mark --mark 0x4000/0x4000 -j MASQUERADE

这两条规则只做一件事就是只要标记了 0x4000/0x4000 的包就一律做 MASQUERADE(SNAT),由于 10.244.1.2 默认是从 flannel.1 转发出去的,因此会把源IP改为 flannel.1 的IP 10.244.0.0

1
2
3
4
5
6
7
8
9
192.168.193.172:xxxx -> 10.106.224.41:8080
|
| DNAT
V
192.168.193.172:xxxx -> 10.244.1.2:8080
|
| SNAT
V
10.244.0.0:xxxx -> 10.244.1.2:8080

剩下的就是常规的走 Vxlan 隧道转发流程了,这里不再赘述。

NodePort

接下来研究下 NodePort 过程,首先创建如下 Service:

1
2
3
# kubectl get svc -l owner=int32bit
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes-bootcamp-v1 NodePort 10.106.224.41 <none> 8080:30419/TCP 3h30m

其中 Service 的 NodePort 端口为 30419。

假设有一个外部IP 192.168.193.197,通过 192.168.193.172:30419 访问服务。

首先到达 PREROUTING 链:

1
2
3
4
5
# iptables-save -t nat | grep -- '-A PREROUTING'
-A PREROUTING -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
# iptables-save -t nat | grep -- '-A KUBE-SERVICES'
...
-A KUBE-SERVICES -m addrtype --dst-type LOCAL -j KUBE-NODEPORTS

PREROUTING 的规则非常简单,凡是发给自己的包,则交给子链 KUBE-NODEPORTS 处理。注意前面省略了判断 ClusterIP 的部分规则。

KUBE-NODEPORTS 规则如下:

1
2
3
# iptables-save -t nat | grep -- '-A KUBE-NODEPORTS'
-A KUBE-NODEPORTS -p tcp -m comment --comment "default/kubernetes-bootcamp-v1:" -m tcp --dport 30419 -j KUBE-MARK-MASQ
-A KUBE-NODEPORTS -p tcp -m comment --comment "default/kubernetes-bootcamp-v1:" -m tcp --dport 30419 -j KUBE-SVC-RPP7DHNHMGOIIFDC

这个规则首先给包打上标记 0x4000/0x4000,然后交给子链 KUBE-SVC-RPP7DHNHMGOIIFDC 处理, KUBE-SVC-RPP7DHNHMGOIIFDC 刚刚已经见面过了,其功能就是按照概率均等的原则 DNAT 到其中一个 Endpoint IP,即 Pod IP,假设为10.244.1.2。

1
2
3
4
5
192.168.193.197:xxxx -> 192.168.193.172:30419
|
| DNAT
V
192.168.193.197:xxxx -> 10.244.1.2:8080

此时发现 10.244.1.2 不是自己的IP,于是经过路由判断目标为 10.244.1.2 需要从 flannel.1 发出去。

接着到了 FORWARD 链,

1
2
3
4
5
# iptables-save -t filter | grep -- '-A FORWARD'
-A FORWARD -m comment --comment "kubernetes forwarding rules" -j KUBE-FORWARD
# iptables-save -t filter | grep -- '-A 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

FORWARD 表在这里只是判断下,只允许打了标记 0x4000/0x4000 的包才允许转发。

最后来到 POSTROUTING 链,这里和 ClusterIP 就完全一样了,在 KUBE-POSTROUTING 中做一次 MASQUERADE(SNAT),最后结果:

1
2
3
4
5
6
7
8
9
192.168.193.197:xxxx -> 192.168.193.172:30419
|
| DNAT
V
192.168.193.197:xxxx -> 10.244.1.2:8080
|
| SNAT
V
10.244.0.0:xxxx -> 10.244.1.2:8080

kube-proxy 使用 iptables 存在的问题

我们发现基于 iptables 模式的 kube-proxy ClusterIP 和 NodePort 都是基于 iptables 规则实现的,我们至少发现存在如下几个问题:

  • iptables 规则复杂零乱,真要出现什么问题,排查 iptables 规则必然得掉层皮。 LOG+TRACE 大法也不好使。
  • iptables 规则多了之后性能下降,这是因为 iptables 规则是基于链表实现,查找复杂度为 O(n),当规模非常大时,查找和处理的开销就特别大。据官方说法,当节点到达5000 个时,假设有 2000 个 NodePort Service,每个 Service 有 10 个 Pod,那么在每个 Node 节点中至少有 20000 条规则,内核根本支撑不住,iptables 将成为最主要的性能瓶颈。
  • iptables 主要是专门用来做主机防火墙的,而不是专长做负载均衡的。虽然通过 iptables 的 statistic 模块以及 DNAT 能够实现最简单的只支持概率轮询的负载均衡,但是往往我们还需要更多更灵活的算法,比如基于最少连接算法、源地址 HASH 算法等。而同样基于 netfilter 的 ipvs 却是专门做负载均衡的,配置简单,基于散列查找 O(1) 复杂度性能好,支持数十种调度算法。因此显然 ipvs 比 iptables 更适合做 kube-proxy 的后端,毕竟专业的人做专业的事,物尽其美。

本文接下来将介绍 kube-proxy 的 ipvs 实现,由于本人之前也是对 ipvs 很陌生,没有用过,专门学习了下 ipvs,因此在第二章简易介绍了下 ipvs,如果已经很熟悉 ipvs 了,可以直接跳过,这一章和 Kubernetes 几乎没有任何关系。

IPVS 简易入门

IPVS 简介

我们接触比较多的是应用层负载均衡,比如 haproxy、nginx、F5 等,这些负载均衡工作在用户态,因此会有对应的进程和监听 socket,一般能同时支持 4 层负载和 7 层负载,使用起来也比较方便。

LVS 是国内章文嵩博士开发并贡献给社区的(章文嵩博士和他背后的负载均衡帝国),主要由 ipvs 和 ipvsadm 组成,ipvs 是工作在内核态的 4 层负载均衡,和 iptables 一样都是基于内核底层 netfilter 实现,netfilter 主要通过各个链的钩子实现包处理和转发。ipvsadm 和 ipvs 的关系,就好比 netfilter 和 iptables 的关系,它运行在用户态,提供简单的 CLI 接口进行 ipvs 配置。

由于 ipvs 工作在内核态,直接基于内核处理包转发,所以最大的特点就是性能非常好。又由于它工作在 4 层,因此不会处理应用层数据,经常有人问 ipvs 能不能做 SSL 证书卸载、或者修改 HTTP 头部数据,显然这些都不可能做的。

我们知道应用层负载均衡大多数都是基于反向代理实现负载的,工作在应用层,当用户的包到达负载均衡监听器 listening 后,基于一定的算法从后端服务列表中选择其中一个后端服务进行转发。当然中间可能还会有一些额外操作,最常见的如 SSL 证书卸载。

而 ipvs 工作在内核态,只处理四层协议,因此只能基于路由或者 NAT 进行数据转发,可以把 ipvs 当作一个特殊的路由器网关,这个网关可以根据一定的算法自动选择下一跳,或者把 ipvs 当作一个多重 DNAT,按照一定的算法把 ip 包的目标地址 DNAT 到其中真实服务的目标 IP。针对如上两种情况分别对应 ipvs 的两种模式–网关模式和 NAT 模式,另外 ipip 模式则是对网关模式的扩展,本文下面会针对这几种模式的实现原理进行详细介绍。

IPVS 用法

ipvsadm 命令行用法和 iptables 命令行用法非常相似,毕竟是兄弟,比如 -L 列举, -A 添加, -D 删除。

1
ipvsadm -A -t 192.168.193.172:32016 -s rr

但是其实 ipvsadm 相对 iptables 命令简直太简单了,因为没有像 iptables 那样存在各种table,table 嵌套各种链,链里串着一堆规则,ipvsadm 就只有两个核心实体,分别为service 和 server,service 就是一个负载均衡实例,而 server 就是后端 member,ipvs术语中叫做 real server,简称 RS。

如下命令创建一个 service 实例 172.17.0.1:32016, -t 指定监听的为 TCP 端口, -s 指定算法为轮询算法 rr(Round Robin),ipvs 支持简单轮询(rr)、加权轮询(wrr)、最少连接(lc)、源地址或者目标地址散列(sh、dh)等 10 种调度算法。

1
ipvsadm -A -t 172.17.0.1:32016 -s rr

然后把 10.244.1.2:8080、10.244.1.3:8080、10.244.3.2:8080 添加到 service 后端 member 中。

1
2
3
ipvsadm -a -t 172.17.0.1:32016 -r 10.244.1.2:8080 -m -w 1
ipvsadm -a -t 172.17.0.1:32016 -r 10.244.1.3:8080 -m -w 1
ipvsadm -a -t 172.17.0.1:32016 -r 10.244.3.2:8080 -m -w 1

其中 -t 指定 service 实例, -r 指定 server 地址, -w 指定权值, -m 即前面说的转发模式,其中 -m 表示为 masquerading,即 NAT 模式, -ggatewaying,即直连路由模式, -i 为 ipip,即 IPIP 隧道模式。

与 iptables-save、iptables-restore 对应的工具 ipvs 也有 ipvsadm-save、ipvsadm-restore。

NAT(network access translation) 模式

​NAT 模式由字面意思理解就是通过 NAT 实现的,但究竟是如何 NAT 转发的,我们通过实验环境验证下。

现环境中 LB 节点 IP 为 192.168.193.197,三个 RS 节点如下:

  • 192.168.193.172:30620
  • 192.168.193.194:30620
  • 192.168.193.226:30620

为了模拟 LB 节点 IP 和 RS 不在同一个网络的情况,在 LB 节点中添加一个虚拟 IP 地址:

1
ip addr add 10.222.0.1/24 dev ens5

创建负载均衡 Service 并把 RS 添加到 Service 中:

1
2
3
4
ipvsadm -A -t 10.222.0.1:8080 -s rr
ipvsadm -a -t 10.222.0.1:8080 -r 192.168.193.194:30620 -m
ipvsadm -a -t 10.222.0.1:8080 -r 192.168.193.226:30620 -m
ipvsadm -a -t 10.222.0.1:8080 -r 192.168.193.172:30620 -m

这里需要注意的是,和应用层负载均衡如 haproxy、nginx 不一样的是,haproxy、nginx进程是运行在用户态,因此会创建 socket,本地会监听端口,而 ipvs 的负载是直接运行在内核态的,因此不会出现监听端口:

1
2
3
4
5
6
root@ip-192-168-193-197:/var/log# netstat -lnpt
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 127.0.0.53:53 0.0.0.0:* LISTEN 674/systemd-resolve
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 950/sshd
tcp6 0 0 :::22 :::* LISTEN 950/sshd

可见并没有监听 10.222.0.1:8080 Socket

Client 节点IP为 192.168.193.226,为了和 LB 节点的虚拟 IP 10.222.0.1通,我们手动添加静态路由如下:

1
ip r add 10.222.0.1 via 192.168.193.197 dev ens5

此时 Client 节点能够 ping 通 LB 节点 VIP:

1
2
3
4
5
6
7
8
root@ip-192-168-193-226:~# ping -c 2 -w 2 10.222.0.1
PING 10.222.0.1 (10.222.0.1) 56(84) bytes of data.
64 bytes from 10.222.0.1: icmp_seq=1 ttl=64 time=0.345 ms
64 bytes from 10.222.0.1: icmp_seq=2 ttl=64 time=0.249 ms

--- 10.222.0.1 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1022ms
rtt min/avg/max/mdev = 0.249/0.297/0.345/0.048 ms

可见 Client 节点到 VIP 的链路没有问题,那是否能够访问我们的 Service 呢?

我们验证下:

1
2
root@ip-192-168-193-226:~# curl -m 2 --retry 1 -sSL 10.222.0.1:8080
curl: (28) Connection timed out after 2001 milliseconds

非常意外的结果是并不通。

在 RS 节点抓包如下:
tcpdump ipvs

我们发现数据包的源 IP 为 Client IP,目标 IP 为 RS IP,换句话说,LB 节点 IPVS 只做了 DNAT,把目标 IP 改成 RS IP了,而没有修改源 IP。此时虽然 RS 和 Client 在同一个子网,链路连通性没有问题,但是由于 Client 节点发出去的包的目标 IP 和收到的包源 IP 不一致,因此会被直接丢弃,相当于给张三发信,李四回的信,显然不受信任

既然 IPVS 没有给我们做 SNAT,那自然想到的是我们手动做 SNAT,在 LB 节点添加如下 iptables 规则:

1
2
iptables -t nat -A POSTROUTING -m ipvs  --vaddr 10.222.0.1 --vport 8080 -j LOG --log-prefix '[int32bit ipvs]'
iptables -t nat -A POSTROUTING -m ipvs --vaddr 10.222.0.1 --vport 8080 -j MASQUERADE

再次检查 Service 是否可以访问:

1
2
root@ip-192-168-193-226:~# curl -m 2 --retry 1 -sSL 10.222.0.1:8080
curl: (28) Connection timed out after 2001 milliseconds

服务依然不通。并且在 LB 节点的 iptables 日志为空:

1
2
root@ip-192-168-193-197:~# cat /var/log/syslog | grep 'int32bit ipvs'
root@ip-192-168-193-197:~#

也就是说,ipvs 的包根本不会经过 iptables nat 表 POSTROUTING 链?

那 mangle 表呢?我们打开 LOG 查看下:

1
iptables -t mangle -A POSTROUTING -m ipvs --vaddr 10.222.0.1 --vport 8080 -j LOG --log-prefix "[int32bit ipvs]"

此时查看日志如下:
iptables_ipvs

我们发现在 mangle 表中可以看到 DNAT 后的包。

只是 mangle 表的 POSTROUTING 并不支持 NAT 功能:
dmesg_log

对比 Kubernetes 配置发现需要设置如下系统参数:

1
sysctl net.ipv4.vs.conntrack=1

再次验证

1
2
3
4
5
6
7
8
root@ip-192-168-193-226:~# curl -i 10.222.0.1:8080
HTTP/1.1 200 OK
Content-Type: text/plain
Date: Wed, 27 Nov 2019 15:28:06 GMT
Connection: keep-alive
Transfer-Encoding: chunked

Hello Kubernetes bootcamp! | Running on: kubernetes-bootcamp-v1-c5ccf9784-g9bkx | v=1

终于通了,查看 RS 抓包:
tcpdump_5

如期望,修改了源 IP 为 LB IP。

原来需要配置 net.ipv4.vs.conntrack = 1 参数,这个问题折腾了一个晚上,不得不说目前 ipvs 的文档都太老了。

前面是通过手动 iptables 实现 SNAT 的,性能可能会有损耗,于是如下开源项目通过修改 lvs 直接做 SNAT:

除了 SNAT 的办法,是否还有其他办法呢?想想我们最初的问题,Client 节点发出去的包的目标 IP 和收到的包源 IP 不一致导致包被丢弃,那解决问题的办法就是把包重新引到 LB 节点上,只需要在所有的 RS 节点增加如下路由即可:

1
ip r add 192.168.193.226 via 192.168.193.197 dev ens5

此时我们再次检查我们的 Service 是否可连接:

1
2
3
4
5
6
7
8
root@ip-192-168-193-226:~# curl -i -m 2 --retry 1 -sSL 10.222.0.1:8080
HTTP/1.1 200 OK
Content-Type: text/plain
Date: Wed, 27 Nov 2019 03:21:47 GMT
Connection: keep-alive
Transfer-Encoding: chunked

Hello Kubernetes bootcamp! | Running on: kubernetes-bootcamp-v1-c5ccf9784-4v9z4 | v=1

结果没有问题。

不过我们是通过手动添加 Client IP 到所有 RS 的明细路由实现的,如果 Client 不固定,这种方案仍然不太可行,所以通常做法是干脆把所有 RS 默认路由指向 LB 节点,即把LB 节点当作所有 RS 的默认网关。

由此可知,用户通过 LB 地址访问服务,LB 节点 IPVS 会把用户的目标 IP 由 LB IP 改为 RS IP,源 IP 不变,包不经过 iptables 的 OUTPUT 直接到达 POSTROUTING 转发出去,包回来的时候也必须先到 LB 节点,LB 节点把目标 IP 再改成用户的源 IP,最后转发给用户。

显然这种模式来回都需要经过 LB 节点,因此又称为双臂模式。

网关(Gatewaying)模式

网关模式(Gatewaying)又称为直连路由模式(Direct Routing)、透传模式,所谓透传即 LB 节点不会修改数据包的源 IP、端口以及目标 IP、端口,LB 节点做的仅仅是路由转发出去,可以把 LB 节点看作一个特殊的路由器网关,而 RS 节点则是网关的下一跳,这就相当于对于同一个目标地址,会有多个下一跳,这个路由器网关的特殊之处在于能够根据一定的算法选择其中一个 RS 作为下一跳,达到负载均衡和冗余的效果。

既然是通过直连路由的方式转发,那显然 LB 节点必须与所有的 RS 节点在同一个子网,不能跨子网,否则路由不可达。换句话说,这种模式只支持内部负载均衡(Internal LoadBalancer)

另外如前面所述,LB 节点不会修改源端口和目标端口,因此这种模式也无法支持端口映射,换句话说 LB 节点监听的端口和所有 RS 节点监听的端口必须一致

现在假设有LB节点IP为 192.168.193.197,有三个 RS 节点如下:

  • 192.168.193.172:30620
  • 192.168.193.194:30620
  • 192.168.193.226:30620

创建负载均衡Service并把RS添加到Service中:

1
2
3
4
ipvsadm -A -t 192.168.193.197:30620 -s rr
ipvsadm -a -t 192.168.193.197:30620 -r 192.168.193.194:30620 -g
ipvsadm -a -t 192.168.193.197:30620 -r 192.168.193.226:30620 -g
ipvsadm -a -t 192.168.193.197:30620 -r 192.168.193.172:30620 -g

注意到我们的 Service 监听的端口 30620 和 RS 的端口是一样的,并且通过 -g 参数指定为直连路由模式(网关模式)。

Client 节点 IP 为 192.168.193.226,我们验证 Service 是否可连接:

1
2
root@ip-192-168-193-226:~# curl -m 5 -sSL 192.168.193.197:30620
curl: (28) Connection timed out after 5001 milliseconds

我们发现并不通,在其中一个 RS 节点 192.168.193.172 上抓包:
tcpdump_1

正如前面所说,LB 是通过路由转发的,根据路由的原理,源 MAC 地址修改为 LB 的MAC 地址,而目标 MAC 地址修改为 RS MAC 地址,相当于 RS 是 LB 的下一跳。

并且源 IP 和目标 IP 都不会修改。问题就来了,我们 Client 期望访问的是 RS,但 RS 收到的目标 IP 却是 LB 的 IP,发现这个目标 IP 并不是自己的 IP,因此不会通过 INPUT链转发到用户空间,这时要不直接丢弃这个包,要不根据路由再次转发到其他地方,总之两种情况都不是我们期望的结果。

那怎么办呢?为了让 RS 接收这个包,必须得让 R S有这个目标 IP 才行。于是不妨在 lo上添加个虚拟 IP,IP 地址伪装成 LB IP 192.168.193.197:

1
ifconfig lo:0 192.168.193.197/32

问题又来了,这就相当于有两个相同的 IP,IP 重复了怎么办?办法是隐藏这个虚拟网卡,不让它回复 ARP,其他主机的 neigh 也就不可能知道有这么个网卡的存在了,参考 Using arp announce/arp ignore to disable ARP

1
2
sysctl net.ipv4.conf.lo.arp_ignore=1
sysctl net.ipv4.conf.lo.arp_announce=2

此时再次从客户端curl:

1
2
root@ip-192-168-193-226:~# curl -m 2 --retry 1 -sSL 192.168.193.197:30620
Hello Kubernetes bootcamp! | Running on: kubernetes-bootcamp-v1-c5ccf9784-4v9z4 | v=1

终于通了。

我们从前面的抓包中知道,源 IP 为 Client IP 192.168.193.226,因此直接回包给 Client即可,不可能也不需要再回到 LB 节点了,即 A->B,B->C,C->A,流量方向是三角形状的,因此这种模式又称为三角模式。

我们从原理中不难得出如下结论:

  • Client、LB 以及所有的 RS 必须在同一个子网。
  • LB 节点直接通过路由转发,因此性能非常高。
  • 不能做端口映射。

ipip 隧道模式

前面介绍了网关直连路由模式,要求所有的节点在同一个子网,而 ipip 隧道模式则主要解决这种限制,LB 节点 IP 和 RS 可以不在同一个子网,此时需要通过 ipip 隧道进行传输。

现在假设有 LB 节点 IP为 192.168.193.77/25,在该节点上增加一个VIP地址:

1
ip addr add 192.168.193.48/25 dev eth0

有三个 RS 节点如下:

  • 192.168.193.172:30620
  • 192.168.193.194:30620
  • 192.168.193.226:30620

如上三个 RS 节点子网掩码均为 255.255.255.128,即 25 位子网,显然和 VIP 192.168.193.48/25 不在同一个子网。

创建负载均衡 Service 并把 RS 添加到 Service 中:

1
2
3
4
ipvsadm -A -t 192.168.193.48:30620 -s rr
ipvsadm -a -t 192.168.193.48:30620 -r 192.168.193.194:30620 -i
ipvsadm -a -t 192.168.193.48:30620 -r 192.168.193.226:30620 -i
ipvsadm -a -t 192.168.193.48:30620 -r 192.168.193.172:30620 -i

注意到我们的 Service 监听的端口 30620 和 RS 的端口是一样的,并且通过 -i 参数指定为 ipip 隧道模式。

在所有的 RS 节点上加载 ipip 模块以及添加 VIP (和直连路由类型):

1
2
3
4
modprobe ipip
ifconfig tunl0 192.168.193.48/32
sysctl net.ipv4.conf.tunl0.arp_ignore=1
sysctl net.ipv4.conf.tunl0.arp_announce=2

Client节点IP为192.168.193.226/25,我们验证 Service 是否可连接:

1
2
3
4
5
6
7
8
9
root@ip-192-168-193-226:~# curl -i -sSL 192.168.193.48:30620
HTTP/1.1 200 OK
Content-Type: text/plain
Date: Wed, 27 Nov 2019 07:05:40 GMT
Connection: keep-alive
Transfer-Encoding: chunked

Hello Kubernetes bootcamp! | Running on: kubernetes-bootcamp-v1-c5ccf9784-dgn74 | v=1
root@ip-192-168-193-226:~#

Service 可访问,我们在 RS 节点上抓包如下:
tcpdump_3

我们发现和直连路由一样,源 IP 和目标 IP 没有修改。

所以 IPIP 模式和网关 (Gatewaying) 模式原理基本一样,唯一不同的是网关 (Gatewaying) 模式要求所有的 RS 节点和 LB 节点在同一个子网,而 IPIP 模式则可以支持跨子网的情况,为了解决跨子网通信问题,使用了 ipip 隧道进行数据传输。

总结

ipvs 是一个内核态的四层负载均衡,支持 NAT、Gateway 以及 IPIP 隧道模式,Gateway 模式性能最好,但 LB 和 RS 不能跨子网,IPIP 性能次之,通过 ipip 隧道解决跨网段传输问题,因此能够支持跨子网。而 NAT 模式没有限制,这也是唯一一种支持端口映射的模式。

我们不难猜想,由于 Kubernetes Service 需要使用端口映射功能,因此 kube-proxy 必然只能使用 ipvs 的 NAT 模式。

kube-proxy 使用 ipvs 模式

配置 kube-proxy 使用 ipvs 模式

使用 kubeadm 安装 Kubernetes 可参考文档 Cluster Created by Kubeadm,不过这个文档的安装配置有问题 kubeadm #1182,如下官方配置不生效:

1
2
3
4
5
6
7
...
kubeProxy:
config:
featureGates:
SupportIPVSProxyMode: true
mode: ipvs
...

需要修改为如下配置:

1
2
3
4
---
apiVersion: kubeproxy.config.k8s.io/v1alpha1
kind: KubeProxyConfiguration
mode: ipvs

可以通过如下命令确认 kube-proxy 是否修改为 ipvs:

1
2
# kubectl get configmaps kube-proxy -n kube-system -o yaml | awk '/mode/{print $2}'
ipvs

Service ClusterIP 原理

创建一个 ClusterIP 类似的 Service 如下:

1
2
# kubectl get svc | grep kubernetes-bootcamp-v1
kubernetes-bootcamp-v1 ClusterIP 10.96.54.11 <none> 8080/TCP 2m11s

ClusterIP 10.96.54.11 为我们查看 ipvs 配置如下:

1
2
3
4
5
# ipvsadm -S -n | grep 10.96.54.11
-A -t 10.96.54.11:8080 -s rr
-a -t 10.96.54.11:8080 -r 10.244.1.2:8080 -m -w 1
-a -t 10.96.54.11:8080 -r 10.244.1.3:8080 -m -w 1
-a -t 10.96.54.11:8080 -r 10.244.2.2:8080 -m -w 1

可见 ipvs 的 LB IP 为 ClusterIP,算法为 rr,RS 为 Pod 的 IP。

另外我们发现使用的模式为 NAT 模式,这是显然的,因为除了 NAT 模式支持端口映射,其他两种均不支持端口映射,所以必须选择 NAT 模式。

由前面的理论知识,ipvs 的 VIP 必须在本地存在,我们可以验证:

1
2
3
4
5
6
7
8
9
10
11
# ip addr show kube-ipvs0
4: kube-ipvs0: <BROADCAST,NOARP> mtu 1500 qdisc noop state DOWN group default
link/ether 46:6b:9e:af:b0:60 brd ff:ff:ff:ff:ff:ff
inet 10.96.0.1/32 brd 10.96.0.1 scope global kube-ipvs0
valid_lft forever preferred_lft forever
inet 10.96.0.10/32 brd 10.96.0.10 scope global kube-ipvs0
valid_lft forever preferred_lft forever
inet 10.96.54.11/32 brd 10.96.54.11 scope global kube-ipvs0
valid_lft forever preferred_lft forever
# ethtool -i kube-ipvs0 | grep driver
driver: dummy

可见 kube-proxy 首先会创建一个 dummy 虚拟网卡 kube-ipvs0,然后把所有的 Service IP 添加到 kube-ipvs0 中。

我们知道基于 iptables 的 Service,ClusterIP 是一个虚拟的 IP,因此这个 IP 是 ping 不通的,但 ipvs 中这个 IP 是在每个节点上真实存在的,因此可以 ping 通:
ping_cluster_ip

当然由于这个 IP 就是配置在本地虚拟网卡上,所以对诊断问题没有一点用处的。

我们接下来研究下 ClusterIP 如何传递的。

当我们通过如下命令连接服务时:

1
curl 10.96.54.11:8080

此时由于 10.96.54.11 就在本地,所以会以这个 IP 作为出口地址,即源 IP 和目标 IP 都是 10.96.54.11,此时相当于:

1
10.96.54.11:xxxx -> 10.96.54.11:8080

其中 xxxx 为随机端口。

然后经过 ipvs,ipvs 会从 RS ip 列中选择其中一个 Pod ip 作为目标 IP,假设为10.244.2.2:

1
2
3
4
5
10.96.54.11:xxxx -> 10.96.54.11:8080
|
| IPVS
v
10.96.54.11:xxxx -> 10.244.2.2:8080

我们从 iptables LOG 可以验证:
iptables_log_1

我们查看 OUTPUT 安全组规则如下:

1
2
3
-A OUTPUT -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
-A KUBE-SERVICES ! -s 10.244.0.0/16 -m comment --comment "Kubernetes service cluster ip + port for masquerade purpose" -m set --match-set KUBE-CLUSTER-IP dst,dst -j KUBE-MARK-MASQ
-A KUBE-MARK-MASQ -j MARK --set-xmark 0x4000/0x4000

其中 ipset 集合 KUBE-CLUSTER-IP 保存着所有的 ClusterIP 以及监听端口。

如上规则的意思就是除了 Pod 以外访问ClusterIP的包都打上 0x4000/0x4000

到了 POSTROUTING 链:

1
2
-A POSTROUTING -m comment --comment "kubernetes postrouting rules" -j KUBE-POSTROUTING
-A KUBE-POSTROUTING -m comment --comment "kubernetes service traffic requiring SNAT" -m mark --mark 0x4000/0x4000 -j MASQUERADE

如上规则的意思就是只要匹配 mark 0x4000/0x4000 的包都做 SNAT,由于 10.244.2.2 是从 flannel.1 出去的,因此源 ip 会改成 flannel.1 的 ip 10.244.0.0

1
2
3
4
5
6
7
8
9
10.96.54.11:xxxx -> 10.96.54.11:8080
|
| IPVS
v
10.96.54.11:xxxx -> 10.244.2.2:8080
|
| MASQUERADE
v
10.244.0.0:xxxx -> 10.244.2.2:8080

最后通过 Vxlan 隧道发到 Pod 的 Node上,转发给 Pod 的 veth,回包通过路由到达源Node 节点,源 Node 节点通过之前的 MASQUERADE 再把目标IP还原为 10.96.54.11。

NodeIP 实现原理

查看 Service 如下:

1
2
3
4
root@ip-192-168-193-172:~# kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 30h
kubernetes-bootcamp-v1 NodePort 10.96.54.11 <none> 8080:32016/TCP 8h

Service kubernetes-bootcamp-v1 的 NodePort 为 32016。

现在假设集群外的一个 IP 192.168.193.197 访问 192.168.193.172:32016:

1
192.168.193.197:xxxx -> 192.168.193.172:32016

最先到达 PREROUTING 链:

1
2
3
4
-A PREROUTING -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
-A KUBE-SERVICES -m addrtype --dst-type LOCAL -j KUBE-NODE-PORT
-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
-A KUBE-MARK-MASQ -j MARK --set-xmark 0x4000/0x4000

如上 4 条规则看起来复杂,其实就做一件事,如果目标地址为 NodeIP,则把包标记 0x4000 / 0x4000

我们查看 ipvs:

1
2
3
4
5
# ipvsadm -S -n | grep 32016
-A -t 192.168.193.172:32016 -s rr
-a -t 192.168.193.172:32016 -r 10.244.1.2:8080 -m -w 1
-a -t 192.168.193.172:32016 -r 10.244.1.3:8080 -m -w 1
-a -t 192.168.193.172:32016 -r 10.244.3.2:8080 -m -w 1

我们发现和 ClusterIP 实现原理非常相似,ipvs Service 的 VIP 为 Node IP,端口为NodePort。ipvs 会选择其中一个 Pod IP 作为 DNAT 目标,这里假设为 10.244.3.2:

1
2
3
4
5
192.168.193.197:xxxx -> 192.168.193.172:32016
|
| DNAT
v
192.168.193.197:xxx --> 10.244.3.2:8080

剩下的到了 POSTROUTING 链就和 Service ClusterIP 完全一样了,只要匹配 0x4000/0x4000 的包就会做SNAT。

总结

Kubernetes 的 ClusterIP 和 NodePort 都是通过 ipvs service 实现的,Pod 当作 ipvs service 的 server,通过 NAT MQSQ 实现转发。

简单来说 kube-proxy 主要在所有的 Node 节点做如下三件事:

  • 如果没有 dummy 类型虚拟网卡,则创建一个,默认名称为 kube-ipvs0;
  • 把 Kubernetes ClusterIP 地址添加到 kube-ipvs0,同时添加到 ipset 中。
  • 创建 ipvs service,ipvs service 地址为 ClusterIP 以及 Cluster Port,ipvs server 为所有的 Endpoint 地址,即 Pod IP 及端口。

使用 ipvs 作为 kube-proxy 后端,不仅提高了转发性能,结合 ipset 还使 iptables 规则变得更“干净”清楚,从此再也不怕 iptables。

更多关于 kube-proxy ipvs 参考 IPVS-Based In-Cluster Load Balancing Deep Dive

总结

本文首先介绍了 kube-proxy 的功能以及 kube-proxy 基于 iptables 的实现原理,然后简单介绍了 ipvs,了解了 ipvs 支持的三种转发模式,最后介绍了 kube-proxy 基于 ipvs 的实现原理。

ipvs 是专门设计用来做内核态四层负载均衡的,由于使用了 hash 表的数据结构,因此相比 iptables 来说性能会更好。基于 ipvs 实现 Service 转发,Kubernetes 几乎能够具备无限的水平扩展能力。随着 Kubernetes 的部署规模越来越大,应用越来越广泛,ipvs必然会取代 iptables 成为 Kubernetes Service 的默认实现后端。

本文作者:int32bit

微信订阅号

-------------本文结束感谢您的阅读-------------