Diviner:肯定性測試的新嘗試

一直以來我對肯定性執行的問題就很感興趣。咱們在多線程模型上花了很大時間。咱們大部分人都應該遇到過一些只在必定機率範圍內發生的 bug。即便你已經準備好了一個修復程序,你也不能肯定它是否是還會再次發生,你所能作的不過是測試,測試,再測試,並但願這樣的問題不會再次出現。git

咱們能夠肯定地進行調試並拍着胸脯說這是 100% 肯定的,這是每一位工程師的夢想,而這個問題已經被解決了。github

在過去的幾個月裏,我一直在學習 TLA+,我如今堅信 TLA+ 是構建複雜的、多線程的、高性能的、(也多是分佈式的)系統的寶貴工具。在爲我全部的項目寫下第一行代碼以前,個人確更喜歡在 TLA+ 中先構建一個設計。但 TLA+ 只能幫助你思考你的設計,並修復其中的設計缺陷。咱們還須要考慮到另外一面:實際執行該系統。數據庫

咱們能夠有一個已經通過 TLA+ 驗證的設計,但若是你寫的代碼是比較容易受到某些併發性 bugs 的攻擊的,而這些 bugs 又是有必定機率會發生的,那這時候該怎麼辦呢?編程

固然,也有一些在解決方案上的嘗試,好比 rr。但在這個領域還有一個真正的精華,那就是 FoundationDB。若是你不是很瞭解 FoundationDB,特別是不清楚它們是如何進行測試的,我強烈推薦如下兩個視頻:網絡

它們所作的,是在 C++ 之上構建一個 actor 模型,而後使用 actor 模型來編寫完整的數據庫邏輯。所以,它們能夠將基於 actor 的代碼徹底注入到一個肯定性的測試框架中,以測試各類併發性問題。多線程

老實說,我以前就看過這些視頻,但那時候比較早,我對他們的解決方案印象並非很深入,緣由在於:他們的模擬框架是在一個單線程中連續運行的,這與真正的設置相差甚遠,在真正的設置中,你是有多個線程在共同運行的。所以你從模擬中獲得的性能數據沒什麼意義。併發

幸運的是,我有一個興趣愛好,就是成爲電腦學的考古專家:我會時不時地把一些相對較老的視頻翻出來,從新觀看一遍,以得到新的理解。這是一個過去的經驗,在這個行業有不少_新_的發明,都只是新瓶裝舊酒。當我最近翻出 FoundationDB 的視頻並再次觀看時,我發現我以前犯了一個很是很是嚴重的錯誤。這確實是一件大事。框架

測試與基準測試是不一樣的

這裏的關鍵是,測試與基準測試是不一樣的。測試的目的毫不是得到實際的運行時間,而是探索程序能夠採用的全部路徑。就像 TLA+ 會探索你設計中全部的狀態同樣,若是一個模擬能夠探索一段代碼能夠採用的全部的執行路徑,那麼就已經足夠了。使用 actor 模型從新組織的代碼,你的邏輯會被天然地分割成不少個小的原子塊,只要你可以枚舉出一個程序能夠有效執行的全部不一樣的執行順序,那麼就算是用一個單線程的測試框架也能夠探索出多線程解決方案中可能致使的全部路徑!異步

實際上,在一個模擬環境中還有不少好處:當你的項目發佈時,人們可能開始使用它這意味着他們會嘗試在許多不一樣的機器上運行你的代碼。而後,這些機器將會探索你的程序可能致使的不一樣的執行狀態,在某種程度上,咱們能夠認爲全部這些機器都在爲你的程序進行測試,尋找 bugs。爲了保證你項目的質量,在理想的狀況下,你應該在全部這些不一樣的機器以前找到新的 bugs。如今,問題就變成了列舉出全部可能的狀態和尋找 bugs 方面的競賽。對於一些流行的產品,用戶運行的機器數量很容易超過項目維護者所擁有的機器數量。問題來了:如何才能在使用極少的機器的狀況下,找到更多的 bugs?async

這個問題的答案,相似於 FoundationDB 解決方案中的模擬設計:首先,咱們在一個 actor 模型的框架下組織邏輯,所以咱們可使用一個單線程模擬測試執行器來運行測試;而後,咱們模擬全部和環境相關的代碼,如計時器、網絡 IOs、文件 IOs 等等。經過這種方式,咱們能夠將咱們項目的核心,也就是大多數 bug 發生的地方,提煉成一段單線程的,連續的代碼,有如下好處:

  • 當一個測試在一個單線程的環境中運行時,一個典型的多核機器就能夠用來同時運行多個測試;
  • 當全部的 IOs 都被模擬出來以後,咱們能夠在測試中運行更少的代碼(好比,咱們能夠跳過整個 TCP/IP 棧),從而更快地進行測試;
  • 有了模擬的 IOs,咱們能夠更容易地模擬異常狀況,好比網絡堵塞;

全部這些好處都意味着模擬方案能夠容許咱們在更少的時間內對代碼進行更多的測試,讓咱們有機會在尋找 bugs 的競速遊戲中取得勝利。在 FoundationDB 的示例中,他們估計在過去的幾年時間裏,經過這種設計,他們已經積累了至關於一萬億 CPU-hours 的模擬壓力測試。直到今天爲止,我尚未看到一個更高級的測試框架設計。

如今只有一個問題了:雖然這個解決方案很好,也被證實很是有效,但咱們能在其餘地方使用它嗎?咱們是否會受到 C++ actor 框架的限制?答案是,固然不會!

Rust:一個基於 Actor 模擬測試的最佳選擇

若是咱們仔細思考一下,作一個 FoundationDB 類型的肯定性模擬測試所須要的只是一個基於 actor 的代碼,而後咱們就能夠根據測試需求對它們進行重組。使人激動的事情在於,Rust,咱們敬愛的用於構建高性能的分佈式軟件的解決方案,已經提供了一個異步/等待設計,這很像 actor 模型(好吧,我並不能算是一名計算機科學教授,我把這個問題留給那些更有資格去評判異步/等待是否是 actor 模型的人)。爲了使其更加有趣,Rust 的可切換運行時間的設計使其成爲這種肯定性模擬測試思想下的最佳選擇:咱們所須要作的,就是在測試中使用不一樣的運行時間,問題就會獲得解決。

接下來就讓咱們有請出本文的主角:Diviner

Diviner

一旦有了這個想法,我以爲它真的太偉大了,不誇張的說,我花了我全部的夜晚和週末來實現這個想法,這纔有了 diviner。它由兩部組成:

  • 一個設計成單線程和具備肯定性的運行時間,所以咱們能夠利用它來構建肯定性模擬測試;
  • 在現有的 Rust 異步庫上面實現的封裝器。封裝器在正常模式下(經過內聯函數和 newtypes)將直接編譯成現有的實現,但在啓用了特殊的 simulation 功能後,它們將被編譯成與上述運行時間集成好的模擬版本,以便進行肯定性測試。如今我是從 async-std 開始,但在將來可能會添加更多的封裝器。

兩個部分結合在一塊兒,diviner 爲異步/等待的 Rust 代碼提供了一個 FoundationDB 類型的肯定性測試解決方案。這裏提供幾個示例,以展現操做時間的能力,這容許咱們以更快的方式測試超時,以及測試併發的 bugs 的能力。使用肯定性 seed,diviner 將肯定性地運行,讓你有機會能夠無限次地調試你的代碼。它的美妙之處在於,它只是一個異步/等待的 Rust 代碼,咱們沒有往 diviner 中引入任何新東西。

我還有一個例子,我但願在將來的幾天內能夠將其實現:

`use byteorder::{ByteOrder, LittleEndian};

use diviner::{

net::{TcpListener, TcpStream},

spawn, Environment,

};

use std::io;

async fn handle(stream: Tcpstream) {

let mut buf = vec![];

loop {

let mut t = vec![0; 1024];

let n = stream.read(&mut t).await.expect("read error!");

if n == 0 {

break;

}

buf.extend_from_slice(&t[..n]);

let l = LittleEndian::read_u32(&buf) as usize;

if buf.len() >= l + 4 {

let content = &buf[4..l + 4];

stream.write(content).await.expect("write error!");

buf = buf.drain(0..l + 4).collect();

}

}

}

async fn server(addr: String) -> Result<(), io::Error> {

let mut listener = TcpListener::bind(addr).await?;

while let Ok((stream, _)) = listener.accept().await {

spawn(handle(stream));

}

Ok(())

}

fn main() {

let e = Environment::new();

let result = e.block_on(async {

let addr = "127.0.0.1:18000";

spawn(async {

server(addr.to_string()).await.expect("server boot error!");

});

let data: Vec<u8> = vec![4, 0, 0, 0, 0x64, 0x61, 0x64, 0x61];

for i in 1..data.len() {

let mut client = TcpStream::connect(addr).await.expect("connect error!");

client

.write(&data[..i])

.await

.expect("client write 1 error!");

client

.write(&data[i..])

.await

.expect("client write 1 error!");

let mut output: Vec<u8> = vec![0; 4];

client.read(&mut output).await.expect("client read error!");

if &output[..] != &data[4..] {

panic!("Invalid response!");

}

}

});

match result {

Ok(val) => println!("The task completed with {:?}", val),

Err(err) => println!("The task has panicked: {:?}", err),

}

}`

這個例子展現的是一個典型的新手錯誤:TCP/IP 協議是基於流的,而不是基於包的。雖然你可能能夠提供一個 1 KB 的緩衝區,可是協議能夠經過任意數量的字節反饋你,在極端狀況下可能只有 1 byte 的數據。在真正的測試中,這是很難模擬的,由於你須要建立一個 TCP/IP 很是擁擠的環境,它只有一個很是小的很是擁擠的窗口。可是有了 diviner,在測試中調整它將會是很是簡單的。你寫的代碼,只是使用了 TcpListener/TCPStream,就像是 async-std 中同名的結構同樣。是的,你將不得不經過 diviner 來導入它們,可是經過內聯函數和 newtype 模式,性能徹底不會受到影響。一旦你願意作出這樣的犧牲,我相信你將會發現一個全新的世界。

這纔是讓我興奮的地方。如今,diviner 還處於很是早期的階段,我將在有空的時候繼續在 diviner 中添加缺乏的部分(好比 async-std 中所缺乏的封裝器)。若是你也有興趣,歡迎來試試,讓我知道你的感覺。

更多關於 CKB 腳本編程的系列文章,歡迎前往 Xuejie 我的博客查看:

或前往 CKB Docs 查看:

做者:Xuejie
原文連接:
https://xuejie.space/2020_04_...
譯者:Jason Chai

相關文章
相關標籤/搜索