❝本文转自 TinyChen 的博客,原文:https://tinychen.com/20220523-k8s-07-loadbalancer-openelb/,版权归原作者所有。欢迎投稿,投稿请添加微信好友:cloud-native-yang
本文主要在 k8s 原生集群上部署 v0.4.4
版本的 OpenELB
作为 k8s 的 LoadBalancer
,主要涉及 OpenELB 的 Layer2 模式和 BGP 模式两种部署方案。由于 BGP 的相关原理和配置比较复杂,这里仅涉及简单的 BGP 配置。
文中使用的 k8s 集群是在 CentOS7 系统上基于 docker
和 calico
组件部署 v1.23.6
版本,此前写的一些关于 k8s 基础知识和集群搭建的一些方案[1],有需要的同学可以看一下。
工作原理
简介
OpenELB 是一个开源的云原生负载均衡器实现,可以在基于裸金属服务器、边缘以及虚拟化的 Kubernetes 环境中使用 LoadBalancer 类型的 Service 对外暴露服务。OpenELB 项目最初由 KubeSphere 社区[2] 发起,目前已作为 CNCF 沙箱项目[3] 加入 CNCF 基金会,由 OpenELB 开源社区维护与支持。
与 MetalLB 类似,OpenELB 也拥有两种主要工作模式:Layer2 模式和 BGP 模式。OpenELB 的 BGP 模式目前暂不支持 IPv6。
无论是 Layer2 模式还是 BGP 模式,核心思路都是通过某种方式将特定 VIP 的流量引到 k8s 集群中,然后再通过 kube-proxy 将流量转发到后面的特定服务。
Layer2 模式
❝Layer2 模式需要我们的 k8s 集群基础环境支持发送 anonymous ARP/NDP packets。因为 OpenELB 是针对裸金属服务器设计的,因此如果是在云环境中部署,需要注意是否满足条件。
下图来自OpenELB 官方[4],这里简单阐述一下 Layer2 模式的工作原理:
![5babe6ac66583fae7f9ab5c643e0617f.png](https://i-blog.csdnimg.cn/blog_migrate/c77931a2974caedd9b37142276144976.png)
图中有一个类型为 LoadBalancer 的 Service,其 VIP 为 192.168.0.91(和 k8s 的节点相同网段),后端有两个 pod(分别为 pod1 和 pod2)
安装在 Kubernetes 集群中的 OpenELB 随机选择一个节点(图中为 worker 1)来处理 Service 请求。当局域网中出现 arp request 数据包来查询 192.168.0.91 的 mac 地址的时候,OpenELB 会进行回应(使用 worker 1 的 MAC 地址),此时路由器(也可能是交换机)将 Service 的 VIP 192.168.0.91 和 worker 1 的 MAC 地址绑定,之后所有请求到 192.168.0.91 的数据包都会被转发到 worker1 上
Service 流量到达 worker 1 后, worker 1 上的 kube-proxy 将流量转发到后端的两个 pod 进行负载均衡,这些 pod 不一定在 work1 上
主要的工作流程就如同上面描述的一般,但是还有几个需要额外注意的点:
如果 worker 1 出现故障,OpenELB 会重新向路由器发送 APR/NDP 数据包,将 Service IP 地址映射到 worker 2 的 MAC 地址,Service 流量切换到 worker 2
主备切换过程并不是瞬间完成的,中间会产生一定时间的服务中断(具体多久官方也没说,实际上应该是却决于检测到节点宕机的时间加上重新选主的时间)
如果集群中已经部署了多个 openelb-manager 副本,OpenELB 使用 Kubernetes 的领导者选举特性算法来进行选主,从而确保只有一个副本响应 ARP/NDP 请求
BGP 模式
OpenELB 的 BGP 模式使用的是gobgp[5]实现的 BGP 协议,通过使用 BGP 协议和路由器建立 BGP 连接并实现 ECMP 负载均衡,从而实现高可用的 LoadBalancer。
我们还是借用官网的图[6]来解释一下这个流程,注意 BGP 模式暂不支持 IPv6。
![a0c5a1c436c084f274c329fc52cb00d0.png](https://i-blog.csdnimg.cn/blog_migrate/32fddb7f10c55e38372d79a15383ba51.png)
图中有一个类型为 LoadBalancer 的 Service,其 VIP 为 172.22.0.2(和 k8s 的节点不同网段),后端有两个 pod(分别为 pod1 和 pod2)
安装在 Kubernetes 集群中的 OpenELB 与 BGP 路由器建立 BGP 连接,并将去往 172.22.0.2 的路由发布到 BGP 路由器,在配置得当的情况下,路由器上面的路由表可以看到 172.22.0.2 这个 VIP 的下一条有多个节点(均为 k8s 的宿主机节点)
当外部客户端机器尝试访问 Service 时,BGP 路由器根据从 OpenELB 获取的路由,在 master、worker 1 和 worker 2 节点之间进行流量负载均衡。Service 流量到达一个节点后,该节点上的 kube-proxy 将流量转发到后端的两个 pod 进行负载均衡,这些 pod 不一定在该节点上
Layer2 Mode
配置 ARP 参数
部署 Layer2 模式需要把 k8s 集群中的 ipvs 配置打开strictARP[7],开启之后 k8s 集群中的 kube-proxy
会停止响应 kube-ipvs0
网卡之外的其他网卡的 arp 请求,而由 OpenELB 接手处理。
strict ARP
开启之后相当于把 将 arp_ignore
设置为 1 并将 arp_announce
设置为 2 启用严格的 ARP,这个原理和 LVS 中的 DR 模式对 RS 的配置一样,可以参考之前的文章中的解释[8]。
❝strict ARP configure arp_ignore and arp_announce to avoid answering ARP queries from kube-ipvs0 interface
# 查看kube-proxy中的strictARP配置
$ kubectl get configmap -n kube-system kube-proxy -o yaml | grep strictARP
strictARP: false
# 手动修改strictARP配置为true
$ kubectl edit configmap -n kube-system kube-proxy
configmap/kube-proxy edited
# 使用命令直接修改并对比不同
$ kubectl get configmap kube-proxy -n kube-system -o yaml | sed -e "s/strictARP: false/strictARP: true/" | kubectl diff -f - -n kube-system
# 确认无误后使用命令直接修改并生效
$ kubectl get configmap kube-proxy -n kube-system -o yaml | sed -e "s/strictARP: false/strictARP: true/" | kubectl apply -f - -n kube-system
# 重启kube-proxy确保配置生效
$ kubectl rollout restart ds kube-proxy -n kube-system
# 确认配置生效
$ kubectl get configmap -n kube-system kube-proxy -o yaml | grep strictARP
strictARP: true
部署 openelb
这里我们还是使用 yaml 进行部署,官方把所有部署的资源整合到了一个文件中,我们还是老规矩先下载到本地再进行部署
$ wget https://raw.githubusercontent.com/openelb/openelb/master/deploy/openelb.yaml
$ kubectl apply -f openelb.yaml
namespace/openelb-system created
customresourcedefinition.apiextensions.k8s.io/bgpconfs.network.kubesphere.io created
customresourcedefinition.apiextensions.k8s.io/bgppeers.network.kubesphere.io created
customresourcedefinition.apiextensions.k8s.io/eips.network.kubesphere.io created
serviceaccount/kube-keepalived-vip created
serviceaccount/openelb-admission created
role.rbac.authorization.k8s.io/leader-election-role created
role.rbac.authorization.k8s.io/openelb-admission created
clusterrole.rbac.authorization.k8s.io/kube-keepalived-vip created
clusterrole.rbac.authorization.k8s.io/openelb-admission created
clusterrole.rbac.authorization.k8s.io/openelb-manager-role created
rolebinding.rbac.authorization.k8s.io/leader-election-rolebinding created
rolebinding.rbac.authorization.k8s.io/openelb-admission created
clusterrolebinding.rbac.authorization.k8s.io/kube-keepalived-vip created
clusterrolebinding.rbac.authorization.k8s.io/openelb-admission created
clusterrolebinding.rbac.authorization.k8s.io/openelb-manager-rolebinding created
service/openelb-admission created
deployment.apps/openelb-manager created
job.batch/openelb-admission-create created
job.batch/openelb-admission-patch created
mutatingwebhookconfiguration.admissionregistration.k8s.io/openelb-admission created
validatingwebhookconfiguration.admissionregistration.k8s.io/openelb-admission created
接下来我们看看都部署了什么 CRD 资源,这几个 CRD 资源主要就是方便我们管理 openelb
,这也是 OpenELB 相对 MetalLB 的优势。
$ kubectl get crd
NAME CREATED AT
bgpconfs.network.kubesphere.io 2022-05-19T06:37:19Z
bgppeers.network.kubesphere.io 2022-05-19T06:37:19Z
eips.network.kubesphere.io 2022-05-19T06:37:19Z
$ kubectl get ns openelb-system -o wide
NAME STATUS AGE
openelb-system Active 2m27s
实际上主要工作的负载就是这两个 jobs.batch
和这一个 deployment
$ kubectl get pods -n openelb-system
NAME READY STATUS RESTARTS AGE
openelb-admission-create-57tzm 0/1 Completed 0 5m11s
openelb-admission-patch-j5pl4 0/1 Completed 0 5m11s
openelb-manager-5cdc8697f9-h2wd6 1/1 Running 0 5m11s
$ kubectl get deploy -n openelb-system
NAME READY UP-TO-DATE AVAILABLE AGE
openelb-manager 1/1 1 1 5m38s
$ kubectl get jobs.batch -n openelb-system
NAME COMPLETIONS DURATION AGE
openelb-admission-create 1/1 11s 11m
openelb-admission-patch 1/1 12s 11m
创建 EIP
接下来我们需要配置 loadbalancerIP 所在的网段资源,这里我们创建一个 Eip 对象来进行定义,后面对 IP 段的管理也是在这里进行。
apiVersion: network.kubesphere.io/v1alpha2
kind: Eip
metadata:
# Eip 对象的名称。
name: eip-layer2-pool
spec:
# Eip 对象的地址池
address: 10.31.88.101-10.31.88.200
# openELB的运行模式,默认为bgp
protocol: layer2
# OpenELB 在其上侦听 ARP/NDP 请求的网卡。该字段仅在protocol设置为时有效layer2。
interface: eth0
# 指定是否禁用 Eip 对象
# false表示可以继续分配
# true表示不再继续分配
disable: false
status:
# 指定 Eip 对象中的IP地址是否已用完。
occupied: false
# 指定 Eip 对象中有多少个 IP 地址已分配给服务。
# 直接留空,系统会自动生成
usage:
# Eip 对象中的 IP 地址总数。
poolSize: 100
# 指定使用的 IP 地址和使用 IP 地址的服务。服务以Namespace/Service name格式显示(例如,default/test-svc)。
# 直接留空,系统会自动生成
used:
# Eip 对象中的第一个 IP 地址。
firstIP: 10.31.88.101
# Eip 对象中的最后一个 IP 地址。
lastIP: 10.31.88.200
ready: true
# 指定IP协议栈是否为 IPv4。目前,OpenELB 仅支持 IPv4,其值只能是true.
v4: true
配置完成后我们直接部署即可
$ kubectl apply -f openelb/openelb-eip.yaml
eip.network.kubesphere.io/eip-layer2-pool created
部署完成后检查 eip 的状态
$ kubectl get eip
NAME CIDR USAGE TOTAL
eip-layer2-pool 10.31.88.101-10.31.88.200 100
创建测试服务
然后我们创建对应的服务
apiVersion: v1
kind: Namespace
metadata:
name: nginx-quic
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-lb
namespace: nginx-quic
spec:
selector:
matchLabels:
app: nginx-lb
replicas: 4
template:
metadata:
labels:
app: nginx-lb
spec:
containers:
- name: nginx-lb
image: tinychen777/nginx-quic:latest
imagePullPolicy: IfNotPresent
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: nginx-lb-service
namespace: nginx-quic
spec:
allocateLoadBalancerNodePorts: false
externalTrafficPolicy: Cluster
internalTrafficPolicy: Cluster
selector:
app: nginx-lb
ports:
- protocol: TCP
port: 80 # match for service access port
targetPort: 80 # match for pod access port
type: LoadBalancer
---
apiVersion: v1
kind: Service
metadata:
name: nginx-lb2-service
namespace: nginx-quic
spec:
allocateLoadBalancerNodePorts: false
externalTrafficPolicy: Cluster
internalTrafficPolicy: Cluster
selector:
app: nginx-lb
ports:
- protocol: TCP
port: 80 # match for service access port
targetPort: 80 # match for pod access port
type: LoadBalancer
---
apiVersion: v1
kind: Service
metadata:
name: nginx-lb3-service
namespace: nginx-quic
spec:
allocateLoadBalancerNodePorts: false
externalTrafficPolicy: Cluster
internalTrafficPolicy: Cluster
selector:
app: nginx-lb
ports:
- protocol: TCP
port: 80 # match for service access port
targetPort: 80 # match for pod access port
type: LoadBalancer
然后我们检查部署状态:
$ kubectl apply -f nginx-quic-lb.yaml
namespace/nginx-quic unchanged
deployment.apps/nginx-lb created
service/nginx-lb-service created
service/nginx-lb2-service created
service/nginx-lb3-service created
$ kubectl get svc -n nginx-quic
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
nginx-lb-service LoadBalancer 10.88.17.248 10.31.88.101 80:30643/TCP 58m
nginx-lb2-service LoadBalancer 10.88.13.220 10.31.88.102 80:31114/TCP 58m
nginx-lb3-service LoadBalancer 10.88.18.110 10.31.88.103 80:30485/TCP 58m
此时我们再查看 eip 的状态可以看到新部署的三个 LoadBalancer 服务:
$ kubectl get eip
NAME CIDR USAGE TOTAL
eip-layer2-pool 10.31.88.101-10.31.88.200 3 100
[root@tiny-calico-master-88-1 tiny-calico]# kubectl get eip -o yaml
apiVersion: v1
items:
- apiVersion: network.kubesphere.io/v1alpha2
kind: Eip
metadata:
annotations:
kubectl.kubernetes.io/last-applied-configuration: |
{"apiVersion":"network.kubesphere.io/v1alpha2","kind":"Eip","metadata":{"annotations":{},"name":"eip-layer2-pool"},"spec":{"address":"10.31.88.101-10.31.88.200","disable":false,"interface":"eth0","protocol":"layer2"},"status":{"firstIP":"10.31.88.101","lastIP":"10.31.88.200","occupied":false,"poolSize":100,"ready":true,"usage":1,"used":{"10.31.88.101":"nginx-quic/nginx-lb-service"},"v4":true}}
creationTimestamp: "2022-05-19T08:21:58Z"
finalizers:
- finalizer.ipam.kubesphere.io/v1alpha1
generation: 2
name: eip-layer2-pool
resourceVersion: "1623927"
uid: 9b091518-7b64-43ae-9fd4-abaf50563160
spec:
address: 10.31.88.101-10.31.88.200
interface: eth0
protocol: layer2
status:
firstIP: 10.31.88.101
lastIP: 10.31.88.200
poolSize: 100
ready: true
usage: 3
used:
10.31.88.101: nginx-quic/nginx-lb-service
10.31.88.102: nginx-quic/nginx-lb2-service
10.31.88.103: nginx-quic/nginx-lb3-service
v4: true
kind: List
metadata:
resourceVersion: ""
selfLink: ""
关于 nodeport
openELB 似乎并不支持 allocateLoadBalancerNodePorts
字段,在指定了 allocateLoadBalancerNodePorts
为 false
的情况下还是为服务创建了 nodeport,查看部署后的配置可以发现参数被修改回了 true
,并且无法修改为 false
。
[root@tiny-calico-master-88-1 tiny-calico]# kubectl get svc -n nginx-quic
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
nginx-lb-service LoadBalancer 10.88.17.248 10.31.88.101 80:30643/TCP 106m
nginx-lb2-service LoadBalancer 10.88.13.220 10.31.88.102 80:31114/TCP 106m
nginx-lb3-service LoadBalancer 10.88.18.110 10.31.88.113 80:30485/TCP 106m
[root@tiny-calico-master-88-1 tiny-calico]# kubectl get svc -n nginx-quic nginx-lb-service -o yaml | grep "allocateLoadBalancerNodePorts:"
allocateLoadBalancerNodePorts: true
关于 VIP
如何查找 VIP
Layer2 模式下,所有的 k8s 节点的 kube-ipvs0 接口上都会看到所有的 VIP,因此确定 VIP 的节点还是和 MetalLB 一样,需要通过查看 pod 中的日志,或者是看 arp 表来确定
$ kubectl logs -f -n openelb-system openelb-manager-5cdc8697f9-h2wd6
...省略一堆日志输出...
{"level":"info","ts":1652949142.6426723,"logger":"IPAM","msg":"assignIP","args":{"Key":"nginx-quic/nginx-lb3-service","Addr":"10.31.88.113","Eip":"eip-layer2-pool","Protocol":"layer2","Unalloc":false},"result":{"Addr":"10.31.88.113","Eip":"eip-layer2-pool","Protocol":"layer2","Sp":{}},"err":null}
{"level":"info","ts":1652949142.65095,"logger":"arpSpeaker","msg":"map ingress ip","ingress":"10.31.88.113","nodeIP":"10.31.88.1","nodeMac":"52:54:00:74:eb:11"}
{"level":"info","ts":1652949142.6509972,"logger":"arpSpeaker","msg":"send gratuitous arp packet","eip":"10.31.88.113","nodeIP":"10.31.88.1","hwAddr":"52:54:00:74:eb:11"}
{"level":"info","ts":1652949142.6510363,"logger":"arpSpeaker","msg":"send gratuitous arp packet","eip":"10.31.88.113","nodeIP":"10.31.88.1","hwAddr":"52:54:00:74:eb:11"}
{"level":"info","ts":1652949142.6849823,"msg":"setup openelb service","service":"nginx-quic/nginx-lb3-service"}
查看 arp 表来确定 MAC 地址从而确定 VIP 所在的节点
$ ip neigh | grep 10.31.88.113
10.31.88.113 dev eth0 lladdr 52:54:00:74:eb:11 STALE
$ arp -a | grep 10.31.88.113
? (10.31.88.113) at 52:54:00:74:eb:11 [ether] on eth0
如何指定 VIP
如果需要指定 VIP,我们还是在 service 中修改 loadBalancerIP
字段从而指定 VIP:
apiVersion: v1
kind: Service
metadata:
name: nginx-lb3-service
namespace: nginx-quic
annotations:
lb.kubesphere.io/v1alpha1: openelb
protocol.openelb.kubesphere.io/v1alpha1: layer2
eip.openelb.kubesphere.io/v1alpha2: eip-layer2-pool
spec:
allocateLoadBalancerNodePorts: true
externalTrafficPolicy: Cluster
internalTrafficPolicy: Cluster
selector:
app: nginx-lb
ports:
- protocol: TCP
port: 80 # match for service access port
targetPort: 80 # match for pod access port
type: LoadBalancer
# 使用 loadBalancerIP 字段从而指定VIP
loadBalancerIP: 10.31.88.113
修改完成之后我们重新部署,openelb 会自动生效并且将服务的 EXTERNAL-IP
变更为新 IP。
$ kubectl get svc -n nginx-quic -o wide
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
nginx-lb-service LoadBalancer 10.88.17.248 10.31.88.101 80:30643/TCP 18h app=nginx-lb
nginx-lb2-service LoadBalancer 10.88.13.220 10.31.88.102 80:31114/TCP 18h app=nginx-lb
nginx-lb3-service LoadBalancer 10.88.18.110 10.31.88.113 80:30485/TCP 18h app=nginx-lb
Layer2 工作原理
我们查看 pod 的日志,可以看到更多的详细信息:
$ kubectl logs -f -n openelb-system openelb-manager-5cdc8697f9-h2wd6
...省略一堆日志输出...
{"level":"info","ts":1652950197.1421876,"logger":"arpSpeaker","msg":"got ARP request, sending response","interface":"eth0","ip":"10.31.88.101","senderIP":"10.31.254.250","senderMAC":"6c:b1:58:56:e9:d4","responseMAC":"52:54:00:56:df:ae"}
{"level":"info","ts":1652950496.9334905,"logger":"arpSpeaker","msg":"got ARP request, sending response","interface":"eth0","ip":"10.31.88.102","senderIP":"10.31.254.250","senderMAC":"6c:b1:58:56:e9:d4","responseMAC":"52:54:00:74:eb:11"}
{"level":"info","ts":1652950497.1327593,"logger":"arpSpeaker","msg":"got ARP request, sending response","interface":"eth0","ip":"10.31.88.113","senderIP":"10.31.254.250","senderMAC":"6c:b1:58:56:e9:d4","responseMAC":"52:54:00:74:eb:11"}
{"level":"info","ts":1652950497.1528928,"logger":"arpSpeaker","msg":"got ARP request, sending response","interface":"eth0","ip":"10.31.88.101","senderIP":"10.31.254.250","senderMAC":"6c:b1:58:56:e9:d4","responseMAC":"52:54:00:56:df:ae"}
{"level":"info","ts":1652950796.9339302,"logger":"arpSpeaker","msg":"got ARP request, sending response","interface":"eth0","ip":"10.31.88.102","senderIP":"10.31.254.250","senderMAC":"6c:b1:58:56:e9:d4","responseMAC":"52:54:00:74:eb:11"}
{"level":"info","ts":1652950797.1334393,"logger":"arpSpeaker","msg":"got ARP request, sending response","interface":"eth0","ip":"10.31.88.113","senderIP":"10.31.254.250","senderMAC":"6c:b1:58:56:e9:d4","responseMAC":"52:54:00:74:eb:11"}
{"level":"info","ts":1652950797.1572392,"logger":"arpSpeaker","msg":"got ARP request, sending response","interface":"eth0","ip":"10.31.88.101","senderIP":"10.31.254.250","senderMAC":"6c:b1:58:56:e9:d4","responseMAC":"52:54:00:56:df:ae"}
可以看到 openelb-manager
会持续的监听局域网中的 ARP request
请求,当遇到请求的 IP 是自己 IP 池里面已经使用的 VIP 时会主动响应。如果 openelb-manager
存在多个副本的时候,它们会先使用 k8s 的选主算法来进行选主,然后再由选举出来的主节点进行 ARP 报文的响应。
❝
In Layer 2 mode, OpenELB uses the leader election feature of Kubernetes to ensure that only one replica responds to ARP/NDP requests.
OpenELB-manager 高可用
默认情况下,openelb-manager
只会部署一个副本,对于可用性要求较高的生产环境可能无法满足需求,官方也给出了部署多个副本[9]的教程。
官方教程的方式是推荐通过给节点添加 label 的方式来控制副本的部署数量和位置,这里我们将其配置为每个节点都运行一个服务(类似于 daemonset)。首先我们给需要部署的节点打上 labels。
# 我们给集群内的三个节点都打上label
$ kubectl label --overwrite nodes tiny-calico-master-88-1.k8s.tcinternal tiny-calico-worker-88-11.k8s.tcinternal tiny-calico-worker-88-12.k8s.tcinternal lb.kubesphere.io/v1alpha1=openelb
# 查看当前节点的labels
$ kubectl get nodes -o wide --show-labels=true | grep openelb
tiny-calico-master-88-1.k8s.tcinternal Ready control-plane,master 16d v1.23.6 10.31.88.1 <none> CentOS Linux 7 (Core) 3.10.0-1160.62.1.el7.x86_64 docker://20.10.14 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=tiny-calico-master-88-1.k8s.tcinternal,kubernetes.io/os=linux,lb.kubesphere.io/v1alpha1=openelb,node-role.kubernetes.io/control-plane=,node-role.kubernetes.io/master=,node.kubernetes.io/exclude-from-external-load-balancers=
tiny-calico-worker-88-11.k8s.tcinternal Ready <none> 16d v1.23.6 10.31.88.11 <none> CentOS Linux 7 (Core) 3.10.0-1160.62.1.el7.x86_64 docker://20.10.14 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=tiny-calico-worker-88-11.k8s.tcinternal,kubernetes.io/os=linux,lb.kubesphere.io/v1alpha1=openelb
tiny-calico-worker-88-12.k8s.tcinternal Ready <none> 16d v1.23.6 10.31.88.12 <none> CentOS Linux 7 (Core) 3.10.0-1160.62.1.el7.x86_64 docker://20.10.14 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=tiny-calico-worker-88-12.k8s.tcinternal,kubernetes.io/os=linux,lb.kubesphere.io/v1alpha1=openelb
然后我们先把副本的数量缩容到 0。
$ kubectl scale deployment openelb-manager --replicas=0 -n openelb-system
接着修改配置,在部署节点的 nodeSelector 字段中增加我们前面新加的 labels
$ kubectl get deployment openelb-manager -n openelb-system -o yaml
...略去一堆输出...
nodeSelector:
kubernetes.io/os: linux
lb.kubesphere.io/v1alpha1: openelb
...略去一堆输出...
扩容副本数量到 3。
$ kubectl scale deployment openelb-manager --replicas=3 -n openelb-system
检查 deployment 状态
$ kubectl get po -n openelb-system -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
openelb-admission-create-fvfzk 0/1 Completed 0 78m 10.88.84.89 tiny-calico-worker-88-11.k8s.tcinternal <none> <none>
openelb-admission-patch-tlmns 0/1 Completed 1 78m 10.88.84.90 tiny-calico-worker-88-11.k8s.tcinternal <none> <none>
openelb-manager-6457bdd569-6n9zr 1/1 Running 0 62m 10.31.88.1 tiny-calico-master-88-1.k8s.tcinternal <none> <none>
openelb-manager-6457bdd569-c6qfd 1/1 Running 0 62m 10.31.88.12 tiny-calico-worker-88-12.k8s.tcinternal <none> <none>
openelb-manager-6457bdd569-gh995 1/1 Running 0 62m 10.31.88.11 tiny-calico-worker-88-11.k8s.tcinternal <none> <none>
至此就完成了 openelb-manager
的高可用部署改造。
BGP Mode
IP | Hostname |
---|---|
10.31.88.1 | tiny-calico-master-88-1.k8s.tcinternal |
10.31.88.11 | tiny-calico-worker-88-11.k8s.tcinternal |
10.31.88.12 | tiny-calico-worker-88-12.k8s.tcinternal |
10.88.64.0/18 | podSubnet |
10.88.0.0/18 | serviceSubnet |
10.89.0.0/16 | OpenELB-BGP-IPpool |
10.31.254.251 | BGP Router |
这里路由器的 AS 号为 64512,OpenELB 的 AS 号为 64154。
创建 BGP 配置
这里我们需要创建两个 CRD,分别为BgpConf[10]和BgpPeer[11],用来配置对端 BGP 路由器的信息和自己 OpenELB 的 BGP 配置。
需要特别注意的是 bgpconf 这里配置的监听端口,OpenELB 在默认情况下会同时监听 IPv4 和 IPv6 网络栈,因此需要确保集群开启了 IPv6 网络栈,否则会无法正确建立 BGP 连接。pod 内的报错日志类似
"listen tcp6 [::]:17900: socket: address family not supported by protocol"
如果 k8s 集群的 CNI 插件使用的是类似 calico 的 bgp 模式,已经使用了服务器的 179 端口,那么
listenPort
就要修改为其他端口避免冲突,这里我们修改为 17900 避免和集群的 calico 使用的 bird 冲突
apiVersion: network.kubesphere.io/v1alpha2
kind: BgpConf
metadata:
# BgpConf 对象的名称,无需设置,因此openelb只认default,设置成别的会被忽略
name: default
spec:
# openelb 的ASN,必须与BgpPeer配置中的spec:conf:peerAS值不同。
as: 64514
# OpenELB 监听的端口。默认值为179(默认 BGP 端口号)。
# 如果 Kubernetes 集群中的其他组件(例如 Calico)也使用 BGP 和 179 端口,则必须设置不同的值以避免冲突。
listenPort: 17900
# 本地路由器ID,通常设置为Kubernetes主节点的主网卡IP地址。
# 如果不指定该字段,则使用openelb-manager所在节点的第一个IP地址。
routerId: 10.31.88.1
---
apiVersion: network.kubesphere.io/v1alpha2
kind: BgpPeer
metadata:
# BgpPeer 对象的名称
name: bgppeer-openwrt-plus
spec:
conf:
# 对端BGP路由器的ASN,必须与BgpConf配置中的spec:as值不同。
peerAs: 64512
# 对端BGP路由器的IP
neighborAddress: 10.31.254.251
# 指定IP协议栈(IPv4/IPv6)。
# 不用修改,因此目前,OpenELB 仅支持 IPv4。
afiSafis:
- config:
family:
afi: AFI_IP
safi: SAFI_UNICAST
enabled: true
addPaths:
config:
# OpenELB 可以发送到对等 BGP 路由器以进行等价多路径 (ECMP) 路由的最大等效路由数。
sendMax: 10
nodeSelector:
matchLabels:
# 如果 Kubernetes 集群节点部署在不同的路由器下,并且每个节点都有一个 OpenELB 副本,则需要配置此字段,以便正确节点上的 OpenELB 副本与对端 BGP 路由器建立 BGP 连接。
# 默认情况下,所有 openelb-manager 副本都会响应 BgpPeer 配置并尝试与对等 BGP 路由器建立 BGP 连接。
openelb.kubesphere.io/rack: leaf1
然后我们部署并查看信息
$ kubectl apply -f openelb/openelb-bgp.yaml
bgpconf.network.kubesphere.io/default created
bgppeer.network.kubesphere.io/bgppeer-openwrt-plus created
$ kubectl get bgpconf
NAME AGE
default 16s
$ kubectl get bgppeer
NAME AGE
bgppeer-openwrt-plus 20s
修改 EIP
和之前的 Layer2 模式类似,我们也需要创建一个 BGP 模式使用的 Eip。
apiVersion: network.kubesphere.io/v1alpha2
kind: Eip
metadata:
# Eip 对象的名称。
name: eip-bgp-pool
spec:
# Eip 对象的地址池
address: 10.89.0.1-10.89.255.254
# openELB的运行模式,默认为bgp
protocol: bgp
# OpenELB 在其上侦听 ARP/NDP 请求的网卡。该字段仅在protocol设置为时有效layer2。
interface: eth0
# 指定是否禁用 Eip 对象
# false表示可以继续分配
# true表示不再继续分配
disable: false
status:
# 指定 Eip 对象中的IP地址是否已用完。
occupied: false
# 指定 Eip 对象中有多少个 IP 地址已分配给服务。
# 直接留空,系统会自动生成
usage:
# Eip 对象中的 IP 地址总数。
poolSize: 65534
# 指定使用的 IP 地址和使用 IP 地址的服务。服务以Namespace/Service name格式显示(例如,default/test-svc)。
# 直接留空,系统会自动生成
used:
# Eip 对象中的第一个 IP 地址。
firstIP: 10.89.0.1
# Eip 对象中的最后一个 IP 地址。
lastIP: 10.89.255.254
ready: true
# 指定IP协议栈是否为 IPv4。目前,OpenELB 仅支持 IPv4,其值只能是true.
v4: true
然后我们部署并查看信息,可以看到两个 eip 资源都存在系统中。
$ kubectl apply -f openelb/openelb-eip-bgp.yaml
eip.network.kubesphere.io/eip-bgp-pool created
[root@tiny-calico-master-88-1 tiny-calico]# kubectl get eip
NAME CIDR USAGE TOTAL
eip-bgp-pool 10.89.0.1-10.89.255.254 65534
eip-layer2-pool 10.31.88.101-10.31.88.200 3 100
配置路由器
以家里常见的 openwrt 路由器为例,我们先在上面安装 quagga 组件,当然要是使用的 openwrt 版本编译了 frr 模块[12]的话推荐使用 frr 来进行配置。
❝如果使用的是别的发行版 Linux(如 CentOS 或者 Debian)推荐直接使用 frr[13] 进行配置。
我们先在 openwrt 上面直接使用 opkg 安装 quagga
$ opkg update
$ opkg install quagga quagga-zebra quagga-bgpd quagga-vtysh
如果使用的 openwrt 版本足够新,是可以直接使用 opkg 安装 frr 组件的
$ opkg update
$ opkg install frr frr-babeld frr-bfdd frr-bgpd frr-eigrpd frr-fabricd frr-isisd frr-ldpd frr-libfrr frr-nhrpd frr-ospf6d frr-ospfd frr-pbrd frr-pimd frr-ripd frr-ripngd frr-staticd frr-vrrpd frr-vtysh frr-watchfrr frr-zebra
如果是使用 frr 记得在配置中开启 bgpd 参数再重启 frr
$ sed -i 's/bgpd=no/bgpd=yes/g' /etc/frr/daemons
$ /etc/init.d/frr restart
路由器这边我们使用 frr 进行 BGP 协议的配置,需要注意的是因为前面我们把端口修改为 17900,因此这里也需要进行同步配置节点端口为 17900。
root@tiny-openwrt-plus:~# cat /etc/frr/frr.conf
frr version 8.2.2
frr defaults traditional
hostname tiny-openwrt-plus
log file /home/frr/frr.log
log syslog
!
password zebra
!
router bgp 64512
bgp router-id 10.31.254.251
no bgp ebgp-requires-policy
!
!
neighbor 10.31.88.1 remote-as 64514
neighbor 10.31.88.1 port 17900
neighbor 10.31.88.1 description 10-31-88-1
neighbor 10.31.88.11 remote-as 64514
neighbor 10.31.88.11 port 17900
neighbor 10.31.88.11 description 10-31-88-11
neighbor 10.31.88.12 remote-as 64514
neighbor 10.31.88.12 port 17900
neighbor 10.31.88.12 description 10-31-88-12
!
!
address-family ipv4 unicast
!maximum-paths 3
exit-address-family
exit
!
access-list vty seq 5 permit 127.0.0.0/8
access-list vty seq 10 deny any
!
line vty
access-class vty
exit
!
部署测试服务
这里我们创建两个服务进行测试,需要注意和上面的 layer2 模式不同,这里的注解要同步修改为使用 bgp 和对应的 eip。
apiVersion: v1
kind: Service
metadata:
name: nginx-lb2-service
namespace: nginx-quic
annotations:
lb.kubesphere.io/v1alpha1: openelb
protocol.openelb.kubesphere.io/v1alpha1: bgp
eip.openelb.kubesphere.io/v1alpha2: eip-bgp-pool
spec:
allocateLoadBalancerNodePorts: false
externalTrafficPolicy: Cluster
internalTrafficPolicy: Cluster
selector:
app: nginx-lb
ports:
- protocol: TCP
port: 80 # match for service access port
targetPort: 80 # match for pod access port
type: LoadBalancer
---
apiVersion: v1
kind: Service
metadata:
name: nginx-lb3-service
namespace: nginx-quic
annotations:
lb.kubesphere.io/v1alpha1: openelb
protocol.openelb.kubesphere.io/v1alpha1: bgp
eip.openelb.kubesphere.io/v1alpha2: eip-bgp-pool
spec:
allocateLoadBalancerNodePorts: false
externalTrafficPolicy: Cluster
internalTrafficPolicy: Cluster
selector:
app: nginx-lb
ports:
- protocol: TCP
port: 80 # match for service access port
targetPort: 80 # match for pod access port
type: LoadBalancer
loadBalancerIP: 10.89.100.100
部署完成之后我们再查看服务的状态,可以看到没有指定 loadBalancerIP
的服务会自动按顺序分配一个可用 IP,而指定了 IP 的会分配为我们手动指定的 IP,同时 layer2 模式的也可以共存。
$ kubectl get eip -A
NAME CIDR USAGE TOTAL
eip-bgp-pool 10.89.0.1-10.89.255.254 2 65534
eip-layer2-pool 10.31.88.101-10.31.88.200 1 100
$ kubectl get svc -n nginx-quic
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
nginx-lb-service LoadBalancer 10.88.15.40 10.31.88.101 80:30972/TCP 2m48s
nginx-lb2-service LoadBalancer 10.88.60.227 10.89.0.1 80:30425/TCP 2m48s
nginx-lb3-service LoadBalancer 10.88.11.160 10.89.100.100 80:30597/TCP 2m48s
再查看路由器上面的路由表,可以看到两个 BGP 模式的 VIP 有多个下一条路由,则说明 ECMP 开启成功
tiny-openwrt-plus# show ip route
Codes: K - kernel route, C - connected, S - static, R - RIP,
O - OSPF, I - IS-IS, B - BGP, E - EIGRP, N - NHRP,
T - Table, v - VNC, V - VNC-Direct, A - Babel, F - PBR,
f - OpenFabric,
> - selected route, * - FIB route, q - queued, r - rejected, b - backup
t - trapped, o - offload failure
K>* 0.0.0.0/0 [0/0] via 10.31.254.254, eth0, 12:56:32
C>* 10.31.0.0/16 is directly connected, eth0, 12:56:32
B>* 10.89.0.1/32 [20/0] via 10.31.88.1, eth0, weight 1, 00:00:28
* via 10.31.88.11, eth0, weight 1, 00:00:28
* via 10.31.88.12, eth0, weight 1, 00:00:28
B>* 10.89.100.100/32 [20/0] via 10.31.88.1, eth0, weight 1, 00:00:28
* via 10.31.88.11, eth0, weight 1, 00:00:28
* via 10.31.88.12, eth0, weight 1, 00:00:28
最后再找一台机器进行 curl 测试
[tinychen /root]# curl 10.89.0.1
10.31.88.1:22432
[tinychen /root]# curl 10.89.100.100
10.31.88.11:59470
总结
OpenELB 的两种模式的优缺点和MetalLB[14]几乎一模一样,两者在这方面的优缺点概况文档可以结合起来一起看:
Layer2 mode 优缺点
优点:
通用性强,对比 BGP 模式不需要 BGP 路由器支持,几乎可以适用于任何网络环境;当然云厂商的网络环境例外
缺点:
所有的流量都会在同一个节点上,该节点的容易成为流量的瓶颈
当 VIP 所在节点宕机之后,需要较长时间进行故障转移(官方没说多久),这主要是因为 OpenELB 使用了 k8s 同款的选主算法来进行选主,当 VIP 所在节点宕机之后重新选主的时间要比传统的 keepalived 使用的 vrrp 协议(一般为 1s)要更长
难以定位 VIP 所在节点,OpenELB 并没有提供一个简单直观的方式让我们查看到底哪一个节点是 VIP 所属节点,基本只能通过抓包或者查看 pod 日志来确定,当集群规模变大的时候这会变得非常的麻烦
改进方案:
有条件的可以考虑使用 BGP 模式
既不能用 BGP 模式也不能接受 Layer2 模式的,基本和目前主流的三个开源负载均衡器无缘了(三者都是 Layer2 模式和 BGP 模式且原理类似,优缺点相同)
BGP mode 优缺点
BGP 模式的优缺点几乎和 Layer2 模式相反
优点:
无单点故障,在开启 ECMP 的前提下,k8s 集群内所有的节点都有请求流量,都会参与负载均衡并转发请求
缺点:
条件苛刻,需要有 BGP 路由器支持,配置起来也更复杂;
ECMP 的故障转移(failover)并不是特别地优雅,这个问题的严重程度取决于使用的 ECMP 算法;当集群的节点出现变动导致 BGP 连接出现变动,所有的连接都会进行重新哈希(使用三元组或五元组哈希),这对一些服务来说可能会有影响;
❝路由器中使用的哈希值通常 不稳定,因此每当后端集的大小发生变化时(例如,当一个节点的 BGP 会话关闭时),现有的连接将被有效地随机重新哈希,这意味着大多数现有的连接最终会突然被转发到不同的后端,而这个后端可能和此前的后端毫不相干且不清楚上下文状态信息。
改进方案:
OpenELB 官方并没有给出 BGP 模式的优缺点分析和改进方案,但是我们可以参考 MetalLB 官方给出的资料:
MetalLB 给出了一些改进方案[15],下面列出来给大家参考一下
使用更稳定的 ECMP 算法来减少后端变动时对现有连接的影响,如“resilient ECMP” or “resilient LAG”
将服务部署到特定的节点上减少可能带来的影响
在流量低峰期进行变更
将服务分开部署到两个不同的 LoadBalanceIP 的服务中,然后利用 DNS 进行流量切换
在客户端加入透明的用户无感的重试逻辑
在 LoadBalance 后面加入一层 ingress 来实现更优雅的 failover(但是并不是所有的服务都可以使用 ingress)
接受现实……(Accept that there will be occasional bursts of reset connections. For low-availability internal services, this may be acceptable as-is.)
OpenELB 优缺点
这里尽量客观的总结概况一些客观事实,是否为优缺点可能会因人而异:
OpenELB 是国内的青云科技团队主导开发的产品(也是 KubeSphere 的开发团队)
OpenELB 的部署方式并不是使用
daemonset
+deployment
的方式,而是使用deployment
+jobs.batch
,OpenELB 的
deployment
默认状态下并没有高可用部署,需要自己手动配置OpenELB 的文档非常少,比 MetalLB 的还要少,仅有少数几篇必要的文档,建议先全部阅读完再开始上手
OpenELB 对 IPv6 的支持不完善(BGP 模式下暂不支持 IPv6)
OpenELB 可以通过注解来同时部署使用BGP 模式和 Layer2 模式
OpenELB 使用了 CRD 来一定程度上改善了 IPAM 的问题,可以通过查看 EIP 的状态来查看 IP 池的分配情况和对应服务
OpenELB 在 Layer2 模式下无法快速定位当前的 VIP 所在节点
OpenELB 官方表示已经有一定的数量的 企业[16] 采用(包括生产和测试环境),但是没有具体披露使用的模式和规模
总的来说,OpenELB 是一款不错的负载均衡器,在前人 MetalLB 的基础上做了一些改进,有一定的社区热度、有一定的专业团队进行维护;但是目前感觉还处于比较初级的阶段,有较多的功能还没有开发完善,使用起来偶尔会有些不大不小的问题。青云科技官方表示会在后面的 KubeSphere v3.3.0 版本内置 OpenELB 用于服务暴露,届时应该会有更多的用户参与进来。
引用链接
[1]
方案: https://tinychen.com/tags/k8s/
[2]KubeSphere 社区: https://kubesphere.io/
[3]沙箱项目: https://www.cncf.io/sandbox-projects/
[4]OpenELB 官方: https://openelb.github.io/docs/concepts/layer-2-mode/
[5]gobgp: https://github.com/osrg/gobgp
[6]官网的图: https://openelb.github.io/docs/concepts/bgp-mode/
[7]strictARP: https://kubernetes.io/docs/reference/config-api/kube-proxy-config.v1alpha1/
[8]文章中的解释: https://tinychen.com/20200427-lvs-principle-introduction/#6、ARP-in-LVS
[9]部署多个副本: https://openelb.github.io/docs/getting-started/configuration/configure-multiple-openelb-replicas/
[10]BgpConf: https://openelb.github.io/docs/getting-started/configuration/configure-openelb-in-bgp-mode/#configure-local-bgp-properties-using-bgpconf
[11]BgpPeer: https://openelb.github.io/docs/getting-started/configuration/configure-openelb-in-bgp-mode/#configure-peer-bgp-properties-using-bgppeer
[12]编译了 frr 模块: http://docs.frrouting.org/projects/dev-guide/en/latest/building-frr-for-openwrt.html
[13]frr: https://frrouting.org/
[14]MetalLB: https://tinychen.com/20220519-k8s-06-loadbalancer-metallb/
[15]改进方案: https://metallb.universe.tf/concepts/bgp/#limitations
[16]企业: https://github.com/openelb/openelb/blob/master/ADOPTERS.md
你可能还喜欢
点击下方图片即可阅读
巧用 Kubernetes 中的 Leader 选举机制来实现自己的 HA 应用
![c00f9c563d37ba5832da436bbbb54ab5.jpeg](https://i-blog.csdnimg.cn/blog_migrate/0b68e49fb64f6a6f2b756df16564be1c.jpeg)
![633abe30926350ab4a4997bdefc9727d.jpeg](https://i-blog.csdnimg.cn/blog_migrate/48391c18d96e47595f90f68458a67ec4.jpeg)
![bb78e1f5fbf262804f992b562824bcc8.jpeg](https://i-blog.csdnimg.cn/blog_migrate/e564d916cb66af71544e82ea43e417e2.jpeg)
![91dc6e9abbca8f6f614eaa9f2b6930f1.jpeg](https://i-blog.csdnimg.cn/blog_migrate/3bf6a7204950b7802cd421c4ebad8660.jpeg)
云原生是一种信仰 🤘
关注公众号
后台回复◉k8s◉获取史上最方便快捷的 Kubernetes 高可用部署工具,只需一条命令,连 ssh 都不需要!
点击 "阅读原文" 获取更好的阅读体验!
发现朋友圈变“安静”了吗?