本文發表於一年多前。舊文章可能包含過時內容。請檢查頁面中的資訊自發布以來是否已變得不正確。

在 Kubernetes 上輕鬆實現 gRPC 負載均衡

許多 gRPC 新使用者驚訝地發現,Kubernetes 的預設負載均衡通常無法與 gRPC 直接配合使用。例如,當您將一個簡單的 gRPC Node.js 微服務應用部署到 Kubernetes 上時,就會出現以下情況:

雖然此處顯示的 `voting` 服務有多個 Pod,但從 Kubernetes 的 CPU 圖表可以清楚地看出,只有一個 Pod 實際在工作——因為只有一個 Pod 接收到任何流量。為什麼會這樣?

在這篇部落格文章中,我們將描述為什麼會發生這種情況,以及如何透過使用 Linkerd(一個 CNCF 服務網格和服務 Sidecar)為任何 Kubernetes 應用新增 gRPC 負載均衡來輕鬆解決此問題。

為什麼 gRPC 需要特殊的負載均衡?

首先,讓我們瞭解為什麼我們需要為 gRPC 做一些特別的事情。

gRPC 越來越成為應用程式開發人員的常見選擇。與 JSON-over-HTTP 等替代協議相比,gRPC 可以提供一些顯著優勢,包括顯著降低(反)序列化成本、自動型別檢查、規範化 API 以及更少的 TCP 管理開銷。

然而,gRPC 也破壞了標準的連線級負載均衡,包括 Kubernetes 提供的負載均衡。這是因為 gRPC 基於 HTTP/2,而 HTTP/2 的設計理念是使用單個長期存在的 TCP 連線,所有請求都透過該連線進行*多路複用*——這意味著在任何時間點,同一連線上都可以有多個請求處於活動狀態。通常情況下,這非常好,因為它減少了連線管理的開銷。然而,這也意味著(正如你可能想象的那樣)連線級均衡用處不大。一旦連線建立,就不再需要進行均衡了。所有請求都將被固定到單個目標 Pod,如下所示:

為什麼這對 HTTP/1.1 沒有影響?

這個問題沒有發生在 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。由於我們不能在連線層面進行均衡,為了實現 gRPC 負載均衡,我們需要從連線均衡轉向**請求**均衡。換句話說,我們需要向每個目標開啟一個 HTTP/2 連線,並在這些連線之間均衡**請求**,如下所示:

從網路術語來看,這意味著我們需要在 L5/L7 而不是 L3/L4 層面做出決策,也就是說,我們需要理解透過 TCP 連線傳送的協議。

我們如何實現這一點?有幾種選擇。首先,我們的應用程式程式碼可以手動維護自己的目標負載均衡池,我們可以配置 gRPC 客戶端使用此負載均衡池。這種方法提供了最大的控制權,但在 Kubernetes 這樣的環境中可能會非常複雜,因為隨著 Kubernetes 重新排程 Pod,池會隨時間變化。我們的應用程式必須監控 Kubernetes API 並保持與 Pod 的同步。

或者,在 Kubernetes 中,我們可以將應用程式部署為無頭服務。在這種情況下,Kubernetes 將為服務建立多個 A 記錄到 DNS 條目中。如果我們的 gRPC 客戶端足夠高階,它可以透過這些 DNS 條目自動維護負載均衡池。但這種方法限制了我們只能使用某些 gRPC 客戶端,而且通常不可能只使用無頭服務。

最後,我們可以採取第三種方法:使用一個輕量級代理。

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

Linkerd 是一個由 CNCF 託管的 Kubernetes *服務網格*。與我們的目的最相關的是,Linkerd 還可以作為*服務 Sidecar*,可以應用於單個服務——即使沒有叢集範圍的許可權。這意味著,當我們向服務新增 Linkerd 時,它會向每個 Pod 新增一個微小、超快的代理,這些代理會監視 Kubernetes API 並自動進行 gRPC 負載均衡。我們的部署看起來就像這樣:

使用 Linkerd 有幾個優點。首先,它適用於任何語言編寫的服務、任何 gRPC 客戶端和任何部署模型(無頭或非無頭)。由於 Linkerd 的代理是完全透明的,它們會自動檢測 HTTP/2 和 HTTP/1.x 並進行 L7 負載均衡,並將所有其他流量作為純 TCP 傳遞。這意味著一切都會**正常工作**。

其次,Linkerd 的負載均衡非常複雜。Linkerd 不僅會監視 Kubernetes API 並隨著 Pod 重新排程自動更新負載均衡池,而且還會使用**指數加權移動平均**的響應延遲來自動將請求傳送到最快的 Pod。如果一個 Pod 速度變慢,即使是暫時性的,Linkerd 也會將流量從它移開。這可以減少端到端尾部延遲。

最後,Linkerd 基於 Rust 的代理速度驚人且體積小巧。它們引入的 p99 延遲小於 1 毫秒,每個 Pod 需要的 RSS 小於 10MB,這意味著對系統性能的影響可以忽略不計。

60 秒內實現 gRPC 負載均衡

Linkerd 非常容易嘗試。只需按照Linkerd 入門說明中的步驟操作——在您的筆記型電腦上安裝 CLI,在您的叢集上安裝控制平面,然後“網格化”您的服務(將代理注入每個 Pod)。您將很快在您的服務上執行 Linkerd,並應立即看到正確的 gRPC 均衡。

讓我們再次看看我們的示例 `voting` 服務,這次是在安裝 Linkerd 之後:

正如我們所看到的,所有 Pod 的 CPU 圖表都處於活動狀態,這表明所有 Pod 現在都在處理流量——無需更改一行程式碼。瞧,gRPC 負載均衡就像魔術一樣!

Linkerd 還提供了內建的流量儀表板,因此我們不再需要從 CPU 圖表中猜測發生了什麼。以下是 Linkerd 圖表,顯示了每個 Pod 的成功率、請求量和延遲百分位數:

我們可以看到每個 Pod 正在獲得大約 5 RPS。我們還可以看到,雖然我們已經解決了負載均衡問題,但我們還需要在服務成功率方面做一些工作。(演示應用程式故意設定了故障——作為讀者的練習,看看你是否可以透過使用 Linkerd 儀表板來找出它!)

總結

如果您有興趣以一種簡單易行的方式為您的 Kubernetes 服務新增 gRPC 負載均衡,無論它使用何種語言編寫、使用何種 gRPC 客戶端或如何部署,您都可以使用 Linkerd 通過幾個命令新增 gRPC 負載均衡。

Linkerd 還有很多其他功能,包括安全性、可靠性、除錯和診斷功能,但這些將在未來的部落格文章中討論。

想了解更多嗎?我們非常歡迎您加入我們快速壯大的社群!Linkerd 是一個 CNCF 專案,託管在 GitHub 上,並在 SlackTwitter郵件列表上擁有一個活躍的社群。快來加入我們吧!