基於Kubernets簡單實現gRPC負載均衡

很多剛剛接觸gRPC的用戶,通常會驚訝於Kubernetes默認提供的負載均衡對於gRPC來說無法實現開箱即用的效果。比如,將一個簡單的基於Node.js實現的gRPC微服務部署在Kubernetes後,如下圖所示:

儘管選舉服務顯示存在多個pod,但是從Kubernetes的CPU圖表可以清晰的看出,只有一個pod能夠接收到流量,處於工作狀態。這是爲什麼?

在本文中,會解釋該現象對應的原因,以及如何在任意Kubernetes應用上修復gRPC負載均衡存在的問題。這需要增加名爲Linkerd的服務網格(service mesh),同時也是一種服務sidecar。

爲什麼gRPC需要額外的負載均衡設置?

首先需要一起來了解下,爲什麼gRPC需要額外的設置。

gRPC正逐漸成爲應用開發者的常見選擇。相比於其他服務協議,如基於HTTP的JSON,gRPC能夠提供更好的特性,包括更低的(反)序列化開銷、自動類型檢查、格式化API,以及更小的TCP管理開銷。

但是,gRPC也打破了標準的連接層負載均衡約定,也就是Kubernetes中默認提供的負載均衡方式。這是因爲gRPC是基於HTTP/2構建,而HTTP/2是面向單個TCP長連接進行設計的,全部的請求都會複用這一個TCP連接。通常情況下,這種方式很棒,因爲這樣可以減少連接管理的開銷。但是,這也意味着(讀者也可以想象到)連接層的負載均衡會失效。一旦連接創建之後,就不會再次觸發負載均衡。指向某個pod的全部請求會封裝在相同的連接之中,如下圖所示:

爲什麼HTTP/1.1不受影響?

在HTTP/1.1中也存在相同的長連接概念,但卻不存在類似問題。這是因爲HTTP/1.1某些特性會使得TCP連接被回收。正因如此,連接層負載均衡對於HTTP/1.1來說就夠用了,無需其他特殊配置。

爲了理解其中原理,需要深入瞭解一下HTTP/1.1。與HTTP/2不同,HTTP/1.1不支持請求複用。每個TCP連接在同一時間只能處理一個HTTP請求。假設客戶端發起請求 GET /foo,需要一直等待直到服務端返回。在一個請求–應答週期內,該連接不能處理其他請求。

通常情況下人們都希望多個請求能夠併發處理。因此爲了實現HTTP/1.1下的併發請求,需要創建多個HTTP/1.1連接,基於這些連接來發送全部請求。此外,HTTP/1.1中的長連接在一段時間後是會過期的,過期連接會被客戶端(或者服務端)銷燬。在這兩點共同作用下,HTTP/1.1請求會在多個TCP連接之間進行循環,所以連接層的負載均衡纔會生效。

gRPC如何實現負載均衡?

回過頭來看一下gRPC。因爲不能在連接層實現負載均衡,所以需要在應用層來完成。換句話說,需要爲每個目標地址創建一個HTTP/2連接,對請求實現負載均衡,如下所示:

在網絡模型中,這意味着需要實現L5/L7層的負載均衡,而不是L3/L4。這樣就需要感知TCP連接上傳輸的協議格式。

如何實現?有這麼幾種選擇。第一種,可以在應用之中手工創建並維護目標地址的連接池,通過配置gRPC客戶端使用該連接池來實現。這種方式控制起來最靈活,但是在Kubernetes這種環境中實現起來非常複雜,因爲Kubernetes每次進行pod層面的調度,連接池都需要進行相應變更。應用需要監控Kubernetes的API,並與pod信息保持實時同步。

在Kubernetes中,還有另外一種方式,是將服務按照無狀態方式進行部署。在該情況下,Kubernetes會在DNS中爲服務創建多條記錄。如果所使用的gRPC客戶端足夠先進,就能通過上述多個DNS記錄來實現負載均衡。但是這種實現方式依賴於使用特定的gRPC客戶端,並且只能使用無狀態模式部署服務。

最後,還有第三種方式:使用一個輕量級代理。

在Kubernetes上使用Linkerd實現gRPC負載均衡

Linkerd是一種基於CNCF的Kubernetes服務網格。Linkerd通過sidecar的方式來實現負載均衡,可以單獨部署到某個服務,甚至不需要集羣許可。這意味着爲服務添加Linkerd,等價於爲每個pod添加了一個很小並且很高效的代理,由這些代理來監控Kubernetes的API,並自動實現gRPC的負載均衡。具體部署方式如下:

使用Linkerd有如下幾個好處。首先,Linkerd支持任何語言下的gRPC客戶端,也支持每種部署模式(無狀態部署或者有狀態部署)。這是因爲Linkerd代理是在傳輸層完成,會自動對HTTP/2和HTTP/1.x進行檢測,並實現L7層的負載均衡,而對其他的流量不做任何處理。這意味着不會產生額外的影響。

其次,Linkerd負載均衡是非常複雜的。除了需要監聽Kubernetes API,並且在pod發生調度後自動更新負載均衡的連接池之外,Linkerd還會根據響應的延遲,使用指數級權重對請求進行調整,優先將請求發送到相應延遲低的pod。如果某個pod響應很慢,甚至只是偶然抖動,Linkerd都會將其上的流量摘掉。這樣做可以減少端到端的延遲。

最後,Linkerd基於rust實現的代理體積非常小,並且速度快的難以置信。Linkerd聲稱百分之99的請求開銷小於1ms,並且對於pod上物理內存的佔用小於10mb。這意味着Linkerd對於系統性能的影響可以忽略不計。

60s實現gRPC負載均衡

測試Linkerd非常簡單。只需要按照Linkerd入門介紹即可。在筆記本上安裝CLI,在集羣上安裝控制層,然後對服務“網格化”(將代理注入每個pod)。Linkerd立刻就能在服務中生效,並實現合理的gRPC路由。

安裝Linkerd之後,再看下選舉服務:

可以看到,CPU圖表中每個pod都被激活,表示每個pod都開始接受流量。而實現這些無需改動任何一行代碼。哈哈,Linkerd就像有魔力一般,實現了gRPC負載均衡。

Linkerd也提供了內置的流量大盤,所以也無需猜測CPU圖表中的變化究竟代表着什麼。下面就是Linkerd的大盤,包括每個pod的成功率,請求大小,以及延遲。

可以看到每個pod每秒大概處理5個請求。同時通過大盤還能發現,服務成功率還存在一些問題需要解決。(在示例應用中,特意構造了一些異常,方便爲讀者演示可以通過Linkerd大盤來觀察服務質量!)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章