什么是混沌
在分布式計算領域,我們無法預測集群將會發生什么,一切皆有可能。在里約熱內盧飄舞的蝴蝶可能會改變芝加哥的氣候,甚至摧毀位於開羅的數據中心。網絡時間協議(NTP)可能出現不同步,CPU 可能會無緣無故地爆表,或更糟糕的是,勤勞的 DBA 可能會在半夜意外地刪除數據。
TiDB 是一款開源的分布式 NewSQL 混合事務 / 分析處理(HTAP)數據庫,其中保存了客戶最重要的數據資產。對於我們的系統來說,容錯是最基本和最重要的需求之一。但如何保證分布式數據庫的容錯?在本文中,我將介紹混沌工程的故障注入工具和技術,以及我們在 TiDB 中的混沌實踐。
為什么我們需要混沌
自從 2011 年 Netflix 開發了 Chaos Monkey 以來,這款軟件變得越來越流行。如果想要構建一個分布式系統,就讓 Chaos Monkey 來“糟蹋”這個集群,這樣有助於構建出一個更具容錯性、彈性和可靠性的系統。
通常情況下,我們會盡可能多地編寫單元測試,確保能夠覆蓋所有的代碼邏輯,也會進行足夠多的集成測試,確保我們的系統可以與其他組件一起工作,還會執行性能測試,用以改進處理數百萬次請求的性能。
然而,這些對於分布式系統來說還遠遠不夠。無論我們做了多少單元測試、集成測試或性能測試,仍然無法保證我們的系統能夠應對生產環境的各種不可預測性。我們可能會遇到磁盤故障、機器斷電、網絡隔離,而這些只是冰山一角。為了讓分布式系統(如 TiDB)更加健壯,我們需要一種方法來模擬不可預知的故障,並測試我們對這些故障的反應。這就是為什么我們需要 Chaos Monkey。
制造“混沌”
Netflix 不僅發明了混沌,而且還引入了“混沌工程”的概念,這是一種用於揭示系統缺陷的系統性方法。混沌工程有它自己的核心原則,市面上還有一本關於混沌工程的書:《Building Confidence in System Behavior through Experiments》。
在 TiDB 中,我們應用混沌工程來觀察系統的狀態、做出假設、進行實驗,並用真實結果驗證這些假設。除了遵循混沌原則,我們也會加入自己的想法。這是我們的五步混沌法:
- 使用 Prometheus 作為監控工具來觀察 TiDB 集群的狀態和行為,並收集集群的度量指標,用以確定一個穩定的系統應該是什么樣的。
- 列出一些失敗性的假設以及我們的預期。以 TiDB 為例:如果我們從集群中分離出一個 TiKV(TiDB 的分布式鍵值存儲層)節點,那么 QPS(每秒查詢次數)應該會下降,但很快會恢復到另一個穩定狀態。
- 從列表中選擇一個假設。
- 通過注入故障和分析結果對所選的假設進行實驗。如果結果與我們的假設不同,則可能(或必然)出現錯誤或遺漏。
- 從列表中選擇另一個假設進行實驗,並重復及自動化這一過程。
在生產環境中運行實驗是混沌工程的原則之一。在為我們的用戶部署 TiDB 之前,我們必須確保它已經經過了嚴格的測試。不過,我們不能在客戶的生產環境中運行這些實驗,因為他們把最關鍵的數據放在 TiDB 中,表示他們對 TiDB 的信任,所以我們不能破壞這種來自不易的信任。 我們能做的是建立我們自己的“戰場”——一個內部生產環境。
目前,我們使用 Jira 進行內部問題跟蹤和項目管理,同時使用了 TiDB 作為數據存儲,可以說,我們是在自食其力。我們可以在 Jira 上運行混沌實驗。當我們的員工在日常工作中使用 Jira 時,在沒有事先發出任何警告的情況下,向 Jira 系統注入各種故障,用以模擬一系列級聯“事故”來識別可能的系統漏洞。我們稱這種做法為“軍事演習”,並且經常在我們的日常運維中做這樣的事情。在下面的章節中,我將介紹我們如何進行故障注入並自動化這一過程。
TiDB 如何進行故障注入
故障注入是一種通過引入故障來測試代碼路徑(尤其是處理錯誤的代碼路徑)以便改進測試覆蓋率的技術。它被認為是開發健壯軟件系統的重要組成部分。有多種方法可用於進行故障注入,在 TiDB 中,故障以下列方式注入:
- 使用 kill -9 強制終止進程,或者使用 kill 來優雅地終止進程,然后重新啟動它。
- 使用 SIGSTOP 掛起進程,或使用 SIGCONT 恢復進程。
- 使用 renice 來調整進程的優先級,或在進程的線程上使用 setpriority。
- 讓 CPU 超載。
- 使用 iptables 或 tc 丟棄或拒絕網絡數據包,或讓網絡數據包延遲。
- 使用 tc 重新排列網絡數據包,並使用代理重新排列 gRPC 請求。
- 使用 iperf 獲取所有網絡吞吐量。
- 使用 libfuse 掛載文件系統並執行 IO 故障注入。
- 鏈接 libfiu 以便進行 IO 故障注入。
- 使用 rm -rf 強制刪除所有數據。
- 使用 echo 0 > file 來毀壞文件。
- 通過拷貝一個巨大的文件來制造 NoSpace 問題。
一些頂級的故障注入工具
內核故障注入
Linux 內核中包含了一個流行的故障注入工具 Fault Injection Framework,開發人員可以用它執行簡單的故障注入來測試設備驅動程序。為了進行更精確的故障注入,例如在用戶讀取文件時返回錯誤,或者調用 malloc 失敗,我們使用以下故障注入過程:
- 重新構建內核,啟用 Fault Injection Framework
- 使用內核故障注入:
echo 1 > /sys/block/vdb/vdb1/make-it-fail
|
|
mount debugfs
/debug -t debugfs
|
|
cd /debug/fail_make_request
|
|
echo 10 > interval # interval
|
|
echo 100 > probability # 100% probability
|
|
echo -1 > times # how many times: -1 means no limit
|
- 在訪問該文件時,可能會出現以下錯誤:
Buffer I/O error on device vdb1, logical block 32538624
lost page write due to I/O error on vdb1
- 我們可以按照如下方式注入 malloc 故障:
echo 1 > cache-filter
|
|
echo 1 > /sys/kernel/slab/ext4_inode_cache/failslab
|
|
echo N > ignore-gfp-wait
|
|
echo -1 > times
|
|
echo 100 > probability
|
cp linux-3.10.1.tar.xz linux-3.10.1.tar.xz.6
cp: cannot create regular file ‘linux-3.10.1.tar.xz.6’: Cannot allocate memory
Linux 內核的 Fault Injection Framework 功能很強大,不過我們需要重新構建內核才能啟用它,因為有些用戶不會在生產環境中開啟這一選項。
SystemTap
注入故障的另一種方法是使用 SystemTap( https://sourceware.org/systemtap/ ),這是一種可用於診斷性能或功能問題的工具。我們使用 SystemTap 來探測內核函數並進行准確的故障注入。例如,我們可以通過執行以下操作來延遲 read/write return 中的 IO 操作:
probe vfs.read.return {
|
|
if (target() != pid()) next
|
|
udelay(300)
|
|
}
|
|
probe vfs.write.return {
|
|
if (target() != pid()) next
|
|
udelay(300)
|
|
}
|
我們也可以改變 IO 返回值。下面我們為 read 注入一個 EINTR,為 write 注入一個 ENOSPC:
probe vfs.
read.return {
|
|
if (target() != pid()) next
|
|
// Interrupted by a signal
|
|
$
return = -4
|
|
}
|
|
probe vfs.
write.return {
|
|
if (target() != pid()) next
|
|
// No space
|
|
$
return = -28
|
|
}
|
Fail
有時候,我們想在特定的地方進行故障注入,例如:
fn save_snapshot() {
|
|
save_data();
|
|
save_meta();
|
|
}
|
我們希望在保存快照數據之后以及保存元數據之前看到系統發生混亂。這個時候應該怎么做?我們可以使用一種稱為 fail( https://www.freebsd.org/cgi/man.cgi?query=fail&sektion=9&apropos=0&manpath=FreeBSD%2B10.0-RELEASE )的機制。我們可以使用 fail 將故障注入到任意的地方。在 Go 語言中,我們可以使用 gofail( https://github.com/coreos/gofail ),而在 Rust 中,我們可以使用 fail-rs( https://github.com/pingcap/fail-rs )。
對於上面的例子,現在我們可以這樣做:
fn save_snapshot() {
|
|
save_data();
|
|
fail_point!("snapshot");
|
|
save_meta();
|
|
}
|
在這個例子中,我們注入一個叫作“snapshot”的故障點,然后觸發它來拋出一個像 FAILPOINTS=snapshot=panic(msg) cargo run 這樣的異常。
故障注入平台
我們已經介紹了一些故障注入方法,除此之外,還有一些平台集成了這些方法。借助這些平台,我們可以進行獨立或並行的故障注入。這些平台中最受歡迎的是 Namazu( https://github.com/osrg/namazu ),一種用於測試分布式系統的可編程模糊調度器。
故障注入平台 Namazu
我們可以在 Namazu 容器中運行系統。在容器中,Namazu 將通過 sched_setattr、帶有熔斷機制的文件系統和帶有 netfilter 的網絡來調度進程。不過,我們發現啟用 Namazu 的文件系統調度程序會導致 CentOS 7 操作系統崩潰,所以我們只在 Ubuntu 上運行 Namazu。
另一個平台是 Jepsen( https://github.com/jepsen-io/jepsen ),主要用於驗證分布式數據庫的線性一致性。Jepsen 使用 Nemeses 來擾亂系統、記錄客戶端操作,並通過操作歷史來驗證線性一致性。
我們開發了一個 Clojure 庫來測試 TiDB,詳情請參閱( https://github.com/pingcap/jepsen/tree/master/tidb )。 Jepsen 已被集成到持續集成(CI)工具中,因此 TiDB 代碼庫中的每個更新都會自動觸發 CI 來執行 Jepsen 測試。
自動化混沌:Schrodinger
到目前為止,我們提到的工具或平台都可用於將故障注入到系統中。但為了測試 TiDB,我們需要自動化這些測試來提高效率和覆蓋率,於是我們開發了 Schrodinger。
2015 年,在我們剛開始開發 TiDB 時,每次提交一個功能,都會執行以下操作:
- 構建 TiDB 二進制文件 ;
- 請管理員分配一些機器進行測試 ;
- 部署 TiDB 二進制文件並運行它們 ;
- 運行測試用例 ;
- 運行 Nemeses 來注入故障 ;
- 在完成所有測試后,清理所有資源並釋放機器。
所有這些任務都涉及乏味的手動操作。隨着 TiDB 代碼庫規模的增長,我們需要同時運行許多測試,而手動方式不具備伸縮性。
為了解決這個問題,我們開發了自動執行混沌工程的測試平台 Schrodinger。我們只需要配置 Schrodinger 執行特定的測試任務就可以了,剩下的事情交給平台。
Schrodinger 基於 Kubernetes(K8s)構建,所以我們不再依賴於物理機器。 K8s 隱藏了機器級別的細節,並幫我們將正確的作業安排到合適的機器上。
K8s 上的 Shrodinger 架構
下面是 Schrodinger 的主頁屏幕截圖,顯示了正在運行的測試的概覽。我們可以看到兩個測試失敗,一個測試仍在運行。如果測試失敗,會向我們的 Slack 頻道發送告警,並通知開發人員解決問題。
Schrodinger 主頁
如何使用 Schrodinger?
使用 Schrodinger 可分為五步:
- 使用 Create Cluster Template 創建一個 TiDB 群集。在下面的截圖中,我們部署了一個帶有 3 個 Placement Driver(PD)服務器、5 個 TiKV 服務器和 3 個 TiDB 服務器的 TiDB 集群。(PD 是 TiDB 集群的管理組件,負責元數據存儲、調度、負載均衡,以及分配事務標識。)
創建一個 TiDB 集群
- 使用 Create Case Template 為集群創建一個測試用例。我們可以使用預先構建的測試用例,如下圖所示的 bank 測試用例,或者讓 Schrodinger 從 Git 源創建一個新用例。
創建一個 TiDB 測試用例
- 創建一個場景來鏈接我們在上一步中配置的集群,並將測試用例添加到該集群。
創建一個測試場景
- 創建一個任務,告訴 Schrodinger TiDB 集群的詳細版本,並附加一個 Slack 頻道用來接受告警。例如,在下面的截圖中,我們讓 Schrodinger 從最新的 master 分支構建整個群集。
創建一個測試任務
- 在創建任務后,Schrodinger 開始自動運行所有測試用例。
Shrondinger 自動化
Schrodinger 現在可以同時在 7 個不同的群集上運行測試,24/7 不間斷。我們的團隊從人工測試中解放出來,只需要配置測試環境和任務就可以了。
在未來,我們將繼續優化這個過程,讓 Chaos Monkey 變得更聰明。我們不想再通過手動的方式配置測試環境和任務,而是讓 Schrodinger“學習”集群,並找出自動注入故障的方法。Netflix 已經在這方面進行了研究,並發表了一篇相關論文:“互聯網規模的自動故障測試研究”( https://people.ucsc.edu/~palvaro/socc16.pdf )。我們基於這項研究開始自己的研發工作,而且很快就會向外部分享我們的進展!
TiDB 中的 TLA+
除了故障注入和混沌工程實踐外,我們還使用 TLA+,這是一門用於設計、建模、文檔化和驗證並發系統的語言,旨在驗證分布式事務和相關算法的正確性。 TLA+ 由 Leslie Lampor 開發,我們已經用它來證明我們的兩階段事務算法(詳見 https://github.com/pingcap/tla-plus )。我們計划在未來使用 TLA+ 來驗證更多的算法。
最后的想法
從我們開始構建 TiDB 那一刻起,就決定使用混沌測試。混沌是檢測分布式系統不確定性、建立系統彈性信心的一種非常好的方式。我們堅信,是否能夠恰當且縝密地應用混沌工程將決定分布式系統的成敗。