这种负载均衡在Kubernetes 中称为Service,VIP 就是Service ClusterIP。因此,可以认为Kubernetes Service是一个四层负载均衡。 Kubernetes也对应七层负载均衡Ingress。本文仅介绍Kubernetes Service。
该服务由kube-proxy 实现。 ClusterIP不会因为Podz状态的变化而改变。需要注意的是,VIP,ClusterIP,是一个假IP。整个集群中不存在该IP。当然不能通过IP协议栈。无法路由,底层Underlay设备无法感知到该IP的存在。因此,ClusterIP只能在单主机(Host Only)范围内可见。该IP无法被其他节点或集群外部访问。
为了让Kubernetes 能够让集群中的所有节点都能访问Service,kube-proxy 会默认在所有Node 节点上创建这个VIP 并实现负载。因此,部署Kubernetes后,发现kube-proxy是一个DaemonSet。
之所以可以在Node节点上实现Service负载,是因为无论Kubernetes使用哪种网络模型,都需要保证满足以下三个条件:
要求容器直接通信,无需任何NAT;要求容器和Node直接通信,无需任何NAT;容器自身看到的IP必须与外界看到的IP一致,即没有IP转换。问题。至少必须满足第2 点。有了上述假设,Kubernetes Service就可以在Node上实现了。否则,Pod IP无法被Node访问,无法实施。
有人说既然kube-proxy是四层负载均衡,那么kube-proxy应该可以使用haproxy、nginx等作为负载后端吧?
其实没有问题,只是唯一需要考虑的就是性能问题。以上负载均衡功能虽强大,但毕竟还是基于用户态转发或反向代理,性能难免不如内核态直接转发。
因此,kube-proxy默认会优先考虑基于内核的加载作为后端实现机制。目前kube-proxy默认通过iptables实现负载。在此之前,还有一种模式叫userspace模式,它实际上是基于iptables实现的。可以认为现在的iptables模式是之前userspace模式的优化。
本节接下来详细介绍kube-proxy iptables模式的实现原理。
1.2 kube-proxy iptables模式实现原理
1.2.1 ClusterIP
首先创建一个ClusterIP类型Service:
# kubectl 获取svc -l 所有者=int32bit
名称类型集群IP 外部IP 端口年龄
kubernetes-bootcamp-v1 ClusterIP 10.106.224.418080/TCP 163m,其中ClusterIP 为10.106.224.41。我们可以验证这个IP本地不存在:
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) 字节数据。
--- 10.106.224.41 ping 统计数据---
发送2个包,接收0个,100%丢包,时间1025ms
root@ip-192-168-193-172:~# ip a | grep 10.106.224.41
root@ip-192-168-193-172:~#so不要尝试去ping ClusterIP,它不可能通的。
此时,访问节点192.168.193.172上的Service时,第一个流量到达OUTPUT链。这里我们只关心nat表的OUTPUT链:
# iptables-save -t nat | iptables-save -t nat | grep -- "-A 输出"
-A OUTPUT -m comment --comment "kubernetes service ports" -j KUBE-SERVICES 链跳转到KUBE-SERVICES 子链:
# iptables-save -t nat | iptables-save -t nat | grep -- "-KUBE-SERVICES"
.
-KUBE 服务! -s 10.244.0.0/16 -d 10.106.224.41/32 -p tcp -m comment --comment "default/kubernetes-bootcamp-v1: 集群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 我们发现有两条相关规则:
第一个负责标记MARK0x4000/0x4000,后面会用到。第二条规则跳转到KUBE-SVC-RPP7DHNHMGOIIFDC 子链。其中KUBE-SVC-RPP7DHNHMGOIIFDC子链规则如下:
# iptables-save -t nat | iptables-save -t nat | grep -- "-A KUBE-SVC-RPP7DHNHMGOIIFDC"
-A KUBE-SVC-RPP7DHNHMGOIIFDC -m 统计--模式随机--概率0.33332999982 -j KUBE-SEP-FTIQ6MSD3LWO5HZX
-A KUBE-SVC-RPP7DHNHMGOIIFDC -m 统计--模式随机--概率0.50000000000 -j KUBE-SEP-SQBK6CVV7ZCKBTVI
-A KUBE-SVC-RPP7DHNHMGOIIFDC -j KUBE-SEP-IAZPHGLZVO2SWOVD 这些规则看起来很复杂,但实际上实现的功能很简单:
跳转到子链KUBE-SEP-FTIQ6MSD3LWO5HZX的概率为1/3,剩余概率为1/2,(1 - 1/3) * 1/2==1/3,即概率跳转到子链的次数是1/3。 KUBE-SEP-SQBK6CVV7ZCKBTVI,留下1/3的概率跳转到KUBE-SEP-IAZPHGLZVO2SWOVD。我们检查子链之一KUBE-SEP-FTIQ6MSD3LWO5HZX规则:
# iptables-save -t nat | 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目标是Endpoints之一,也就是Pod服务。
可以看出,子链KUBE-SVC-RPP7DHNHMGOIIFDC的作用就是根据等概率原理,DNAT到其中一个Endpoint IP,即Pod IP,假设是10.244.1.2。
这相当于:
192.168.193.172:xxxx-10.106.224.41:8080
|
|DNAT
V
192.168.193.172:xxxX -10.244.1.2:8080 然后来到POSTROUTING 链:
# iptables-save -t nat | iptables-save -t nat | grep -- "-A 后路由"
-A POSTROUTING -m comment --comment "kubernetes postrouting 规则" -j KUBE-POSTROUTING
# iptables-save -t nat | iptables-save -t nat | grep -- "-A KUBE-POSTROUTING"
-A KUBE-POSTROUTING -m comment --comment "需要SNAT 的kubernetes 服务流量" -m mark --mark0x4000/0x4000 -j MASQUERADE 这两条规则只做一件事,即只要数据包标记为0x4000/0x4000在做MASQUERADE(SNAT)时,由于10.244.1.2默认是从flannel.1转发的,所以源IP会被更改为flannel.1的IP 10.244.0.0。
192.168.193.172:xxxx-10.106.224.41:8080
|
|DNAT
V
192.168.193.172:xxxx-10.244.1.2:8080
|
|网络地址转换
V
10.244.0.0:xxxx -10.244.1.2:8080 剩下的就是常规的Vxlan隧道转发过程。这里我就不详细说了。如果有兴趣,可以参考我之前的文章,简单讲一下几种主流Docker网络的实现原理。
1.2.2 NodePort
接下来研究NodePort流程。首先创建如下Service:
# kubectl 获取svc -l 所有者=int32bit
名称类型集群IP 外部IP 端口年龄
kubernetes-bootcamp-v1 NodePort 10.106.224.418080:30419/TCP 3h30m Service 的NodePort 端口为30419。
假设有一个外部IP 192.168.193.197,通过192.168.193.172:30419访问服务。
首先到达PREROUTING 链:
# iptables-save -t nat | iptables-save -t nat | grep -- "-预路由"
-A PREROUTING -m comment --comment "kubernetes 服务门户" -j KUBE-SERVICES
# iptables-save -t nat | iptables-save -t nat | grep -- "-KUBE-SERVICES"
.
-A KUBE-SERVICES -m addrtype --dst-type LOCAL -j KUBE-NODEPORTSPREROUTING 的规则非常简单。所有发送给自己的包裹都会交给子链KUBE-NODEPORTS进行处理。请注意,之前已省略了一些用于确定ClusterIP 的规则。
KUBE-NODEPORTS规则如下:
# iptables-save -t nat | 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。
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链,
# iptables-save -t 过滤器| grep -- "-A FORWARD"
-A FORWARD -m comment --comment "kubernetes 转发规则" -j KUBE-FORWARD
# iptables-save -t 过滤器| grep -- "-A KUBE-FORWARD"
-A KUBE-FORWARD -m conntrack --ctstate 无效-j DROP
-A KUBE-FORWARD -m comment --comment "kubernetes 转发规则" -m mark --mark0x4000/0x4000 -j ACCEPTFORWARD 表仅用于此处判断。仅允许转发标记为0x4000/0x4000 的数据包。
最后,我们来到POSTROUTING 链,它与ClusterIP 完全相同。在KUBE-POSTROUTING中做一次MASQUERADE(SNAT),最终结果为:
192.168.193.197:xxxx -192.168.193.172:30419
|
|DNAT
V
192.168.193.197:xxxx-10.244.1.2:8080
|
|网络地址转换
V
10.244.0.0:xxxx -10.244.1.2:8080
1.3 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和DNAT的统计模块可以实现最简单的只支持概率轮询的负载均衡,但是我们往往需要更灵活的算法,比如最少连接算法、源地址HASH算法等。IPVS,这也是基于在Netfilter上,专门从事负载平衡。它配置简单,基于哈希搜索的O(1)复杂度性能良好,支持数十种调度算法。因此,很明显ipvs比iptables更适合kube-proxy的后端。毕竟,专业的人做专业的事,才能做到最好。本文接下来将介绍kube-proxy的ipvs实现。由于之前对ipvs很不熟悉,也没有使用过,所以专门研究了ipvs。因此,我在第2章中简单介绍了ipvs。如果你已经熟悉ipvs,可以直接跳过,这一章几乎与Kubernetes无关。
另外,由于我也是ipvs的初学者,水平有限,难免会犯错误。欢迎指正!
2 IPVS 简易入门
2.1 IPVS简介
我们接触的比较多的是应用层负载均衡,比如haproxy、nginx、F5等,这些负载均衡工作在用户态,所以会有对应的进程和监听socket,一般可以支持4层同时的负载。而且7层负载,使用起来也更加方便。
LVS是由中国的张文松博士(张文松博士和他背后的负载均衡帝国)开发并贡献给社区的。主要由ipvs和ipvsadm组成。 ipvs是一个工作在内核态的4层负载均衡。和iptables一样,它是基于内核底层的。 Netfilter实现,netfilter主要通过各个链中的hook来实现报文的处理和转发。 ipvsadm 和ipvs 之间的关系就像netfilter 和iptables 之间的关系。它在用户模式下运行,并为ipvs 配置提供简单的CLI 界面。
由于ipvs工作在内核态,直接基于内核处理数据包转发,所以它最大的特点就是性能非常好。并且由于它工作在第4层,因此不处理应用层数据。人们经常会问ipvs 是否可以卸载SSL 证书或修改HTTP 标头数据。显然这些都是不可能的。
我们知道,大多数应用层负载均衡都是基于反向代理来实现负载的。它工作在应用层。当用户的数据包到达负载均衡监听器监听时,根据一定的算法从后端服务列表中选择其中一个后端。服务已转发。当然,中间可能还会有一些额外的操作,最常见的就是SSL证书卸载。
IPVS工作在内核态,只处理四层协议,因此只能基于路由或NAT转发数据。 IPVS可以看作是一个特殊的路由器网关。该网关可以根据一定的算法自动选择下一跳,或者将IPvs视为多次DNAT,根据一定的算法将IP数据包的目标地址DNAT到真实服务的目标IP。以上两种情况对应了ipvs的两种模式——网关模式和NAT模式。另外,ipip模式是网关模式的扩展。本文将详细介绍这些模式的实现原理。
2.2 IPVS用法
ipvsadm 命令行用法与iptables 命令行用法非常相似。毕竟他们是兄弟,比如-L表示枚举,-A表示添加,-D表示删除。
ipvsadm -A -t 192.168.193.172:32016 -s rr 但其实ipvsadm相对于iptables命令来说太简单了,因为没有像iptables那样的各种表。表嵌套在各种链中,链中存在一堆规则。 ipvsadm 只有两个核心实体:service 和server。 Service是负载均衡实例,server是后端成员。在IPVS术语中,它被称为真实服务器,简称RS。
以下命令创建服务实例172.17.0.1:32016,-t指定监听的TCP端口,-s指定算法为轮询算法rr(Round Robin),ipvs支持简单轮询(rr)、加权轮询(wrr),最小有是连接(lc)、源地址或目标地址散列(sh、dh)等10种调度算法。
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 添加到服务后端成员。
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指定服务实例,-r指定服务器地址,-w指定权重,-m是上面提到的转发模式,其中- m表示It is masquerading,即NAT模式,-g表示gatewaying,即直接路由模式,-i表示ipip,ji表示IPIP隧道模式。
iptables-save和iptables-restore对应的ipvs工具还包括ipvsadm-save和ipvsadm-restore。
2.3 NAT(network access translation)模式
NAT模式字面意思就是通过NAT来实现,但是NAT是如何转发的,我们通过实验环境验证了。
当前环境中LB节点IP为192.168.193.197,三个RS节点分别为:
192.168.193.172:30620192.168.193.194:30620192.168.193.226:30620 为了模拟LB节点IP和RS不在同一网络的情况,给LB节点添加虚拟IP地址:
ip addr add 10.222.0.1/24 dev ens5 创建负载均衡Service,并将RS添加到Service :中
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的负载是直接运行在内核态的,因此不会出现监听端口:
root@ip-192-168-193-197:/var/log# netstat -lnpt
有效的互联网连接(仅限服务器)
Proto Recv-Q Send-Q 本地地址外部地址状态PID/程序名称
tcp 0 0 127.0.0.53:53 0.0.0.0:* 监听674/systemd-resolve
tcp 0 0 0.0.0.0:22 0.0.0.0:* 监听950/sshd
tcp6 0 0 :22 :* 监听950/sshd可见并没有监听10.222.0.1:8080 Socket。
客户端节点IP为192.168.193.226。为了与LB节点的虚拟IP 10.222.0.1通信,
我们手动添加静态路由如下: ip r add 10.222.0.1 via 192.168.193.197 dev ens5此时Client节点能够ping通LB节点VIP: 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呢? 我们验证下: 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节点抓包如下: image我们发现数据包的源IP为Client IP,目标IP为RS IP,换句话说,LB节点IPVS只做了DNAT,把目标IP改成RS IP了,而没有修改源IP。此时虽然RS和Client在同一个子网,链路连通性没有问题,但是由于Client节点发出去的包的目标IP和收到的包源IP不一致,因此会被直接丢弃,相当于给张三发信,李四回的信,显然不受信任。 既然IPVS没有给我们做SNAT,那自然想到的是我们手动做SNAT,在LB节点添加如下iptables规则: 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是否可以访问: 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日志为空: 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查看下: iptables -t mangle -A POSTROUTING -m ipvs --vaddr 10.222.0.1 --vport 8080 -j LOG --log-prefix "[int32bit ipvs]"此时查看日志如下: image我们发现在mangle表中可以看到DNAT后的包。 只是可惜mangle表的POSTROUTING并不支持NAT功能: image对比Kubernetes配置发现需要设置如下系统参数: sysctl net.ipv4.vs.conntrack=1再次验证: 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抓包: image如期望,修改了源IP为LB IP。 原来需要配置net.ipv4.vs.conntrack=1参数,这个问题折腾了一个晚上,不得不说目前ipvs的文档都太老了。 前面是通过手动iptables实现SNAT的,性能可能会有损耗,于是如下开源项目通过修改lvs直接做SNAT: 小米运维部在LVS的FULLNAT基础上,增加了SNAT网关功能,参考xiaomi-sa/dsnatlvs-snat除了SNAT的办法,是否还有其他办法呢?想想我们最初的问题,Client节点发出去的包的目标IP和收到的包源IP不一致导致包被丢弃,那解决问题的办法就是把包重新引到LB节点上,只需要在所有的RS节点增加如下路由即可: ip r add 192.168.193.226 via 192.168.193.197 dev ens5此时我们再次检查我们的Service是否可连接: 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节点,因此又称为双臂模式。2.4 网关(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:30620192.168.193.194:30620192.168.193.226:30620创建负载均衡Service并把RS添加到Service中: 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是否可连接: 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上抓包: image正如前面所说,LB是通过路由转发的,根据路由的原理,源MAC地址修改为LB的MAC地址,而目标MAC地址修改为RS MAC地址,相当于RS是LB的下一跳。 并且源IP和目标IP都不会修改。问题就来了,我们Client期望访问的是RS,但RS收到的目标IP却是LB的IP,发现这个目标IP并不是自己的IP,因此不会通过INPUT链转发到用户空间,这时要不直接丢弃这个包,要不根据路由再次转发到其他地方,总之两种情况都不是我们期望的结果。 那怎么办呢?为了让RS接收这个包,必须得让RS有这个目标IP才行。于是不妨在lo上添加个虚拟IP,IP地址伪装成LB IP 192.168.193.197: ifconfig lo:0 192.168.193.197/32问题又来了,这就相当于有两个相同的IP,IP重复了怎么办?办法是隐藏这个虚拟网卡,不让它回复ARP,其他主机的neigh也就不可能知道有这么个网卡的存在了,参考Using arp announce/arp ignore to disable ARP。 sysctl net.ipv4.conf.lo.arp_ignore=1 sysctl net.ipv4.conf.lo.arp_announce=2此时再次从客户端curl: 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节点直接通过路由转发,因此性能非常高。不能做端口映射。2.5 ipip隧道模式
前面介绍了网关直连路由模式,要求所有的节点在同一个子网,而ipip隧道模式则主要解决这种限制,LB节点IP和RS可以不在同一个子网,此时需要通过ipip隧道进行传输。 现在假设有LB节点IP为192.168.193.77/25,在该节点上增加一个VIP地址: ip addr add 192.168.193.48/25 dev eth0 有三个RS节点如下: 192.168.193.172:30620192.168.193.194:30620192.168.193.226:30620如上三个RS节点子网掩码均为255.255.255.128,即25位子网,显然和VIP 192.168.193.48/25不在同一个子网。 创建负载均衡Service并把RS添加到Service中: 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(和直连路由类型): modprobe ipip ifconfig tunl0 192.168.193.48/32 sysctl net.ipv4.conf.tunl0.arp_ignore=1 sysctl net.ipv4.conf.tunl0.arp_announce=2Client节点IP为192.168.193.226/25,我们验证Service是否可连接: 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节点上抓包如下: image我们发现和直连路由一样,源IP和目标IP没有修改。 所以IPIP模式和网关(Gatewaying)模式原理基本一样,唯一不同的是网关(Gatewaying)模式要求所有的RS节点和LB节点在同一个子网,而IPIP模式则可以支持跨子网的情况,为了解决跨子网通信问题,使用了ipip隧道进行数据传输。2.4 总结
ipvs是一个内核态的四层负载均衡,支持NAT、Gateway以及IPIP隧道模式,Gateway模式性能最好,但LB和RS不能跨子网,IPIP性能次之,通过ipip隧道解决跨网段传输问题,因此能够支持跨子网。而NAT模式没有限制,这也是唯一一种支持端口映射的模式。 我们不难猜想,由于Kubernetes Service需要使用端口映射功能,因此kube-proxy必然只能使用ipvs的NAT模式。3 kube-proxy使用ipvs模式
3.1 配置kube-proxy使用ipvs模式
使用kubeadm安装Kubernetes可参考文档Cluster Created by Kubeadm,不过这个文档的安装配置有问题kubeadm #1182,如下官方配置不生效: kubeProxy: config: featureGates: SupportIPVSProxyMode: true mode: ipvs需要修改为如下配置: --- apiVersion: kubeproxy.config.k8s.io/v1alpha1 kind: KubeProxyConfiguration mode: ipvs可以通过如下命令确认kube-proxy是否修改为ipvs: # kubectl get configmaps kube-proxy -n kube-system -o yaml | awk "/mode/{print $2}" ipvs3.2 Service ClusterIP原理
创建一个ClusterIP类似的Service如下: # kubectl get svc | grep kubernetes-bootcamp-v1 kubernetes-bootcamp-v1 ClusterIP 10.96.54.118080/TCP 2m11sClusterIP 10.96.54.11为我们查看ipvs配置如下: # 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必须在本地存在,我们可以验证: # ip addr show kube-ipvs0 4: kube-ipvs0: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通: image当然由于这个IP就是配置在本地虚拟网卡上,所以对诊断问题没有一点用处的。 我们接下来研究下ClusterIP如何传递的。 当我们通过如下命令连接服务时: curl 10.96.54.11:8080此时由于10.96.54.11就在本地,所以会以这个IP作为出口地址,即源IP和目标IP都是10.96.54.11,此时相当于: 10.96.54.11:xxxx ->10.96.54.11:8080其中xxxx为随机端口。 然后经过ipvs,ipvs会从RS ip列中选择其中一个Pod ip作为目标IP,假设为10.244.2.2: 10.96.54.11:xxxx ->10.96.54.11:8080 | | IPVS v 10.96.54.11:xxxx ->10.244.2.2:8080我们从iptables LOG可以验证: image我们查看OUTPUT安全组规则如下: -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其中ipsetj集合KUBE-CLUSTER-IP保存着所有的ClusterIP以及监听端口。 如上规则的意思就是除了Pod以外访问ClusterIP的包都打上0x4000/0x4000。 到了POSTROUTING链: -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如上规则的意思就是只要匹配mark0x4000/0x4000的包都做SNAT,由于10.244.2.2是从flannel.1出去的,因此源ip会改成flannel.1的ip10.244.0.0: 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。3.3 NodeIP实现原理
查看Service如下: root@ip-192-168-193-172:~# kubectl get svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes ClusterIP 10.96.0.1443/TCP 30h kubernetes-bootcamp-v1 NodePort 10.96.54.118080:32016/TCP 8hService kubernetes-bootcamp-v1的NodePort为32016。 现在假设集群外的一个IP 192.168.193.197访问192.168.193.172:32016: 192.168.193.197:xxxx ->192.168.193.172:32016最先到达PREROUTING链: -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: # 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: 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。3.4 总结
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.4 总结
本文首先介绍了kube-proxy的功能以及kube-proxy基于iptables的实现原理,然后简单介绍了ipvs,了解了ipvs支持的三种转发模式,最后介绍了kube-proxy基于ipvs的实现原理。如果你还想了解更多这方面的信息,记得收藏关注本站。
【深入解析:IPVS在Kubernetes中的实践与应用技巧】相关文章:
2.米颠拜石
3.王羲之临池学书
8.郑板桥轶事十则
用户评论
正好想学习一下 IPVS,这个教程看起来不错!
有5位网友表示赞同!
看标题感觉可以把我从零基础带到理解 IPVS 的精髓啊。
有5位网友表示赞同!
之前一直对 Kube 的负载均衡机制一头雾水,这篇文章刚好能解决了我的疑问吧?
有19位网友表示赞同!
IPVS 和 Nginx Ingress 哪个比较好呢?看文章内容应该可以对比一下。
有5位网友表示赞同!
希望能涵盖 IPVS 的配置技巧和常见问题解决方法。
有5位网友表示赞同!
学习IPVS,主要是为了提升 Kubernetes 平台的性能吧?
有15位网友表示赞同!
最近在公司项目中用到 Kube 的负载均衡,这篇文章正好对标我的需求。
有6位网友表示赞同!
看个简介啥时候就能学会啊?
有14位网友表示赞同!
希望能有实践操作结合的内容,才能真正理解 IPVS 的工作原理。
有12位网友表示赞同!
以前听过IPVS这东西,也没仔细了解过,这个教程应该能弥补一下知识漏洞吧。
有5位网友表示赞同!
Kubernetes 相关的学习资料越来越多了,这款教程看起来可信度高!
有11位网友表示赞同!
看名字感觉很专业,希望内容也能深入浅出。
有9位网友表示赞同!
IPVS 的配置文件配置起来复杂吗?这篇文章能给我一些指导吧?
有9位网友表示赞同!
文章里会不会谈到 IPVS 和其它均衡方案的比较呀?
有5位网友表示赞同!
学习完这学东西后,我能自己搭建一个 IPVS 负载均衡系统吗?
有12位网友表示赞同!
希望文章能够涵盖 IPVS 的常见问题和解决方案。
有18位网友表示赞同!
在 Kubernetes 中使用 IPVS 的场景有哪些?文章里会不会介绍?
有12位网友表示赞同!
IPVS 和其他 Load Balancer 方案有什么区别呢?期待文章的对比分析。
有6位网友表示赞同!
学习新的技术总是一个挑战,但也很有收获。希望这个教程能帮助我真正掌握 IPVS。
有16位网友表示赞同!