1. 简介

kubernetes集群没有L4负载均衡,对外暴漏服务时,只能使用nodePort的方式,比较麻烦,必须要记住不同的端口号。

LoadBalancer:使用云提供商的负载均衡器向外部暴露服务,外部负载均衡器可以将流量路由到自动创建的 NodePort 服务和 ClusterIP 服务上。

img

MetalLB 是裸机 Kubernetes 集群的负载均衡器实现,使用标准路由协议。

它提供了两个功能:

  • 地址分配(address allocation):当创建 LoadBalancer Service 时,MetalLB 会为其分配 IP 地址。这个 IP 地址是从预先配置的 IP 地址库获取的。同样,当 Service 删除后,已分配的 IP 地址会重新回到地址库。
  • 对外公告(external announcement):分配了 IP 地址之后,需要让集群外的网络知道这个地址的存在。使用了标准路由协议实现:
    • Layer2 模式:ARP(ipv4)、NDP(ipv6)
    • BGP 模式

不管是Layer2模式还是BGP模式,两者都不使用Linux的网络栈,即无法使用诸如ip命令准确的查看VIP所在的节点和相应的路由,相对应的是在每个节点上面都能看到一个kube-ipvs0网卡接口上面的IP。同时,两种模式都只是负责把VIP的请求引到对应的节点上面,之后的请求怎么到达pod,按什么规则轮询等都是由kube-proxy实现的。

对应的的两种工作负载:

  • ControllerDeployment,用于监听 Service 的变更,分配/回收 IP 地址。
  • SpeakerDaemonSet,对外广播 Service 的 IP 地址。把服务类型为LoadBalancer的服务的EXTERNAL-IP公布到网络中去,确保客户端能够正常访问到这个IP。

2. 部署

2.1 测试服务

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.21.4
        ports:
        - name: http
          containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: nginx-svc
spec:
  ports:
  - name: http
    port: 8080
    protocol: TCP
    targetPort: 80
  selector:
    app: nginx
  type: LoadBalancer

2.2 MetalLB

# 1. ipvs 开启严格 arp模式
$ kubectl edit configmap -n kube-system kube-proxy
...
ipvs:
  strictARP: true

# 2. 安装组件
$ mkdir -p $HOME/metallb && cd $_
$ wget https://raw.githubusercontent.com/metallb/metallb/v0.13.10/config/manifests/metallb-native.yaml

$ kubectl apply -f metallb-native.yaml

$ kubectl get pod -n metallb-system
NAME                          READY   STATUS    RESTARTS      AGE
controller-6f5c46d94b-b7psv   1/1     Running   1 (29s ago)   80s
speaker-6v8hn                 1/1     Running   0             80s
speaker-nzkb6                 1/1     Running   0             79s
speaker-wjqxx                 1/1     Running   0             79s

组件说明:

  • metallb-system/controller:负责IP地址的分配,以及service和endpoint的监听
  • metallb-system/speaker:负责保证service地址可达,在Layer 2模式下,speaker会负责ARP请求应答

3. Layer2 模式

  • 每个service会有集群中的一个node来负责。当服务客户端发起ARP解析的时候,对应的node会响应该ARP请求,之后,该service的流量都会指向该node(看上去该node上有多个地址)。

  • 并不是真正的负载均衡,因为流量都会先经过一个node后,再通过kube-proxy转给多个endpoints。如果该node故障,MetalLB会迁移 IP到另一个node,并重新发送ARP告知客户端迁移。

  • 更为通用,不需要用户有额外的设备;但由于Layer 2模式使用ARP/NDP,地址池分配需要跟客户端在同一子网,地址分配略为繁琐。

img

3.1 工作原理

  • Speaker 工作负载类型是 DeamonSet ,在每台节点上都调度一个 Pod。首先,几个 Pod 会先进行选举,选举出 LeaderLeader 获取所有 LoadBalancer 类型的 Service,将已分配的 IP 地址绑定到当前主机到网卡上。即所有 LoadBalancer 类型的 Service 的 IP 同一时间都是绑定在同一台节点的网卡上。

  • 当外部主机有请求要发往集群内的某个 Service,需要先确定目标主机网卡的 mac 地址。这是通过发送 ARP 请求,Leader 节点的会以其 mac 地址作为响应。外部主机会在本地 ARP 表中缓存下来,下次会直接从 ARP 表中获取。

  • 请求到达节点后,节点再通过 kube-proxy 将请求负载均衡目标 Pod。所以说,假如Service 是多 Pod 这里有可能会再跳去另一台主机。

img

3.2 试验

配置 layer2 IP池 (layer2.yaml):

apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
  name: layer2-pool
  namespace: metallb-system
spec:
  addresses:
  - 192.168.3.170-192.168.3.179
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
  name: layer2-lb
  namespace: metallb-system
spec:
  ipAddressPools:
  - layer2-pool

查看结果:

# 获取
$ kubectl get svc nginx-svc
NAME        TYPE           CLUSTER-IP     EXTERNAL-IP     PORT(S)          AGE
nginx-svc   LoadBalancer   10.96.155.95   192.168.3.170   8080:31717/TCP   9m34s

$ kubectl get ep nginx-svc
NAME        ENDPOINTS                                           AGE
nginx-svc   10.244.172.62:80,10.244.172.63:80,10.244.46.53:80   9m38s

$ ipvsadm -Ln | grep 192.168.3.170 -A 3
TCP  192.168.3.170:8080 rr
  -> 10.244.46.53:80              Masq    1      0          0
  -> 10.244.172.62:80             Masq    1      0          0
  -> 10.244.172.63:80             Masq    1      0          0

# 每个节点自动增加配置
$ ip addr show kube-ipvs0 | grep 192.168.3.170 -A 1
    inet 192.168.3.170/32 scope global kube-ipvs0
       valid_lft forever preferred_lft forever

查询实际的工作代理节点:

$ curl -I 192.168.3.170:8080
HTTP/1.1 200 OK
Server: nginx/1.21.4
Date: Thu, 10 Aug 2023 06:44:21 GMT
Content-Type: text/html
Content-Length: 615
Last-Modified: Tue, 02 Nov 2021 14:49:22 GMT
Connection: keep-alive
ETag: "61814ff2-267"
Accept-Ranges: bytes

$ arp 192.168.3.170
Address                  HWtype  HWaddress           Flags Mask            Iface
192.168.3.170            ether   52:54:00:63:8f:d3   C                     br0

根据mac地址,查询实际的节点:

$ ip addr | grep 52:54:00:63:8f:d3 -A 1 -B 1
2: ens3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether 52:54:00:63:8f:d3 brd ff:ff:ff:ff:ff:ff
    inet 192.168.3.105/24 brd 192.168.3.255 scope global ens3

3.3 清理

kubectl delete -f layer2.yaml

kubectl get pod -n metallb-system | grep Running | awk '{print $1}' | xargs kubectl delete pod -n metallb-system

4. BGP 模式

  • 所有node都会跟上级路由器建立BGP连接,并会告知路由器应该如何转发service流量
  • 是真正的 Load Balancer

在这里插入图片描述

4.1 工作原理

BGP模式不限于一个二层网络里,各个节点都会与交换机建立BGP Peer,宣告Service External IP的下一跳为自身,这样通过ECMP实现了一层负载。客户端请求通过交换机负载到后端某个节点后,再由Kube-proxy进行转发

img

4.2 路由配置

通过 openwrt 旁路由实现 BGP。使用命令 vtysh 进行 AS 配置

  • 本地:AS 65000

  • 远端:AS 65001

$ vtysh
OpenWrt# conf t
OpenWrt(config)# router bgp 65000
OpenWrt(config-router)# neighbor 192.168.3.103 remote-as 65001
OpenWrt(config-router)# neighbor 192.168.3.103 description master-01
OpenWrt(config-router)# neighbor 192.168.3.104 remote-as 65001
OpenWrt(config-router)# neighbor 192.168.3.104 description worker-01
OpenWrt(config-router)# neighbor 192.168.3.105 remote-as 65001
OpenWrt(config-router)# neighbor 192.168.3.105 description worker-02
OpenWrt(config-router)# exit
OpenWrt(config)# exit

OpenWrt# show ip bgp summary
BGP router identifier 192.168.3.180, local AS number 65000
RIB entries 0, using 0 bytes of memory
Peers 3, using 27 KiB of memory

Neighbor        V         AS MsgRcvd MsgSent   TblVer  InQ OutQ Up/Down  State/PfxRcd
192.168.3.103   4 65001       0       6        0    0    0 never    Active
192.168.3.104   4 65001       0       4        0    0    0 never    Active
192.168.3.105   4 65001       0       3        0    0    0 never    Active

Total number of neighbors 3

OpenWrt# exit

查看 bgpd 端口监听 179 是否已打开:

$  netstat -lantp | grep -E 'zebra|bgpd'
tcp        0      0 0.0.0.0:179             0.0.0.0:*               LISTEN      5355/bgpd
tcp        0      0 0.0.0.0:2601            0.0.0.0:*               LISTEN      5350/zebra
tcp        0      0 0.0.0.0:2605            0.0.0.0:*               LISTEN      5355/bgpd
tcp        0      0 :::179                  :::*                    LISTEN      5355/bgpd
tcp        0      0 :::2601                 :::*                    LISTEN      5350/zebra
tcp        0      0 :::2605                 :::*                    LISTEN      5355/bgpd

4.3 试验

配置BPG IP池 (bgp.yaml):

apiVersion: metallb.io/v1beta2
kind: BGPPeer
metadata:
  name: sample
  namespace: metallb-system
spec:
  myASN: 65001
  peerASN: 65000
  peerAddress: 192.168.3.180
---
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
  name: first-pool
  namespace: metallb-system
spec:
  addresses:
  - 192.168.0.10-192.168.0.99
---
apiVersion: metallb.io/v1beta1
kind: BGPAdvertisement
metadata:
  name: example
  namespace: metallb-system
spec:
  ipAddressPools:
  - first-pool

查看结果:

$ kubectl get svc nginx-svc
NAME        TYPE           CLUSTER-IP      EXTERNAL-IP    PORT(S)          AGE
nginx-svc   LoadBalancer   10.96.167.171   192.168.0.10   8080:31323/TCP   25h

$ kubectl get ep nginx-svc
NAME        ENDPOINTS                                           AGE
nginx-svc   10.244.172.59:80,10.244.172.60:80,10.244.46.52:80   25h

# 每个节点均添加
$ ip addr show kube-ipvs0 | grep 192.168.0.10 -A 1
    inet 192.168.0.10/32 scope global kube-ipvs0
       valid_lft forever preferred_lft forever
       
$ curl -I 192.168.0.10:8080
HTTP/1.1 200 OK
Server: nginx/1.21.4
Date: Thu, 10 Aug 2023 03:39:37 GMT
Content-Type: text/html
Content-Length: 615
Last-Modified: Tue, 02 Nov 2021 14:49:22 GMT
Connection: keep-alive
ETag: "61814ff2-267"
Accept-Ranges: bytes

$ ipvsadm -Ln | grep 192.168.0 -A 3
TCP  192.168.0.10:8080 rr
  -> 10.244.46.52:80              Masq    1      0          0
  -> 10.244.172.59:80             Masq    1      0          0
  -> 10.244.172.60:80             Masq    1      0          0

客户端(windows, 192.168.3.3)添加路由:

route add 192.168.0.0 MASK 255.255.0.0 192.168.3.180

访问:https://192.168.0.10:8080 ok

4.4 清理

kubectl delete -f bgp.yaml

kubectl get pod -n metallb-system | grep Running | awk '{print $1}' | xargs kubectl delete pod -n metallb-system

5. IP地址共享

默认情况下,MetalLB只会将一个IP地址分配到一个LoadBalancer Service上,用户可以通过spec.loadBalancerIP来指定自己想用的IP,如果用户指定了已被分配了的IP会,则会报错。但MetalLB也提供了方式去支持多个Service共享相同的IP,主要为了解决:K8S不支持对LoadBalancer Service中的Port指定多协议;有限的IP地址资源。

具体的方式是:创建两个Service,并加上metallb.universe.tf/allow-shared-ip为Key的annotation,表明Service能容忍使用共享的LoadBalancerIP;然后通过spec.loadBalancerIP给两个Service指定共享的IP。

IP地址共享也有限制:

1)两个Service的metallb.universe.tf/allow-shared-ip值是一样的。

2)两个Service的“端口”(带协议)不同,比如tcp/53udp/53是属于不同的“端口”。

3)两个Service对应的后端Pod要一致,如果不一致,那么他们的externalTrafficPolicy需要都是Cluster,不然会无法进行正确的BGP。

6. 总结

6.1 nodeport

在创建LoadBalancer服务时,会默认创建一个nodeport服务,可通过配置allocateLoadBalancerNodePorts来关闭。

不同的loadbalancer实现原理不同,有的需要依赖nodeport来进行流量转发,有些则直接转发请求到pod中。

对于MetalLB而言,是通过kube-proxy将请求的流量直接转发到pod,因此可以关闭nodeport。

apiVersion: v1
kind: Service
metadata:
  name: nginx-svc
spec:
  allocateLoadBalancerNodePorts: false
  externalTrafficPolicy: Cluster
  internalTrafficPolicy: Cluster
  ports:
  - name: http
    port: 8080
    protocol: TCP
    targetPort: 80
  selector:
    app: nginx
  type: LoadBalancer
  loadBalancerIP: 192.168.0.20  

验证:

$ kubectl delete svc nginx-svc
$ kubectl apply -f nginx-svc-disable-nodeport.yaml

$ kubectl get svc nginx-svc
NAME        TYPE           CLUSTER-IP     EXTERNAL-IP    PORT(S)    AGE
nginx-svc   LoadBalancer   10.96.54.226   192.168.0.20   8080/TCP   3s

6.2 Layer2

优点:

  • 通用性强,不需要BGP路由器支持,几乎可以适用于任何网络环境,云厂商例外

缺点:

  • 所有的流量都会在同一个节点上,该节点的容易成为流量的瓶颈;
  • 当VIP所在节点宕机之后,需要较长时间进行故障转移(一般在10s),主要是因为MetalLB使用了memberlist来进行选主,当VIP所在节点宕机之后重新选主的时间要比传统的keepalived使用的vrrp协议要更长;
  • 难以定位VIP所在节点,没有提供一个简单直观的方式查看到底哪一个节点是VIP所属节点,只能通过抓包或者查看pod日志来确定,当集群规模变大的时会非常麻烦

6.3 BGP

优点:

  • 无单点故障,在开启ECMP的前提下,k8s集群内所有的节点都有请求流量,都会参与负载均衡并转发请求

缺点:

  • 条件苛刻,需要有BGP路由器支持,配置起来也更复杂;
  • ECMP的故障转移(failover)并不是特别地优雅,这个问题的严重程度取决于使用的ECMP算法;当集群的节点出现变动导致BGP连接出现变动,所有的连接都会进行重新哈希(使用三元组或五元组哈希),这对一些服务来说可能会有影响;

7. 附录

7.1 BGP

BGP,Border Gateway Protocol,边界网关协议

BGP是互联网上一个核心的去中心化自治路由协议。它通过维护IP路由表或“前缀”表来实现自治系统(AS)之间的可达性,属于矢量路由协议。BGP不使用传统的内部网关协议(IGP)的指标,而使用基于路径、网络策略或规则集来决定路由。因此,它更适合被称为矢量性协议,而不是路由协议。

BGP的邻居关系(或称通信对端/对等实体,peer)是通过人工配置实现的,对等实体之间通过TCP端口179建立会话交换数据。BGP路由器会周期地发送19字节的保持存活(keep-alive)消息来维护连接(默认周期为60秒)。在各种路由协议中,只有BGP使用TCP作为传输层协议。

同一个AS自治系统中的两个或多个对等实体之间运行的BGP被称为iBGP(Internal/Interior BGP)。归属不同的AS的对等实体之间运行的BGP称为eBGP(External/Exterior BGP)。在AS边界上与其他AS交换信息的路由器被称作边界路由器(border/edge router),边界路由器之间互为eBGP对端。在Cisco IOS中,iBGP通告的路由距离为200,优先级比eBGP和任何内部网关协议(IGP)通告的路由都低。其他的路由器实现中,优先级顺序也是eBGP高于IGP,而IGP又高于iBGP。

iBGP和eBGP的区别主要在于转发路由信息的行为。例如,从eBGP peer获得的路由信息会分发给所有iBGP peer和eBGP peer,但从iBGP peer获得的路由信息仅会分发给所有eBGP peer。所有的iBGP peer之间需要全互联。

在这里插入图片描述

三个核心名词:

  • 自治系统(AS)
  • 内部网关协议(IGP)
  • 外部网关协议(EGP)

7.1.1 自治系统 AS

自制系统(Autonomous system,缩写 AS),是指在互联网中,一个或多个实体管辖下的所有IP 网络和路由器的组合,它们对互联网执行共同的路由策略。自治系统编号都是16位长的整数,这最多能被分配给65536个自治系统。自治系统编号被分成两个范围。第一个范围是公开的ASN,从1到64511,它们可在互联网上使用;第二个范围是被称为私有编号的从64512到65535的那些,它们仅能在一个组织自己的网络内使用。

简单理解,电信、移动、联通都有自己的 AS 编号,且不只一个。除了互联网公开的 ASN 以外,私有的编号可以在内部使用。比如我可以我的家庭网络中使用私有编号创建几个 AS。

7.1.2 内部路由协议 IGP

内部路由协议(Interior Gateway Protocol,IGP)是指在一个自治系统(AS)内部所使用的一种路由协议。

7.1.3 外部网关协议 EGP

外部网关协议(Exterior Gateway Protocol,EGP)是一个已经过时互联网路由协议。已由 BPG 取代。

7.1.4 BGP

BPG 是为了替换 EGP 而创建的,而除了应用于 AS 外部,也可以应用在 AS 内部。因此又分为 EBGP 和 IBGP。

7.2 OpenWrt

7.2.1 安装

wget https://mirrors.aliyun.com/openwrt/releases/19.07.4/targets/x86/64/openwrt-19.07.4-x86-64-combined-ext4.img.gz

gunzip openwrt-19.07.4-x86-64-combined-ext4.img.gz
cp openwrt-19.07.4-x86-64-combined-ext4.img  openwrt.img

# 双网口(wan+lan)
virt-install --name=openwrt --vcpus=1 --ram=512 --os-type=generic --disk path=/root/eli/openwrt.img,bus=ide --autostart --network bridge=br0,model=e1000 --network bridge=br0,model=e1000 --import --noautoconsole --graphics vnc,listen=0.0.0.0,password=123456

# 单网口(lan)
virt-install --name=openwrt --vcpus=1 --ram=512 --os-type=generic --disk path=/root/eli/openwrt.img,bus=ide --autostart --network bridge=br0,model=e1000 --import --noautoconsole --graphics vnc

修改网络配置:

# 改成与宿主机在同一网络下
$ vi /etc/config/network
...
config interface 'lan'
        option type 'bridge'
        option ifname 'eth0'
        option proto 'static'
        option ipaddr '192.168.3.180'
        option netmask '255.255.255.0'
        option ip6assign '60'
        option gateway '192.168.3.1'
        option dns '114.114.114.114'
        
$ service network restart

登录配置页面:http://192.168.3.180

7.2.2 Quagga

为了让 OpenWrt 支持 BGP,需要安装路由软件套件 Quagga。它提供了 OSPFv2、OSPFv3、RIP v1 v2、RIPng 和 BGP-4 的实现。

Quagga 架构由核心守护进程和 zebra 组成,后者作为底层 Unix 内核的抽象层,并通过 Unix 或者 TCP 向 Quagga 客户端提供 Zserv API。正是这些 Zserv 客户端实现了路由协议,并将路由的更新发送给 zebra 守护进程。当前 Zserv 的实现:

img

Quagga 的守护进程可以通过网络可访问的 CLI(简称 vty)进行配置。 CLI 遵循与其他路由软件类似的风格。还额外提供了一个工具 vtysh,充当了所有守护进程的聚合前端,允许在一个地方管理所有 Quagga 守护进程的所有功能。

$ opkg update && opkg install quagga quagga-zebra quagga-bgpd quagga-vtysh

$ netstat -lantp | grep -E 'zebra|bgpd'
tcp        0      0 0.0.0.0:2601            0.0.0.0:*               LISTEN      5350/zebra
tcp        0      0 0.0.0.0:2605            0.0.0.0:*               LISTEN      5355/bgpd
tcp        0      0 :::2601                 :::*                    LISTEN      5350/zebra
tcp        0      0 :::2605                 :::*                    LISTEN      5355/bgpd

参考资料:

https://metallb.universe.tf/configuration/

https://tinychen.com/20220519-k8s-06-loadbalancer-metallb/

https://atbug.com/load-balancer-service-with-metallb-bgp-mode/

https://ieevee.com/tech/2019/06/30/metallb.html

Logo

K8S/Kubernetes社区为您提供最前沿的新闻资讯和知识内容

更多推荐