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

相关文章
相关标签/搜索