攀登規模化的高峰 - 螞蟻集團大規模 Sigma 叢集 ApiServer 優化實踐

SOFAStack發表於2021-09-29

文|唐博(花名:博易 螞蟻集團技術專家)

​ 譚崇康(花名:見雲 螞蟻集團高階技術家)

本文 10316 字 閱讀 18 分鐘

螞蟻集團執行著全球最大的 Kubernetes(內部稱為 Sigma) 叢集之一。Kubernetes 社群官方以 5K node 作為 Kubernetes 規模化的事實標準,而螞蟻集團在 2019 年的時候,就已經維護著單叢集規模超過 1W node 的 Kubernetes 叢集。

這不僅僅是單叢集節點量級上的差異,更是業務規模的差異,業務多樣化和複雜度上的差異。

一個形象化的比喻就是,如果官方以及跟著官方的 Kubernetes 使用者能想象到的 Kubernetes 的叢集規模是泰山,那麼螞蟻集團在官方的解決方案之上已經實現了一個珠穆朗瑪峰。

螞蟻集團的 Kubernetes 的演進,從 2018 年至今已經走過了 3 年多的歲月,雖然在 2019 年的時候就構建了萬臺叢集的規模,但時至今日,無論是業務形態還是叢集的伺服器都發生了巨大的變化。

- 首先,當時的叢集萬臺節點,主要還是偏小規格的伺服器,而如今都是大機型,雖然機器的數量也是萬臺,實際管理的 CPU 數量已經成倍增長。

- 其次是當時叢集裡面幾乎全量是 Long running 的線上業務,Pod 的建立頻率每天只有幾千個,如今我們的叢集上幾乎跑滿了流式計算和離線計算業務等按需分配的 Pod,因此在 Pod 數量上成倍增長,實際管理的 Pod 數量超過了百萬。

- 最後,是 Serverless 的業務快速發展,Serverless Pod 的生命週期基本在分鐘級甚至是秒級,叢集每天的 Pod 建立量也超過了幾十萬,伴隨著大量的 Kubernetes list watch 和 CRUD 請求,叢集的 apiserver 承受了數倍於以往的壓力。

因此在業務 Serverless 的大背景下,我們在螞蟻啟動了大規模 Sigma 叢集的效能優化方案,根據業務的增長趨勢,我們設定的目標是,構建 1.4W 個節點規模的叢集,同時通過技術優化,期望達成在請求延遲上不會因為規模的原因有所下降,能夠對齊社群標準,即 create/update/delete 請求的天級別 P99 RT 在 1s 之內。

可想而知,挑戰是非常巨大的。

PART. 1 大規模叢集的挑戰

毋庸置疑,大規模叢集帶來了很多挑戰:

- 隨著叢集規模增大,故障的爆炸半徑也將擴大。Sigma 叢集承載了螞蟻集團諸多重要應用,保障叢集的穩定和業務的穩定是最基礎也是優先順序最高的要求。

- 使用者大量的 list 操作,包括 list all,list by namespace,list by label 等,均會隨著叢集的規模增大而開銷變大。這些合理或者不合理的 list 請求,將讓 apiserver 的記憶體在短時間內快速增長,出現 OOM 異常,無法對外響應請求。此外,業務方的 list 請求也會因為 apiserver 無法處理請求而不斷重試,造成 apiserver 重啟後因過載不可恢復服務能力,影響整個叢集的可用性。

- 大量 List 請求透過 apiserver 直接訪問 etcd 服務,也會讓 etcd 例項的記憶體劇增而出現 OOM 異常。

- 隨著業務量的增長,特別是離線任務的增多,create/update/delete 等請求的數量也迅速增加,導致客戶端請求 apiserver 的 RT 極速上升,進而使得排程器和一些控制器因為選主請求超時而丟主。

- 業務量增長將加劇 etcd 由於 compact 等操作自身存在的效能問題,而使 etcd 的 P99 RT 暴漲,進而導致 apiserver 無法響應請求。

- 叢集中的控制器服務,包括 Kubernetes 社群自帶的控制器例如 service controller,cronjob controller 以及業務的 operator 等,自身存在的效能問題都將在大規模叢集面前被進一步放大。這些問題將進一步傳導到線上業務,導致業務受損。

如計算機學科的古老格言所說:

All problems in computer science can be solved by another level of indirection, except for the problem of too many layers of indirection... and the performance problems. 」

大規模叢集既是照妖鏡,也是試金石。

PART. 2 大規模叢集的收益

誠然,構建一個大規模的 Kubernetes 叢集也提供了諸多收益:

- 為執行大型服務提供更為便利的基礎設施 ,便於應對業務擴容時大幅飆升的資源需求。例如在雙十一等電商大促活動期間,可以通過擴充套件現有叢集而不是新建其它小叢集來應對業務的增長。同時叢集管理者可以管理更少的叢集,並且以此來簡化基礎架構管理 。

- 為大資料和機器學習的離線計算任務提供更多的資源,為分時複用/分時排程等排程手段提供更大的施展空間,讓離線的計算任務在線上業務的低峰期時可以使用更多的資源進行計算,享受極致彈性和極速交付。

- 還有非常重要的一點,在更大的叢集中可以通過更加豐富的編排排程手段來更為有效地提升叢集整體的資源利用率。

PART. 3 SigmaApiServer效能優化

Sigma apiserver 元件是 Kubernetes 叢集的所有外部請求訪問入口,以及 Kubernetes 叢集內部所有元件的協作樞紐。apiserver 具備了以下幾方面的功能:

- 遮蔽後端資料持久化元件 etcd 的儲存細節,並且引入了資料快取,在此基礎上對於資料提供了更多種類的訪問機制。

- 通過提供標準 API,使外部訪問客戶端可以對叢集中的資源進行 CRUD 操作。

- 提供了 list-watch 原語,使客戶端可以實時獲取到資源中資源的狀態。

我們對於 apiserver 效能提升來說可以從兩個層面進行拆解,分別是 apiserver 的啟動階段和 apiserver 的執行階段。

apiserver 啟動階段 的效能優化有助於

- 減少升級變更影響時長/故障恢復時長,減少使用者可感知的不可用時間,給 Sigma 終端使用者提供優質的服務體驗(面向業務的整體目標是 Sigma 月度可用性 SLO 達到 99.9%,單次故障不可用時間 < 10min)。

- 減少因為釋出時客戶端重新 list 全量資源而導致的 apiserver 壓力過大情況出現。

apiserver 執行階段 的效能優化的意義在於:

- 穩定支援更大規模的 Kubernetes 叢集。

- 提高 apiserver 在正常平穩執行的狀態中,單位資源的服務能力;即提高可以承受的請求併發和 qps, 降低請求 RT。

- 減少客戶端的超時以及超時導致的各種問題;在現有資源下提供更多的流量接入能力;

整體優化思路

構建一個大規模的 Kubernetes 叢集以及效能優化不是一件容易的事,如 Google Kubernetes Engine K8s 規模化文章所言:

「The scale of a Kubernetes cluster is like a multidimensional object composed of all the cluster’s resources—and scalability is an envelope that limits how much you can stretch that cube. The number of pods and containers, the frequency of scheduling events, the number of services and endpoints in each service—these and many others are good indicators of a cluster’s scale.

The control plane must also remain available and workloads must be able to execute their tasks.

What makes operating at a very large scale harder is that there are dependencies between these dimensions. 」

也就是說,叢集的規模化和效能優化需要考慮叢集中各個維度的資訊,包括 pod、node,configmap、service、endpoint 等資源的數量,pod 建立/排程的頻率,叢集內各種資源的變化率等等,同時需要考慮這些不同維度之間的互相的依賴關係,不同維度的因素彼此之間構成了一個多維的空間。

圖片

為了應對如此多的變數對大規模叢集帶來的複雜影響,我們採用了探索問題本質以不變應萬變的方法。為了可以全面而且系統化地對 apiserver 進行優化,我們由下到上把 apiserver 整體分為三個層面,分別為儲存層(storage)、快取層(cache)、訪問層(registry/handler)

- 底層的 etcd 是 Kubernetes 後設資料的儲存服務,是 apiserver 的基石。儲存層提供 apiserver 對 etcd 訪問功能,包括 apiserver 對 etcd 的 list watch,以及 CRUD 操作。

- 中間的快取層相當於是對 etcd 進行了一層封裝,提供了一層對來自客戶端且消耗資源較大的 list-watch 請求的資料快取,以此提升了 apiserver 的服務承載能力。同時,快取層也提供了按條件搜尋的能力。

- 上面的訪問層提供了處理 CRUD 請求的一些特殊邏輯,同時對客戶端提供各種資源操作服務。

針對上面提出的不同層面,一些可能的優化項如下:

圖片

同時,為了更好地衡量 apiserver 的效能,我們為 Kubernetes apiserver 制定了詳細的 SLO,包括 create/update/delete 等操作的 P99 RT 指標,list 在不同規模資源情況下的 P99 RT 指標等。

同時,在 SLO 的牽引下對 apiserver 進行優化,讓我們可以在一個更大規模的 Kubernetes 叢集下依然為使用者提供更好的 API 服務品質。

快取層優化

「List 走 watchCache」

由於 apiserver 在從 etcd list 資料時會獲取大量資料,並且進行反序列化和過濾操作,因此會消耗大量記憶體。一些使用者的客戶端包含了不規範的訪問 apiserver 的行為,例如某些客戶端可能每隔幾秒就會 list 一次,並且不帶有 resourceversion。這些客戶端對於 apiserver 造成了很大的記憶體壓力,也曾經險些造成叢集故障。為了應對這些不規範的使用者訪問,以及減少 apiserver 的 CPU/memory 消耗,我們對 list 操作進行了修改,讓使用者的不規範 list 操作全部走 watchCache。也就是說,使用者在進行 list 操作時,請求將不會透傳到後端的 etcd 服務。

在我們的一個大規模叢集中,apiserver 記憶體會飆升到 400G 導致幾十分鐘便會出現 OOM,期間 apiserver 對於 etcd 的訪問的 RT 也會高達 100s 以上,幾乎不可用。在讓使用者全部 list 操作走 apiserver 的 watchCache 之後,apiserver 的記憶體基本穩定在 100G 左右,有 4 倍的提升,RT 也可以穩定在 50ms 量級。List 走 watchCache 也是出於 list-watch 原語的最終一致性考慮的,watch 會持續監聽相關資源的資訊,因此不會有資料一致性的影響。

後續我們也在考慮是否可以把 get 操作也從 watchCache 進行操作,例如等待 watchCache 一定毫秒的時間進行資料同步,以此進一步減小 apiserver 對 etcd 的壓力,同時也可以繼續保持資料一致性。

「watchCache size 自適應」

在資源變化率(churn rate)比較大的叢集中,apiserver 的 watchCache 大小對 apiserver 的整體穩定性和客戶端訪問量多少起著很大的作用。

太小的 watchCache 會使得客戶端的 watch 操作因為在 watchCache 裡面查詢不到相對應的 resource vesion 的內容而觸發 too old resource version 錯誤,從而觸發客戶端進行重新 list 操作。而這些重新 list 操作又會進一步對於 apiserver 的效能產生負面的反饋,對整體叢集造成影響。極端情況下會觸發 list -> watch -> too old resource version -> list 的惡性迴圈。相應地,太大的 watchCache 又會對於 apiserver 的記憶體使用造成壓力。

因此,動態地調整 apiserver watchCache 的大小,並且選擇一個合適的 watchCache size 的上限對於大規模大規模叢集來說非常重要。

我們對於 watchCache size 進行了動態的調整,根據同一種資源(pod/node/configmap) 的變化率(create/delete/update 操作的頻次) 來動態調整 watchCache 的大小;並且根據叢集資源的變化頻率以及 list 操作的耗時計算了 watchCache size 大小的上限。

在這些優化和改動之後,客戶端的 watch error(too old resource version)幾乎消失了。

圖片

「增加 watchCache index」

在分析螞蟻集團的業務之後發現,新計算(大資料實時/離線任務,機器學習離線任務)的業務對於各種資源的 list 有特定的訪問模式,spark 和 blink 等業務方有大量的 list by label 操作,也就是通過標籤來查詢 pod 的訪問量很多。

通過對 apiserver 日誌進行分析,我們提取出了各個業務方 list by label 比較多的操作,並且在 watchCache 增加了相應地增加了相關 label 的索引。在對同等規模的資源進行 list by label 操作時,客戶端 RT 可以有 4-5 倍的提升。

下圖為上述 watchCache 優化內容簡介:

圖片

儲存層優化

在資源更新頻率比較快的情況下,GuaranteedUpdate 會進行大量的重試,同時造成不必要的 etcd 的壓力。Sigma 給 GuaranteedUpdate 增加了指數退避的重試策略,減少了 update 操作的衝突次數,也減少了 apiserver 對於 etcd 的更新壓力。

在大規模高流量叢集中,我們發現 apiserver 的一些不合理的日誌輸出會造成 apiserver 嚴重的效能抖動。例如,我們調整了 GuaranteedUpdate/delete 等操作在更新或者刪除衝突時的日誌輸出級別。這減少了磁碟 io 操作,降低了客戶端訪問 apiserver 的請求響應時間。此外,在叢集資源變化率很高的情況下," fast watch slow processing" 的日誌也會非常多。這主要是表明 apiserver 從 etcd watch 事件之後,在快取裡面構建 watchCache 的速率低於從 etcd watch 到事件的速率,在不修改 watchCache 資料結構的情況下暫時是無法改進的。因此我們也對 slow processing 日誌級別進行了調整,減少了日誌輸出。

接入層優化

Golang profiling 一直是用於對 Go 語言編寫的應用的優化利器。在對 apiserver 進行線上 profiling 的時候,我們也發現了不少熱點,並對其進行了優化。

例如:

- 在使用者 list event 時可以看到 events.GetAttrs/ToSelectableFields 會佔用很多的 CPU,我們修改了 ToSelectableFields, 單體函式的 CPU util 提升 30%,這樣在 list event 時候 CPU util 會有所提升。

圖片

- 另外,通過 profiling 可以發現,當 metrics 量很大的時候會佔用很多 CPU,在削減了 apiserver metrics 的量之後,大幅度降低了 CPU util。

圖片

- Sigma apiserver 對於鑑權模型採用的是 Node、RBAC、Webhook,對於節點鑑權,apiserver 會在記憶體當中構建一個相對來說很大的圖結構,用來對 Kubelet 對 apiserver 的訪問進行鑑權。

當叢集出現大量的資源(pod/secret/configmap/pv/pvc)建立或者變更時,這個圖結構會進行更新;當 apiserver 進行重啟之後,圖結構會進行重建。在大規模叢集中,我們發現在 apiserver 重啟過程中,Kubelet 會因為 apiserver 的 node authorizer graph 還在構建當中而導致部分 Kubelet 請求會因為許可權問題而受阻。定位到是 node authorizer 的問題後,我們也發現了社群的修復方案,並 cherry-pick 回來進行了效能上的修復提升。

etcd 對於每個儲存的資源都會有 1.5MB 大小的限制,並在請求大小超出之後返回 etcdserver: request is too large;為了防止 apiserver 將大於限制的資源寫入 etcd,apiserver 通過 limitedReadBody 函式對於大於資源限制的請求進行了限制。我們對 limitedReadBody 函式進行了改進,從 http header 獲取 Content-Length 欄位來判斷 http request body 是否超過了 etcd 的單個資源(pod,node 等)的 1.5MB 的儲存上限。

當然也不是所有方案都會有所提升。例如,我們進行了一些其它編碼方案測試,把 encoding/json 替換成為了 jsoniter。相比之下,apiserver 的 CPU util 雖有降低但是 memory 使用有很大的增高,因此會繼續使用預設的 encoding/json。

etcd 拆分相關優化

除此之外,etcd 拆分對於客戶端訪問 apiserver 的請求的 RT 也有很大提升。在大規模叢集中,我們採用了多份拆分方式,其中一份 etcd 是 Pod。在 etcd 拆分的過程中,我們發現拆分出來的 etcd 的 resource version 會小於原有 apiserver 的resource version,因此會導致客戶端 list-watch apiserver 時長時間 hang ,無法收到新的 Pod 相關的事件。

為了解決這個 etcd 拆分時遇到的問題,我們對 apiserver 的 watch 介面進行了修改,增加了 watch 操作的 timeout 機制。客戶端的 watch 操作最多等待 3s,如果 resource version 不匹配,直接返回 error 讓 客戶端進行重新 list ,以此避免了在 etcd 拆分過程中造成的客戶端因 resource version hang 住的問題。

其它優化

除此之外為了保障 apiserver 的高可用,螞蟻 Kubernetes apiserver 進行了分層分級別的限流,採用了 sentinel-go 加 APF 的限流方案。其中 sentinel-go 來限制總量,進行了 ua 維度,verb 維度等多維度混合限流,防止服務被打垮,APF 來保障不同業務方之間的流量可以公平介入。然而,sentinel-go 中自帶了週期性記憶體採集功能,我們將其關閉之後帶來了一定的 CPU 利用率的提升。

另外,我們也在和客戶端一起優化 apiserver 的訪問行為。截止目前,Sigma 和業務方一起對 blink operator(flink on K8s) / tekton pipeline / spark operator 等服務進行了 apiserver 使用方式方法上的程式碼優化。

優化效果

下圖分別為我們兩個叢集分鐘級別流量的對比,其中一個叢集的業務由於業務合併有了一個跨越式的增長,叢集的節點規模範圍,超過萬臺。可以看出來,隨著業務的逐漸上升,叢集的壓力出現了數倍的壓力提升。各類寫請求都有明顯的上升。其中 create 和 delete 請求比較明顯,create 請求由每分鐘 200 個左右上升到了每分鐘 1000 個左右,delete 請求由每分鐘 2.7K 個 上升到了 5.9K 個。經過我們的優化,隨著業務方面的遷移逐步推進,在規模和負載持續上升的背景下,整體叢集執行平穩,基本上達成了叢集優化的預期。

圖片

基礎資源

在各型別的流量隨著業務增長有不同程度的上升的情況下,經過優化,apiserver CPU 利用率下降了約 7%。但是在記憶體上,增多了 20% 左右,這是因為 watchCache 在開啟動態調整後相比之前快取了更多的不同類別資源(node/pod等)的物件。

快取更多資源物件帶來的收益是,減少了客戶端的重連並且降低了 list 操作個數,同時也間接減少了客戶端各類操作的 RT,提升了整體叢集和執行的業務的穩定性。當然後續也會繼續針對減少 apiserver 的記憶體使用量進行優化。

圖片

RT

寫請求的 RT 對於叢集和業務的穩定性是最關鍵的指標之一。經優化過後,客戶端訪問 apiserver 的各類寫請求的 P99,P90,P50 RT 均有明顯的下降,並且數值更加趨於平穩,表明 apiserver 在向著高效且穩定的方向發展。

圖片

(注:RT 對比在包括 etcd 拆分之後進行)

Watch error 和 list 個數

不合理的 watchCache 大小會使得客戶端的 watch 操作因為在 watchCache 裡面查詢不到相對應的 resource vesion 的內容而觸發 too old resource version 錯誤,也就是下面的 watch error,進而會引發客戶端對 apiserver 的重新 list。

在優化之後,pod 的每分鐘 watch error 的個數下降約 25%,node 的 watch error 下降為 0;相應的 list 操作個數每分鐘也下降了 1000 個以上。

圖片

PART. 4 未來之路

總體來說,提升一個分散式系統整體的能力,我們可以從以下方面入手:

1.提升系統自身架構,提高穩定性與效能

2.管理系統接入方的流量,優化系統接入方的使用方法和架構

3.對系統依賴的服務進行優化

對應到 apiserver 的效能優化來說,未來我們還將從以下幾個方面繼續深入:

圖片
  1. 針對 apiserver 自身,一些可能的優化點包括:優化 apiserver 啟動總時間,提升 watchCache 構建速度;threadSafeStore 資料結構優化;對 get 操作採用快取;對 apiserver 存入 etcd 的資料進行壓縮,減小資料大小,藉此提升 etcd 效能 等等。
  2. 除了優化 apiserver 本身之外,螞蟻 Sigma 團隊也在致力於優化 apiserver 上下游的元件。例如 etcd 多 sharding,非同步化等高效方案;以及對於各種大資料實時和離線任務的 operator 的整體鏈路的優化。
  3. 當然 SLO 的牽引必不可少,同時也會在各個指標的量化上進行增強。只有這些協調成為一個有機的整體,才能說我們有可能達到為執行在基礎設施上面的業務方提供了優質的服務。

構建大規模叢集道阻且長。

後續我們會繼續在上面列舉的各方面進一步投入,並且為更多的線上任務、離線任務、新計算任務提供更好的執行環境。

同時,我們也將進一步提升方法論,從快取、非同步化、水平拆分/可擴充套件性、合併操作、縮短資源建立鏈路等大方向上進行下一步的優化。隨著叢集規模的繼續增長,效能優化的重要性也會日益凸顯,我們將朝著構建和維護對於使用者來說高效可靠高保障的大規模 Kubernetes 叢集這一目標繼續努力,就像 Kubernetes 這個名字的寓意一樣,為應用程式保駕護航!

-

「參考資料」

.【Kubernetes Scalability thresholds】

https://github.com/kubernetes...

.【Kubernetes scalability and performance SLIs/SLOs】

https://github.com/kubernetes...

.【Watch latency SLI details】

https://github.com/kubernetes...

.【Bayer Crop Science seeds the future with 15000-node GKE clusters】

https://cloud.google.com/blog...

.【Openstack benchmark】

https://docs.openstack.org/de...

-

「求賢若渴」

螞蟻集團 Kubernetes 叢集排程系統支撐了螞蟻集團線上、實時業務的百萬級容器資源排程, 向上層各類金融業務提供標準的容器服務及動態資源排程能力, 肩負螞蟻集團資源成本優化的責任。我們有業界規模最大 Kubernetes 叢集,最深入的雲原生實踐,最優秀的排程技術。歡迎有意在 Kubernetes/雲原生/容器/核心隔離混部/排程/叢集管理深耕的同學加入,北京、上海、杭州期待大家的加入。

聯絡郵箱 xiaoyun.maoxy@antgroup.com

本週推薦閱讀

圖片SOFAJRaft 在同程旅遊中的實踐

圖片技術風口上的限流

圖片螞蟻集團萬級規模 k8s 叢集 etcd 高可用建設之路

圖片2021 年雲原生技術發展現狀及未來趨勢

圖片

相關文章