Kubernetes 有個很方便的地方:只要修改 deployment 的 spec.replicas
數字,就能橫向擴展 pod,以應付更大的流量負載需求。
這一招,對於 stateless 的 HTTP 服務很管用,也是 Kubernetes 入門教學愛用的例子。但是,對於 gRPC 呢?
gRPC 是以 HTTP/2 作為傳輸協定,而 HTTP/2 的特點是持久連線:
One connection per origin
all HTTP/2 connections are persistent, and only one connection per origin is required, which offers numerous performance benefits. – Introduction to HTTP/2
既然 gRPC 會盡可能維持既有連線,不會傻傻地一直斷線與重連,那麼,當 Kubernetes 橫向擴展 pod 了,gRPC 流量是否也能順便平均分散到新長出來的 pod 身上,還是死咬著原本的連線不放?
做個簡單的實驗吧。
實驗環境
實驗程式放在 https://github.com/William-Yeh/grpc-lb
實驗所需環境:
實驗進行之前,請先編譯所需的執行檔及容器映像檔:
# Generate native binaries in `out` directory:
./build.sh
# Generate Docker images:
skaffold build
會產生以下程式:
-
執行檔
out/server
& 映像檔addr-server
:接收 HTTP 及 gRPC 連線,傳回自己的 IP 位址。 -
執行檔
client-http
& 映像檔addr-client-http
:透過 HTTP 查詢 server 位址。 -
執行檔
client-grpc
& 映像檔addr-client-grpc
:透過 gRPC 查詢 server 位址。
實驗一:純 Kubernetes 模式
首先實驗看看,若 server 程式跑在 Kubernetes 裡,除了 HTTP 流量會自動負載均衡之外,是否 gRPC 流量也享有同等待遇。
為了方便一眼看出整體狀況,請將終端機面板配置如下:
請依照以下步驟進行實驗:
-
建立
grpc-lb
namespace:kubectl create ns grpc-lb
-
在
grpc-lb
裡執行 server,此時spec.replicas
為1
:skaffold dev -n grpc-lb
-
執行 client-http,透過 HTTP 連線到 server:
out/client-http http://127.0.0.1:30080/addr
-
執行 client-grpc,透過 gRPC 連線到 server:
out/client-grpc localhost:30051
因為現在 server pod 只有一個,所以 client-http 及 client-grpc 都只會連線到同一個 IP 位址(即此例的 10.1.0.8
)。
現在,讓我們把 server pod 的 spec.replicas
數字擴展成 5
:
kubectl scale -n grpc-lb \
--replicas=5 deployment/addr-server
如下圖所示,當 server pod 數目從 1 變成 5,Kubernetes 的確開始將 HTTP 流量平均分配到 5 個 pods 身上(即此例的 10.1.0.8
~ 10.1.0.12
)。可是 gRPC 流量卻仍然綁在舊的那一個 pod 身上(即此例的 10.1.0.8
)。
可見,Kubernetes 並沒有對 gRPC 進行負載均衡。
為什麼?
L4 vs L7 負載均衡
根據 “gRPC Load Balancing inside Kubernetes” 一文所述,Kubernetes 原生的負載均衡機制,是建立在 L4:
When you create a Service in Kubernetes, it creates a layer 4 proxy and load balance connections to your pods using iptables, the service endpoint is one IP and a port hiding your real pods.
因此,對於不具備真正持久連線的 HTTP 來說,Kubernetes 原生的 L4 負載均衡機制足以應付 1;但是,對於真正具有持久連線的 gRPC,L4 就行不通了。
文中列出三種解決方法:
-
Client 端的負載均衡:叫 client 維持一個 gRPC connection pool,主動與每一個 gRPC server pods 都維持連線。缺點是:必須修改 client 程式碼,也暴露 server pods 的一些內部細節。
-
Kubernetes edge 端的負載均衡:透過 ingress 之類的機制。
-
Kubernetes service mesh:透過 sidecar 之類的機制。
為了簡單起見,我參考 Kubernetes 官網文章 “gRPC Load Balancing on Kubernetes without Tears” 的做法,用 Linkerd 2 這個 service mesh 方案來實驗。 2
實驗二:Service mesh 模式
根據 Linkerd 官網 FAQ 所述:
Linkerd 1.x is built on the “Twitter stack”: Finagle, Netty, Scala, and the JVM.
Linkerd 2.x is built in Rust and Go. It is significantly faster and lighter weight than 1.x, but currently only supports Kubernetes.
Linkerd 2 比前一版有長足的進步,又是 SMI 陣營的一員,頗值得一試。
Linkerd 前置作業
-
安裝 Linkerd 2 到現行的 Kubernetes cluster:
# Install linkerd install | kubectl apply -f - # Check linkerd check
-
先刪掉舊的
grpc-lb
namespace:kubectl delete ns grpc-lb
-
建立新的
grpc-lb
namespace,並注入 Linkerd:kubectl apply -f ns.yml
-
檢查一下
grpc-lb
namespace 的 data plane 是否正常:linkerd -n grpc-lb check --proxy
我們把 grpc-lb
namespace 圈成一塊 service mesh 疆域。如果一切順利,在 grpc-lb
namespace 裡面的東西,會自動被 Linkerd 接管——包括 gRPC 負載均衡。
Replicas = 1
首先實驗看看,若我們將 addr-client-grpc 與 server 都放在 service mesh 疆域內,兩者間的 gRPC 流量是否會自動負載均衡。我也保留一份舊的 client-grpc 故意「不」讓它跑在 Kubernetes 裡面,作為對照組。
為了方便一眼看出整體狀況,請將終端機面板配置如下:
請依照以下步驟進行實驗:
-
移掉
skaffold.yaml
這一行的註解符號:deploy: kubectl: manifests: - server.yml #- client-grpc.yml ← 請讓這一行生效!
-
在
grpc-lb
namespace 裡執行 addr-client-grpc 及 server,此時 server 的spec.replicas
為1
:skaffold dev -n grpc-lb
-
等前一個步驟跑到穩定狀態之後,在 Kubernetes 外面也執行一份舊的 client-grpc 作為對照組:
out/client-grpc localhost:30051
此時,因為 server pod 只有一個,所以,位於 service mesh 疆域內的 addr-client-grpc 及疆域外的 client-grpc 都只會連線到同一個 IP 位址(即此例的 10.1.0.71
)。
Replicas = 5
一切就緒,讓我們把 server pod 的 spec.replicas
數字擴展成 5
:
kubectl scale -n grpc-lb \
--replicas=5 deployment/addr-server
如下圖所示,當 server pod 數目從 1 變成 5,Kubernetes 的確開始將 service mesh 疆域內 addr-client-grpc 產生的 gRPC 流量平均分配到 5 個 pods 身上(即此例的 10.1.0.71
~ 10.1.0.75
)。可是「不」位於 service mesh 疆域的 client-grpc 產生的 gRPC 流量,就仍然綁在舊的那一個 pod 身上(即此例的 10.1.0.71
)。
Linkerd 儀表板
讓我們看看酷炫一點的東西吧!請用以下命令打開 Linkerd 儀表板:
linkerd dashboard &
請切換到 grpc-lb
namespace,觀察三個角色之間的網路互連架構,並檢視一些效率指標。如下圖所示,Linkerd 2.7.0 對於 gRPC load balancing 的表現很不錯,即使打開 mesh sidecar,P99 latency 也仍然壓在 10ms 左右的水準:
從下圖也可以看到 5 個 server pods 都有輪流服務到 addr-client-grpc。其中,最後一個 pod 比較可憐,湊巧服務到獨佔連線的外部 client-grpc:
簡單的實驗,示範在 Kubernetes 裡,可以透過 service mesh 替 gRPC 加上負載均衡機制。