乐趣区

关于rust:DatenLord|重新思考Rust-Async如何实现高性能IO

作者:王璞

长期以来,计算机系统 IO 的速度始终没能跟上 CPU 速度的晋升,相比而言 IO 往往成为零碎的性能瓶颈,计算工作期待 IO 存取数据,成为高性能零碎的一大性能瓶颈。本文先分析 IO 性能瓶颈的本源,而后举例说明如何解决 IO 瓶颈,最初简要介绍咱们在高性能 IO 方面的尝试。

IO 性能瓶颈

当用户程序执行 IO 操作时,绝大多数状况下是调用操作系统内核提供的零碎调用来执行 IO 操作,最常见的 IO 零碎调用是 read 和 write。在古代计算机体系结构和操作系统的架构下,导致程序 IO 性能瓶颈次要有三大因素:阻塞、上下文切换、内存拷贝。上面别离简述为什么这三个因素会导致程序性能降落。

阻塞

阻塞比拟好了解,比方用户程序调用 read 零碎调用来读取数据,如果要读取的数据没有筹备好(没有命中缓存),那用户程序就会被阻塞,导致用户程序休眠。等要读的数据加载到零碎态的内存之后,内核再唤醒用户程序来读取数据。

阻塞对用户程序性能最大的影响在于,用户程序会被强制休眠,而且用户程序什么时候被唤醒也无法控制,程序休眠期间什么都不能做。于是阻塞带来了大量的休眠等待时间。如果程序把大量工夫花在阻塞期待 IO 上,天然 IO 效率低下,进而导致程序性能受影响。

上下文切换

上下文切换是操作系统的基本概念。内核运行在零碎态,用户程序运行在用户态,这么做次要是处于平安的思考,限度用户程序的权限。用户程序调用零碎调用执行 IO 操作,会产生上下文切换,比方用户程序调用 read 零碎调用来读取数据,用户程序的上下文被保存起来,而后切换到内核态执行 read 零碎调用,当 read 零碎调用执行结束,再切换回用户程序。

上下文切换的代价不小,一方面内核要保留上下文现场,另一方面 CPU 的流水线也会被上下文切换打断,须要从新加载指令。上下文切换等同一次中断操作,于是零碎调用也被称软中断。频繁的上下文操作对计算机系统带来很大的开销,导致程序执行效率大大降低,进而极大影响程序的性能。

此外,阻塞的时候,肯定会产生上下文切换。还是沿用 read 操作的例子,用户程序在调用 read 零碎调用后,内核发现要读的数据没有命中缓存,那用户程序会被强制休眠导致阻塞,内核调度其余程序来运行。然而上下文切换的时候不肯定有阻塞。比方 read 操作的时候,如果用户程序在调用 read 零碎调用之后,缓存命中,则 read 零碎调用胜利返回,把该用户程序的上下文切换回来持续运行。

内存拷贝

内存拷贝对程序性能的影响次要源于内存的访问速度跟不上 CPU 的速度,加之内存的拜访带宽无限,亦称内存墙(Memory Wall)。

当初的 CPU 频率都是几个 GHz,每个 CPU 指令的执行工夫在纳秒量级。然而 CPU 拜访内存要花很多工夫,因为 CPU 收回读取内存的指令后,不是马上能拿到数据,要等一段时间。CPU 拜访内存的提早大概在几十纳秒量级,比 CPU 指令的执行工夫差不多慢一个数量级。

再者,内存的拜访带宽也是无限的,DDR4 内存总的带宽大概几十 GB 每秒。尽管看着不小,然而每个程序在运行时都要拜访内存,不论是加载程序指令,执行计算操作,还是执行 IO 操作,都须要拜访内存。

当产生内存拷贝时,CPU 把数据从内存读出来,再写到内存另外的中央。因为内存拜访提早比 CPU 指令执行工夫慢很多,再加上内存带宽无限,于是 CPU 也不是随时能拜访内存,CPU 的访存指令会在 DDR 控制器的队列里排队。因而内存拷贝对于 CPU 来说是很花工夫的操作,数据没有从内存读出来就不能执行后续写入操作,导致大量 CPU 期待,使得程序性能降落。

如何实现高性能 IO

针对下面提到的三种影响 IO 性能的因素,上面举三个例子,Rust Async,io_uring 和 RDMA,别离来介绍如何解决这三种影响程序性能的 IO 问题。

Rust Async

Rust Async 异步编程通过协程、waker 机制,局部解决了阻塞和上下文切换的问题。

首先,Rust Async 采纳协程机制,在某个异步工作被阻塞后,自行切换执行下一个异步工作,一方面防止了工作线程被阻塞,另一方面也防止了工作线程被内核上下文切换。Rust Async 底层依附操作系统的异步机制,比方 Linux 的 epoll 机制,来告诉 IO 是否实现,进而唤醒 waker 来调度异步工作。

然而,Rust Async 依然有阻塞。Rust Async 里工作线程没有被阻塞,不过被阻塞的是 waker,所以 Rust Async 是把阻塞从工作线程搬到了 waker 身上。

此外,Rust Async 无奈防止上下文切换。Rust Async 采纳 Reactor 的 IO 形式:比方用户程序要读取数据,发动 read 异步工作,假设该工作被阻塞放到期待队列,当该工作要读取的数据被内核筹备好之后,该工作被唤醒,持续调用 read 零碎调用把数据从内核里读到用户内存空间,这次 read 零碎调用因为要读的数据曾经被内核加载到零碎态内存里,所以不会产生阻塞,然而 read 零碎调用还会有上下文切换。

Rust Async 运行在用户态,而阻塞和上下文切换是操作系统内核决定的。要想进一步防止阻塞和上下文切换,就得在内核上做文章。

io_uring

io_uring 是 Linux 提供的原生异步接口,不仅反对异步 IO,还能够反对异步零碎调用。io_uring 在内核与用户程序之间建设发送队列 SQ 和实现队列 CQ,用户程序把 IO 申请通过 SQ 发给内核,而后内核把 IO 执行结束的音讯通过 CQ 发给用户程序。采纳 io_uring,一方面防止了阻塞,另一方面也防止了上下文切换。

io_uring 采纳 Proactor 的 IO 形式,Proactor 是绝对 Reactor 而言。比方用户程序采纳 io_uring 来读取数据,先把 read 申请放到发送队列 SQ,而后用户程序能够去执行其余工作,或者定期轮询实现队列 CQ(当然用户程序也能够抉择休眠被异步唤醒,但这样就会有上下文切换,不过这个上下文切换是用户程序自行抉择的)。IO 实现的时候,io_uring 会把用户程序要读的数据加载到 read 申请里的 buffer,而后 io_uring 在 CQ 里放入实现音讯,告诉用户程序 IO 实现。这样当用户程序收到 CQ 里的实现音讯后,能够间接应用 read 申请 buffer 里的数据,而不须要再调用 read 零碎调用来加载数据。

所以 io_uring 通过内核的反对,能够实现无阻塞和无上下文切换,进一步晋升了 IO 的性能。然而 io_uring 还无奈防止内存拷贝,比方 read 操作的时候,数据是先从 IO 设施读到内核空间的内存里,而后内核空间的数据再在复制到用户空间的内存。内核这么做是出于平安和简化 IO 的思考。然而要想防止内存拷贝,那就得实现内核旁路(kernel bypass),防止内核参加 IO。

RDMA

RDMA 是罕用于超算核心、高端存储等畛域的高性能网络计划。RDMA 须要非凡的网卡反对,RDMA 网卡具备 DMA 性能,能够实现 RDMA 网卡间接拜访用户态内存空间。在 RDMA 网卡和用户态内存之间的数据传输(即数据通路),齐全不须要 CPU 参加,更无需内核参加。用户程序通过 RDMA 传输数据时,是调用 RDMA 的用户态 library 接口,而后间接和 RDMA 网卡打交道。所以 RDMA 传输数据的整个数据通路是在用户态实现,没有内核参加,既没有阻塞也没有上下文切换,更没有内存拷贝,因而采纳 RDMA 能够取得十分好的网络 IO 性能。

尽管 RDMA 通过内核旁路防止了阻塞、上下文切换和内存拷贝,实现了高性能网络 IO,然而 RDMA 也是有代价的。首先,RDMA 编程复杂度大大提高,RDMA 有本人的编程接口,不再采纳内核提供的 socket 接口,RDMA 的接口偏底层,而且调试不够敌对。另外,用户程序采纳 RDMA 之后要自行治理内存,保障内存平安,防止竞争拜访:比方用户要通过 RDMA 网路发送数据,在数据没有发送实现前,用户程序要保障寄存待发送数据的用户内存空间不能被批改,不然会导致发送的数据谬误,而且即使用户程序在曾经开始发送但还没有发送实现前批改了待发送的数据,RDMA 也不会报错,因为 RDMA 的用户态 library 也无法控制用户程序的内存空间来保证数据一致性。这极大地减少了用户程序的开发难度。比照内核执行写操作,用户程序调用 write 零碎调用之后,内核把待写的数据先缓存在内核空间的内存,而后就能够告诉用户程序写操作实现,回头内核再把写数据写入设施。尽管内核有做内存拷贝,然而保障了数据一致性,,也升高了用户程序执行 IO 操作的开发复杂度。

咱们的尝试

DatenLord 的指标是打造高性能分布式存储系统,咱们在开发 DatenLord 的过程中,始终在摸索如何实现高性能 IO。

尽管 Rust Async 异步编程理念十分不错,用相似同步 IO 语意实现异步 IO。然而咱们认为 Rust Async 更多是异步 IO 的编程框架,还称不上是高性能 IO 框架。于是咱们尝试把 Rust Async 跟 io_uring 和 RDMA 联合,以实现高性能 IO。

首先,Rust Async 与 io_uring 的联合工作,尽管 Rust 社区在这方面也有不少相似的尝试,然而咱们的重点是如何在 io_uring 执行异步 IO 的时候防止内存拷贝,这方面 Rust 社区的工作还很少。咱们尝试采纳 Rust 的 ownership 机制来避免用户程序批改提交给 io_uring 用于执行 IO 操作的用户态内存,一方面防止内存拷贝,一方面保障内存平安。感兴趣的敌人能够看下 ring-io

另外,咱们也在尝试联合 Rust Async 与 RDMA,这方面 Rust 社区的工作不多。RDMA 性能虽好,然而开发复杂度很大,而且调试不敌对。咱们尝试采纳 Rust friendly 的形式来实现 RDMA 接口的异步封装,同时解决 RDMA 程序须要开发者自行治理内存的问题,从而大大降低采纳 Rust 开发 RDMA 程序的难度。感兴趣的敌人能够看下 async-rdma

最初,欢送对高性能 IO 感兴趣的敌人们分割咱们,跟咱们一起交换探讨。

咱们的联系方式:dev@datenlord.io

退出移动版