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

1000 個節點及以上:Kubernetes 1.2 的效能與擴充性更新

編者註: 這是關於 Kubernetes 1.2 新功能的 深入文章系列 中的第一篇

我們很榮幸宣布,隨著 1.2 版本的發布,Kubernetes 現在支援 1000 個節點的叢集,並且大多數 API 操作的第 99 個百分位尾部延遲降低了 80%。這表示在短短六個月內,我們將整體規模擴大了 10 倍,同時保持了絕佳的使用者體驗 — 第 99 個百分位 Pod 啟動時間少於 3 秒,而大多數 API 操作的第 99 個百分位延遲為數十毫秒(例外的是 LIST 操作,在非常大的叢集中需要數百毫秒)。

口說無憑,示範勝於雄辯。請看這裡!

在上面的影片中,您看到叢集擴展到每秒 1 千萬次查詢 (QPS),超過 1,000 個節點,包括滾動更新,零停機時間且不影響尾部延遲。這規模足以成為網際網路上排名前 100 的網站之一!

在這篇部落格文章中,我們將介紹為實現此結果所做的工作,並討論我們未來擴展到更高規模的一些計畫。

方法論 

我們根據以下服務等級目標 (SLO) 基準測試 Kubernetes 的擴充性

  1. API 回應速度 1 99% 的 API 呼叫在 1 秒內傳回 
  2. Pod 啟動時間:99% 的 Pod 及其容器(具有預先提取的映像檔)在 5 秒內啟動。  我們說 Kubernetes 擴展到一定數量的節點,前提是這兩個 SLO 都符合。我們持續收集並報告上述測量結果,作為專案測試架構的一部分。這組測試分為兩個部分:API 回應速度和 Pod 啟動時間。

使用者層級抽象的 API 回應速度2 

Kubernetes 為使用者提供高階抽象概念來表示其應用程式。例如,ReplicationController 是一種抽象概念,代表 Pod 的集合。列出所有 ReplicationController 或列出給定 ReplicationController 的所有 Pod 是一種非常常見的使用案例。另一方面,幾乎沒有人會想要列出系統中的所有 Pod — 例如,30,000 個 Pod(1000 個節點,每個節點 30 個 Pod)代表約 150MB 的資料(~5kB/Pod * 30k 個 Pod)。因此,此測試使用 ReplicationController。

對於此測試(假設 N 為叢集中的節點數),我們

  1. 建立大約 3xN 個不同大小(5、30 和 250 個副本)的 ReplicationController,總共具有 30xN 個副本。我們隨著時間推移分散它們的建立(即,我們不會一次啟動所有副本),並等待直到所有副本都正在執行。 

  2. 對每個 ReplicationController 執行一些操作(擴展它、列出其所有實例等),隨著時間推移分散這些操作,並測量每個操作的延遲。這類似於真實使用者在正常叢集操作過程中可能執行的操作。 

  3. 停止並刪除系統中的所有 ReplicationController。  有關此測試的結果,請參見下面的「Kubernetes 1.2 的指標」部分。

對於 v1.3 版本,我們計畫透過也建立服務、部署、DaemonSet 和其他 API 物件來擴展此測試。

Pod 啟動端對端延遲3 

使用者也非常關心 Kubernetes 排程和啟動 Pod 所需的時間。這不僅在初始建立時如此,而且當 ReplicationController 需要建立替換 Pod 以接替節點故障時也是如此。

我們(假設 N 為叢集中的節點數)

  1. 建立一個具有 30xN 個副本的單一 ReplicationController,並等待直到所有副本都正在執行。我們也正在執行高密度測試,具有 100xN 個副本,但在叢集中節點較少。 

  2. 啟動一系列單一 Pod ReplicationController - 每 200 毫秒一個。對於每個控制器,我們測量「總端對端啟動時間」(定義如下)。 

  3. 停止並刪除系統中的所有 Pod 和複製控制器。  我們將「總端對端啟動時間」定義為從用戶端向 API 伺服器發送建立 ReplicationController 的請求的那一刻,到透過 watch 將「執行中且就緒」的 Pod 狀態傳回用戶端的那一刻之間的時間。這表示「Pod 啟動時間」包括建立 ReplicationController 並進而建立 Pod、排程器排程該 Pod、Kubernetes 設定 Pod 內網路、啟動容器、等待直到 Pod 成功回應健康檢查,然後最終等待直到 Pod 將其狀態報告回 API 伺服器,然後 API 伺服器透過 watch 將其報告給用戶端。

雖然我們可以透過排除例如等待透過 watch 報告,或直接建立 Pod 而不是透過 ReplicationController 來大幅縮短「Pod 啟動時間」,但我們認為,廣泛的定義映射到最真實的使用案例,最適合真實使用者了解他們可以從系統中期望獲得的效能。

Kubernetes 1.2 的指標 

那麼結果如何呢?我們在 Google Compute Engine 上執行測試,根據 Kubernetes 叢集的大小設定主 VM 的大小。特別是對於 1000 個節點的叢集,我們為主節點使用 n1-standard-32 VM(32 個核心,120GB RAM)。

API 回應速度 

以下兩個圖表呈現了 Kubernetes 1.2 版本和 1.0 版本在 100 個節點叢集上第 99 個百分位 API 呼叫延遲的比較。(條形越小越好)

我們分別呈現 LIST 操作的結果,因為這些延遲明顯更高。請注意,我們在此期間稍微修改了我們的測試,因此針對 v1.0 執行目前的測試將導致比過去更高的延遲。

我們也針對 1000 個節點的叢集執行了這些測試。注意:我們不支援 GKE 上大於 100 個節點的叢集,因此我們沒有指標可以比較這些結果。但是,客戶已報告自 Kubernetes 1.0 以來在 1,000 多個節點的叢集上執行。

由於 LIST 操作明顯更大,我們再次將它們分開呈現:兩種叢集大小的所有延遲都遠遠在我們的 1 秒 SLO 內。

Pod 啟動端對端延遲 

「Pod 啟動延遲」(如「Pod 啟動端對端延遲」部分中所定義)的結果在下圖中呈現。為了參考,我們也在圖表的第一部分呈現了 v1.0 在 100 個節點叢集上的結果。

如您所見,我們大幅降低了 100 個節點叢集中的尾部延遲,現在可以在我們測量的最大叢集大小中提供低 Pod 啟動延遲。值得注意的是,1000 個節點叢集的指標,無論是 API 延遲還是 Pod 啟動延遲,通常都優於六個月前報告的 100 個節點叢集的指標!

我們是如何做出這些改進的? 

為了在過去六個月中在規模和效能方面取得這些顯著的進展,我們對整個系統進行了許多改進。以下列出了一些最重要的改進。

由於大多數 Kubernetes 控制邏輯都在由 etcd watch(透過 API 伺服器)保持更新的有序一致快照上運作,因此資料到達的輕微延遲不會對叢集的正確運作產生影響。這些獨立的控制器迴圈,為了系統的可擴展性而設計為分散式,很樂意為了提高整體吞吐量而犧牲一點延遲。

在 Kubernetes 1.2 中,我們利用這個事實,透過新增 API 伺服器讀取快取來提高效能和擴充性。透過此變更,API 伺服器的用戶端可以從 API 伺服器中的記憶體內快取讀取資料,而不是從 etcd 讀取資料。快取直接從 etcd 透過背景中的 watch 更新。那些可以容忍檢索資料延遲的用戶端(通常快取的延遲在數十毫秒的量級)可以完全從快取提供服務,從而減少 etcd 上的負載並提高伺服器的吞吐量。這是 v1.1 中開始的最佳化延續,我們在 v1.1 中新增了直接從 API 伺服器而不是 etcd 提供 watch 的支援:https://github.com/kubernetes/kubernetes/blob/master/docs/proposals/apiserver-watch.md。 

感謝 Google 的 Wojciech Tyczynski 以及 Red Hat 的 Clayton Coleman 和 Timothy St. Clair 的貢獻,我們能夠將仔細的系統設計與 etcd 的獨特優勢結合起來,以提高 Kubernetes 的擴充性和效能。 

Kubernetes 1.2 也從每個節點的 Pod 角度提高了密度 — 對於 v1.2,我們測試並宣傳單一節點上最多 100 個 Pod(相較於 1.1 版本中的 30 個 Pod)。由於 Kubernetes 社群透過實作 Pod 生命週期事件產生器 (PLEG) 的勤奮工作,才有可能實現此改進。

Kubelet(Kubernetes 節點代理程式)為每個 Pod 提供一個工作執行緒,負責管理 Pod 的生命週期。在較早的版本中,每個工作執行緒都會定期輪詢底層容器執行階段 (Docker) 以偵測狀態變更,並執行任何必要的動作以確保節點的狀態與所需狀態相符(例如,透過啟動和停止容器)。隨著 Pod 密度的增加,來自每個工作執行緒的並行輪詢會使 Docker 執行階段不堪重負,從而導致嚴重的可靠性和效能問題(包括額外的 CPU 使用率,這是擴展的主要限制因素之一)。

為了解決這個問題,我們引入了一個新的 Kubelet 子元件 — PLEG — 來 集中化 狀態變更偵測,並為工作執行緒產生生命週期事件。由於消除了並行輪詢,我們能夠將 Kubelet 和容器執行階段的穩態 CPU 使用率降低 4 倍。這也使我們能夠採用更短的輪詢週期,以便更快地偵測和回應變更。 

  • 改進的排程器吞吐量 來自 CoreOS 的 Kubernetes 社群成員(Hongchao Deng 和 Xiang Li)協助深入研究 Kubernetes 排程器,並在不犧牲準確性或彈性的情況下大幅提高吞吐量。他們將排程 30,000 個 Pod 的總時間縮短了近 1400%!您可以在這裡閱讀一篇關於他們如何解決問題的精彩部落格文章:https://coreos.com/blog/improving-kubernetes-scheduler-performance.html 

  • 更有效率的 JSON 剖析器 Go 的標準函式庫包含一個彈性且易於使用的 JSON 剖析器,可以使用反射 API 編碼和解碼任何 Go 結構。但是這種彈性是有代價的 — 反射會分配許多必須由執行階段追蹤和垃圾 收集的小物件。我們的分析證實了這一點,顯示用戶端和伺服器的大部分時間都花在序列化上。鑑於我們的類型不會頻繁變更,我們懷疑可以透過程式碼產生繞過大量的反射。

在調查 Go JSON 環境並進行一些初步測試後,我們發現 ugorji 編碼解碼器 函式庫提供了最顯著的加速 — 在使用產生的序列化器時,JSON 編碼和解碼效能提高了 200%,並顯著減少了物件分配。在向上游函式庫貢獻修正程式以處理我們的一些複雜結構後,我們將 Kubernetes 和 go-etcd 用戶端函式庫切換過來。連同 JSON 上下層的一些其他重要最佳化,我們能夠大幅降低幾乎所有 API 操作(尤其是讀取)的 CPU 時間成本。 

resync.png

在這兩種情況下,問題都由 Kubernetes 社群成員偵錯和/或修復,包括來自 Red Hat 的 Andy Goldstein 和 Jordan Liggitt,以及來自網易的 Liang Mingqiang。 

Kubernetes 1.3 及更高版本 

當然,我們的工作尚未完成。我們將繼續投入改進 Kubernetes 效能,因為我們希望它能擴展到數千個節點,就像 Google 的 Borg 一樣。感謝我們對測試基礎架構的投入以及我們對團隊如何在生產環境中使用容器的關注,我們已經確定了改進規模的下一步。 

Kubernetes 1.3 的計畫: 

  1.  我們的主要瓶頸仍然是 API 伺服器,它將大部分時間都花在封送處理和解除封送處理 JSON 物件上。我們計畫新增對協定緩衝區的支援,作為 API 的可選路徑,用於元件間通訊以及在 etcd 中儲存物件。使用者仍然可以使用 JSON 與 API 伺服器通訊,但由於大多數 Kubernetes 通訊都是叢集內通訊(API 伺服器到節點、排程器到 API 伺服器等),我們預期主節點上的 CPU 和記憶體使用率將顯著降低。 

  2.  Kubernetes 使用標籤來識別物件集;例如,識別哪些 Pod 屬於給定的 ReplicationController 需要迭代命名空間中的所有 Pod,並選擇那些與控制器的標籤選取器相符的 Pod。為標籤新增高效索引器,可以利用現有的 API 物件快取,將可以快速找到與標籤選取器相符的物件,從而使這個常見操作快得多。 

  3. 排程決策基於許多不同的因素,包括根據請求的資源分散 Pod、分散具有相同選取器的 Pod(例如,來自相同的服務、ReplicationController、Job 等)、節點上是否存在所需的容器映像檔等。這些計算,尤其是選取器分散,有很多改進的機會 — 請參閱 https://github.com/kubernetes/kubernetes/issues/22262,了解僅一個建議的變更。 

  4. 我們也對即將發布的 etcd v3.0 版本感到興奮,該版本是針對 Kubernetes 使用案例而設計的 — 它將提高效能並引入新功能。來自 CoreOS 的貢獻者已經開始為將 Kubernetes 移至 etcd v3.0 奠定基礎(請參閱 https://github.com/kubernetes/kubernetes/pull/22604)。  雖然此清單並未涵蓋所有圍繞效能的努力,但我們樂觀地認為,我們將實現與從 Kubernetes 1.0 到 1.2 一樣大的效能提升。 

結論 

在過去六個月中,我們顯著提高了 Kubernetes 的擴充性,使 v1.2 能夠執行 1000 個節點的叢集,並具有與我們先前僅在小得多的叢集上實現的相同的卓越回應速度(根據我們的 SLO 衡量)。但這還不夠 — 我們希望將 Kubernetes 推向更遠、更快的地方。Kubernetes v1.3 將進一步提高系統的擴充性和回應速度,同時繼續新增使建構和執行最嚴苛的基於容器的應用程式變得更容易的功能。 

請加入我們的社群,協助我們建構 Kubernetes 的未來!有很多參與方式。如果您對擴充性特別感興趣,您會對以下內容感興趣: 


1我們排除對「事件」的操作,因為這些操作更像是系統記錄,並非系統正常運作所必需。
2這是 Kubernetes github 儲存庫中的 test/e2e/load.go。
3這是 Kubernetes github 儲存庫中的 test/e2e/density.go 測試 
4我們正在研究在下一個版本中對此進行最佳化,但就目前而言,使用較小的主節點可能會導致顯著的(數量級)效能下降。我們鼓勵任何針對 Kubernetes 執行基準測試或嘗試複製這些發現的人使用大小相似的主節點,否則效能將會受到影響。