
阿裡妹導讀:本文主要介紹阿裡巴巴和螞蟻金服在大規模生産環境中落地 Kubernetes 的過程中,在叢集規模上遇到的典型問題以及對應的解決方案,内容包含對 etcd、kube-apiserver、kube-controller 的若幹性能及穩定性增強,這些關鍵的增強是阿裡巴巴和螞蟻金服内部上萬節點的 Kubernetes 叢集能夠平穩支撐 2019 年天貓 618 大促的關鍵所在。
文内藏福利,向下滑滑滑,免費課程立刻領取~
背景
從阿裡巴巴最早期的 AI 系統(2013)開始,叢集管理系統經曆了多輪的架構演進,到 2018 年全面的應用 Kubernetes ,這期間的故事是非常精彩的。這裡忽略系統演進的過程,不去讨論為什麼 Kubernetes 能夠在社群和公司内部全面的勝出,而是将焦點關注到應用 Kubernetes 中會遇到什麼樣的問題,以及我們做了哪些關鍵的優化。
在阿裡巴巴和螞蟻金服的生産環境中,容器化的應用超過了 10k 個,全網的容器在百萬的級别,運作在十幾萬台主控端上。支撐阿裡巴巴核心電商業務的叢集有十幾個,最大的叢集有幾萬的節點。在落地 Kubernetes 的過程中,在規模上面臨了很大的挑戰,比如如何将 Kubernetes 應用到超大規模的生産級别。
羅馬不是一天就建成的,為了了解 Kubernetes 的性能瓶頸,我們結合阿裡和螞蟻的生産叢集現狀,估算了在 10k 個節點的叢集中,預計會達到的規模:
- 20w pods
- 100w objects
我們基于 Kubemark 搭建了大規模叢集模拟的平台,通過一個容器啟動多個(50個)Kubemark 程序的方式,使用了 200 個 4c 的容器模拟了 10k 節點的 kubelet。在模拟叢集中運作常見的負載時,我們發現一些基本的操作比如 Pod 排程延遲非常高,達到了驚人的 10s 這一級别,并且叢集處在非常不穩定的狀态。
當 Kubernetes 叢集規模達到 10k 節點時,系統的各個元件均出現相應的性能問題,比如:
- etcd 中出現了大量的讀寫延遲,并且産生了拒絕服務的情形,同時因其空間的限制也無法承載 Kubernetes 存儲大量的對象;
- API Server 查詢 pods/nodes 延遲非常的高,并發查詢請求可能位址後端 etcd oom;
- Controller 不能及時從 API Server 感覺到在最新的變化,處理的延時較高;當發生異常重新開機時,服務的恢複時間需要幾分鐘;
- Scheduler 延遲高、吞吐低,無法适應阿裡業務日常運維的需求,更無法支援大促态的極端場景。
etcd improvements
為了解決這些問題,阿裡雲容器平台在各方面都做了很大的努力,改進 Kubernetes 在大規模場景下的性能。
首先是 etcd 層面,作為 Kubernetes 存儲對象的資料庫,其對 Kubernetes 叢集的性能影響至關重要。
- 第一版本的改進,我們通過将 etcd 的資料轉存到 tair 叢集中,提高了 etcd 存儲的資料總量。但這個方式有一個顯著的弊端是額外增加的 tair 叢集,增加的運維複雜性對叢集中的資料安全性帶來了很大的挑戰,同時其資料一緻性模型也并非基于 raft 複制組,犧牲了資料的安全性。
- 第二版本的改進,我們通過将 API Server 中不同類型的對象存儲到不同的 etcd 叢集中。從 etcd 内部看,也就對應了不同的資料目錄,通過将不同目錄的資料路由到不同的後端 etcd 中,進而降低了單個 etcd 叢集中存儲的資料總量,提高了擴充性。
- 第三版本的改進,我們深入研究了 etcd 内部的實作原理,并發現了影響 etcd 擴充性的一個關鍵問題在底層 bbolt db 的 page 頁面配置設定算法上:随着 etcd 中存儲的資料量的增長,bbolt db 中線性查找“連續長度為 n 的 page 存儲頁面”的性能顯著下降。
為了解決該問題,我們設計了基于 segregrated hashmap 的空閑頁面管理算法,hashmap 以連續 page 大小為 key, 連續頁面起始 page id 為 value。通過查這個 segregrated hashmap 實作 O(1) 的空閑 page 查找,極大地提高了性能。在釋放塊時,新算法嘗試和位址相鄰的 page 合并,并更新 segregrated hashmap。
通過這個算法改進,我們可以将 etcd 的存儲空間從推薦的 2GB 擴充到 100GB,極大的提高了 etcd 存儲資料的規模,并且讀寫無顯著延遲增長。除此之外,我們也和谷歌工程師協作開發了 etcd raft learner(類 zookeeper observer)/fully concurrent read 等特性,在資料的安全性和讀寫性能上進行增強。這些改進已貢獻開源,将在社群 etcd 3.4 版本中釋出。
API Server improvements
Efficient node heartbeats
在 Kubernetes 叢集中,影響其擴充到更大規模的一個核心問題是如何有效的處理節點的心跳。在一個典型的生産環境中 (non-trival),kubelet 每 10s 彙報一次心跳,每次心跳請求的内容達到 15kb(包含節點上數十計的鏡像,和若幹的卷資訊),這會帶來兩大問題:
- 心跳請求觸發 etcd 中 node 對象的更新,在 10k nodes 的叢集中,這些更新将産生近 1GB/min 的 transaction logs(etcd 會記錄變更曆史);
- API Server 很高的 CPU 消耗,node 節點非常龐大,序列化/反序列化開銷很大,處理心跳請求的 CPU 開銷超過 API Server CPU 時間占用的 80%。
- 為了解決這個問題,Kubernetes 引入了一個新的 build-in Lease API ,将與心跳密切相關的資訊從 node 對象中剝離出來,也就是上圖中的 Lease 。原本 kubelet 每 10s 更新一次 node 對象更新為:
-
每 10s 更新一次 Lease 對象,表明該節點的存活狀态,Node Controller 根據該 Lease 對象的狀态來判斷節點是否存活;
處于相容性的考慮,降低為每 60s 更新一次 node 對象,使得 EvictionManager 等可以繼續按照原有的邏輯工作。
因為 Lease 對象非常小,是以其更新的代價遠小于更新 node 對象。kubernetes 通過這個機制,顯著的降低了 API Server 的 CPU 開銷,同時也大幅減小了 etcd 中大量的 transaction logs,成功将其規模從 1000 擴充到了幾千個節點的規模,該功能在社群 Kubernetes-1.14 中已經預設啟用,更多細節詳見 KEP-0009。
API Server load balancing
在生産叢集中,出于性能和可用性的考慮,通常會部署多個節點組成高可用 Kubernetes 叢集。但在高可用叢集實際的運作中,可能會出現多個 API Server 之間的負載不均衡,尤其是在叢集更新或部分節點發生故障重新開機的時候。這給叢集的穩定性帶來了很大的壓力,原本計劃通過高可用的方式分攤 API Server 面臨的壓力,但在極端情況下所有壓力又回到了一個節點,導緻系統響應時間變長,甚至擊垮該節點繼而導緻雪崩。
下圖為壓測叢集中模拟的一個 case,在三個節點的叢集,API Server 更新後所有的壓力均打到了其中一個 API Server 上,其 CPU 開銷遠高于其他兩個節點。
解決負載均衡問題,一個自然的思路就是增加 load balancer。前文的描述中提到,叢集中主要的負載是處理節點的心跳,那我們就在 API Server 與 kubelet 中間增加 lb,有兩個典型的思路:
- API Server 測增加 lb,所有的 kubelets 連接配接 lb,典型的雲廠商傳遞的 Kubernetes 叢集,就是這一模式;
- kubelet 測增加 lb,由 lb 來選擇 API Server。
通過壓測環境驗證發現,增加 lb 并不能很好的解決上面提到的問題,我們必須要深入了解 Kubernetes 内部的通信機制。深入到 Kubernetes 中研究發現,為了解決 tls 連接配接認證的開銷,Kubernetes 用戶端做了很多的努力確定“盡量複用同樣的 tls 連接配接”,大多數情況下用戶端 watcher 均工作在下層的同一個 tls 連接配接上,僅當這個連接配接發生異常時,才可能會觸發重連繼而發生 API Server 的切換。其結果就是我們看到的,當 kubelet 連接配接到其中一個 API Server 後,基本上是不會發生負載切換。為了解決這個問題,我們進行了三個方面的優化:
- API Server:認為用戶端是不可信的,需要保護自己不被過載的請求擊潰。當自身負載超過一個門檻值時,發送 429 - too many requests 提醒用戶端退避;當自身負載超過一個更高的門檻值時,通過關閉用戶端連接配接拒絕請求;
- Client:在一個時間段内頻繁的收到 429 時,嘗試重建連接配接切換 API Server;定期地重建連接配接切換 API Server 完成洗牌;
- 運維層面,我們通過設定 maxSurge=3 的方式更新 API Server,避免更新過程帶來的性能抖動。
如上圖左下角監控圖所示,增強後的版本可以做到 API Server 負載基本均衡,同時在顯示重新開機兩個節點(圖中抖動)時,能夠快速的自動恢複到均衡狀态。
List-Watch & Cacher
List-Watch 是 Kubernetes 中 Server 與 Client 通信最核心一個機制,etcd 中所有對象及其更新的資訊,API Server 内部通過 Reflector 去 watch etcd 的資料變化并存儲到記憶體中,controller/kubelets 中的用戶端也通過類似的機制去訂閱資料的變化。
在 List-Watch 機制中面臨的一個核心問題是,當 Client 與 Server 之間的通信斷開時,如何確定重連期間的資料不丢,這在 Kubernetes 中通過了一個全局遞增的版本号 resourceVersion 來實作。如下圖所示 Reflector 中儲存這目前已經同步到的資料版本,重連時 Reflector 告知 Server 自己目前的版本(5),Server 根據記憶體中記錄的最近變更曆史計算用戶端需要的資料起始位置(7)。
這一切看起來十分簡單可靠,但是……
在 API Server 内部,每個類型的對象會存儲在一個叫做 storage 的對象中,比如會有:
- Pod Storage
- Node Storage
- Configmap Storage
- ……
每個類型的 storage 會有一個有限的隊列,存儲對象最近的變更,用于支援 watcher 一定的滞後(重試等場景)。一般來說,所有類型的類型共享一個遞增版本号空間(1, 2, 3, ..., n),也就是如上圖所示,pod 對象的版本号僅保證遞增不保證連續。Client 使用 List-Watch 機制同步資料時,可能僅關注 pods 中的一部分,最典型的 kubelet 僅關注和自己節點相關的 pods,如上圖所示,某個 kubelet 僅關注綠色的 pods (2, 5)。
因為 storage 隊列是有限的(FIFO),當 pods 的更新時隊列,舊的變更就會從隊列中淘汰。如上圖所示,當隊列中的更新與某個 Client 無關時,Client 進度仍然保持在 rv=5,如果 Client 在 5 被淘汰後重連,這時候 API Server 無法判斷 5 與目前隊列最小值(7)之間是否存在用戶端需要感覺的變更,是以傳回 Client too old version err 觸發 Client 重新 list 所有的資料。為了解決這個問題,Kubernetes 引入 watch bookmark 機制:
bookmark 的核心思想概括起來就是在 Client 與 Server 之間保持一個“心跳”,即使隊列中無 Client 需要感覺的更新,Reflector 内部的版本号也需要及時的更新。如上圖所示,Server 會在合适的适合推送目前最新的 rv=12 版本号給 Client,使得 Client 版本号跟上 Server 的進展。bookmark 可以将 API Server 重新開機時需要重新同步的事件降低為原來的 3%(性能提高了幾十倍),該功能有阿裡雲容器平台開發,已經釋出到社群 Kubernetes-1.15 版本中。
Cacher & Indexing
除 List-Watch 之外,另外一種用戶端的通路模式是直接查詢 API Server,如下圖所示。為了保證用戶端在多個 API Server 節點間讀到一緻的資料,API Server 會通過擷取 etcd 中的資料來支援 Client 的查詢請求。從性能角度看,這帶來了幾個問題:
- 無法支援索引,查詢節點的 pod 需要先擷取叢集中所有的 pod,這個開銷是巨大的;
- 因為 etcd 的 request-response 模型,單次請求查詢過大的資料會消耗大量的記憶體,通常情況下 API Server 與 etcd 之間的查詢會限制請求的資料量,并通過分頁的方式來完成大量的資料查詢,分頁帶來的多次的 round trip 顯著降低了性能;
- 為了確定一緻性,API Server 查詢 etcd 均采用了 Quorum read ,這個查詢開銷是叢集級别,無法擴充的。
為了解決這個問題,我們設計了 API Server 與 etcd 的資料協同機制,確定 Client 能夠通過 API Server 的 cache 擷取到一緻的資料,其原理如下圖所示,整體工作流程如下:
- t0 時刻 Client 查詢 API Server;
- API Server 請求 etcd 擷取目前的資料版本 rv@t0;
- API Server 請求進度的更新,并等待 Reflector 資料版本達到 rv@t0;
- 通過 cache 響應使用者的請求。
這個方式并未打破 Client 的一緻性模型(感興趣的可以自己論證一下),同時通過 cache 響應使用者請求時我們可以靈活的增強查詢能力,比如支援 namespace nodename/labels 索引。該增強大幅提高了 API Server 的讀請求處理能力,在萬台規模叢集中典型的 describe node 的時間從原來的 5s 降低到 0.3s(觸發了 node name 索引),其他如 get nodes 等查詢操作的效率也獲得了成倍的增長。
Context-Aware
API Server 接收請求并完成請求需要通路外部服務,如通路 etcd 将資料持久化、通路 Webhook Server 完成擴充性的 Admission 或者 Auth,甚至是 API Server 自己通路自己(loopback client) 去完成 ServiceAccount 的鑒權工作。
在這種 API Server 處理請求模型的架構下,就有如下這樣的問題:當一個用戶端的請求已經被用戶端主動結束、或者逾時結束時,如果 API Server 還依然還在為這個請求去請求外部的服務的資料、并沒有也在第一時間及時停止請求,那麼就會導緻 Goruntine 和資源的“積壓”。而用戶端在主動結束、或者逾時結束它的請求之後,因為 Kubernetes 面向終态的架構,用戶端勢必會立刻又發起新的請求,進而使得“積壓”甚至是洩露的 Goruntine 和資源越來越多,最終導緻 API Server OOM 和 crash。
我們都知道 golang 中使用 context 來表示“上下文”的含義。API Server 請求外部服務的“上下文”就是用戶端發起請求,那麼當用戶端的請求結束之後,API Server 也應該立刻回收 API Server 請求外部服務的資源,即這類請求也應該立刻停止并退出,隻有這樣,API Server 才能提高吞吐并不會被積壓的 Goruntine 和資源所拖累。
阿裡巴巴和螞蟻金服的工程師發現并參與了 API Server 全鍊路的 context-aware 的優化工作,Kubernetes v1.16 版本已經将 Admission、Webhook 等優化為 context-aware,進而進一步提升 API Server 的性能和吞吐。
Requests Flood Prevention
API Server 對于接收處理請求的自我保護能力太過薄弱,目前可以說除了 max-inflight filter 做了限制最大讀、寫并發外,沒有其它能夠限制請求數量和并發的功能。這帶來一個非常大的問題:API Server 可能因為接收并處理太多的請求進而導緻 API Server OOM 或者崩潰。
雖然 API Server 是一個内部的系統,幾乎沒有外來請求的攻擊,所有的請求都來自 Kubernetes 内部的元件和子產品,API Server 也可能因為内部的請求量過大而導緻自己身崩潰。根據我們的觀察和經驗,API Server 接收過多請求處理而導緻崩潰的主要場景有如下兩部分:
- API Server 自身重新開機或者更新
我們知道 Kubernetes 是以 API Server 為中心的系統。當 API Server 重新開機或者更新之後,所有的元件 client 都斷開了連接配接并開始重新請求 API Server,特别是重建立立 List/Watch 需要比較大的資源開銷。而 API Server 與 etcd 有自己的 cache 層,當用戶端的 Informer List 請求到來之時,如果 cache 還未 ready 就會去請求 etcd,而大量的從 etcd List 資源可能會将 API Server 與 etcd 網絡鍊路打滿,甚至出現 API Server 和 etcd 的 OOM。而剛啟動的 API Server 就陷入 crash,勢必會導緻用戶端更大量的請求,進而陷入雪崩狀态。
對于這種場景,我們采用“主動拒絕”請求的方式。在 API Server 剛啟動之時,如果 API Server 和 etcd 之間的 cache 還未 Ready,API Server 就會拒絕耗資源較大的請求,如 List 資源的請求:隻有在 cache Ready 之後,API Server 才向用戶端提供 List 資源的服務,否則傳回 429 讓用戶端等待一短時間後重試。隻有這樣,API Server 才能将接受大規模請求的主要瓶頸優化到 API Server 和用戶端網絡上 IO 的瓶頸。
- 用戶端元件出現 Bug,瘋狂的請求 API Server
特别是 DaemonSet 元件出現 Bug,那麼請求量将乘以節點數目。我們線上上發生過 Daemonset 出現 Bug,上萬個節點一直瘋狂 List Pod 進而導緻 API Server crash 的案例。
對于這種的場景,我們采用應急限流的方案。我們實作了可以動态配置,根據請求來源的 User-Agent 去做限流。當再次出現此類問題時,我們從監控圖表裡發現有問題的 User-Agent 并将它限流。隻有在 API Server 健康的前提下,我們才能對 DaemonSet 做出修複并更新。在 API Server crash 時,DaemonSet 的更新也失效了,進而陷入叢集無法挽救的局面。
采用 User-Agent 而非根據 Identity 資訊(請求的使用者資訊) 做限流原因是因為 API Server 做請求的身份識别也需要耗費資源,很可能因為在為大量請求做身份識别過程中就出現 API Server 資源耗盡的情況。其次,我們可以從監控中快速的發現有問題的請求的 User-Agent,進而做到更快速的響應。
阿裡和螞蟻金服的工程師已經将該限流方案的 User Story 和優化方式已經送出到社群。
Controller failover
在 10k node 的生産叢集中,Controller 中存儲着近百萬的對象,從 API Server 擷取這些對象并反序列化的開銷是無法忽略的,重新開機 Controller 恢複時可能需要花費幾分鐘才能完成這項工作,這對于阿裡巴巴規模的企業來說是不可接受的。為了減小元件更新對系統可用性的影響,我們需要盡量的減小 controller 單次更新對系統的中斷時間,這裡通過如下圖所示的方案來解決這個問題:
- 預啟動備 controller informer ,提前加載 controller 需要的資料;
- 主 controller 更新時,會主動釋放 Leader Lease,觸發備立即接管工作。
通過這個方案,我們将 controller 中斷時間降低到秒級别(更新時 < 2s),即使在異常當機時,備僅需等待 leader lease 的過期(預設 15s),無需要花費幾分鐘重新同步資料。通過這個增強,顯著的降低了 controller MTTR,同時降低了 controller 恢複時對 API Server 的性能沖擊。該方案同樣适用于 scheduler。
Customized scheduler
由于曆史原因,阿裡巴巴的排程器采用了自研的架構,因時間的關系本次分享并未展開排程器部分的增強。這裡僅分享兩個基本的思路,如下圖所示:
Equivalence classes:典型的使用者擴容請求為一次擴容多個容器,是以我們通過将 pending 隊列中的請求劃分等價類的方式,實作批處理,顯著的降低 Predicates/Priorities 的次數;
Relaxed randomization:對于單次的排程請求,當叢集中的候選節點非常多時,我們并不需要評估叢集中全部節點,在挑選到足夠的節點後即可進入排程的後續處理(通過犧牲求解的精确性來提高排程性能)。
總結
阿裡巴巴和螞蟻金服通過一系列的增強與優化,成功将 Kubernetes 應用到生産環境并達到了單叢集 10000 節點的超大規模,具體包括:
- 通過将索引和資料分離、資料 shard 等方式提高 etcd 存儲容量,并最終通過改進 etcd 底層 bbolt db 存儲引擎的塊配置設定算法,大幅提高了 etcd 在存儲大資料量場景下的性能,通過單 etcd 叢集支援大規模 Kubernetes 叢集,大幅簡化了整個系統架構的複雜性;
- 通過落地 Kubernetes 輕量級心跳、改進 HA 叢集下多個 API Server 節點的負載均衡、ListWatch 機制中增加 bookmark、通過索引與 Cache 的方式改進了 Kubernetes 大規模叢集中最頭疼的 List 性能瓶頸,使得穩定的運作萬節點叢集成為可能;
- 通過熱備的方式大幅縮短了 controller/scheduler 在主備切換時的服務中斷時間,提高了整個叢集的可用性;
- 阿裡巴巴自研排程器在性能優化上最有效的兩個思路:等價類處理以及随機松弛算法。
通過這一系列功能增強,阿裡巴巴和螞蟻金服成功将内部最核心的業務運作在上萬節點的 Kubernetes 叢集之上,并經曆了 2019 年 618 大促的考驗。
Kubernetes 是為生産環境而設計的容器排程管理系統,一經推出便迅速蹿紅,它的很多設計思想都契合了微服務和雲原生應用的設計法則。随着對 K8s 系統使用的加深和加廣,會有越來越多有關雲原生應用的設計模式産生出來,使得基于 K8s 系統設計和開發生産級的複雜雲原生應用變得像啟動一個單機版容器服務那樣簡單易用。
那麼什麼是“雲原生”?作為雲計算時代的開發者和從業者,我們該如何在“雲原生”的技術浪潮中站穩腳跟,在将雲原生落地的同時實作自我價值的有效提升呢?
現在,我們邀請來自全球“雲原生”技術社群的親曆者和領軍人物,為每一位中國開發者講解和剖析關于“雲原生”的方方面面,用視訊的方式揭示這次雲計算變革背後的技術思想和本質。
學習位址
金牌講師帶來重磅課程,點選“
CNCF×Alibaba雲原生技術公開課”,登入後,即可線上學習。
課程全程免費,我們将帶來:
- 完善的知識體系,打造屬于自己的雲原生技術樹;
- 了解雲原生技術背後的思想與本質;
- 與知識體系相輔相成的動手實踐;
- 一線技術團隊雲原生技術最佳實踐。
适合人群:
- 計算機科學、軟體工程等領域的軟體工程師和大學生;
- 使用/嘗試使用容器和Kubernetes技術的應用程式開發者;
- 具有基本伺服器端知識、正在探索容器技術的軟體開發者和技術管理者;
- 希望了解雲原生技術棧基本原理的技術管理者和開發者。
部分講師介紹
原文釋出時間為:2019-10-16
作者:曾凡松(逐靈)
本文來自雲栖社群合作夥伴“
阿裡技術”,了解相關資訊可以關注“
”。