k8s 中的 svc,除了 clusterIP 和 nodePort 之外,还有一个 LoadBalancer,一般来说,公有云厂商都会有自己的 LB 服务,那么如果我们想在 bare metal 上实现自己的 LoadBalancer ,该如何实现呢?
一、概述
二层负载均衡的理论基础,是 arp 协议(Address Resolution Protocol)和 ndp 协议(Neighbor Discovery Protocol)。
假设 k8s 存在以下 node,信息如下
name | IP | mac | iface |
---|---|---|---|
node1 | 192.168.37.101 | 00:0c:29:a1:98:bc | eth0 |
node2 | 192.168.37.102 | 00:0c:29:11:9a:b3 | eth0 |
node3 | 192.168.37.103 | 00:0c:29:64:cc:26 | eth0 |
node4 | 192.168.37.104 | 00:0c:29:64:2c:47 | eth0 |
假设我们要自己实现二层的 lb,名称为 lb2,可以这样实现:
- 在广播域内选择一个未被使用的 IP 作为 VIP,比如
10.31.8.200
- 在 node1、node2、node3 上部署应用 lb2,通过 k8s leader 选举 node1 为 leader,node1 的 mac 地址
00:0c:29:a1:98:bc
- lb2 要在 node1 的 eth0 网卡上实现 arp 响应功能,响应从广播域内发过来的 arp 请求,如果请求的 ip 是
10.31.8.200
,返回 node1 eth0 的 mac 地址 - 当流量来自
10.31.8.200
时,将通过 node1 转发到实际的 pod
如果 node1 出现故障或宕机
- node2 通过 leader 选举获得 leader
- node2 发送 arp 广播包,宣告
10.31.8.200
的 mac 地址为 node2 的 mac 地址00:0c:29:11:9a:b3
- lb2 要在 node2 上实现 arp 响应功能,抢答从广播域内发过来的 arp 请求,返回 node2 的 mac 地址
- 当流量来自
10.31.8.200
时,将通过 node2 转发到实际的 pod
参考下图
二、layer2 LB 实现的一些细节
1、arp 内核参数的配置
由于 10.31.8.200
不是任何网卡的实际 IP,为了实现返回这个 IP 的 mac 地址,我们需要修改arp内核参数 arp_ignore
和arp_announce
,对应到 k8s 上,就是需要将 strictARP设置为 true
2、arp 广播与响应
2.1、arp 的广播
arp 的广播相对比较容易实现,按照 arp 协议编写对应的代码即可,在 arp 的请求中,将目标写为 FF:FF:FF:FF
即可。
hwAddr, err := a.resolveIP(nodeIP)
if err != nil {
// ...
}
// 缓存 vip 和 node 的 mac 地址
a.setMac(ip.String(), hwAddr)
fb, err := generateArp(
a.intf.HardwareAddr,
op,
hwAddr, // node mac address => 00:0c:29:a1:98:bc
ip, // vip: 10.31.8.200
ethernet.Broadcast, // net.HardwareAddr{0xff, 0xff, 0xff, 0xff, 0xff, 0xff}
ip, // vip: 10.31.8.200
)
2.2 arp响应
arp 的响应需要指定网卡,一般是node ip 对应的网卡
// iface.Name = eth0
ctrl.Log.Info(fmt.Sprintf("use interface %s to speak arp", iface.Name))
if v4 {
speaker, err := newARPSpeaker(iface)
if err != nil {
return nil, err
}
return speaker, nil
}
// 当收到的 ip 是 10.31.8.200,从缓存 map 中读取当前机器的 mac 地址
hwAddr := a.getMac(pkt.TargetIP.String())
if hwAddr == nil {
return dropReasonUnknowTargetIP
}
// ...
fb, err := generateArp(
a.intf.HardwareAddr,
arp.OperationReply,
*hwAddr, // mac 地址,比如 node1 的 mac 地址 00:0c:29:a1:98:bc
pkt.TargetIP, // IP, 这里为 10.31.8.200
pkt.SenderHardwareAddr, // 源 mac 地址
pkt.SenderIP, // 源IP
)
三、LB 在 kubernetes 中的创建
apiVersion: v1
kind: Service
metadata:
name: nginx
annotations:
lb2: 10.31.8.200
spec:
ports:
- port: 80
targetPort: 80
selector:
app: nginx
type: LoadBalancer
loadBalancerIP: 10.31.8.200
status:
loadBalancer:
ingress:
- ip: 10.31.8.200
在我们的 LB 实现程序中,可以 watch k8s svc 资源的创建,如果含有特定的 label 或者 annotation,我们可以配置 10.31.8.200 => svc-nginx
可以通过 ipvs 来完成流量的转发,将流量转发到对应的 endpoint 上
ipvsadm -A -t 10.31.8.200:80 -s rr
ipvsadm -a -t 10.31.8.200:80 -r 10.8.65.15:80
ipvsadm -a -t 10.31.8.200:80 -r 10.8.65.16:80
ipvsadm -a -t 10.31.8.200:80 -r 10.8.65.12:80
# ipvsadm -ln
TCP 10.31.8.200 rr
-> 10.8.65.15:80 Masq 1 0 0
-> 10.8.65.16:80 Masq 1 0 0
-> 10.8.66.12:80 Masq 1 0 0
四、客户端的流量访问
由于 LB 服务会向整个广播域宣告 ARP,所以 node4 默认会存有 10.31.8.200 的 mac 地址,如果 arp 过期,也可以通过 arp 请求获取到 mac 地址
who has 10.31.8.200 ? Tell 192.168.37.104
有了 mac 地址之后,就可以开始通信了,当流量到达 node1,再通过 ipvs 转发到实际的 Pod 上去。