爲何 K8s 集羣達萬級規模,阿里購物體驗還能如絲順滑?

阿里妹導讀:本文主要介紹阿里巴巴和螞蟻金服在大規模生產環境中落地 Kubernetes 的過程當中,在集羣規模上遇到的典型問題以及對應的解決方案,內容包含對 etcd、kube-apiserver、kube-controller 的若干性能及穩定性加強,這些關鍵的加強是阿里巴巴和螞蟻金服內部上萬節點的 Kubernetes 集羣可以平穩支撐 2019 年天貓 618 大促的關鍵所在。
文內藏福利,向下滑滑滑,免費課程馬上領取~

背景

從阿里巴巴最先期的 AI 系統(2013)開始,集羣管理系統經歷了多輪的架構演進,到 2018 年全面的應用 Kubernetes ,這期間的故事是很是精彩的。這裏忽略系統演進的過程,不去討論爲何 Kubernetes 可以在社區和公司內部全面的勝出,而是將焦點關注到應用 Kubernetes 中會遇到什麼樣的問題,以及咱們作了哪些關鍵的優化。node

在阿里巴巴和螞蟻金服的生產環境中,容器化的應用超過了 10k 個,全網的容器在百萬的級別,運行在十幾萬臺宿主機上。支撐阿里巴巴核心電商業務的集羣有十幾個,最大的集羣有幾萬的節點。在落地 Kubernetes 的過程當中,在規模上面臨了很大的挑戰,好比如何將 Kubernetes 應用到超大規模的生產級別。golang

羅馬不是一天就建成的,爲了瞭解 Kubernetes 的性能瓶頸,咱們結合阿里和螞蟻的生產集羣現狀,估算了在 10k 個節點的集羣中,預計會達到的規模:算法

  • 20w pods
  • 100w objects

咱們基於 Kubemark 搭建了大規模集羣模擬的平臺,經過一個容器啓動多個(50個)Kubemark 進程的方式,使用了 200 個 4c 的容器模擬了 10k 節點的 kubelet。在模擬集羣中運行常見的負載時,咱們發現一些基本的操做好比 Pod 調度延遲很是高,達到了驚人的 10s 這一級別,而且集羣處在很是不穩定的狀態。數據庫

當 Kubernetes 集羣規模達到 10k 節點時,系統的各個組件均出現相應的性能問題,好比:後端

  1. etcd 中出現了大量的讀寫延遲,而且產生了拒絕服務的情形,同時因其空間的限制也沒法承載 Kubernetes 存儲大量的對象;
  2. API Server 查詢 pods/nodes 延遲很是的高,併發查詢請求可能地址後端 etcd oom;
  3. Controller 不能及時從 API Server 感知到在最新的變化,處理的延時較高;當發生異常重啓時,服務的恢復時間須要幾分鐘;
  4. Scheduler 延遲高、吞吐低,沒法適應阿里業務平常運維的需求,更沒法支持大促態的極端場景。

etcd improvements

爲了解決這些問題,阿里雲容器平臺在各方面都作了很大的努力,改進 Kubernetes 在大規模場景下的性能。設計模式

首先是 etcd 層面,做爲 Kubernetes 存儲對象的數據庫,其對 Kubernetes 集羣的性能影響相當重要。api

  • 初版本的改進,咱們經過將 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(包含節點上數十計的鏡像,和若干的卷信息),這會帶來兩大問題:

  1. 心跳請求觸發 etcd 中 node 對象的更新,在 10k nodes 的集羣中,這些更新將產生近 1GB/min 的 transaction logs(etcd 會記錄變動歷史);
  2. API Server 很高的 CPU 消耗,node 節點很是龐大,序列化/反序列化開銷很大,處理心跳請求的 CPU 開銷超過 API Server CPU 時間佔用的 80%。

  1. 爲了解決這個問題,Kubernetes 引入了一個新的 build-in Lease API ,將與心跳密切相關的信息從 node 對象中剝離出來,也就是上圖中的 Lease 。本來 kubelet 每 10s 更新一次 node 對象升級爲:
  2. 每 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,有兩個典型的思路:

  1. API Server 測增長 lb,全部的 kubelets 鏈接 lb,典型的雲廠商交付的 Kubernetes 集羣,就是這一模式;
  2. kubelet 測增長 lb,由 lb 來選擇 API Server。

經過壓測環境驗證發現,增長 lb 並不能很好的解決上面提到的問題,咱們必需要深刻理解 Kubernetes 內部的通訊機制。深刻到 Kubernetes 中研究發現,爲了解決 tls 鏈接認證的開銷,Kubernetes 客戶端作了不少的努力確保「儘可能複用一樣的 tls 鏈接」,大多數狀況下客戶端 watcher 均工做在下層的同一個 tls 鏈接上,僅當這個鏈接發生異常時,纔可能會觸發重連繼而發生 API Server 的切換。其結果就是咱們看到的,當 kubelet 鏈接到其中一個 API Server 後,基本上是不會發生負載切換。爲了解決這個問題,咱們進行了三個方面的優化:

  1. API Server:認爲客戶端是不可信的,須要保護本身不被過載的請求擊潰。當自身負載超過一個閾值時,發送 429 - too many requests 提醒客戶端退避;當自身負載超過一個更高的閾值時,經過關閉客戶端鏈接拒絕請求;
  2. Client:在一個時間段內頻繁的收到 429 時,嘗試重建鏈接切換 API Server;按期地重建鏈接切換 API Server 完成洗牌;
  3. 運維層面,咱們經過設置 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 的對象中,好比會有:

  1. Pod Storage
  2. Node Storage
  3. Configmap Storage
  4. ……

每一個類型的 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 的查詢請求。從性能角度看,這帶來了幾個問題:

  1. 沒法支持索引,查詢節點的 pod 須要先獲取集羣中全部的 pod,這個開銷是巨大的;
  2. 由於 etcd 的 request-response 模型,單次請求查詢過大的數據會消耗大量的內存,一般狀況下 API Server 與 etcd 之間的查詢會限制請求的數據量,並經過分頁的方式來完成大量的數據查詢,分頁帶來的屢次的 round trip 顯著下降了性能;
  3. 爲了確保一致性,API Server 查詢 etcd 均採用了 Quorum read ,這個查詢開銷是集羣級別,沒法擴展的。

爲了解決這個問題,咱們設計了 API Server 與 etcd 的數據協同機制,確保 Client 可以經過 API Server 的 cache 獲取到一致的數據,其原理以下圖所示,總體工做流程以下:

  1. t0 時刻 Client 查詢 API Server;
  2. API Server 請求 etcd 獲取當前的數據版本 rv@t0;
  3. API Server 請求進度的更新,並等待 Reflector 數據版本達到 rv@t0;
  4. 經過 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 單次升級對系統的中斷時間,這裏經過以下圖所示的方案來解決這個問題:

  1. 預啓動備 controller informer ,提早加載 controller 須要的數據;
  2. 主 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 節點的超大規模,具體包括:

  1. 經過將索引和數據分離、數據 shard 等方式提升 etcd 存儲容量,並最終經過改進 etcd 底層 bbolt db 存儲引擎的塊分配算法,大幅提升了 etcd 在存儲大數據量場景下的性能,經過單 etcd 集羣支持大規模 Kubernetes 集羣,大幅簡化了整個系統架構的複雜性;
  2. 經過落地 Kubernetes 輕量級心跳、改進 HA 集羣下多個 API Server 節點的負載均衡、ListWatch 機制中增長 bookmark、經過索引與 Cache 的方式改進了 Kubernetes 大規模集羣中最頭疼的 List 性能瓶頸,使得穩定的運行萬節點集羣成爲可能;
  3. 經過熱備的方式大幅縮短了 controller/scheduler 在主備切換時的服務中斷時間,提升了整個集羣的可用性;
  4. 阿里巴巴自研調度器在性能優化上最有效的兩個思路:等價類處理以及隨機鬆弛算法。

經過這一系列功能加強,阿里巴巴和螞蟻金服成功將內部最核心的業務運行在上萬節點的 Kubernetes 集羣之上,並經歷了 2019 年 618 大促的考驗。

Kubernetes 是爲生產環境而設計的容器調度管理系統,一經推出便迅速躥紅,它的不少設計思想都契合了微服務和雲原生應用的設計法則。隨着對 K8s 系統使用的加深和加廣,會有愈來愈多有關雲原生應用的設計模式產生出來,使得基於 K8s 系統設計和開發生產級的複雜雲原生應用變得像啓動一個單機版容器服務那樣簡單易用。

那麼什麼是「雲原生」?做爲雲計算時代的開發者和從業者,咱們該如何在「雲原生」的技術浪潮中站穩腳跟,在將雲原生落地的同時實現自我價值的有效提高呢?

如今,咱們邀請來自全球「雲原生」技術社區的親歷者和領軍人物,爲每一位中國開發者講解和剖析關於「雲原生」的方方面面,用視頻的方式揭示此次雲計算變革背後的技術思想和本質。

學習地址

金牌講師帶來重磅課程,點擊「CNCF×Alibaba雲原生技術公開課」,登陸後,便可在線學習。

課程全程免費,咱們將帶來:

  • 完善的知識體系,打造屬於本身的雲原生技術樹;
  • 理解雲原生技術背後的思想與本質;
  • 與知識體系相輔相成的動手實踐;
  • 一線技術團隊雲原生技術最佳實踐。

適合人羣:

  • 計算機科學、軟件工程等領域的軟件工程師和大學生;
  • 使用/嘗試使用容器和Kubernetes技術的應用程序開發者;
  • 具備基本服務器端知識、正在探索容器技術的軟件開發者和技術管理者;
  • 但願理解雲原生技術棧基本原理的技術管理者和開發者。

部分講師介紹


本文做者:曾凡鬆(逐靈)

閱讀原文

本文來自雲棲社區合做夥伴「阿里技術」,如需轉載請聯繫原做者。

相關文章
相關標籤/搜索