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