來自阿里P10的故障分析:從滴滴的故障我們能學到什麼

小猿姐聊技術發表於2023-12-28

1 月 27 日晚滴滴發生了大範圍、長時間的故障。官方訊息說是“ 底層系統軟體發生故障”,而據網上的小道訊息,一個規模非常大的 K8s 叢集進行線上熱升級,因為某些原因,所有 Pod(容器)被 kill,而 K8s 的後設資料已經被新版本 K8s 修改,無法回滾,因此恢復時間拉的很長。

從滴滴近期分享的技術文章來看,這個說法並不是空穴來風。滴滴團隊近兩個月正在把公司內部的 K8s 從 1.12 升級到 1.20,1.12 是 2018 年 9 月釋出的,而 1.20 是 2020 年 12 月,對高速發展的 K8s 專案來說,兩個版本存在相當大的差距。K8s 官方推薦的方法是沿著一個個版本升上去。 但滴滴團隊認為多次升級風險更高,跨越 8 個版本一把升。而且為了避免中斷業務,在不重啟容器的情況下原地升級,滴滴團隊還修改了 kubelet 的程式碼

這個升級流程如果一切正常理論上是 work 的。我推測還是 遇上了未考慮到的意外因素,比如運維誤操作,造成了這次大規模的故障。從我的經驗來說,遵循以下設計原則,可以極大的 降低風險機率、減小故障範圍

一、控制規模,用多個小規模 K8s 叢集的聯邦代替一個大 K8s

先說我的⼀個經歷。早年阿⾥雲發起過⼀個 5K 項⽬,⽤ 5000 臺物理機組成⼀個 ODPS(即後來的 MaxCompute)大叢集來⽀持阿⾥內部的⼤資料業務,5K 項⽬在 13 年上半年進⼊攻堅階段,我作為技術⻣⼲被抽調到這個項⽬⾥。ODPS 有⼀個元件叫⼥媧,類似 Hadoop 的 Name Node,提供名字解析、分散式鎖等服務。當 5K 叢集進⾏機房級掉電測試時,⼏千臺伺服器(其中有⼏萬個服務)在重啟後會向 3 臺⼥媧伺服器發起遠端調⽤去解析彼此的地址,就像 DDoS 攻擊⼀樣,瞬時流量會持續打滿千兆⽹卡,造成整個 5K 叢集⼀波⼜⼀波的雪崩。

這段經歷告訴我, 當一個叢集規模很大時,很容易在意想不到的地方發生類似的問題。因此,在設計系統時,我傾向於 把叢集的規模控制在⼀個合理的範圍。例如把⼀個資源池的⼤⼩控制在⼏百臺機器的規模,當需要更多資源時,不是去擴充套件單個資源池的⼤⼩,⽽是去新建資源池,擴充套件資源池的數量。換句話說, 為了解決業務增長問題,不要 scale up 單個叢集,⽽是 scale out 出更多的叢集

另一個要考慮的問題是 爆炸半徑。再舉個例子,PolarDB 的底層儲存元件叫 PolarStore,從外部來看,PolarStore 是⼀個可⽆限擴容的儲存服務,但內部實現上,我將其設計成由一個個隔離開的⼩叢集組成,每個叢集⼏⼗到上百臺伺服器。這個設計 2017 年投入商用,雖然之後 PolarDB 的規模逐年激增,但 PolarStore 服務從來沒出現過大範圍的故障。K8s 叢集也是⼀樣。特別⼤規模的 K8s 叢集,例如上萬臺的,其實都存在爆炸半徑過⼤的的穩定性⻛險。 與其不斷最佳化與提⾼ K8s 的規模極限,不如梳理業務,把這些巨型 K8s 叢集拆為多個⼤⼩適中的叢集

實際系統設計和開發中,我們可能會因為多種原因傾向於選擇大叢集。我聽過的⼀個理由是為了避免跨 K8s 叢集⽹絡不連通問題,就⼲脆把所有 Pod 都塞到了⼀個 K8s 叢集⾥。但⽹絡連通性的問題還是應該由⽹絡⽅案來解,比如這個問題可以透過負載均衡器(LB)暴露 Pod 地址,或給 Pod 額外分配⼀個 Underlay ⽹絡地址(例如物理網路或者 VPC 網路的地址)來解決,而不應該將其和架構設計相耦合。 KubeBlocks 的⽤戶經常來諮詢我們,⼀套 KubeBlocks 能不能管理⼏千或者上萬個資料庫例項。我們給出的答案是 能,但我們推薦採⽤多個 K8s 叢集⽔平擴充套件的⽅法,每個 K8s 叢集都是⼀個資源池,各⾃部署⼀套⾃治的 KubeBlocks,然後再把這些資源池註冊到我們的中⼼管理叢集⾥來,透過這種架構擴充套件到⼏⼗萬個資料庫例項數都不存在問題。這本質上是⼀種 將多個 K8s 叢集組成聯邦的⽅案

二、避免單點,一個 K8s 叢集也應該被視作一個單點

架構師們一直都很小心謹慎的避免單點故障。服務要進行冗餘部署,資料庫要有主備。在機房內,網路要有雙上聯交換機,伺服器除了市電供電,還要準備柴油發電機應急。在支付寶機房被挖斷光纖後,大家又開始重視機房級別的高可用容災,業務要做跨機房甚至是跨地域的部署。如果一個機房斷網、斷電,那流量要能快速的從故障機房切走,業務處理以及底層的儲存都要切換到其他機房。比如,八年前我在設計 RDS 專有云雙機房部署方案的時候,要考慮單機房災難發生時,資料庫的主備複製關係、負載均衡還有 IP 網段如何從主機房漂移到備機房,以及故障恢復時流量如何再切回主機房。

但 K8s 經常被架構師們視作是一個業已具有多機房容災、高可用能力的分散式系統。從而 忽視了把高可用業務部署在單 K8s 叢集上的固有風險。K8s 是一個管理容器編排的系統軟體,如同所有的軟體系統,遇到非預期的事件,例如本次滴滴故障中跨多個版本的原地升級,也是有機率徹底掛掉的。這個時候,業務和儲存系統在單 K8s 裡縱使有再多的副本,也要歇菜。

因此我們建議架構師們在設計部署方案時, 要把 K8s 視作存在單點風險的單元,一個 K8s 是一個部署單元,把過去多單元多活的技術方案移植到多 K8s 多活的場景下來。不僅是無狀態的服務,服務所依賴的資料庫的副本也要跨 K8s 部署。除了資料面,在控制面管理業務負載與資料庫的 K8s operator 管理軟體也需要做到多 K8s 多活。這類似於,在 RDS 系統裡,除了資料庫要做高可用,RDS 的管控系統必須要實現雙活,否則故障發生時,自動化系統都失效了,所有操作都得人工執行,自然會增加故障的處理時長。這個能力我們會在 KubeBlocks 裡支援上。

額外的好處是,如果你的業務部署在多個 K8s 叢集中,那麼  K8s 的升級策略可以更加靈活,可以透過逐個升級 K8s 叢集,這樣能夠進一步降低升級過程中可能出現的風險。

三、擁抱重啟,把重啟和遷移視作常態

回到 K8s 的升級,K8s 官方推薦的方式是這樣的,逐一地將每個節點上的 Pod 驅逐到其他節點上去,從叢集中移除節點,升級,然後再將它重新加入到叢集,這是一種滾動升級機制(Rolling)。而 AWS EKS 還支援一種藍綠部署機制(Blue-Green),建立一個新的節點組,使用新的 K8s 版本,然後,將 Pod 從舊的節點組遷移到新的節點組,實現藍綠部署,一旦所有的 Pod 都已經成功遷移到新的節點組,再可以刪除舊的節點組。兩種方法都需要遷移和重啟 Pod。這次故障裡,滴滴採用了非常規的 K8s 升級手段,其中一個重要的動機是避免 Pod 重啟影響業務。這其實代表了一類 old-school 的伺服器管理理念。

在 DevOps 中,"Pets vs Cattle" 是一個常用的比喻,用來描述兩種不同的伺服器管理策略。Pets(寵物,例如貓)代表的是那些我們精心照料和維護的伺服器。當它們出現問題時,我們會盡一切可能去修復,而不是直接替換它們。每一個 Pet 都是獨一無二的,有自己的名字,我們知道它們的效能,甚至它們的"性格"(例如,某個伺服器可能會經常出現某種特定的問題)。過去工程師和運維們非常的厭惡伺服器重啟,以至於雲廠商 ECS 團隊的一個奮鬥目標就是把虛擬機器做到如小型機般的可靠,為此不斷的改進虛擬機器熱遷移等技術。

Cattle(牲口,例如牛)代表的是那些我們可以隨意新增或刪除的伺服器。我們不會對它們進行個別管理,而是將它們視為一個整體來管理。如果其中的任何一個出現問題或者變化,我們通常會選擇直接替換,而不是修復。Cattle 的例子包括在雲環境中執行的虛機,或者是在 Kubernetes 叢集中執行的 Pod。甚至可以把 K8s 叢集本身也視作 Cattle,如果 K8s 出現問題,或者是 K8s 叢集要做版本升級,直接把這個 K8s 換掉,把老的 K8s 裡的 Pod 直接遷移到新的 K8s 裡。

把 Pod 看做 Pets,就會想盡一切辦法來避免 Pod 重啟。而把 Pod 看做 Cattle,就會換一個思維, 把 Pod 的重啟和遷移作為一個需求來設計系統。我建議工程師們採用後一種思維。不要害怕 Pod 重啟和遷移,而是 把處理 Pod 重啟、遷移以及遇到問題回滾的程式碼視為系統的正常執行例程的一部分。在複雜系統設計中,期待並規劃故障的發生,而不是試圖阻止它們發生, 透過定期升級系統,驗證處理重啟、遷移、回滾的程式碼,確保系統在面對重啟和遷移這種常態時能夠正常運作。把重啟和遷移視為常態,而不是異常,這種思維方式能夠幫助我們設計出更可靠、更健壯的系統。

因此,在 KubeBlocks 執行資料庫大版本升級時,我們並不推薦原地升級,因為原地升級總有一天會踩到坑的。我們會 新建一個高版本的資料庫例項,透過全量和增量資料遷移將資料匯入到新例項中,透過資料庫代理層保持來自應用端的網路連線,降低在例項間切換對業務的影響。在升級完成後,我們還會保持老版本和新版本的資料庫同時執行一段時間並且維持雙向同步,在業務確認升級不造成非預期影響後再清理老版本的例項。

四、資料面的可用性和控制面要解耦

最後想說的一個經驗是 資料面的可用性要和控制面解耦。我先舉兩個例子,這兩個都是儲存系統,但是基於不同理念設計的:

第一個系統是 PolarDB 的儲存系統 PolarStore。PolarStore 採取了控制面與資料面分離的理念(詳情參考我在 VLDB 2018 年發表的"PolarFS"論文)。資料面的讀寫操作都僅依賴查詢快取在本地的全量後設資料。控制面僅僅在執行管理操作,例如建立卷、卷的擴縮容、節點當機發起資料遷移、叢集擴縮容的時候需要修改後設資料才會被強依賴。控制面對後設資料的修改會透過後設資料通知機制非同步更新到資料面的快取裡。這個設計的優點是高度的可靠性,即使整個控制面不可用,在資料面讀寫檔案都可以正常完成,這對資料庫業務而言很重要。

而在另一個系統中,控制面與資料面是耦合的。這個系統有三個很重要的 Master 節點,Master 節點除了承擔控制面的任務外,還承擔了一部分資料面的職責。舉個例子,在這個系統中,資料是以 Append only 的形式不斷追加到一個日誌流中,而日誌流會按 64MB 分割為 chunk,每寫滿一批 chunk,資料面的節點就要找 Master 節點分配下一批新 chunk 的排程策略。這個設計有一個缺限,就是 Master 節點一旦當機,整個儲存叢集很快就無法寫入新資料。為了克服這個缺陷,Master 從三副本改為了五副本,同時 Master 還採用了 Sharding 的方案來提高吞吐能力。

我還想到第三個例子,前陣子阿里雲的史詩級故障,物件儲存的關鍵路徑裡依賴了 RAM 的鑑權邏輯,因此 RAM 出現故障時,也造成了物件儲存的不可用。這幾個儲存例子告訴我們, 資料面的可用性如果和控制面解耦,那麼控制面掛掉對資料面的影響很輕微。否則,要麼要不斷去提高控制面的可用性,要麼就要接受故障的級聯發生。

KubeBlocks 也採取了控制面與資料面分離的設計,控制麵包括 KubeBlocks operator、K8s API Server、Scheduler、Controller Manager 和 etcd 儲存,它負責整個叢集的管理,包括排程、資源分配、物件生命週期管理等功能。而資料面則是在 Pods 中執行的容器,包含各種資料庫的 SQL 處理與資料儲存元件。KubeBlocks 可以保證即使控制面的節點全部當機,資料面仍然可用。而結合資料庫核心、代理與負載均衡的協同,還可以進一步做到控制面失敗,資料面仍然可以執行高可用切換。

結語

控制規模、避免單點、擁抱重啟、資料面的可用性和控制面解耦。這些點是我過去十多年在設計 RDS 和 PolarDB 這樣的大規模雲服務時所重視的一些設計原則,這些原則可以幫助防禦系統出現大規模故障。在開發 KubeBlocks 的過程中,我發現這些原則在 K8s 的場景下仍然是有效的,希望可以幫助架構師和工程師們設計更穩定的系統。

關於作者

曹偉(鳴嵩),雲猿生資料創始人 & CEO,前阿里雲資料庫總經理/研究員,雲原生資料庫 PolarDB 創始人。中國計算機學會資料庫專委會、開源專委會執行專委,獲得 2020 年中國電子學會科技進步一等獎,在 SIGMOD、VLDB、ICDE、FAST、USENIX ATC 等資料庫與儲存國際學術會議發表論文 20 餘篇。


來自 “ ITPUB部落格 ” ,連結:https://blog.itpub.net/70035809/viewspace-3001948/,如需轉載,請註明出處,否則將追究法律責任。

相關文章