深度解讀 Chaos Mesh®,探索雲原生混沌工程的奧祕

本篇文章整理自我司研發工程師楊可奧以及 PingCAP 工程效率負責人、Chaos Mesh 負責人周強在 GoCN 開源說上的演講實錄。

本文首先介紹了對混沌工程這一概念的描述,分享了混沌工程的動機和實踐方式以及 Chaos Mesh 項目的發展情況。在後半部分,介紹了 Chaos Mesh 項目本身的架構,並涉及到在 Go 的生態環境中對容器等基本概念進行操作。乾貨十足,enjoy~本文首先介紹了對混沌工程這一概念的描述,分享了混沌工程的動機和實踐方式以及 Chaos Mesh 項目的發展情況。在後半部分,介紹了 Chaos Mesh 項目本身的架構,並涉及到在 Go 的生態環境中對容器等基本概念進行操作。乾貨十足,enjoy~

混沌工程概述

現在的技術潮流在向着大規模集羣、超複雜的分佈式系統與微服務架構演進。在演進的過程 當中,雖然給我們帶來了不少的便利,同時也帶來了許多的麻煩。其中之一便是 —— 當一個節點發生錯誤的時候,我們無法預料它將產生怎樣的蝴蝶效應。它將只牽涉到部分服務還是會讓所有服務崩潰?它能夠自愈嗎?更可怕的是,隨着計算規模的擴大,故障發生的可能性也越來越大。對於一個個人電腦用戶來說,可能用到更換電腦硬盤也不曾發生過損壞;而對於服務器集羣來說,每天都可能會有數塊磁盤損壞需要更換。

無論是雲計算的領頭羊 AWS,或是面向工程師們的 GitHub,還是互聯網巨頭 Google,都無法逃離故障的命運。

混沌工程是一門新興的技術學科,他的初衷是通過實驗性的方法,讓人們建立對於複雜分佈式系統在生產中抵禦突發事件能力的信心。

而混沌工程便是在這樣糟糕的環境下,讓開發者、運維對複雜系統仍然保持信心的方法。

混沌工程歷史

混沌工程已經走過了十一個年頭了。從最初Netflix提出這個概念,到 16 年 Gremlin 給出了 混動工程的商業產品,試圖形成混沌工程服務的商業模式。Chaos Mesh 是在 2019 年末開源的,現在也成爲了最受關注的混沌工程項目之一。

混沌工程步驟

如果想要爲你管理的項目引入混沌工程,那麼可以依照以下五步的循環:

不斷進行這五步的循環,將對工程的穩定性產生明顯的提升。以混沌工程在 TiDB 上的實驗爲例:

1. 我們立下期望,TiDB 在刪除一個節點之後應該能夠在短時間內恢復。

2. 進行了刪除節點的混沌實驗。

3. 發現前兩次 TiDB 都在短時間內恢復了,而第三次卻花了很長的時間才恢復。

4. 調查這之中是否存在一些 Bug,修復之後再次進行實驗,看看還有沒有這個問題。

反覆多次地進行這樣的步驟,都 TiDB 的穩定性就產生了一些幫助。

Chaos Mesh 的社區

在短短的一年間, Chaos Mesh 的社區也已經不斷壯大了,現在擁有了超過 3400 個星星,近 90 個貢獻者,也是 CNCF Sandbox 項目。現在 Chaos Mesh 也已經擁有了涉及 Pod 的生命 週期、IO 、網絡、資源壓力、公有云等諸多方面的數十種不同類型的錯誤注入方式;還擁有一個功能豐富的儀表盤。這一切成就離不開來自社區的幫助。無論是對 Bug 的報告、對工程、設計的意見,還是直接提交代碼,都是對 Chaos Mesh 項目的巨大貢獻。

Chaos Mesh 整體架構

Chaos Mesh 的整體架構如圖中所展示,可以自上而下分爲三個部分: 

1. 用戶輸入、觀測的部分。

2. 監聽資源變化,進行注入/恢復的 Controller 組件。

3. 在具體節點上進行故障注入的 Chaos Daemon。

這部分文章將依照這三個部分展開,自上而下梳理 Chaos Mesh 的架構。

用戶輸入、觀測的部分

用戶輸入的部分總是以用戶的操作爲起點,以 Kubernetes API Server 爲重點,不直接和 Chaos Mesh 的 Controller 交互。一切的用戶操作最終都將反映爲某個 Chaos 資源的變更(比如 NetworkChaos 資源的變更)。這保障了 Chaos Mesh Controller 的事件來源的單純性。Chaos Mesh 提供了三種輸入的方式:

1. 通過 kubectl 等命令行工具,將一個 YAML 文件提交至 Kubernetes 服務器這樣做能夠清晰地知道提交的內容,通過 kubectl 這一 Kubernetes 用戶都能熟練使用的工具,將 Chaos Mesh 的資源操作方式和 Pod、Deployment 等其他原生資源的提交方式統一起來,方便用戶上手、嘗試。而除了使用 kubectl apply 命令來提交一個混沌實驗之外,還可以通過 kubectl describe 命令來查詢實驗的狀態和錯誤信息,使用 kubectl patch 來修改實驗將實驗暫停、恢復。(如圖,一個典型的描述網絡分區的 NetworkChaos 資源文件)。

2. 通過 kubernetes/client-go 等包來對 Chaos 資源進行增刪查改。這樣做的好處是能夠方便地集成進已有的測試流程。如果用戶已經搭建好一個可編程的測試平臺,那麼就可以通過這種方式在任何自己期望的時機插入、恢復 混沌實驗,如果說將 Chaos Mesh 提供的豐富的錯誤注入能力作爲測試的武器庫,那麼可編程的方式就是使用這些武器的最靈活的方式。

3. 通過 Chaos Dashboard 提供的 Web UI 界面對混沌試驗進行操作和觀測。Chaos Dashboard 提供了一套友好的用戶界面,同時也提供了依照 RBAC 的權限管控機制,用戶需要輸入自己的 Token 才能夠進行操作。在這部分 UI 中用戶能夠管理和觀察已有的錯誤,同時也能將錯誤注入歸檔並在將來複用。

**監聽資源變化的 Controller **

Chaos Mesh 的 Controller 總是隻接受來自 Kubernetes API Server 的事件 —— 這種事件會描述“什麼資源發生了什麼變化”,比如一個名爲 network-test 的 NetworkChaos 資源被創建了。在工程實踐中,對資源的具體變化進行響應太過複雜了;而往往選擇使用“同步” 的方式 —— 將資源中描述的情形向真實狀態中同步,比如當 Pod 建立時會將這種描述給具象化爲一個(或多個)容器;反過來的 "同步" 也是存在的,當容器意外死亡、 啓動失敗的時候錯誤狀態也會同步至 Pod 中。所以無論是怎樣的變化(無論是增刪查改 中的哪一個),Controller 要做的都只是決定以下兩件事情:

1. 現在應該注入還是應該恢復還是要等待?

2. 如果需要注入/恢復,應該怎麼做?

對於第一個問題,Controller 將必要的信息存儲在資源中,比如下一次要注入的時間、 下一次要恢復的時間。這樣在響應變化的時候,就能通過當前時間來推斷應該注入還是恢復還是等待。

而對於第二個問題,Controller 需要根據測試類型的不同進行不同的判斷,比如刪除 Pod 就能通過向 Kubernetes API Server 發送請求直接完成,對於公有云的混沌測試 ——比如起停虛擬機、移除磁盤則會通過每個公有云不同的 API 來完成,而其他稍稍複雜的錯誤注入(比如後文會提到的時間偏移和網絡延遲 )就需要 Chaos Daemon 的幫助在每個節點上進行一些操作來注入。

Chaos Daemon 注入實現

要知道如何在雲環境下注入,第一個問題就是弄清楚我們在注入什麼 —— Pod 的實體是 Container 的話那麼 Container 的實體是什麼呢?在常規的語境下,Container 的一個實體指的是一個進程與它所屬的 Namespace 和 Cgroup。其中 Namespace 與 Cgroup 起到了一個與其他進程隔離的作用。Namespace 控制着可見性,掌管着這個進程能夠看到哪些東西(看到哪些文件、看到哪些進程);而 Cgroup 控制着資源分配:在一個 cgroup 內允許佔用多少 CPU 時間、佔用多少內存、產生多少 Pid。而如果要進行錯誤注入 —— 比如讓一個容器內的 CPU 資源被喫滿、讓一個 Pod 與其他的 Pod 的網絡連接斷開,需要做的第一件事情就是侵入到對應的 Namespace 及 Cgroup 中去。

侵入 Namespace

侵入 Cgroup 的實現是頗爲簡單的,在進程啓動之後將它加入到對應 Cgroup 的進程 名單中即可。而 Namespace 的注入在 Go 的獨特線程環境下要困難一些。Chaos Mesh 借鑑了 runc 的方案,自己實現了一個chaos-mesh/nsexec 來侵入 Namespace 。實現的原理是在進程啓動的時候通過設置 LD_PRELOAD 環境變量來加載一個名爲nsexec.so 的動態鏈接庫,這個動態鏈接庫的contructor 中調用 setns 系統調用來將自身 設置到目標名字空間中去。執行流程如圖所示:

這一工具相比 nsenter 這個現成工具的好處主要有兩點:

1. 擁有更適合 Chaos Mesh 的信號處理機制,能夠方便地控制子進程的死亡。

2. 能夠和 mnt namespace 配合工作,只要是當前 mnt namespace 存在的二進制文件,都能夠在目標名字空間中運行,而不需要目標名字空間中存在。在常見的distroless 的鏡像中,可能不存在任何常見的二進制文件,而 nsexec 能夠應對這種情況。

具體注入實現的方法

現在 Chaos Mesh 已經擁有了非常豐富的功能,受限於篇幅不可能單獨介紹每一種注入的實現,更重要的可能是注入實現背後的方法和路徑。在設計一個錯誤注入的實現時 ,依照以下的流程進行考慮被證實是非常有用的:

1. 考慮正常情況下程序工作的方式。

2. 在正常的調用途徑中有哪些可注入的地方。

3. 對這些可注入的地方進行注入。

以下我們以時間偏移爲例,照着這種思考範式來設計它的實現:

1. 考慮正常情況下程序工作的方式。正常情況下,一個程序是如何獲得時間的呢?大部分程序會選擇使用編程語言標準庫中攜帶的函數,比如 Rust 程序會使用Instant::now(), Go 程序會使用time.Now() ,C 生態的程序會使用glibc 中的 clock_gettime 函數。而這些函數都會以 vDSO 的方式調用操作系統提供的 clock_gettime。vDSO 也是由操作系統提供的函數功能。但與系統調用不同,vDSO 由操作系統在進程啓動的時候自動地加載入進程的內存空間,格式與動態鏈接庫的 ELF 格式相同。應用程序只需要解析這段 ELF 的一些段,就能找到clock_gettime 函數 並調用它。

2. 在正常的調用途徑中有哪些可注入的地方?因爲 vDSO 是存在於目標進程的內存空間中的,如果我們能夠修改這部分內存空間就好了。而事實上ptrace 系統調用給了我們修改另一進程的內存的能力。

3. 對這些可注入的地方進行注入,於是時間偏移的實現方式呼之欲出——只需要準備好一個有偏移的clock_gettime 的實現,用它覆蓋住原有的clock_gettime 函數的實現就好了。

使用類似的方法,也同樣能夠破解其他類型注入的難題——比如對於文件系統的注入方式,我們可以通過 FUSE 提供一個存在延遲的文件系統,而這一文件系統以真實的磁 盤上的文件系統爲存儲後端即可(這樣用戶在恢復之後仍然能操作這些文件)。

以上爲這篇文章介紹的全部內容了。如果讀者對 Chaos Mesh 項目感興趣,想要成爲用戶或貢獻者,歡迎加入我們的 Slack Channel (CNCF Slack 的 #project-chaos-mesh 頻道) 來一同討論如何應用混沌工程、如何讓 Chaos Mesh 變得更好。

構造一個穩定安全的軟件服務使用環境,離不開社區的幫助。無論是使用 Chaos Mesh 還是爲 Chaos Mesh 提交代碼,相信都是 在邁向這一美好的目標。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章