之前的文章<<Kubernetes Service网络通信路径>>介绍了kubernetes的几种Service。如果要暴露服务给kubernetes集群外使用,可以选择NodePort和LoadBalancer。但LoadBalancer现在主要在各大公有云厂商上能够原生支持。而使用NodePort暴露服务,将使用一个非常大的端口,无法使用原始的端口号来暴露服务,比如mysql的3306端口。
Service的官方文档中介绍了一种辅助方式, 叫External-IP, 可以在worker节点上会通过该IP来暴露服务,而且可以使用在任意类型的service上。集群外的用户就可以通过该IP来访问服务。但如果这个IP只存在于一个worker节点上,那么就不具备高可用的能力了,我们需要在多个worker节点上配置这个VIP:Virtual IP。我们可以使用LVS(也叫做IPVS)的DR(Director Routing)模式作为外部负载均衡器将流量分发到多个worker节点上,同时保持数据包的目的地址为该VIP。
DR模式只会修改数据包的目的MAC地址为后端RealServer的MAC地址,因而要求负载均衡器Director和RealServer在同一个二层网络,而且响应包不会经过Director。
下面我们来实验如何使用LVS的DR模式实现service负载均衡。
我们在之前的实验集群中创建一个类型为ClusterIP(默认类型)的service, 指定一个外部IP:
1 | apiVersion: v1 |
创建服务:
1 | kubectl apply -f whoami.yaml |
查看服务,可以看到whoami的EXTERNAL-IP为10.240.0.201:
1 | [root@master1 ~]# kubectl get svc -o wide |
在worker节点上检查iptables规则,可以看到在KUBE-SERVICES链中添加了EXTERNAL-IP相关的规则:
1 | -A KUBE-SERVICES ! -s 10.230.0.0/16 -d 10.32.0.60/32 -p tcp -m comment --comment "default/whoami:web cluster IP" -m tcp --dport 80 -j KUBE-MARK-MASQ |
当数据包目的地址为10.240.0.201:80时,跳转到KUBE-SVC-*链,从而分发到相应的pod中。
我们在节点上添加上这个VIP:
1 | [root@node1 ~]# ip addr add 10.240.0.201/32 dev lo |
因为这个VIP需要在多个worker节点上存在,因而把它配置在lo上,并抑制相应网卡上对该VIP的ARP响应:
1 | sysctl -w net.ipv4.conf.eth1.arp_ignore = 1 |
在节点上尝试访问VIP, 可以成功访问:
1 | [root@node1 ~]# curl http://10.240.0.201 |
接着我们在worker节点所在二层网络再启动一台虚拟机作为LVS的Director。在该机器上给与worker节点二层互通的网卡添加VIP:
1 | ip addr add 10.240.0.201/32 dev eth1 |
使用ipvsadm创建负载均衡服务, 并使用DR模式添加两个worker节点做为后端的RealServer:
1 | ipvsadm -A -t 10.240.0.201:80 -s rr |
查看负载均衡服务:
1 | [root@lb1 ~]# ipvsadm -L -n |
环境配置完成。我们找一台客户端访问VIP:10.240.0.201, 同时在Director机器上抓包,可以看到:
1 | [root@lb1 ~]# tcpdump -ieth1 -nn -e tcp port 80 |
数据包的目的MAC地址被修改为node2上eth1的MAC地址, 而且响应包并不经过Director:
1 | [root@node2 ~]# ip link show dev eth1 |
根据之前网上的这篇文章, worker节点可以不设置VIP,因为VIP并不需要由用户态程序来接收流量,而是直接由iptables来进行数据包转换。
在大多数场景下这是正确的。但是如果需要直接从worker节点上通过VIP访问该服务时就需要在worker节点上配置VIP了。数据包在从worker节点发送出去时,会经由nat:OUTPUT和nat:POSTROUTING来处理。iptables的NAT实现是基于conntrack来实现的,发出时系统会建立conntrack条目。iptables的nat:PREROUTING和nat:POSTROUTING的处理都会调用nf_nat_ipv4_fn函数。当数据包由LVS Director把数据包返回到自身这台RealServer时, nat:PREROUTING阶段调用nf_nat_ipv4_fn:
1 | switch (ctinfo) { |
这时nf_nat_initialized会返回0, 因而跳过do_chain的调用,也就不再会执行nat:PREROUTING所设置的链和规则,放行通过进入到路由决策阶段。但由于数据包的源IP是本机地址,默认情况下Linux路由实现不允许非loopback设备之外的设备所进入的数据包源地址为本机地址, 因而该数据包会被丢弃。
但内核提供了参数accept_local可以允许这种包通过:
1 | accept_local - BOOLEAN |
修改相应参数放行数据包:
1 | sysctl -w net.ipv4.conf.eth1.rp_filter=1 |
再次从worker节点访问VIP, 同时开启tcpdump抓包分析:
1 | [root@node1 ~]# curl http://10.240.0.201 |
可以看到返回路由错误,而从抓包结果看,我们放行数据包后,根据目的地址,数据包会再被发送出去,从而形成环路。直到IP包的ttl减为0返回了路由错误。
1 | [root@node1 ~]# tcpdump -ieth1 tcp port 80 -nn -e -v |
本文只是简单实验可行性。如果用于生产环境,需要额外的方案考虑,比如:
LVS本身可以配合keepalived使用主备模式保证Director的HA- 使用
OSPF的ECMP来配置多主的Director集群(可以参考之前的文章<<基于Cumulus VX实验ECMP+OSPF负载均衡>>) - 省略
LVS的Director层,直接使用OSPF的ECMP将流量分发到worker节点的VIP
参考: