在 Kubernetes 叢集中使用 MetalLB 作為 Load Balancer(上)

語言: CN / TW / HK

TL;DR

網路方面的知識又多又雜,很多又是系統核心的部分。原本自己不是做網路方面的,系統核心知識也薄弱。但恰恰是這些陌生的內容滿滿的誘惑,加上現在的工作跟網路關聯更多了,逮住機會就學習下。

這篇以 Kubernetes LoadBalancer 為起點,使用 MetalLB 去實現叢集的負載均衡器,在探究其工作原理的同時瞭解一些網路的知識。

由於 MetalLB 的內容有點多,一步步來,今天這篇僅介紹其中簡單又容易理解的部分,不出意外還會有下篇(太複雜,等我搞明白先 :D)。

LoadBalancer 型別 Service

由於 Kubernets 中 Pod 的 IP 地址不固定,重啟後 IP 會發生變化,無法作為通訊的地址。Kubernets 提供了 Service 來解決這個問題,對外暴露。

Kubernetes 為一組 Pod 提供相同的 DNS 名和虛擬 IP,同時還提供了負載均衡的能力。這裡 Pod 的分組通過給 Pod 打標籤( Label )來完成,定義 Service 時會宣告標籤選擇器( selector )將  Service 與 這組 Pod 關聯起來。

根據使用場景的不同,Service 又分為 4 種類型: ClusterIPNodePortLoadBalancer 和  ExternalName ,預設是  ClusterIP 。這裡不一一詳細介紹,有興趣的檢視  Service 官方文件 [1]

除了今天的主角 LoadBalancer 外,其他 3 種都是比較常用的型別。 LoadBalancer 官方的解釋是:

使用雲提供商的負載均衡器向外部暴露服務。外部負載均衡器可以將流量路由到自動建立的 NodePort 服務和  ClusterIP 服務上。

lb-service

看到“雲提供商提供”幾個字時往往望而卻步,有時又需要 LoadBalancer 對外暴露服務做些驗證工作(雖然除了 7 層的 Ingress 以外,還可以使用 NodePort 型別的 Service),而 Kubernetes 官方並沒有提供實現。比如下面要介紹的  MetalLB [2] 就是個不錯的選擇。

MetalLB 介紹

MetalLB 是裸機 Kubernetes 叢集的負載均衡器實現,使用標準路由協議。

注意:MetalLB 目前還是 beta 階段。

前文提到 Kubernetes 官方並沒有提供 LoadBalancer 的實現。各家雲廠商有提供實現,但假如不是執行在這些雲環境上,建立的  LoadBalancer Service 會一直處於  Pending 狀態(見下文 Demo 部分)。

MetalLB 提供了兩個功能:

  • 地址分配:當建立  LoadBalancer Service 時,MetalLB 會為其分配 IP 地址。這個 IP 地址是從 預先配置的 IP 地址庫 獲取的。同樣,當 Service 刪除後,已分配的 IP 地址會重新回到地址庫。

  • 對外廣播:分配了 IP 地址之後,需要讓叢集外的網路知道這個地址的存在。MetalLB 使用了標準路由協議實現:ARP、NDP 或者 BGP。

廣播的方式有兩種,第一種是 Layer 2 模式,使用 ARP(ipv4)/NDP(ipv6) 協議;第二種是 BPG。

今天主要介紹簡單的 Layer 2 模式 ,顧名思義是 OSI 二層的實現。

具體實現原理,看完 Demo 再做分析,等不及的同學請直接跳到最後。

執行時

MetalLB 執行時有兩種工作負載:

  • Controler: Deployment ,用於監聽 Service 的變更,分配/回收 IP 地址。

  • Speaker: DaemonSet ,對外廣播 Service 的 IP 地址。

Demo

安裝之前介紹下網路環境,Kubernetes 使用 K8s 安裝在 Proxmox 的虛擬機器 [3] 上。

安裝 K3s

安裝 K3s,這裡需要通過 --disable servicelb 禁用 k3s 預設的 servicelb。

參考 K3s 文件 [4] ,預設情況下 K3s 使用  Traefik [5] ingress 控制器 和  Klipper [6] Service 負載均衡器來對外暴露服務。

curl -sfL https://get.k3s.io | sh -s - --disable traefik --disable servicelb --write-kubeconfig-mode 644 --write-kubeconfig ~/.kube/config

建立工作負載

使用 nginx 映象,建立兩個工作負載:

kubectl create deploy nginx --image nginx:latest --port 80 -n default
kubectl create deploy nginx2 --image nginx:latest --port 80 -n default

同時為兩個 Deployment 建立 Service,這裡型別選擇 LoadBalancer

kubectl expose deployment nginx --name nginx-lb --port 8080 --target-port 80 --type LoadBalancer -n default
kubectl expose deployment nginx2 --name nginx2-lb --port 8080 --target-port 80 --type LoadBalancer -n default

檢查 Service 發現狀態都是 Pending 的,這是因為安裝 K3s 的時候我們禁用了 LoadBalancer 的實現:

kubectl get svc -n default
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.43.0.1 <none> 443/TCP 14m
nginx-lb LoadBalancer 10.43.108.233 <pending> 8080:31655/TCP 35s
nginx2-lb LoadBalancer 10.43.26.30 <pending> 8080:31274/TCP 16s

這時就需要 MetalLB 登場了。

安裝 MetalLB

使用官方提供 manifest 來安裝,目前最新的版本是 0.12.1 。此外,還可以其他安裝方式供選擇,比如  Helm [7]Kustomize [8] 或者  MetalLB Operator [9]

kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.12.1/manifests/namespace.yaml
kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.12.1/manifests/metallb.yaml

kubectl get po -n metallb-system
NAME READY STATUS RESTARTS AGE
speaker-98t5t 1/1 Running 0 22s
controller-66445f859d-gt9tn 1/1 Running 0 22s

此時再檢查 LoadBalancer Service 的狀態仍然是 Pending 的,嗯?因為,MetalLB 要為 Service 分配 IP 地址,但 IP 地址不是憑空來的,而是需要預先提供一個地址庫。

這裡我們使用 Layer 2 模式,通過  Configmap 為其提供一個 IP 段:

apiVersion: v1
kind: ConfigMap
metadata:
namespace: metallb-system
name: config
data:
config: |
address-pools:
- name: default
protocol: layer2
addresses:
- 192.168.1.30-192.168.1.49

此時再檢視 Service 的狀態,可以看到 MetalLB 為兩個 Service 分配了 IP 地址 192.168.1.30192.168.1.31

kubectl get svc -n default
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.43.0.1 <none> 443/TCP 28m
nginx-lb LoadBalancer 10.43.201.249 192.168.1.30 8080:30089/TCP 14m
nginx2-lb LoadBalancer 10.43.152.236 192.168.1.31 8080:31878/TCP 14m

可以請求測試下:

curl -I 192.168.1.30:8080
HTTP/1.1 200 OK
Server: nginx/1.21.6
Date: Wed, 02 Mar 2022 15:31:15 GMT
Content-Type: text/html
Content-Length: 615
Last-Modified: Tue, 25 Jan 2022 15:03:52 GMT
Connection: keep-alive
ETag: "61f01158-267"
Accept-Ranges: bytes

curl -I 192.168.1.31:8080
HTTP/1.1 200 OK
Server: nginx/1.21.6
Date: Wed, 02 Mar 2022 15:31:18 GMT
Content-Type: text/html
Content-Length: 615
Last-Modified: Tue, 25 Jan 2022 15:03:52 GMT
Connection: keep-alive
ETag: "61f01158-267"
Accept-Ranges: bytes

macOS 本地使用 arp -a 檢視 ARP 表可以找到這兩個 IP 及 mac 地址,可以看出兩個 IP 都繫結在同一個網絡卡上,此外還有虛擬機器的 IP 地址。也就是說 3 個 IP 繫結在該虛擬機器的  en0 上:

而去虛擬機器(節點)檢視網絡卡(這裡只能看到系統繫結的 IP):

Layer 2 工作原理

Layer 2 中的 Speaker 工作負載是 DeamonSet 型別,在每臺節點上都排程一個 Pod。首先,幾個 Pod 會先進行選舉,選舉出 LeaderLeader 獲取所有  LoadBalancer 型別的 Service,將已分配的 IP 地址繫結到當前主機到網絡卡上。 也就是說,所有  LoadBalancer  型別的 Service 的 IP 同一時間都是繫結在同一臺節點的網絡卡上。

當外部主機有請求要發往叢集內的某個 Service,需要先確定目標主機網絡卡的 mac 地址(至於為什麼,參考 維基百科 [10] )。這是通過傳送 ARP 請求, Leader 節點的會以其 mac 地址作為響應。外部主機會在本地 ARP 表中快取下來,下次會直接從 ARP 表中獲取。

請求到達節點後,節點再通過 kube-proxy 將請求負載均衡目標 Pod。所以說,假如Service 是多 Pod 這裡有可能會再跳去另一臺主機。

sequence

優缺點

優點很明顯,實現起來簡單(相對於另一種 BGP 模式下路由器要支援 BPG)。就像筆者的環境一樣,只要保證 IP 地址庫與叢集是同一個網段即可。

當然缺點更加明顯了, Leader 節點的頻寬會成為瓶頸;與此同時,可用性欠佳,故障轉移需要 10 秒鐘的時間( 每個 speaker 程序有個 10s 的迴圈 [11] )。

參考

[1] 

Service 官方文件: https://kubernetes.io/zh/docs/concepts/services-networking/service/#publishing-services-service-types

[2] 

MetalLB: https://metallb.universe.tf

[3] 

Proxmox 的虛擬機器: https://atbug.com/deploy-vm-on-proxmox-with-terraform/

[4] 

K3s 文件: https://rancher.com/docs/k3s/latest/en/networking/

[5] 

Traefik: https://rancher.com/docs/k3s/latest/en/networking/

[6] 

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

[7] 

Helm: https://metallb.universe.tf/installation/#installation-with-helm

[8] 

Kustomize: https://metallb.universe.tf/installation/#installation-with-kustomize

[9] 

MetalLB Operator: https://metallb.universe.tf/installation/#using-the-metallb-operator

[10] 

維基百科: https://zh.wikipedia.org/wiki/%E5%9C%B0%E5%9D%80%E8%A7%A3%E6%9E%90%E5%8D%8F%E8%AE%AE

[11] 

每個 speaker 程序有個 10s 的迴圈: https://github.com/metallb/metallb/blob/main/internal/layer2/announcer.go#L51

[12] 

地址解析協議: https://zh.wikipedia.org/wiki/%E5%9C%B0%E5%9D%80%E8%A7%A3%E6%9E%90%E5%8D%8F%E8%AE%AE

[13] 

MetalLB 概念: https://metallb.universe.tf/concepts/