百億次的錘鍊 - 地獄模式的分佈式系統測試

本文以近期開源的Dragonboat多組Raft庫爲例,介紹Dragonboat這樣一個典型分佈式系統是如何作測試的。Dragonboat以Go實現,能在普通硬件上提供每秒1000萬次以上的強一致讀寫,它是目前github.com上速度最快的功能完整的多組Raft開源庫。歡迎你們試用,並請點Star支持:Dragonboathtml

最大的誤導git

常看到有系統吹捧本身可靠的方法是說某大型活動用了它,或說是某某公司某內部項目用了,從而得出可靠的結論,生產環境儼然成了廉價公關軟文口中的測試平臺。其實衆所周知,某活動全場意外當機重啓的節點數少之又少,磁盤毀損一全年才2-4%,而故障性的網絡分區在不少DevOps崗的整個職業生涯裏也就遇到幾回而已,以致於多年來各一線公司網絡分區形成故障的故事收集起來也才寫滿幾頁紙。某活動扛住了或是某項目用了,這些徹底不是軟件可靠與否的充要條件。github

事實實際上是殘酷的。曾閱讀過國內排名前四的某司一個共識庫,30分鐘代碼讀下來找到多處數據丟失毀損的bug,實現則是很典型的那種打死也不願寫測試的全裸奔模式。對於軟件,任何沒法用代碼來驗證的廉價營銷式說辭,不說、不看、不聽:golang

相較於廉價的文宣,Dragonboat對系統正確性滿心敬畏,老老實實以完備的測試方案、開源的測試代碼、公開可驗證的測試結果數據三者來提供切實的保障。服務器

常規測試網絡

常規測試部分,Dragonboat作了:app

Dragonboat內各Package的測試覆蓋率基本均在90%以上。以Raft協議實現來看,不正確地刪改一行代碼基本就能觸發多個測試錯誤。這些測試代碼,連同下面要介紹的monkey testing,nightly build時在race detector被打開的狀況下運行。對於長期被人詬病的Golang的error處理方式,的確容易由於人爲疏忽形成error返回有漏檢的可能,但僅gometalinter一個軟件收錄的靜態檢測工具就有多種能對付它。對各輸入的Fuzz testing初聽來或許有些畫蛇添足,但一跑Fuzz testing幾十秒就發現bug的例子比比皆是,Dragonboat開發中也曾遇到。分佈式

基於Raft協議的自測工具

Raft協議對內部數據有嚴格限定要求,好比顯而易見的就有:測試

  • 全部entry的Index值始終應該是連續且嚴格遞增的
  • 全部entry的Term值應該是單向的
  • 當Index與Term肯定時,entry內容是惟一肯定的

這些都提供較小代價下運行時自測的機會。僅以第一項爲例,在Dragonboat中它被落實到多個點位上,對應用透明的進行自測:

  • 在節點完成了協議規定的檢查,即將append log時
  • 在entry被commit之後,準備由Raft協議返回供複製狀態機執行時
  • 複製狀態機即將執行entry,由該entry Index對比當前最新已執行的entry的Index值時

這些自測在Dragonboat中沒法經過任何設置予以關閉,甚至在Benchmark跑分時也嚴格限定必須進行。

磁盤文件IO測試

磁盤文件IO要作正確有多難,能夠先看兩個事實:

  • Golang的標準庫在MacOS上默認的使用,基本必然出現丟數據
  • 專業測試顯示,包括git、leveldb、ZooKeeper等最著名項目,丟數據的bug曾有一堆

假設文件系統的可靠是天經地義的吧?很遺憾,這種假設也是高危動做

TS Pillai的這張總結圖表直觀顯示文件IO作正確有多難。

爲解決這些磁盤文件IO的挑戰,Dragonboat首先選擇不自做聰明的處處本身去作文件操做,把存儲儘量交給RocksDB,並對基於RocksDB的系統加以各種測試:

  • 使用ScyllaDB的charybdefs實現的磁盤錯誤注入測試
  • 使用自動開關的掉電數據完整性測試

前者能夠模擬諸如RocksDB試圖讀一個sst文件的內容時第二次讀操做返回錯誤,幫助檢查Dragonboat是否按照設計正確地處理這樣的IO錯誤。掉電測試檢查fsync是否被正確配置(如MacOS上是否fcntl(fd, F_FULLFSYNC)了)與調用,是否IO邏輯上有丟數據的問題。

看了上述介紹,可能有人以爲這是小題大作,從Turbo C就開始玩的文件操做有啥難?hehe,文件操做方面是git、ZooKeeper的做者經驗多,仍是您更牛?

as we know, there are known knowns; there are things we know we know. We also know there are known unknowns; that is to say we know there are some things we do not know. But there are also unknown unknowns—the ones we don't know we don't know.

Donald Rumsfeld

Monkey Testing

Monkey Testing有時也稱爲Chaos Engineering,目的在於自動測試系統在各組件失效當機狀況下系統是否依舊能按設計提供應有的服務。與Fuzz testing的隨機數據輸入不一樣,Monkey Testing / Chaos Engineering着眼於隨機破壞性事件對系統的影響。

Dragonboat的monkey testing中,各類隨機破壞性事件的組合被注入到一個多節點的測試環境裏,在一年多的自動測試期間,致使了百億數量級次數的Raft節點重啓事件,發現並修正了大量Raft協議實現與相關輔助功能的bug。整個測試流程及其耗時,堪稱地獄模式。具體的,在monkey testing中,被注入的隨機事件有:

  • 隨意的中止各節點
  • 隨意刪除節點全部Raft數據
  • 隨意丟棄傳輸中的消息
  • 隨意網絡分割節點暫時阻斷通信

在上述大量注入的隨機破壞性事件前提下,同時在上述多節點測試環境上運行大量Raft組實例,進行Raft的讀寫測試。該monkey testing環境同時內建一組三個節點的Drummer系統,三個Drummer節點觀測、維護各Raft組健康信息,並在發現Raft組的成員失效之後,試圖在其它節點上經過Raft組成員變動,新增並啓動一個新的Raft成員,替換已失效的Raft成員。

上述三節點的Drummer自己也是一個基於Dragonboat的Raft實現的無單點系統,且在monkey testing中一樣會被注入上述隨機錯誤。Drummer的上述監控、修復Raft組的業務邏輯是在自身一樣面對大量被注入的隨機破壞性事件的前提下完成的,這進一步驗證了此類具體實際業務邏輯下,Dragonboat的Raft實現的可靠性。

在一個節點平均存活僅幾分鐘的狀況下,在幾臺服務器上每晚即可完成千萬次量級的節點隨機失效與重啓測試。在此及其嚴酷的測試環境中,同時向系統施加Raft讀寫請求,配合大量後臺的Raft快照保存與快照恢復操做,嚴格的後驗檢查確保:

  • Jepsen的Knossos和porcupine檢查,絕無違反稱爲linearizability的強一致性
  • Raft組在有Quorum的時候需可用
  • 用戶應用狀態機狀態一致
  • Raft組成員一致
  • 磁盤上保存的Raft Entry Log一致

一部分Jepsen可讀格式的edn log已被公佈,可供你們使用各自選定的Linearizability checker檢驗:https://github.com/lni/knossos-data

相關文章
相關標籤/搜索