写作锈灵药

2020-11-30 21:53:28

我是Elixir的忠实拥护者,这不是什么秘密,因此,当我开始进行Rust开发时,我试图将一些想法从Elixir带入Rust的世界。这篇文章描述了我正在构建的将Elixir的功能带到Rust的一些工具。

很难仅选择其中的几个,但是我相信Elixir的最大优势来自使用Erlang作为底层虚拟机,特别是来自以下两个属性:

除非您亲自体验,否则这很难解释。我在职业生涯的早期就知道不要在处理请求时创建线程。线程很重,很昂贵,而且线程太多会使您的整个计算机瘫痪。在大多数情况下,使用线程池就足够了,但是一旦并发任务数超过了池中的线程数,这种方法就会失败。

让我们看一个例子:假设一个rust应用程序仅创建2000个线程,每100毫秒唤醒一次,然后立即进入睡眠状态。

使用std :: thread;使用std :: time :: Duration fn main(){用于_ in 0 .. 2_000 {线程::生成(||循环{线程:: sleep(持续时间:: from_millis(100));}); }线程::睡眠(持续时间:: from_secs(1_000)); }

即使线程不执行任何操作,只要在我的MacBook上运行该命令,它就会在几秒钟后重新启动。这使得与线程进行大量并发是不切实际的。有许多解决此问题的方法。 Elixir选择的一种方法是使用称为流程的东西抽象并发任务。它们非常轻巧,因此即使运行200万也不会构成挑战。

使用异步Rust可以实现惊人的并发性和性能,但是使用异步Rust并不像编写常规Rust代码那样简单,并且它没有为Elixir Processes提供相同的功能。

在思考了很长时间之后,我如何制作一些可以在Rust中重组Elixir Processes的东西之后,我想到了引入一个中间步骤WebAssembly的想法。 WebAssembly是Rust可以定位的低级字节码规范。这个想法很简单,您无需将其编译为x86-64的Rust,而是将其编译为WASM目标。从那里,我将构建一组库和一个WebAssembly运行时,以展示Rust Processes的概念。与操作系统进程或线程相反,它们是轻量级的,具有较小的内存占用空间,可快速创建和终止,并且调度开销较低。在其他语言中,它们也称为绿色线程和goroutine,但是我将它们称为与Elixir的命名约定保持一致的进程。

让我们看一个相同的Rust示例,但是现在使用Lunatic实现了。同时,我们将并行进程数提高到20k。

使用疯子::: Channel,Process}; fn main(){let channel:Channel = Channel :: new(0); for _ in 0 .. 20_000 {Process :: spawn((),process).unwrap(); } channel .receive(); } fn process(_:()){循环{Process :: sleep(100); }}

要运行此程序,您需要首先将此Rust代码编译为.wasm文件:

与之前的示例相反,即使我使用的并发任务多10倍,此操作在我的2013年末Macbook上也不会打h,并且CPU利用率极低。让我们检查一下这里到底发生了什么。

Lunatic产生的进程实际上是在充分利用异步Rust提供的功能。它们被安排在窃取异步执行器的工作之上,与async-std相同。调用Process :: sleep(100)实际上会调用smol的at函数。

等一等!您可能会问自己,如果没有.await关键字,该如何工作。 Lunatic采用与Go,Erlang和基于绿色线程的Rust早期实现相同的方法。它创建了一个小的堆栈来执行该过程,并在您的应用程序需要更多时增加它。这比异步Rust在进行编译时计算确切的堆栈大小要低一些,但是我会说这是一个合理的折衷。

现在您可以编写常规的阻塞代码,但是如果您在等待,执行程序将负责将您的进程移出执行线程,因此您永远不会阻塞线程。

如前所述,对操作系统而言,调度线程是一项艰巨的任务。要将一个正在执行的线程替换为另一个正在执行的线程,需要完成许多工作(包括保存所有寄存器和某些线程状态)。但是,在Lunatic流程之间进行切换只会做最少的工作。 libfringe库率先提出了一个想法,并使用了一些asm!宏魔术,Lunatic让Rust编译器找出上下文切换期间要保留的最小寄存器数。这使得计划Lunatic流程的成本为零。在我的机器上通常为1ns,相当于一个函数调用。

在用户空间中调度进程而不使用线程的另一个好处是,即使您的应用程序行为不当,其他应用程序仍将继续在您的计算机上正常运行。

现在,我们了解了Lunatic如何允许您创建具有大量并发性的应用程序,让我们看一下容错能力。

也许最著名的Eralng / Elixir哲学是“让它崩溃”。如果您要构建复杂的系统,则不可能预测所有故障情况。不可避免地会在您的应用程序中失败,但是这种失败不会使整个事情崩溃。

Elixir进程是完全隔离的,并且只能通过消息相互通信。这样一来,您就可以设计应用程序,使故障保持在一个进程内,而不会影响其余进程。

与这里的Erlang相比,Lunatic提供了更强大的保证。每个Lunatic进程都有自己的堆,堆栈和syscall。

使用疯子::: Process,net}; //一旦WASI获得网络支持,您就可以使用Rust的`std :: net :: TcpStream`。使用std :: io :: {BufRead,Write,BufReader}; fn main(){让监听器= net :: TcpListener :: bind(“ 127.0.0.1:1337”).unwrap();同时让Ok(tcp_stream)=监听器.accept(){Process :: spawn(tcp_stream,handle).unwrap(); }} fn句柄(mut tcp_stream:net :: TcpStream){let mut buf_reader = BufReader :: new(tcp_stream .clone());循环{let mut buffer = String :: new(); buf_reader .read_line(&mut buffer).unwrap(); tcp_stream .write(buffer .as_bytes()).unwrap(); }}

该应用程序在localhost:1337上侦听tcp连接,产生一个处理每个传入连接的进程,并仅回显传入的行。

您会注意到的第一件事是,即使此应用程序将完全利用Rust的异步IO,也不会使用任何async或.await关键字。

另外,即使我们调用了崩溃的不安全C代码,tcp连接也被完全封装在Process中:

在这种情况下,崩溃仅包含在一个连接中。在Elixir中无法实现这样的事情,因为如果对C函数的调用崩溃,它将占用整个虚拟机。

Lunatic独有的另一个功能是可以限制进程的系统调用访问权限。如果我们将之前的生成调用替换为:

从handle函数内部调用的任何代码都将禁止使用syscall进行文件系统访问。这也适用于C依赖项,因为强制执行的级别很低。它使您可以表达流程的沙盒需求,并可以毫无恐惧地使用任何依赖项。我不知道任何其他允许您执行此操作的运行时。

这只是Lunatic将提供的功能的预告片。还有更多功能。一旦有了这个基础,就会打开一个新的可能性世界。我很兴奋的一些功能:

将进程从一台机器透明地移动到另一台机器的能力。编程模型依赖于通过消息进行通信的过程,并且如果这些消息是在本地发送或在网络上的不同计算机之间发送的,则实际上并不重要。

热装。现在,有了WASM字节码作为中间步骤,就可以从中生成新的JIT机器代码,并在整个系统仍在运行时替换它。

运行完整的应用程序,并将其编译为WASM。一个示例是将文件读取/写入从应用程序重定向到tcp流,因为我们完全负责syscall。这样做的好处是您可以使用代码对执行环境进行建模。

Lunatic仍处于起步阶段,因此还有很多工作要做。如果您对此感到兴奋或有一些想使用Lunatic的想法,请通过电子邮件[email protected]或通过Twitter @bkolobara与我联系。

我也想借此机会对Rust,Wasmer,W​​asmtime,Lucet和waSCC上的团队表示非常感谢。没有这个项目的所有辛苦工作,就不可能构建Lunatic。

附言如果您想更多地了解Erlang和Elixir的魔力,这是SašaJurić我最喜欢的演讲之一:Erlang和Elixir的灵魂。认真地去看吧!