雲原生負載均衡器之 OpenELB 使用指南
本文主要在 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 模式的工作原理:
-
圖中有一個類型爲 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。
-
圖中有一個類型爲 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 請求,而由 MetalLB 接手處理。
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
雲原生實驗室 戰略上藐視雲原生,戰術上重視雲原生
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/9uxSmtnL2WadYybCQzl_Fg