作者: 王璞
长期以来,计算机系统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