作者:CloudWeGo Rust Team

GitHub: github.com/bytedance/m…

一、概述

尽管 Tokio 现在现已是 Rust 异步运转时的事实规范,但要完结极致功用的网络中间件还有一定间隔。为了这个目标,CloudWeGo Rust Team 探究依据 io-uring 为 Rust 供给异步支撑,并在此基础上研发通用网关。

本文包括以下内容:

  1. 介绍 Rust 异步 Runtime;

  2. Monoio 的一些规划精要;

  3. Runtime 比照选型与运用。

二、Rust 异步机制

凭借 Rustc 和 llvm,Rust 能够生成足够高效且安全的机器码。可是一个运用程序除了核算逻辑以外往往还有 IO,特别是关于网络中间件,IO 其实是占了适当大比例的。

程序做 IO 需求和操作系统打交道,编写异步程序通常并不是一件简略的作业,在 Rust 中是怎样解决这两个问题的呢?比方,在 C++里边,或许经常会写一些 callback ,可是咱们并不想在 Rust 里边这么做,这样的话会遇到许多生命周期相关的问题。

Rust 答应自行完结 Runtime 来调度使命和履行 syscall;并供给了 Future 等一致的接口;其他内置了 async-await 语法糖从面向 callback 编程中解放出来。

字节开源 Monoio :基于 io-uring 的高性能 Rust Runtime
字节开源 Monoio :基于 io-uring 的高性能 Rust Runtime

Example

这儿从一个简略的比如下手,看一看这套系统到底是怎样作业的。

当并行下载两个文件时,在任何语言中都能够发动两个 Thread,分别下载一个文件,然后等候 thread 履行结束;但并不想为了 IO 等候发动多余的线程,假如需求等候 IO,咱们希望这时线程能够去干其他,等 IO 安排妥当了再做就好。

这种依据事情的触发机制在 cpp 里边常常会以 callback 的方式遇见。Callback 会打断咱们的接连逻辑,导致代码可读性变差,其他也容易在 callback 依靠的变量的生命周期上踩坑,比方在 callback 履行前提前开释了它会引用的变量。

但在 Rust 中只需求创立两个 task 并等候 task 履行结束即可。

字节开源 Monoio :基于 io-uring 的高性能 Rust Runtime

这个比如比较线程的话,异步 task 会高效许多,但编程上并没有因而杂乱多少。

第二个比如,现在 mock 一个异步函数 do_http,这儿直接回来一个 1,其实里边或许是一堆异步的长途恳求;在此之上还想对这些异步函数做一些组合,这儿假设是做两次恳求,然后把两次的成果加起来,最终再加一个 1 ,便是这个比如里边的 sum 函数。经过 Async 和 Await 语法能够十分友爱地把这些异步函数给嵌套起来。

#[inline(never)]
async fn do_http() -> i32 {
    // do http request in async way
    1
}
pub async fn sum() -> i32 {
    do_http().await + do_http().await +1
}

这个进程和写同步函数是十分像的,也就说是在面向进程编程,而非面向状况编程。运用这种机制能够避开写一堆 callback 的问题,带来了编程的十分大的快捷性。

Async Await 背后的秘密

经过这两个比如能够得知 Rust 的异步是怎样用的,以及它写起来的确十分方便。那么它背后到底是什么原理呢?

#[inline(never)]
async fn do_http( ) -> i32 {
    // do http request in async way
    1
}
pub async fn sum() -> i32 {
    do_http().await + do_http().await + 1
}

字节开源 Monoio :基于 io-uring 的高性能 Rust Runtime

方才的比如运用 Async + Await 编写,其生成结构终究完结 Future trait 。

Async + Await 其实是语法糖,能够在 HIR 阶段被打开为 Generator 语法,然后 Generator 又会在 MIR 阶段被编译器打开成状况机。

字节开源 Monoio :基于 io-uring 的高性能 Rust Runtime

Future 笼统

Future trait 是规范库里界说的。它的接口十分简略,只要一个相关类型和一个 poll 办法。

pub trait Future {
    type Output;
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
pub enum Poll<T> {
    Ready(T),
    Pending,
}

Future 描述状况机对外露出的接口:

  1. 推进状况机履行:Poll 办法顾名思义便是去推进状况机履行,给定一个使命,就会推进这个使命做状况转化。

  2. 回来履行成果:

    1. 遇到了堵塞:Pending
    2. 履行结束:Ready + 回来值

能够看出,异步 task 的实质便是完结 Future 的状况机。程序能够运用 Poll 办法去操作它,它或许会告知程序现在遇到堵塞,或者说使命履行完了并回来成果。

既然有了 Future trait,咱们完全能够手动地去完结 Future。这样一来,完结出来的代码要比 Async、Await 语法糖去打开的要易读。下面是手动生成状况机的样例。假如用 Async 语法写,或许直接一个 async 函数回来一个 1 就能够;咱们手动编写需求自界说一个结构体,并为这个结构体完结 Future。

// auto generate
async fn do_http() -> i32 {
    // do http request in async way
    1
}
// manually impl
fn do_http() -> DOHTTPFuture { DoHTTPFuture }
struct DoHTTPFuture;
impl Future for DoHTTPFuture {
    type Output = i32;
    fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Self::Output>{
        Poll::Ready(1)
    }
}

Async fn 的实质便是回来一个完结了 Future 的匿名结构,这个类型由编译器主动生成,所以它的名字不会露出给咱们。而咱们手动完结就界说一个 Struct DoHTTPFuture,并为它完结 Future,它的 Output 和 Async fn 的回来值是相同的,都是 i32 。这两种写法是等价的。

由于这儿只需求马上回来一个数字 1,不触及任何等候,那么咱们只需求在 poll 完结上马上回来 Ready(1) 即可。 前面举了 sum 的比如,它做的作业是异步逻辑的组合:调用两次 do http,最终再把两个成果再加一起。这时分假如要手动去完结的话,就会略微杂乱一些,由于会触及到两个 await 点。一旦触及到 await,其实质上就变成一个状况机。

为什么是状况机呢?由于每次 await 等候都有或许会卡住,而线程此刻是不能停止作业并等候在这儿的,它有必要切出去履行其他使命;为了下次再恢复履行前面使命,它所对应的状况有必要存储下来。这儿咱们界说了 FirstDoHTTP 和 SecondDoHTTP 两个状况。完结 poll 的时分,便是去做一个 loop,loop 里边会 match 当时状况,去做状况转化。

// auto generate
async fn sum( ) -> i32 {
    do_http( ).await + do http( ).await + 1
}
// manually impl
fn sum() -> SumFuture { SumFuture::FirstDoHTTP(DoHTTPFuture) }
enum SumFuture {
    FirstDoHTTP(DOHTTPFuture),
    SecondDoHTTP( DOHTTPFuture, i32),
}
impl Future for SumFuture {
    type Output = i32;
    fn poll(self: Pin<&mut Self>, cx: &mut Context<' >) -> Poll<Self::0utput> {
        let this = self.get mut( );
        loop {
            match this {
                SumFuture::FirstDoHTTP(f) => {
                    let pinned = unsafe { Pin::new_unchecked(f) };
                    match pinned.poll(cx) {
                        Poll::Ready(r) => {
                            *this = SumFuture::SecondDoHTTP(DOHTTPFuture,r);
                        }
                        Poll::Pending => {
                            return Pol::Pending;
                        }
                    }
                }
                SumFuture::SecondDoHTTP(f, prev_sum) => {
                    let pinned = unsafe { Pin::new_unchecked(f) };
                    return match pinned.poll( cx) {
                        Poll::Ready(r) => Poll::Ready(*prev_sum + r + 1),
                        Poll::Pending => Pol::Pending,
                    };
                }
            }
        }
    }
}

Task, Future 和 Runtime 的联系

咱们这儿以 TcpStream 的 Read/Write 为例整理整个机制和组件的联系。

首要当咱们创立 TCP stream 的时分,这个组件内部就会把它注册到一个 poller 上去,这个 poller 能够简略地认为是一个 epoll 的封装(详细运用什么 driver 是依据渠道而异的)。

依照顺序来看,现在有一个 task ,要把这个 task spawn 出去履行。那么 spawn 实质上便是把 task 放到了 runtime 的使命行列里,然后 runtime 内部会不停地从使命行列里边取出使命而且履行——履行便是推进状况机动一动,即调用它的 poll 办法,之后咱们就来到了第2步。

字节开源 Monoio :基于 io-uring 的高性能 Rust Runtime

咱们履行它的 poll 办法,实质上这个 poll 办法是用户完结的,然后用户就会在这个 task 里边调用 TcpStream 的 read/write。这两个函数内部终究是调用 syscall 来完结功用的,但在履行 syscall 之前需求满意条件:这个 fd 可读/可写。假如它不满意这个条件,那么即使咱们履行了 syscall 也仅仅拿到了 WOULD_BLOCK 过错,白白支付功用。初始状况下咱们会设定新参加的 fd 自身便是可读/可写的,所以第一次 poll 会履行 syscall。当没有数据可读,或者内核的写 buffer 满了的时分,这个 syscall 会回来 WOULD_BLOCK 过错。在感知到这个过错后,咱们会修正 readiness 记载,设定这个 fd 相关的读/写为不可读/不可写状况。这时咱们只能对外回来 Pending。

之后来到第四步,当咱们使命行列里边使命履行完了,咱们现在所有使命都卡在 IO 上了,所有的 IO 或许都没有安排妥当,此刻线程就会继续地堵塞在 poller 的 wait 办法里边,能够简略地认为它是一个 epoll_wait 相同的东西。当依据 io_uring 完结的时分,这或许对应另一个 syscall。

此刻堕入 syscall 是合理的,由于没有使命需求履行,咱们也不需求轮询 IO 状况,堕入 syscall 能够让出 CPU 时刻片供同机的其他使命运用。假如有任何 IO 安排妥当,这时分咱们就会从 syscall 回来,而且 kernel 会告知咱们哪些 fd 上的哪些事情现已安排妥当了。比方说咱们关心的是某一个 FD 它的可读,那么这时分他就会把咱们关心的 fd 和可读这件事告知咱们。 咱们需求符号 fd 对应的 readiness 为可读状况,并把等在它上面的使命给叫醒。前面一步咱们在做 read 的时分,有一个使命是等在这儿的,它依靠 IO 可读事情,现在条件满意了,咱们需求从头调度它。叫醒的实质便是把使命再次放到 task queue 里,完结上是经过 Waker 的 wake 相关办法做到的,wake 的处理行为是 runtime 完结的,最简略的完结便是用一个 Deque 存放使命,wake 时 push 进去,杂乱一点还会考虑使命盗取和分配等机制做跨线程的调度。

当该使命被 poll 时,它内部会再次做 TcpStream read,它会发现 IO 是可读状况,所以会履行 read syscall,而此刻 syscall 就会正确履行,TcpStream read 对外会回来 Ready。

Waker

方才提到了 Waker,接下来介绍 waker 是如何作业的。咱们知道 Future 实质是状况机,每次推它转一转,它会回来 Pending 或者 Ready ,当它遇到 io 堵塞回来 Pending 时,谁来感知 io 安排妥当? io 安排妥当后怎样从头驱动 Future 工作?

pub trait Future {
    type Output;
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
pub struct Context<'a> {
    //能够拿到用于唤醒Task的Waker
    waker: & a Waker,
    //符号字段,忽略即可
    _marker: PhantomData<fn(&'a ()) -> &'a ()>,
}

Future trait 里边除了有包括自身状况机的可变以借用以外,还有一个很重要的是 Context,Context 内部当时只要一个 Waker 有意义,这个 waker 咱们能够暂时认为它便是一个 trait object ,由 runtime 构造和完结。它完结的作用,便是当咱们去 wake 这个 waker 的时分,会把使命从头加回使命行列,这个使命或许马上或者稍后被履行。

举另一个比如来整理整个流程。

字节开源 Monoio :基于 io-uring 的高性能 Rust Runtime

用户运用 listener.accept() 生成 AcceptFut 并等候:

  1. fut.await 内部运用 cx 调用 Future 的 poll 办法
  2. poll 内部履行 syscall
  3. 当时无连接拨入,kernel 回来 WOULD_BLOCK
  4. 将 cx 中的 waker clone 并暂存于 TcpListener 相关结构内
  5. 本次 poll 对外回来 Pending
  6. Runtime 当时无使命可做,控制权交给 Poller
  7. Poller 履行 epoll_wait 堕入 syscall 等候 IO 安排妥当
  8. 查找并符号所有安排妥当 IO 状况
  9. 假如有相关 waker 则 wake 并铲除
  10. 等候 accept 的 task 将再次参加履行行列并被 poll
  11. 再次履行 syscall
  12. 12/13. kernel 回来 syscall 成果,poll 回来 Ready

Runtime

  1. 先从 executor 看起,它有一个履行器和一个使命行列,它的作业是不停地取出使命,推进使命运转,之后在所有使命履行结束有必要等候时,把履行权交给 Reactor。
  2. Reactor 拿到了履行权之后,会与 kernel 打交道,等候 IO 安排妥当,IO安排妥当好了之后,咱们需求符号这个 IO 的安排妥当状况,而且把这个 IO 所相关的使命给唤醒。唤醒之后,咱们的履行权又会从头交回给 executor 。在 executor 履行这个使命的时分,就会调用到 IO 组件所供给的一些才干。
  3. IO 组件要能够供给这些异步的接口,比方说当用户想用 tcb stream 的时分,得用 runtime 供给的一个 TcpStream, 而不是直接用规范库的。第二,能够将自己的 fd 注册到 Reactor 上。第三,在 IO 没有安排妥当的时分,咱们能把这个 waker 放到使命相相关的区域里。

整个 Rust 的异步机制大概便是这样。

字节开源 Monoio :基于 io-uring 的高性能 Rust Runtime

三、Monoio 规划

以下将会分为四个部分介绍 Monoio Runtime 的规划要点:

  1. 依据 GAT(Generic associated types) 的异步 IO 接口;
  2. 上层无感知的 Driver 勘探与切换;
  3. 如何兼顾功用与功用;
  4. 供给兼容 Tokio 的接口

依据 GAT 的纯异步 IO 接口

首要介绍一下两种告诉机制。第一种是和 epoll 相似的,依据安排妥当状况的一种告诉。第二种是 io-uring 的方式,它是一个依据“完结告诉”的方式。

字节开源 Monoio :基于 io-uring 的高性能 Rust Runtime

在依据安排妥当状况的方式下,使命会经过 epoll 等候并感知 IO 安排妥当,并在安排妥当时再履行 syscall。但在依据“完结告诉”的方式下,Monoio 能够更懒:直接告知 kernel 当时使命想做的作业就能够放手不管了。

io_uring 答使用户和内核共享两个无锁行列,submission queue 是用户态程序写,内核态消费;completion queue 是内核态写,用户态消费。经过 enter syscall 能够将行列中放入的 SQE 提交给 kernel,并可选地堕入并等候 CQE。

在 syscall 密集的运用中,运用 io_uring 能够大大削减上下文切换次数,而且 io_uring 自身也能够削减内核中数据复制。

字节开源 Monoio :基于 io-uring 的高性能 Rust Runtime

这两种方式的差异会很大程度上影响 Runtime 的规划和 IO 接口。在第一种方式下,等候时是不需求持有 buffer 的,只要履行 syscall 的时分才需求 buffer,所以这种方式下能够答使用户在真实调用 poll 的时分(如 poll_read)传入 &mut Buffer;而在第二种方式下,在提交给 kernel 后,kernel 能够在任何时分拜访 buffer,Monoio 有必要确保在该使命对应的 CQE 回来前 Buffer 的有效性。

假如运用现有异步 IO trait(如 tokio/async-std 等),用户在 read/write 时传入 buffer 的引用,或许会导致 UAF 等内存安全问题:假如在用户调用 read 时将 buffer 指针推入 uring SQ,那么假如用户运用 read(&mut buffer) 创立了 Future,但马上 Drop 它,并 Drop buffer,这种行为不违反 Rust 借用查看,但内核还将会拜访现已开释的内存,就或许会踩踏到用户程序后续分配的内存块。 所以这时分一个解法,便是去捕获它的所有权,当生成 Future 的时分,把所有权给 Runtime,这时分用户无论如何都拜访不到这个 buffer 了,也就确保了在 kernel 回来 CQE 前指针的有效性。这个解法学习了 tokio-uring 的做法。 Monoio 界说了 AsyncReadRent 这个 trait。所谓的 Rent ,即租赁,适当于是 Runtime 先把这个 buffer 从用户手里拿过来,待会再还给用户。这儿的 type read future 是带了生命周期泛型的,这个泛型其实是 GAT 供给了一个才干,现在 GAT 现已安稳了,现已能够在 stable 版别里边去运用它了。当要完结相关的 Future 的时分,凭借 TAIT 这个 trait 能够直接运用 async-await 方式来写,比较手动界说 Future 要方便友爱许多,这个 feature 现在还没安稳(现在改名叫 impl trait in assoc type 了)。

当然,转移所有权会引进新的问题。在依据安排妥当状况的方式下,取消 IO 只需求 Drop Future 即可;这儿假如 Drop Future 就或许导致连接上数据流过错(Drop Future 的瞬间有或许 syscall 刚好现已成功),而且一个更严峻的问题是一定会丢掉 Future 捕获的 buffer。针对这两个问题 Monoio 支撑了带取消才干的 IO trait,取消时会推入 CancelOp,用户需求在取消后继续等候原 Future 履行结束(由于它现已被取消了,所以会预期在较短的时刻内回来),对应的 syscall 或许履行成功或失败,并返还 buffer。

上层无感知的 Driver 勘探和切换

第二个特性是支撑上层无感知的 Driver 勘探和切换。

trait OpAble {
    fn uring_op(&mut self) -> io_uring::squeue::Entry;
    fn legacy_interest(&self) -> Option<(ready::Diirection, usize)>;
    fn legacy_call(&mut self) -> io::Result<u32>;
}
  1. 经过 Feature 或代码指定 Driver,并有条件地做运转时勘探
  2. 露出一致的 IO 接口,即 AsyncReadRent 和 AsyncWriteRent
  3. 内部运用 OpAble 一致组件完结(对 Read、Write 等 Op 做笼统)

详细来说,比方想做 accept、connect 或者 read、write 之类的,这些 op 是完结了 OpAble 的,实践对应这三个 fn :

  1. uring_op:生成对应 uring SQE
  2. legacy_interest:回来其重视的读写方向
  3. legacy_call:直接履行syscall

字节开源 Monoio :基于 io-uring 的高性能 Rust Runtime

整个流程会将一个完结了 opable 的结构 submit 到的 driver 上,然后会回来一个完结了 future 的东西,之后它 poll 的时分和 drop 的时分详细地会分发到两个 driver 完结中的一个,就会用这三个函数里边的一个或者两个。

功用

功用是 Monoio 的出发点和最大的长处。除了 io_uring 带来的提升外,它规划上是一个 thread-per-core 方式的 Runtime。

  1. 所有 Task 均仅在固定线程运转,无使命盗取。
  2. Task Queue 为 thread local 结构操作无锁无竞争。

高功用主要源于两个方面:

  1. Runtime内部高功用:根本等价于裸对接syscall

  2. 用户代码高功用:结构尽量 thread local 不跨线程

使命盗取和 thread-per-core 两种机制的比照:

假如用 tokio 的话,或许某一个线程上它的使命十分少,或许现已空了,可是另一个线程上使命十分多。那么这时分比较闲的线程就能够把使命从比较忙的使命上偷走,这一点和 Golang 十分像。这种机制能够较充分的运用 CPU,应对通用场景能够做到较好的功用。

但跨线程自身会有开支,多线程操作数据结构时也会需求锁或无锁结构。但无锁也不代表没有额外开支,比较纯本线程操作,跨线程的无锁结构会影响缓存功用,CAS 也会支付一些无效 loop。除此之外,更重要的是这种方式也会影响用户代码。

举个比如,咱们内部需求一个 SDK 去搜集本程序的一些打点,并把这些打点聚合之后去上报。在依据 tokio 的完结下,要做到极致的功用就比较困难。假如在 thread-per-core 结构的 Runtime 上,咱们完全能够将聚合的 Map 放在 thread-local 中,不需求任何锁,也没有任何竞争问题,只需求在每个线程上发动一个使命,让这个使命定时清空并上报 thread local 中的数据。而在使命或许跨线程的场景下,咱们就只能用全局的结构来聚合打点,用一个全局的使命去上报数据。聚合用的数据结构就很难不运用锁。

所以这两种方式各有各的长处,thread-per-core 方式下关于能够较独立处理的使命能够到达更好的功用。共享更少的东西能够做到更好的功用。可是 thread-per-core 的缺点是在使命自身不均匀的情况下不能充分运用 CPU。关于特定场景,如网关署理等,thread-per-core 更容易充分运用硬件功用,做到比较好的水平扩展性。当时广泛运用 nginx 和 envoy 都是这种方式。

字节开源 Monoio :基于 io-uring 的高性能 Rust Runtime

咱们做了一些 benchmark,Monoio 的功用水平扩展性是十分好的。当 CPU 核数增加的时分,只需求增加对应的线程就能够了。

功用性

Thread-per-core 不代表没有跨线程才干。用户依旧能够运用一些跨线程共享的结构,这些和 Runtime 无关;Runtime 供给了跨线程等候的才干。

使命在本线程履行,但能够等候其他线程上的使命,这个是一个很重要的才干。举例来说,用户需求用单线程去拉取长途装备,并下发到所有线程上。依据这个才干,用户就能够十分轻松地完结这个功用。

字节开源 Monoio :基于 io-uring 的高性能 Rust Runtime

跨线程等候的实质是在其他线程唤醒本线程的使命。完结上咱们在 Waker 中符号使命的所属权,假如当时线程并不是使命所属线程,那么 Runtime 会经过无锁行列将使命发送到其所属线程上;假如此刻目标线程处于休眠状况(堕入 syscall 等候 IO),则运用事先安插的 eventfd 将其唤醒。唤醒后,目标线程会处理跨线程 waker 行列。

除了供给跨线程等候才干外,Monoio 也供给了 spawn_blocking 才干,供用户履行较重的核算逻辑,以免影响到同线程的其他使命。

兼容接口

由于现在许多组件(如 hyper 等)绑定了 tokio 的 IO trait,而前面讲了由于地层 driver 的原因这两种 IO trait 不或许一致,所以生态上会比较困难。关于一些非热途径的组件,需求答使用户以兼容方法运用,即使支付一些功用代价。

字节开源 Monoio :基于 io-uring 的高性能 Rust Runtime

// tokio way
let tcp = tokio::net::TcpStream: connect("1.1.1.1.1:80").await.unwrap();
// monoio way(with monoio-compat)
let tcp = monoio_compat::StreamWrapper::new(monoio_tcp);
let monoio_tcp = monoio::net::TcpStream::connect("1.1.1.1:80").await.unwrap();
// both of them implements tokio:: io::AsyncReadd and tokio:: io: AsyncWrite

咱们供给了一个 Wrapper,内置了一个 buffer,用户运用时需求多支付一次内存复制开支。经过这种方法,咱们能够为 monoio 的组件包装出 tokio 的兼容接口,使其能够运用兼容组件。

四、Runtime 比照&运用

这部分介绍 runtime 的一些比照选型和运用。

前面现已提到了关于均匀调度和 thread-per-core 的一些比照,这儿主要说一下运用场景。关于较大量的轻使命,thread-per-core 方式是合适的。特别是署理、网关和文件 IO 密集的运用,运用 Monoio 就十分合适。

还有一点,Tokio 致力于一个通用跨渠道,可是 Monoio 规划之初便是为了极致功用,所以是期望以 io_uring 为主的。虽然也能够支撑 epoll 和 kqueue,但仅作 fallback。比方 kqueue 其实便是为了让用户能够在 Mac 上去开发的便利性,其实不期望用户真的把它跑在这(未来将支撑 Windows)。

生态部分,Tokio 的生态是比较全的,Monoio 的比较缺乏,即使有兼容层,兼容层自身是有开支的。Tokio 有使命盗取,能够在较多的场景体现很好,但其水平扩展性不佳。Monoio 的水平扩展就比较好,可是对这个业务场景和编程模型其实是有限制的。所以 Monoio 比较合适的一些场景便是署理、网关还有缓存数据聚合等。以及还有一些会做文件 io 的,由于 io_uring 对文件 io 十分好。假如不必 io_uring 的话,在 Linux 下其实是没有真异步的文件 io 能够用的,只要用 io_uring 才干做到这一点。还适用于这种文件 io 比较密集的,比方说像 DB 类型的组件。

字节开源 Monoio :基于 io-uring 的高性能 Rust Runtime

Tokio-uring 其实是一个构建在 tokio 之上的一层,有点像是一层分发层,它的规划比较漂亮,咱们也参阅了它里边的许多规划,比方说像那个传递所有权的这种方式。可是它还是依据 tokio 做的,在 epoll 之上运转 uring,没有做到用户通明。当组件在完结时,只能在运用 epoll 和运用 uring 中二选一,假如选择了 uring,那么编译产品就无法在旧版别 linux 上运转。而 Monoio 很好的支撑了这一点,支撑动态勘探 uring 的可用性。

Monoio 运用

  1. Monoio Gateway: 依据 Monoio 生态的网关服务,咱们优化版别 Benchmark 下来功用优于 Nginx
  2. Volo: CloudWeGo Team 开源的 RPC 框架,现在在集成中,PoC 版别功用比较依据 Tokio 提升 26%

咱们也在内部做了一些业务业务试点,未来咱们会从提升兼容性和组件建设上下手,便是让它更好用。


项目地址

  • Monoio GitHub: github.com/bytedance/m…

  • CloudWeGo:github.com/cloudwego

  • CloudWeGo 官网:www.cloudwego.io