使用 API 流增強 Kubernetes API 伺服器效率

高效管理 Kubernetes 叢集至關重要,尤其是隨著叢集規模的不斷增長。大型叢集面臨的一個重大挑戰是由 list 請求引起的記憶體開銷。

在現有的實現中,kube-apiserver 處理 list 請求時,會在將任何資料傳輸給客戶端之前,先在記憶體中組裝整個響應。但如果響應體非常大,比如幾百兆位元組呢?此外,想象一下這樣的場景:多個 list 請求同時湧入,可能是在短暫的網路中斷之後。雖然 API 優先順序和公平性已被證明可以合理地保護 kube-apiserver 免受 CPU 過載的影響,但它對記憶體保護的影響明顯較小。這可以用單個 API 請求對資源消耗的不同性質來解釋——CPU 使用率在任何給定時間都受一個常數的限制,而記憶體是不可壓縮的,可以與處理物件的數量成比例增長且無上限。這種情況構成了真正的風險,可能會在幾秒鐘內因記憶體不足(OOM)情況而壓垮並導致任何 kube-apiserver 崩潰。為了更好地理解這個問題,讓我們看看下面的圖表。

Monitoring graph showing kube-apiserver memory usage

該圖顯示了 kube-apiserver 在一次綜合測試期間的記憶體使用情況。(更多細節請參見綜合測試部分)。結果清楚地表明,增加 Informer 的數量會顯著增加伺服器的記憶體消耗。值得注意的是,在大約 16:40,伺服器在僅服務 16 個 Informer 時就崩潰了。

為什麼 kube-apiserver 會為 list 請求分配如此多的記憶體?

我們的調查發現,這種大量的記憶體分配是因為伺服器在向客戶端傳送第一個位元組之前必須:

  • 從資料庫中獲取資料,
  • 將資料從其儲存格式反序列化,
  • 最後,透過將資料轉換並序列化為客戶端請求的格式來構建最終響應。

這一系列操作導致了大量的臨時記憶體消耗。實際使用量取決於許多因素,如頁面大小、應用的過濾器(例如標籤選擇器)、查詢引數以及單個物件的大小。

不幸的是,無論是 API 優先順序和公平性、Golang 的垃圾回收,還是 Golang 的記憶體限制,都無法防止系統在這種情況下耗盡記憶體。記憶體會突然且迅速地被分配,僅僅幾個請求就可能迅速耗盡可用記憶體,導致資源枯竭。

根據 API 伺服器在節點上的執行方式,它可能因在這些不受控制的峰值期間超出配置的記憶體限制而被核心透過 OOM 殺死,或者如果沒有配置限制,它可能對控制平面節點產生更壞的影響。最糟糕的是,在第一個 API 伺服器出現故障後,相同的請求很可能會命中高可用性設定中的另一個控制平面節點,並可能產生相同的影響。這可能是一種難以診斷和恢復的情況。

流式 list 請求

今天,我們激動地宣佈一項重大改進。隨著 watch list 功能在 Kubernetes 1.32 中升級到 Beta 版,client-go 使用者可以透過將 list 請求切換為(一種特殊的)watch 請求來選擇性加入(在明確啟用 WatchListClient 特性門控後)流式列表。

Watch 請求由 watch cache 提供服務,這是一個旨在提高讀取操作可伸縮性的記憶體快取。透過單獨流式傳輸每個專案而不是返回整個集合,新方法保持了恆定的記憶體開銷。API 伺服器受 etcd 中允許的單個物件最大大小加上一些額外分配的限制。與傳統的 list 請求相比,這種方法極大地減少了臨時記憶體使用,確保了系統更高效、更穩定,尤其是在給定型別的物件數量眾多或平均物件較大的叢集中,儘管使用了分頁,記憶體消耗仍然很高。

基於從綜合測試中獲得的見解(見綜合測試),我們開發了一個自動化的效能測試,以系統地評估 watch list 功能的影響。這個測試複製了相同的場景,生成大量帶有大負載的 Secret,並擴充套件 Informer 的數量來模擬繁重的 list 請求模式。自動化測試會定期執行,以監控在啟用和停用該功能時伺服器的記憶體使用情況。

結果顯示,啟用 watch list 功能後有了顯著的改進。當該功能開啟時,kube-apiserver 的記憶體消耗穩定在大約 2 GB。相比之下,當該功能停用時,記憶體使用量增加到大約 20 GB,增長了 10 倍!這些結果證實了新的流式 API 的有效性,它減少了臨時記憶體佔用。

為你的元件啟用 API 流式傳輸

升級到 Kubernetes 1.32。確保你的叢集使用 etcd 3.4.31+ 或 3.5.13+ 版本。更改你的客戶端軟體以使用 watch list。如果你的客戶端程式碼是用 Golang 編寫的,你需要為 client-go 啟用 WatchListClient。有關啟用該功能的詳細資訊,請閱讀為 Client-Go 引入特性門控:增強靈活性和控制力

接下來是什麼?

在 Kubernetes 1.32 中,該功能在 kube-controller-manager 中預設啟用,儘管它仍處於 Beta 狀態。這將最終擴充套件到其他核心元件,如 kube-scheduler 或 kubelet;一旦該功能普遍可用,如果不是更早的話。我們鼓勵其他第三方元件在 Beta 階段選擇加入該功能,尤其是當它們有可能訪問大量資源或具有潛在大型物件的型別時。

目前,API 優先順序和公平性list 請求分配了一個合理的小成本。這是必要的,以便為 list 請求足夠廉價的平均情況提供足夠的並行性。但這與許多大型物件的突發異常情況不匹配。一旦 Kubernetes 生態系統的大部分切換到 watch listlist 成本估算可以改為更大的值,而不會在平均情況下降低效能,從而增加對未來可能仍然會衝擊 API 伺服器的這類請求的保護。

綜合測試

為了重現這個問題,我們進行了一項手動測試,以瞭解 list 請求對 kube-apiserver 記憶體使用的影響。在測試中,我們建立了 400 個 Secret,每個包含 1MB 的資料,並使用 Informer 來檢索所有 Secret。

結果令人震驚,僅 16 個 Informer 就足以導致測試伺服器記憶體耗盡並崩潰,這表明在這種情況下記憶體消耗會多麼迅速地增長。

特別感謝 @deads2k 在塑造此功能方面的幫助。

Kubernetes 1.33 更新

自從這個功能啟動以來,Marek Siarkowicz 將一項新技術整合到了 Kubernetes API 伺服器中:流式集合編碼。Kubernetes v1.33 引入了兩個相關的特性門控,StreamingCollectionEncodingToJSONStreamingCollectionEncodingToProtobuf。這些功能透過流進行編碼,避免一次性分配所有記憶體。此功能與現有的 list 編碼逐位相容,能節省更多伺服器端記憶體,且無需對客戶端程式碼進行任何更改。在 1.33 中,WatchList 特性門控預設是停用的。