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_ignorearp_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 上去。

五、LB 的开源实现

开源的 metalbopenelb 中,都有二层的实现,基本原理与上面相似。