這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
背景
在之前的文章 測試分布式系統的線性一致性 以及 使用 Porcupine 進行線性一致性測試 中,我介紹了 Go 的線性一致性測試載入器 Porcupine 以及一些簡單使用的例子,這裡我將簡單介紹一下基於 Porcupine 的一款簡單的分布式線性一致性測試架構:Chaos。
對於分布式系統的線性一致性測試,通常我們都會使用 jepsen,TiDB 當然也支援 jepsen,那麼為啥還是費力的再去搗鼓一個線性一致性測試架構呢?我覺得主要有以下幾點:
- Clojure:jepsen 使用的是 clojure,一門跑在 JVM 上面的函數式程式設計語言。雖然它很強大,但我並不精通。所以每次看 jepsen 的代碼對我都是一種折磨,而且我們 team 裡面大部分同學也完全不會。
- OOM: 的 linearizability check 只要稍微跑長一點時間,就非常容易 OOM,所以我們的測試 case 都不會跑特別久。
我其實一直有一個用 Go 寫一個線性一致性測試架構的想法,但主要困難在於如何去 linearizability check,幸運的是我找到了 porcupine,自然整個工作就能開動了,於是先搗鼓了一個簡單的 chaos,如果可行就繼續完善。
架構
類似於 jepsen,chaos 也將 DB service 跑在五個 node 上面,node 的命名就是 n1 到 n5,我們也能夠通過名字串連到對應的 node 上,譬如我們可以通過 ssh n1
就能直接登入到 node n1。
Chaos 也有一個 controller 節點,用來控制整個叢集,包括初始化要測試的 DB,建立對應的 client 跑實際的 test,啟動 nemesis 來幹擾系統,最後驗證 history 的 linearizability 等。架構圖如下:
不同於 jepsen 的地方在於,在 jepsen 裡面,controller 是全部通過 ssh 發送命令到 node 節點去執行所有的操作,但 chaos 會在每一個 node 上面啟動一個 agent,controller 通過 HTTP API 跟 agent 互動,來操作 node。之所以這麼設計,主要就是想用 Go 直接寫相關的 DB,nemesis 邏輯,而不是像 jepsen 那樣每次用 Linux 的 command 來操作。
但是用 agent 唯一問題在於需要顯式的在不同的 node 上面先啟動 agent,使用上面比 jepsen 稍微麻煩一點,但也可以通過指令碼來搞定。
因為 Go 是一門靜態語言,所以如果我們需要在 chaos 裡面驗證自己 DB 的線性一致性,需要首先實現相關的 interface,然後註冊給 chaos,這樣 chaos 才能對其驗證。這裡,我們以 TiDB 為例來進行說明。
DB
DB interface 對應的就是我們實際要進行測試的 DB,DB interface 定義如下:
type DB interface { SetUp(ctx context.Context, nodes []string, node string) error TearDown(ctx context.Context, nodes []string, node string) error Start(ctx context.Context, node string) error Stop(ctx context.Context, node string) error Kill(ctx context.Context, node string) error IsRunning(ctx context.Context, node string) bool Name() string}
我們在 SetUp 函數裡面初始化整個 DB 叢集,用 TearDown 來析構整個叢集。Start,Stop 等函數的含義非常明了,這裡不做說明。Name 就是 DB 名字,因為我們是要註冊給 chaos 的,所以名字必須唯一,譬如我們的 TiDB 的 Name 就是
“tidb”。
參數 nodes 就是整個叢集所在的 Node 資訊,通常就是 n1 到 n5,node 就是當前 Node 節點的名字。
在 TiDB 中,我們在 SetUp 函數裡面,下載 TiDB binary,解壓放到固定位置,然後更新設定檔,然後啟動整個叢集。而 TearDown 則是直接發送 kill 命令幹掉了整個叢集。在 Start 函數裡面,我們會在每個 Node 上面分別啟動 pd-server,tikv-server 和 tidb-server。
當我們實現了 TiDB 的 DB 介面之後,我們就通過 RegisterDB
函數將 TiDB 註冊到 chaos,這樣我們就能在 agent 裡面通過 DB name 找到 TiDB 並操作了。
Client
Client 就是 controller 這邊用來跟要測試的 DB 互動的組件。Client interface 定義如下:
type Client interface { SetUp(ctx context.Context, nodes []string, node string) error TearDown(ctx context.Context, nodes []string, node string) error Invoke(ctx context.Context, node string, request interface{}) interface{} NextRequest() interface{}}
Invoke 函數就是 Client 實際給 DB 發送命令的介面,因為我們並不知道不同 DB client 的命令參數,所以這裡的 request 就是一個 interface。Invoke 執行完畢會返回一個 response,我們也不知道各個 client 實際的 response,也用 interface 來表示。
NextRequest 返回的是下一個可以被 Invoke 的 request,因為只有 client 自己知道如何去構造一個 request。
在 TiDB bank case 裡面,我們會定義一個 bank client,每次 NextRequest 的時候會隨機播放是查詢所有賬戶的資料,還是選擇兩個賬戶進行轉賬。如果是 read,那麼 response 就是查詢的資料,如果是 transfer,那麼 response 就是是否成功。這裡需要注意,對於分布式系統來說,一個操作可能有三種結果,成功,失敗和未知,所以我們在 response 這邊也需要考慮處理 Unknown 的情況。具體可以參考 issue 上面的討論。
因為我們有 5 個 Node,controller 這邊每個 Node 會有一個 client 對應,所以實際我們也需要實現一個 client creator,用來產生多個 client。
type ClientCreator interface { Create(node string) Client}
Linearizability check
上面我們說到了 client 的介面,我們會用 NextRequest 產生一個 request,然後去 invoke 這個 request,得到一個 response。Controller 這邊會將 request 和 response 都記錄到一個 history 檔案裡面。所以一次 operation,是有一個 request 和 一個 response 兩個事件的。
為了簡單,我們是將 request 和 response 直接用 JSON 編碼寫入到 history 裡面的。當測試跑完之後,我們需要分析這個 history 檔案是否是線性一致的。首先,我們就需要去解析這個 history,這裡我們需要實現一個 record parser:
type RecordParser interface { OnRequest(data json.RawMessage) (interface{}, error) OnResponse(data json.RawMessage) (interface{}, error) OnNoopResponse() interface{}}
當 parser 讀取一行 record 之後,我們會首先判斷這行 record 是 request 還是 response,然後調用對應的 RecordParser 介面,再對資料進行解碼成實際的類型。
這裡需要注意 OnNoopResposne
介面,上面說過,所以 Unknown 的 response,我們在 OnResponse
這個函數需要返回 nil,讓 chaos 先忽略這次事件,然後在最後調用 OnNoopResposne
得到一個 Response,補全之前的 operation。
要實現 linearizability check,我們還需要實現自己的 porcupine model,然後調用函數 VerifyHistory(historyFile string, m porcupine.Model, p RecordParser)
來對產生的 history 進行驗證。
在 TiDB bank 的 porcupine model 關鍵 step 函數定義如下:
Step: func(state interface{}, input interface{}, output interface{}) (bool, interface{}) { st := state.([]int64) inp := input.(bankRequest) out := output.(bankResponse) if inp.Op == 0 { // read ok := out.Unknown || reflect.DeepEqual(st, out.Balances) return ok, state } // for transfer if !out.Ok && !out.Unknown { return true, state } newSt := append([]int64{}, st...) newSt[inp.From] -= inp.Amount newSt[inp.To] += inp.Amount return out.Ok || out.Unknown, newSt}
如果是 read 操作,那麼就判斷這次的結果跟上次狀態的是否一致,或者是否是 Unknown,如果是 transfer,那麼就在現有狀態上面,執行一次轉賬操作,返回新的狀態。
Nemesis
在跑測試的時候,controller 也會週期性執行一些 nemesis 操作去幹擾整個系統,譬如一下子 kill 所有的 DB,或者 drop 掉相關從一些 Node 上面發過來的網路包這些。Nemesis interface 定義如下:
type Nemesis interface { Invoke(ctx context.Context, node string, args ...string) error Recover(ctx context.Context, node string, args ...string) error Name() string}
因為 nemesis 也是要註冊給 chaos 使用,所以 Name 必須唯一。我們使用 Invoke 對系統幹擾,然後 Recover 恢複系統。當實現了自己的 nemesis 之後,也需要調用 RegisterNemesis
來進行註冊,這樣 agent 才能使用。
在 controller 這邊,我們需要實現 NemesisGenerator:
type NemesisGenerator interface { Generate(nodes []string) []*NemesisOperation Name() string}
Generate 會對每個 Node 產生一個 NemesisOperation 操作,NemesisOperation 裡面就定義了需要執行的 nemesis,以及相關的參數,和執行時間。Controller 會將 NemesisOperation 發送給 agent,讓 agent 去執行對應的 nemesis。
Agent and Controller
當我們定義好自己的 DB,Client,Nemesis 等之後,我們就需要將其整合到一起了。我們需要在 agent 裡面首先註冊自己的 DB 以及相關的 nemesis。在 cmd/agent/main.go
檔案裡面,TiDB 相關的註冊代碼如下:
// register nemesis_ "github.com/siddontang/chaos/pkg/nemesis"// register tidb_ "github.com/siddontang/chaos/tidb"
然後啟動 node, 之後我們通過
NewController(cfg *Config, clientCreator core.ClientCreator, nemesisGenerators []core.NemesisGenerator) *Controller`
建立一個 controller,controller 需要接受一個 ClientCreator 以及一個 nemesis generator 的列表。Config 裡面會指定這次測試每個 client 最多發送的 request 個數,以及整個測試執行的時間,以及要操作的 DB name 等。
啟動 controller,執行測試,最後結束之後,會有一個 history 檔案產生,我們就可以驗證線性一致性了。
總結
Chaos 現階段只是一個非常初級的版本,還有很多工作需要完善,譬如更好的 interface 定義,更便於使用這些。但現在至少是能 work 的,現在只有 TiDB 的轉賬測試,後面,我會給 TiDB 多加入幾個線性一致性測試,如果大家感興趣,也歡迎加入其他開源項目的線性一致性測試 case。
chaos: https://github.com/siddontang/chaos