摘要:作者張博康本文為源碼解析系列的第五篇,為大家介紹在測試中使用的周邊庫。而對于行為的情況會特殊一些,在中并不做實際的動作,而是返回并通過傳參給閉包產生自定義的返回值。
作者:張博康
本文為 TiKV 源碼解析系列的第五篇,為大家介紹 TiKV 在測試中使用的周邊庫 fail-rs。
fail-rs 的設計啟發于 FreeBSD 的 failpoints,由 Rust 實現。通過代碼或者環境變量,其允許程序在特定的地方動態地注入錯誤或者其他行為。在 TiKV 中通常在測試中使用 fail point 來構建異常的情況,是一個非常方便的測試工具。
Fail point 需求在我們的集成測試中,都是簡單的構建一個 KV 實例,然后發送請求,檢查返回值和狀態的改變。這樣的測試可以較為完整地測試功能,但是對于一些需要精細化控制的測試就鞭長莫及了。我們當然可以通過 mock 網絡層提供網絡的精細模擬控制,但是對于諸如磁盤 IO、系統調度等方面的控制就沒辦法做到了。
同時,在分布式系統中時序的關系是非常關鍵的,可能兩個操作的執行順行相反,就導致了迥然不同的結果。尤其對于數據庫來說,保證數據的一致性是至關重要的,因此需要去做一些相關的測試。
基于以上原因,我們就需要使用 fail point 來復現一些 corner case,比如模擬數據落盤特別慢、raftstore 繁忙、特殊的操作處理順序、錯誤 panic 等等。
基本用法 示例在詳細介紹之前,先舉一個簡單的例子給大家一個直觀的認識。
還是那個老生常談的 Hello World:
#[macro_use] extern crate fail; fn say_hello() { fail_point!(“before_print”); println!(“Hello World~”); } fn main() { say_hello(); fail::cfg("before_print", "panic"); say_hello(); }
運行結果如下:
Hello World~ thread "main" panicked at "failpoint before_print panic" ...
可以看到最終只打印出一個 Hello World~,而在打印第二個之前就 panic 了。這是因為我們在第一次打印完后才指定了這個 fail point 行為是 panic,因此第一次在 fail point 不做任何事情之后正常輸出,而第二次在執行到 fail point 時就會根據配置的行為 panic 掉!
Fail point 行為當然 fail point 不僅僅能注入 panic,還可以是其他的操作,并且可以按照一定的概率出現。描述行為的格式如下:
[%][ *] [(args...)][-> ]
pct:行為被執行時有百分之 pct 的機率觸發
cnt:行為總共能被觸發的次數
type:行為類型
off:不做任何事
return(arg):提前返回,需要 fail point 定義時指定 expr,arg 會作為字符串傳給 expr 計算返回值
sleep(arg):使當前線程睡眠 arg 毫秒
panic(arg):使當前線程崩潰,崩潰消息為 arg
print(arg):打印出 arg
pause:暫停當前線程,直到該 fail point 設置為其他行為為止
yield:使當前線程放棄剩余時間片
delay(arg):和 sleep 類似,但是讓 CPU 空轉 arg 毫秒
args:行為的參數
比如我們想在 before_print 處先 sleep 1s 然后有 1% 的機率 panic,那么就可以這么寫:
"sleep(1000)->1%panic"定義 fail point
只需要使用宏 fail_point! 就可以在相應代碼中提前定義好 fail point,而具體的行為在之后動態注入。
fail_point!("failpoint_name"); fail_point!("failpoint_name", |_| { // 指定生成自定義返回值的閉包,只有當 fail point 的行為為 return 時,才會調用該閉包并返回結果 return Error }); fail_point!("failpoint_name", a == b, |_| { // 當滿足條件時,fail point 才被觸發 return Error })動態注入 環境變量
通過設置環境變量指定相應 fail point 的行為:
FAILPOINTS="= ; = ;..."
注意,在實際運行的代碼需要先使用 fail::setup() 以環境變量去設置相應 fail point,否則 FAILPOINTS 并不會起作用。
#[macro_use] extern crate fail; fn main() { fail::setup(); // 初始化 fail point 設置 do_fallible_work(); fail::teardown(); // 清除所有 fail point 設置,并且恢復所有被 fail point 暫停的線程 }代碼控制
不同于環境變量方式,代碼控制更加靈活,可以在程序中根據情況動態調整 fail point 的行為。這種方式主要應用于集成測試,以此可以很輕松地構建出各種異常情況。
fail::cfg("failpoint_name", "actions"); // 設置相應的 fail point 的行為 fail::remove("failpoint_name"); // 解除相應的 fail point 的行為內部實現
以下我們將以 fail-rs v0.2.1 版本代碼為基礎,從 API 出發來看看其背后的具體實現。
fail-rs 的實現非常簡單,總的來說,就是內部維護了一個全局 map,其保存著相應 fail point 所對應的行為。當程序執行到某個 fail point 時,獲取并執行該全局 map 中所保存的相應的行為。
全局 map 其具體定義在 FailPointRegistry。
struct FailPointRegistry { registry: RwLock>>, }
其中 FailPoint 的定義如下:
struct FailPoint { pause: Mutex, pause_notifier: Condvar, actions: RwLock >, actions_str: RwLock , }
pause 和 pause_notifier 是用于實現線程的暫停和恢復,感興趣的同學可以去看看代碼,太過細節在此不展開了;actions_str 保存著描述行為的字符串,用于輸出;而 actions 就是保存著 failpoint 的行為,包括概率、次數、以及具體行為。Action 實現了 FromStr 的 trait,可以將滿足格式要求的字符串轉換成 Action。這樣各個 API 的操作也就顯而易見了,實際上就是對于這個全局 map 的增刪查改:
fail::setup() 讀取環境變量 FAILPOINTS 的值,以 ; 分割,解析出多個 failpoint name 和相應的 actions 并保存在 registry 中。
fail::teardown() 設置 registry 中所有 fail point 對應的 actions 為空。
fail::cfg(name, actions) 將 name 和對應解析出的 actions 保存在 registry 中。
fail::remove(name) 設置 registry 中 name 對應的 actions 為空。
而代碼到執行到 fail point 的時候到底發生了什么呢,我們可以展開 fail_point! 宏定義看一下:
macro_rules! fail_point { ($name:expr) => {{ $crate::eval($name, |_| { panic!("Return is not supported for the fail point "{}"", $name); }); }}; ($name:expr, $e:expr) => {{ if let Some(res) = $crate::eval($name, $e) { return res; } }}; ($name:expr, $cond:expr, $e:expr) => {{ if $cond { fail_point!($name, $e); } }}; }
現在一切都變得豁然開朗了,實際上就是對于 eval 函數的調用,當函數返回值為 Some 時則提前返回。而 eval 就是從全局 map 中獲取相應的行為,在 p.eval(name) 中執行相應的動作,比如輸出、等待亦或者 panic。而對于 return 行為的情況會特殊一些,在 p.eval(name) 中并不做實際的動作,而是返回 Some(arg) 并通過 .map(f) 傳參給閉包產生自定義的返回值。
pub fn eval小結) -> R>(name: &str, f: F) -> Option { let p = { let registry = REGISTRY.registry.read().unwrap(); match registry.get(name) { None => return None, Some(p) => p.clone(), } }; p.eval(name).map(f) }
至此,關于 fail-rs 背后的秘密也就清清楚楚了。關于在 TiKV 中使用 fail point 的測試詳見 github.com/tikv/tikv/tree/master/tests/failpoints,大家感興趣可以看看在 TiKV 中是如何來構建異常情況的。
同時,fail-rs 計劃支持 HTTP API,歡迎感興趣的小伙伴提交 PR。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/17973.html
摘要:而源碼解析系列文章則是會從源碼層面給大家抽絲剝繭,讓大家知道我們內部到底是如何實現的。我們希望通過該源碼解析系列,能讓大家對有一個更深刻的理解。 作者:唐劉 TiKV 是一個支持事務的分布式 Key-Value 數據庫,有很多社區開發者基于 TiKV 來開發自己的應用,譬如 titan、tidis。尤其是在 TiKV 成為 CNCF 的 Sandbox 項目之后,吸引了越來越多開發者的...
閱讀 1437·2019-08-29 17:14
閱讀 1645·2019-08-29 12:12
閱讀 727·2019-08-29 11:33
閱讀 3261·2019-08-28 18:27
閱讀 1442·2019-08-26 10:19
閱讀 904·2019-08-23 18:18
閱讀 3524·2019-08-23 16:15
閱讀 2539·2019-08-23 14:14