捕獲和增強原生系統的可觀測性來發現錯誤
作者:唐劉
在對 TiDB 進行 Chaos 實踐的時候,我一直在思考如何更好的發現 TiDB 整個系統的故障。最開始,我們參考的就是 Chaos Engineering 裡面的方式,觀察系統的穩定狀態,注入一個錯誤,然後看 metrics 上面有啥異常,這樣等實際環境中出現類似的 metrics,我們就知道發現了什麼故障。
但這套機制其實依賴於如何去注入錯誤,雖然現在我們已經有了很多種錯誤注入的方式,但總有一些實際的情況我們沒有料到。所以後來我們又考慮了另外的一種方式,也就是直接對 metrics 歷史進行學習,如果某一段時間 metrics 出現了不正常的波動,那麼我們就能報警。但這個對我們現階段來說難度還是有點大,只使用了幾種策略,對 QPS,Latency 這些進行了學習,並不能很好的定位到具體出了什麼樣的問題。
所以我一直在思考如何更好的去發現系統的故障。最近,剛好看到了 OSDI 2018 一篇 Paper,Capturing and Enhancing In Situ System Observability for Failure Detection,眼睛一亮,覺得這種方式也是可以來實踐的。
大家都知道,在生產環境中,故障是無處不在,隨時可能發生的,譬如硬體問題,軟體自身的 bug,或者運維使用了一個錯誤的配置這些。雖然多數時候,我們的系統都做了容錯保護,但我們還是需要能儘快的發現故障,才好進行故障轉移。
但現實世界並沒有那麼美好,很多時候,故障並不是很明顯的,譬如整個程序掛掉,機器壞掉這些,它們處於一種時好時壞的狀態,我們通常稱為「Gray Failure
上面是作者舉的一個 ZooKeeper 的例子,client 已經完全不能跟 Leader 進行互動了,但是 Leader 卻仍然能夠給 Follower 傳送心跳,同時也能響應外面 Monitor 發過來的探活命令。
如果從外面的 Monitor 看來,這個 ZooKeeper 叢集還是正常的,但其實它已經有故障了。而這個故障其實 client 是知道的,所以故障檢測的原理很簡單,從發起請求的這一端來觀察,如果發現有問題,那就是有故障了。而這也是這篇論文的中心思想。
在論文裡面,作者認為,任何嚴重的 Gray Failure 都是能夠被觀察到的,如果發起請求的這邊遇到了錯誤,自然下一件事情就是將這個錯誤給彙報出去,這樣我們就知道某個地方出現了故障。於是作者開發了 Panorama 這套系統,來對故障進行檢測。
整體架構
先來說說 Panorama 一些專業術語。
Panorama 整體結構如下:
Panorama 通過一些方式,譬如靜態分析程式碼進行程式碼注入等,將 Observer 跟要觀察的 Subject 進行繫結,Observer 會將 Subject 的一些資訊記錄並且彙報給本地的一個 Local Observation Store(LOS)。本地一個決策引擎就會分析 LOS 裡面的資料來判斷這個元件的狀態。如果多個 LOS 裡面都有對某個 Subject 的 observation,那麼 LOS 會相互交換,用來讓中央的 verdict 更好的去判斷這個 component 的狀態。
故障判定
而用來判斷一個 component 是不是有故障也比較容易,採用的是一種大多數 bounded-look-back 演算法。對於一個 subject,它可能會有很多 observations,首先我們會對這些 observations 按照 observer 進行分組,對每組單獨進行分析。在每個組裡面,observations 會按照時間從後往前檢查,並且按照 context 進行聚合。如果一個被觀察的 observation 的 status 跟記錄前面相同 context 的 observation status 狀態不一樣,就繼續 loop-back,直到遇到一個新的 status。對於一個 context,如果最後的狀態是 unhealthy 或者 healthy 的狀態沒有達到多數,就會被認為是 unhealthy 的。
通過這種方式,我們在每組裡面得到了每個 context 的狀態,然後又會在多個組裡面進行決策,也就是最常用的大多數原則,哪個狀態最多,那麼這個 context 對應的狀態就是哪一個。這裡我們需要額外處理下 PENDING 這個狀態,如果當前狀態是 HEALTHY 而之前老的狀態是 PENDING,那麼 PENDING 就會變成 HEALTHY,而如果一直是 PENDING 狀態並超過了某個閾值,就會退化成 UNHEALTHY。
Observability
這裡再來說說 Observability 的模式。對於分散式系統來說,不同 component 之間的互動並不是同步的,我們會面臨如下幾種情況:
如果兩個元件 C1 和 C2 是同步互動,那麼當 C1 給 C2 傳送請求,我們就完全能在 C1 這一端知道這次請求成功還是失敗了,但是對於非同步的情況,我們可能面臨一個問題,就是 C1 給 C2 發了請求,但其實這個請求是放到了非同步訊息佇列裡面,但 C1 覺得是成功了,可是後面的非同步佇列卻失敗了。所以 Panorama 需要有機制能正確處理上面多種情況。
為了能更好的從 component 上面得到有用的 observations,Panorama 會用一個離線工具對程式碼進行靜態分析,發現一些關鍵的地方,注入鉤子,這樣就能去彙報 observations 了。
通常執行時錯誤是非常有用的能證明有故障的證據,但是,並不是所有的錯誤都需要彙報,Panorama 僅僅會關係跨 component 邊界產生的錯誤,因為這也是通過發起請求端能觀察到的。Panorama 對於這種跨域的函式呼叫稱為 observation boundaries。對於 Panorama 來說,第一件事情就是定位 observation boundaries。通常有兩種 boundaries,程序間互動和執行緒間互動。程序間互動通常就是 socket I/O,RPC,而執行緒間則是在一個程序裡面跨越執行緒的呼叫。這些 Panorama 都需要分析出來。
當定位了 observation boundaries 之後,下一件事情就是確定 observer 和 subject 的標識。譬如對於程序間互動的 boundaries,observer 的標識就可能是這個程序在系統裡面的唯一標識,而對於 subject,我們可以用 method 名字,或者是函式的一個引數,類裡面的一個欄位來標識。
然後我們需要去確定 observation points,也就是觀測點。通常這些點就是程式碼處理異常的地方,另外可能就是一些正常處理返回結果但會對外報錯的地方。
上面就是一個簡單分析程式碼得到 observation points 的例子,但這個仍然是同步的,對於 indirection 的,還需要額外處理。
對於非同步請求,我們知道,通過發出去之後,會非同步的處理結果,所以這裡分為了兩步,叫做 ob-origin 和 ob-sink。如下:
對於 ob-origin,程式碼分析的時候會先給這個 observation 設定成 PENDING 狀態,只有對應的 ob-sink 呼叫並且返回了正確的結果,才會設定成 HEALTHY。因為 ob-origin 和 ob-sink 是非同步的,所以程式碼分析的時候會加上一個特殊的欄位,包含 subject 的標識和 context,這樣就能讓 ob-origin 和 ob-sink 對應起來。
小結
上面大概介紹了 Panorama 的架構以及一些關鍵的知識點是如何實現的,簡單來說,就是在一些關鍵程式碼路徑上面注入 hook,然後通過 hook 對外將相關的狀態給彙報出去,在外面會有其他的分析程式對拿到的資料進行分析從而判定系統是否在正常工作。它其實跟加 metrics 很像,但 metrics 只能看出哪裡出現了問題,對於想更細緻定位具體的某一個問題以及它的上下文環境,倒不是特別的方便。這點來說 Panorama 的價值還是挺大的。
Panorama 的程式碼已經開源,總的來說還是挺簡單的,但我沒找到核心的程式碼分析,注入 hook 這些,有點遺憾。但理解了大概原理,其實先強制在程式碼寫死也未嘗不可。另一個比較可行的辦法就是進行在程式碼裡面把日誌新增詳細,這樣就不用程式碼注入了,而是在外面寫一個程式來分析日誌,其實 Panorama 程式碼裡面提供了日誌分析的功能,為 ZooKeeper 來設計的,但作者自己也說到,分析日誌的效果比不上直接在程式碼裡面進行注入。
那對我們來說,有啥可以參考的呢?首先當然是這一套故障檢查的理念,既然 Panorama 已經做出來並且能發現故障量,自然我們也可以在 TiDB 裡面實施。因為我們已經有在 Go 和 Rust 程式碼裡面使用 fail 來進行錯誤注入的經驗,所以早期手寫監控程式碼也未嘗不可,但也可以直接完善日誌,提供一個程式來分析日誌就成。如果你對這塊感興趣,想把 Panorama 相關的東西應用到 TiDB 中來,歡迎聯絡我 [email protected]。