本話題系列文章整理自 PingCAP NewSQL Meetup 第 26 期劉奇分享的《深度探索分佈式系統測試》議題現場實錄。文章較長,爲方便你們閱讀,會分爲上中下三篇,本文爲中篇。node
接上篇:
固然測試可能會讓你代碼變得沒有那麼漂亮,舉個例子:python
這是知名的 Kubernetes 的代碼,就是說它有一個 DaemonSetcontroller,這 controller 裏面注入了三個測試點,好比這個地方注入了一個 handler ,你能夠認爲全部的注入都是 interface。好比說你寫一個簡單的 1+1=2 的程序,假設咱們寫一個計算器,這個計算器的功能就是求和,那這就很難注入錯誤。因此你必需要在你正確的代碼裏面去注入測試邏輯。再好比別人 call 你的這個 add 的 function,而後你是否是有一個 error?這個 error 的問題是它可能永遠不會返回一個 error,因此你必需要人肉的注進去,而後看應用程序是否是正確的行爲。說完了加法,再說咱們作一個除法。除法你們知道可能有處理異常,那上面是否是能正常處理呢?上面沒有,上面寫着一個好比說 6 ÷ 3,而後寫了一個 test,coverage 100%,可是一個除零異常,系統就崩掉了,因此這時候就須要去注入錯誤。大名鼎鼎的 Kubernetes 爲了測試各類異常邏輯也採用相似的方式,這個結構體不算長,大概是十幾個成員,而後裏面就注入了三個點,能夠在裏面注入錯誤。mysql
那麼在設計 TiDB 的時候,咱們當時是怎麼考慮 test 這個事情的?首先一個百萬級的 test 不可能由人肉來寫,也就是說你若是從新定義一個本身的所謂的 SQL 語法,或者一個 query language,那這個時候你須要構建百萬級的 test,即便全公司去寫,寫個兩年都不夠,因此這個事情顯然是不靠譜的。可是除非說個人 query language 特別簡單,好比像 MongoDB 早期的那種,那我一個「大於多少」的這種,或者 equal 這種條件查詢特別簡單的,那你確實是不須要構建這種百萬級的 test。可是若是作一個 SQL 的 database 的話,那是須要構建這種很是很是複雜的 test 的。這時候這個 test 又不能全公司的人寫個兩年,對吧?因此有什麼好辦法呢?MySQL 兼容的各類系統都是能夠用來 test 的,因此咱們當時兼容 MySQL 協議,那意味着咱們可以取得大量的 MySQL test。不知道有沒有人統計過 MySQL 有多少個 test,產品級的 test 很嚇人的,千萬級。而後還有不少 ORM, 支持 MySQL 的各類應用都有本身的測試。你們知道,每一個語言都會 build 本身的 ORM,而後甚至是一個語言的 ORM 都有好幾個。好比說對於 MySQL 可能有排第一的、排第二的,那咱們能夠把這些全拿過來用來測試咱們的系統。sql
但對於有些應用程序而言,這時候就比較坑了。就是一個應用程序你得把它 setup 起來,而後操做這個應用程序,好比 WordPress,然後再看那個結果。因此這時候咱們爲了不剛纔人肉去測試,咱們作了一個程序來自動化的 Record---Replay。就是你在首次運行的時候,咱們會記錄它全部執行的 SQL 語句,那下一次我再須要從新運行這個程序的時候怎麼辦?我不須要運行這個程序了,我不須要起來了,我只須要把它前面記錄的 SQL record 從新回放一遍,就至關因而我模擬了程序的整個行爲。因此咱們在這部分是這樣作的自動化。
那麼剛剛說了那麼多,實際上作的是什麼?實際上作的都是正確路徑的測試,那幾百萬個 test 也都是作的正確的路徑測試,可是錯誤的路徑怎麼辦?很典型的一個例子就是怎麼作 Fault injection。硬件比較簡單粗暴的模擬網絡故障能夠拔網線,好比說測網絡的時候能夠把這個網線拔掉,可是這個作法是極其低效的,並且它是無法 scale 的,由於這個須要人的參與。數據庫
而後還有好比說 CPU,這個 CPU 的損壞機率其實也挺高的,特別是對於過保了的機器。而後還有磁盤,磁盤大概是三年百分之八點幾的損壞率,這是一篇論文裏面給出的數據。我記得 Google 好像以前給過一個數據,就是 CPU、網卡還有磁盤在多少年以內的損壞率大概是什麼樣的。服務器
還有一個你們不太關注的就是時鐘。先前,咱們發現系統時鐘是有回跳的,而後咱們果斷在程序裏面加個監測模塊,一旦系統時鐘回跳,咱們立刻把這個檢測出來。固然咱們最初監測出這個東西的時候,用戶是以爲不可能吧,時鐘還會有回跳?我說不要緊,先把咱們程序開了監測一下,而後過段時間就檢測到,系統時鐘最近回跳了。因此怎麼配 NTP 很重要。而後還有更多的,好比說文件系統,你們有沒有考慮過你寫磁盤的時候,磁盤出錯會怎麼辦?好,寫磁盤的時候沒有出錯,成功了,而後磁盤一個扇區壞了,讀出來的數據是損壞的,怎麼辦?你們有沒有 checksum ?沒有 checksum 而後咱們直接用了這個數據,而後直接給用戶返回了,這個時候多是很要命的。若是這個數據恰好存的是個元數據,而元數據又指向別的數據,而後你又根據元數據的信息去寫入另一份數據,那就更要命了,可能數據被進一步破壞了。網絡
因此比較好的作法是什麼?多線程
Fault injection架構
Hardware併發
disk error
network card
cpu
clock
Software
file system
network & protocol
Simulate everything
模擬一切東西。就是磁盤是模擬的,網絡是模擬的,那咱們能夠監控它,你能夠在任什麼時候間、任何的場景下去注入各類錯誤,你能夠注入任何你想要的錯誤。好比說你寫一個磁盤,我就告訴你磁盤滿了,我告訴你磁盤壞了,而後我可讓你 hang 住,好比 sleep 五十幾秒。咱們確實在雲上面出現過這種狀況,就是咱們一次寫入,而後被 hang 了爲 53 秒,最後才寫進去,那確定是網絡磁盤,對吧?這種事情實際上是很嚇人的,可是確定沒有人會想說我一次磁盤寫入而後要耗掉 53 秒,可是當 53 秒出現的時候,整個程序的行爲是什麼?TiDB 裏面用了大量的 Raft,因此當時出現一個狀況就是 53 秒,而後全部的機器就開始選舉了,說這確定是哪兒不對,從新把 leader 都選出來了,這時候卡 53 秒的哥們說「我寫完了」,而後整個系統狀態就作了一次全新的遷移。這種錯誤注入的好處是什麼?就是知道當出錯的時候,你的錯誤能嚴重到什麼程度,這個事情很重要,就是 predictable,整個系統要可預測的。若是沒有作錯誤路徑的測試,那很簡單的一個問題,如今假設走到其中一條錯誤路徑了,整個系統行爲是什麼?這一點不知道是很嚇人的。你不知道是否可能破壞數據;仍是業務那邊會 block 住;仍是業務那邊會 retry?
之前我遇到一個問題頗有意思,當時咱們在作一個消息系統,有大量鏈接會連這個,一個單機大概是連八十萬左右的鏈接,就是作消息推送。而後我記得,當時的 swap 分區開了,開了是什麼概念?當你有更多鏈接打進來的時候,而後你內存要爆了對吧?內存爆的話會自動啓用 swap 分區,但一旦你啓用 swap 分區,那你係統就卡成狗了,外面用戶斷連以後他就失敗了,他得重連,可是重連到你正常程序能響應,可能又須要三十秒,而後那個用戶確定以爲超時了,又切斷鏈接又重連,就形成一個什麼狀態呢?就是系統永遠在重試,永遠沒有一次成功。那這個行爲是否是能夠預測?這種錯誤當時有沒有作很好的測試?這都是很是重要的一些教訓。
硬件測試之前的辦法是這樣的(Joke):
假設我一個磁盤壞了,假設我一個機器掛了,還有一個假設它不必定壞了也不必定掛了,好比說它着火了會怎麼樣?前兩個月吧,是瑞士仍是哪一個地方的一個銀行作測試,那哥們也挺逗的,人肉對着服務器這樣吹氣,來看監控數據那個變化,而後那邊立刻開始報警。這還只是吹氣而已,那若是更復雜的測試,好比說你着火從哪一個地方開始燒,先燒到硬盤、或者先燒到網卡,這個結果可能也是不同的。固然這個成本很高,而後也不是能 scale 的一種方案,同時也很難去複製。
這不只僅是硬件的監控,也能夠認爲是作錯誤的注入。好比說一個集羣我如今燒掉一臺會怎麼樣?着火了,很典型的嘛,雖然重要的機房都會有這種防火、防水等各類的策略,可是真的着火的時候怎麼辦?固然你不能真去燒,這一燒可能就不止壞一臺機器了,但咱們須要使用 Fault injection 來模擬。
我介紹一下到底什麼是 Fault injection。給一個直觀的例子,你們知道全部人都用過 Unix 或者 Linux 的系統,你們都知道,不少人習慣打開這個系統第一行命令就是 ls 來列出目錄裏面的文件,可是你們有沒有想過一個有意思的問題,若是你要測試 ls 命令實現的正確性,怎麼測?若是沒有源代碼,這個系統該怎麼測?若是把它當成一黑盒這個系統該怎麼測?若是你 ls 的時候磁盤出現錯誤怎麼辦?若是讀取一個扇區讀取失敗會怎麼辦?
這個是一個很好玩的工具,推薦你們去玩一下。就是當你尚未作更深刻的測試以前,能夠先去理解一下到底什麼是 Fault injection,你就能夠體驗到它的強大,一會咱們用它來找個 MySQL 的 bug。
libfiu - Fault injection in userspace
It can be used to perform fault injection in the POSIX API without having to modify the application's source code, that can help to test failure handling in an easy and reproducible way.
那這個東西主要是用來 Hook 這些 API 的,它很重要的一點就是它提供了一個 library ,這個 library 也能夠嵌到你的程序裏面去 hook 那些 API。就好比說你去讀文件的時候,它能夠給你返回這個文件不存在,能夠給你返回磁盤錯誤等等。最重要的是,它是能夠重來的。
舉一個例子,正常來說咱們敲 ls 命令的時候,確定是可以把當前的目錄顯示出來。
這個程序乾的是什麼呢?就是 run,指定一個參數,如今是要有一個 enable_random,就是後面全部的對於 IO 下面這些 API 的操做,有 5% 的失敗率。那第一次是運氣比較好,沒有遇到失敗,因此咱們把整個目錄列出來了。而後咱們從新再跑一次,這時候它告訴我有一次讀取失敗了,就是它 read 這個 directory 的時候,遇到一個 Bad file descriptor,這時候能夠看到,列出來的文件就比上面的要少了,由於有一條路徑讓它失敗了。接下來,咱們進一步再跑,發現剛列出來一個目錄,而後下次讀取就出錯了。而後後面再跑一次的時候,此次運氣也比較好,把這整個都列出來了,這個還只是模擬的 5% 的失敗率。就是有 5% 的機率你去 read、去 open 的時候會失敗,那麼這時候能夠看到 ls 命令的行爲仍是很 stable 的,就是沒有什麼常見的 segment fault 這些。
你們可能會說這個還不太好玩,也就是找找 ls 命令是否有 bug 嘛,那咱們復現 MySQL bug 玩一下。
Bug #76020
InnoDB does not report filename in I/O error message for reads
fiu-run -x -c "enable_random name=posix/io/*,probability=0.05" bin/mysqld --basedir=/data/ushastry/server/mysql-5.6.24 --datadir=/data/ushastry/server/mysql-5.6.24/76020 --core-file --socket=/tmp/mysql_ushastry.sock --port=15000
2015-05-20 19:12:07 31030 [ERROR] InnoDB: Error in system call pread(). The operating system error number is 5.
2015-05-20 19:12:07 7f7986efc720 InnoDB: Operating system error number 5 in a file operation.
InnoDB: Error number 5 means 'Input/output error'.
2015-05-20 19:12:07 31030 [ERROR] InnoDB: File (unknown):
'read' returned OS error 105. Cannot continue operation
這是用 libfiu 找到的 MySQL 的一個 bug,這個 bug 是這樣的,bug 編號是 76020,是說 InnoDB 在出錯的時候沒有報文件名,那用戶給你報了錯,你這時候就傻了對吧?這個究竟是什麼地方出錯了呢?而後這個地方它怎麼出來的?你能夠看到它仍是用咱們剛纔提到的 fiu-run,而後來模擬,模擬的失敗機率仍是這麼多,能夠看到,咱們的參數一個沒變,這時把 MySQL 啓動,而後跑一下,出現了,能夠看到 InnoDB 在報的時候確實沒有報 filename ,File : 'read' returned OS error,而後這邊是 auto error,你不知道是哪個文件名。
換一個思路來看,假設沒有這個東西,你復現這個 bug 的成本是什麼?你們能夠想一想,若是沒有這個東西,這個 bug 應該怎麼復現,怎麼讓 MySQL 讀取的東西出錯?正常路徑下你讓它讀取出錯太困難了,可能好多年沒出現過。這時咱們進一步再放大一下,這個在 5.7 裏面還有,也是在 MySQL 裏面極可能有十幾年你們都沒怎麼遇到過的,但這種 bug 在這個工具的輔助下,立刻就能出來。因此 Fault injection 它帶來了很重要的一個好處就是讓一個東西能夠變得更加容易重現。這個仍是模擬的 5% 的機率。這個例子是我昨天晚上作的,就是我要給你們一個直觀的理解,可是分佈式系統裏面錯誤注入比這個要複雜。並且若是你遇到一個錯誤十年都沒出現,你是否是太孤獨了? 這個電影你們可能還有印象,威爾史密斯主演的,全世界就一我的活着,惟一的夥伴是一條狗。
實際上不是的,比咱們痛苦的人大把的存在着。
舉 Netflix 的一個例子,下圖是 Netflix 的系統。
他們在 2014 年 10 月份的時候寫了一篇博客,叫《 Failure Injection Testing 》,是講他們整個系統怎麼作錯誤注入,而後他們的這個說法是 Internet Scale,就是整個多數據中心互聯網的這個級別。你們可能記得 Spanner 剛出來的時候他們叫作 Global Scale,而後這地方能夠看到,藍色是注射點,黑色的是網絡調用,就是全部這些請求在這些狀況下面,全部這些藍色的框框都有可能出錯。你們能夠想想,在 Microservice 系統上,一個業務調用可能涉及到幾十個系統的調用,若是其中一個失敗了會怎麼樣?若是是第一次第一個失敗,第二次第二個失敗,第三次第三個失敗是怎麼樣的?有沒有系統作過這樣的測試?有沒有系統在本身的程序裏面去很好的驗證過是否是每個能夠預期的錯誤都是可預測的,這個變得很是的重要。這裏以 cache 爲例,就說每一次訪問 Cassandra 的時候可能出錯,那麼也就給了咱們一個錯誤的注入點。
而後咱們談談 OpenStack
OpenStack fault-injection library:
大名鼎鼎的 OpenStack 其實也有一個 Failure Injection Library,而後我把這個例子也貼到這裏,你們有興趣能夠看一下這個 OpenStack 的 Failure Injection。這之前你們可能不太關注,其實你們在這一點上都很痛苦, OpenStack 如今還有一堆人在罵,說穩定性太差了,其實他們已經很努力了。可是整個系統確實是作的異乎尋常的複雜,由於組件太多。若是你出錯的點特別多,那可能會帶來另一個問題,就是出錯的點之間還能組合,就是先 A 出錯,再 B 出錯,或者 AB 都出錯,這也就幾種狀況,還好。那你要是有十萬個錯誤的點,這個組合怎麼弄?固然如今還有新的論文在研究這個,2015 年的時候好像有一篇論文,講的就是會探測你的程序的路徑,而後在對應的路徑下面去注入錯誤。
再來講 Jepsen
Jepsen: Distributed Systems Safety Analysis
你們全部聽過的知名的開源分佈式系統基本上都被它找出來過 bug。可是在這以前你們都以爲本身仍是很 OK 的,咱們的系統仍是比較穩定的,因此當新的這個工具或者新的方法出現的時候,就好比說我剛纔提到的那篇可以線性 Scale 的去查錯的那篇論文,那個到時候查錯力就很驚人了,由於它可以自動幫你探測。另外我介紹一個工具 Namazu,後面講,它也很強大。這裏先說Jepsen, 這貨算是重型武器了,不管是 ZooKeeper、MongoDB 以及 Redis 等等,全部這些所有都被找出了 bug,如今用的全部數據庫都是它找出的 bug,最大的問題是小衆語言 closure 編寫的,擴展起來有點麻煩。我先說說 Jepsen 的基本原理,一個典型使用 Jepsen 的測試經過會在一個 control node上面運行相關的 clojure 程序,control node 會使用 ssh 登錄到相關的系統 node(jepsen 叫作 db node)進行一些測試操做。
當咱們的分佈式系統啓動起來以後,control node 會啓動不少進程,每個進程都能使用特定的 client 訪問到咱們的分佈式系統。一個 generator 爲每個進程生成一系列的操做,好比 get/set/cas,讓其執行。每個操做都會被記錄到 history 裏面。在執行操做的同時,另外一個 nemesis 進程會嘗試去破壞這個分佈式系統,譬如使用 iptable 斷開網絡鏈接等,當全部操做執行完畢以後,jepsen 會使用一個 checker 來分析驗證系統的行爲是否符合預期。PingCAP 的首席架構師唐劉寫過兩篇文章介紹咱們實際怎麼用 Jepsen 來測試 TiDB,你們能夠搜索一下,我這裏就不詳細展開了。
FoundationDB
It is difficult to be deterministic
Random
Disk Size
File Length
Time
Multithread
FoundationDB 這就是前輩了,2015 年被 Apple 收購了。他們爲了解決錯誤注入的問題,或者說怎麼去讓它重現的這個問題,作了不少事情,很重要的一個事情就是 deterministic 。若是我給你同樣的輸入,跑幾遍,是否是能獲得同樣的輸出?這個聽起來好像很科學、很天然,可是實際上咱們絕大多數程序都是作不到的,好比說大家有判斷程序裏面有隨機數嗎?有多線程嗎?有判斷磁盤空間嗎?有判斷時間嗎?你再一次判斷的時候仍是同樣的嗎?你再跑一次,一樣的輸入,但行爲已經不同了,好比你生了一個隨機數,好比你判斷磁盤空間,此次判斷和下次判斷多是不同的。
因此他們爲了作到「我給你同樣的輸入,必定能獲得同樣的輸出」,花了大概兩年的時間作了一個庫。這個庫有如下特性:它是個單線程的,而後是個僞併發的。爲何?由於若是用多線程你怎麼讓它這個相同的輸入變成相同的輸出,誰先拿到鎖呢?這裏面的問題不少,因此他們選擇使用單線程,可是單線程自己有單線程的問題。並且好比你用 Go 語言,那你單線程它也是個併發的。而後它的語言規範就告訴咱們說,若是一個 select 做用在兩個 channel 上,兩個 channel 都 ready 的時候,它會隨機的一個,就是在語言定義的規範上面,就已經不可能讓你獲得一個 deterministic 了。但還好 FoundationDB 是用 C++ 寫的。
FoundationDB
Single-threaded pseudo-concurrency
Simulated the implementation of all the external communication
Determinism
Disasters happen more frequently here than in the real world.
另外 FoundationDB 模擬了全部的網絡,就是兩個之間認爲經過網絡通信,對吧?其實是經過它本身模擬的一套東西在通信。它裏面有一個很重要的觀點就是說,若是磁盤損壞,出現的機率是三年百分之八的話,那麼在用戶那出現的機率是三年百分之八。可是在用戶那一旦出現了,那證實就很嚴重了,因此他們對待這個問題的辦法是什麼?就是我經過本身的模擬系統讓它每時每刻都在產生。它們大概是每兩分鐘產生一次磁盤損壞,也就是說它比現實中的機率要高几十萬倍,因此它就以爲它調的技術 more frequently,就是我這種錯誤出現的更加頻繁,那網卡損壞的機率是多少?這都是極低的,可是你能夠用這個系統讓它每分每秒都產生,這樣一來你就讓你的系統遇到這種錯誤的機率是比現實中要大很是很是多。那你重現,好比說現實中跑三年能重現一次,你可能跑三十秒就能重現一次。
但對於一個 bug 來講最可怕的是什麼?就是它不能重現。發現一個 bug,後來講我 fix 了,而後不能重現了,那你到底 fix 了沒有?不知道,這個事情就變得很是的恐怖。因此經過 deterministic 確定能保證重現,我只要把個人輸入重放一次,我把它錄下來,每一次我把它錄下來一次,而後只要是曾經出現過,我重放,必定能出現。固然這個代價太大了,因此如今學術界走的是另一條路,不是徹底 deterministic,可是我只須要它 reasonable。好比說我在三十分鐘內能把它重現也是不錯的,我並不須要在三秒內把它重現。因此,每前一步要付出相應的成本代價。
未完待續…