Bootstrap

linux 关于aio存在的一些问题

转自http://www.wzxue.com/linux-kernel-aio这个奇葩/
1. 动机

Asynchronous I/O帮助用户程序提高CPU和IO设备的利用率和提高程序性能,特别是在高负载的IO操作下。比如各种代理服务器,数据库,流服务器等等。

2. 什么是AIO

很多人会将AIO理解成磁盘IO的异步方案,会将AIO狭隘化为类epoll接口在磁盘IO的特殊化,其实AIO应该是横架于整个内核的接口,它把所有的IO包括(本地设备,网络,管道等)以统一的异步接口提供给用户程序,每个子系统都针对接口实现自己的异步方案,而同步IO(Synchronous IO)只是在内核内部的”AIO+Blocking”.

QQ20130504-1

3. AIO的设计方案
  • 统一的IO方案,来代替目前现存的不同事件监听方式,并且更加高效
  • 更清晰的接口和使用方法
  • 多个子系统(块设备,网络模块)可以更好的优化
  • 把事件注册和通知方法分离(相反的是信号IO方案)
  • 强化内核的No-Copy特征
4. 替代方案
  • Glibc
  • 信号IO: 这个方案就不说了,除了实时外没啥优点,信号机制在操作系统中本来就是个被过度设计的典范。信号IO不仅在大量IO操作时可能会因为信号队列溢出导致没法通知,还有糟糕的性能也使得这东西没法用。
5. AIO可以做的事

如Kernel关于AIO的文档中所说,AIO可以一次性发出大量的read/write调用并且通过通用块层的IO调度来获得更好的性能,用户程序也可以减少过多的同步负载,还可以在业务逻辑中更灵活的进行并发控制和负载均衡。
另外相对于其他实现如用户多线程后台同步,Glibc等实现也减少了线程的负载和上下文切换。

6. AIO实现的几个问题
  • AIO路径的资源竞争问题:AIO可能阻塞在资源的请求上如内存,请求队列slot,inode semaphore

  • AIO读写操作的调度序列:设计问题类似于目前流行的系统架构方案的权衡,目标是并发最大化,延迟最小化,但每个AIO读写操作到底优先选择何种方式进行,在请求队列中是按FIFO,优先级队列还是根据现在POSIX标准读请求优于写请求。由于目前的内核AIO方案仍然是有阻塞的,无法在有阻塞的情况下选择更好的调度方案,更好的是去做一些经验性的临时考虑。

  • 操作完成的唤醒方案:是不是需要实时的signal,如果多个线程同时监听如何避免线程“饿死”和最小化唤醒的负载

  • AIO与预读窗口的问题: 还包括sendfile的AIO实现

  • 与epoll结合的问题: 如Linux kernel的eventfd这个新的API可以为epoll和AIO提供好的整合,这篇《linux异步IO编程实例分析》阐述了结合的方法。(还有signalfd和timerfd都是类似的场景使用)

  • Filesystem/Buffered AIO: Buffered IO与Direct IO的资源冲突和数据完整问题

7. AIO实现方案

QQ20130504-2

上图显示的Sync IO在读写时的阻塞点,AIO试图在阻塞点采取一些异步来达到不阻塞的效果。

在03年,Suparna Bhattacharya提出了AIO在Linux kernel的设计方案,里面谈到了用Full async state machine模型来避免阻塞,把一系列的阻塞点用状态机来驱动,把用户态的buffer映射到内核来驱动(举个简单的例子,就是Memcached的网络IO状态机)。这个模型被应用到Linux kernel 2.4.

Linux kernel 2.6与2.4是两个不同的实现模型,2.4的模型虽然达到了很好的非阻塞效果,但与Sync IO采用两条不同的代码路径并且非常复杂难以Debug(更多的原因?)。

fig1

图来自[Linux Asynchronous I/O Design: Evolution & Challenges](http://ftp.sunet.se/pub/os/Linux/kernel.org/linux/kernel/people/suparna/aio-linux.pdf)

Linux kernel 2.6使用的Retry based模型,也就是把同步IO的阻塞点全部exit and retry并且在caller的地址空间上进行。并且早已经把Sync IO迁移到AIO的代码路径上,也就是说,现在Sync IO使用的就是AIO+blocking。

这里的最大问题是如何访问task struct,众所周知,系统调用利用current宏访问task struct,但是异步就导致后续的retry无法再次访问之前的task struct,因此现在的AIO将接口分为提交和执行两阶段,提交阶段能访问current但是会block,然后执行阶段就可以无需访问current,最后的结果就造成大量重复的代码和函数重复,由于current的访问问题。这无疑是内核是无法接受的结果,就如果Linux kernel 2.4中的问题一样。

在07年由于Ingo Molnar躲开了乱摊子的AIO项目,Zach Brown试图重新打开局面,迁移了大量的aio.c的实现到sync io,并且提出了syslet方案,也就是使用内核线程让大部分阻塞点异步,将Sync IO和Async IO代码路径统一,见《The return of syslets》,syslet利用了系统调度的tricks,如果异步IO线程阻塞,内核调度就会转移到其他线程,这样使得如果不阻塞的系统调用仍然正常进行并且避免了不必要的额外线程,而阻塞的调用会转移到其他线程上。syslet的问题就是会改变调用进程的ID,因为它需要去让另一个内核线程访问current,这个改变会让内核理解变得晦涩也是无法接受的。在07年后,syslet方案就已经寸步难行了。更多的(syslet/threadlet/fibril)方案陆续涌现。

09年Zach又提出了一个妥协的方案《LCA: A new approach to asynchronous I/O》,其主要是利用内核线程池来接手每一个IO操作,这样使得IO操作能异步进行。总体来说,这个方案并没有什么新颖之处,Zach似乎只是为了做到POSIX feature和较好的代码统一,而且增加了性能负载。Linus也在presentation表示这个方案改变了太多AIO原先的实现和方案,更带来了新的问题。

好吧,先不管AIO的接口统一和能不能完全异步,为什么直到现在仍然只能使用Direct IO模式呢(目前AIO支持Ext2,Ext3,Ext4等文件系统)?

AIO+DIO绕过了VFS高速缓存固然让大的数据库系统如Innodb,DB2非常高兴,然是普通应用仍然无法尝试它,DIO的缓存绕过让每个想要AIO的普通应用都需要建立自己的缓存系统而不是依赖内核(不建立缓存的lighttpd已经证明DIO面对Sync Buffered IO仍有性能落差)。

在过去的几年了,大量的Filesystem/Buffered AIO补丁出现但一直没进入主线,主要原因还是实现的Buffered AIO仍然会增加Block points,比如write操作,很可能因为内存问题导致阻塞。而这对于AIO的设计目标是背离的,无法做到非阻塞就无法更好的利用AIO。如Make io_submit non-blocking很明显没有解决阻塞问题,只是单纯引入了Buffered IO,这显然不是内核想要的。并且io_submit仍旧存在阻塞(disk block的分配),更多的补丁试图在io_submit的上减少路径,更快的return。

还有就是Buffered IO会与DIO如何结合并且减少semaphore竞争和meta-data完整性问题《Linux AIO Performance and Robustness for
Enterprise Workloads》

Linux kernel 2.6时期的AIO的补丁如FSAIO,AIO-epoll,POSIX AIO enablement和Syslets & threadlets都是没能进入主线。

总的来说,FS/Buffered IO的实现非常多,但是在解决AIO阻塞点存在问题之前,任何的这类实现都是徒劳的。不要小看可能极少出现的阻塞情况,如果一个应用依赖于无阻塞的AIO,在AIO的内核态任何block的行为都会给应用带来灾难,如果应用在实现时就要防备内核的block那么就会给应用实现上带来额外的成本还不如不用AIO。

8. AIO现在

AIO这个项目似乎还在泥潭上,从commit log上看lib-aio还是一直在commit,并且表示性能每年都有50%+的提升,但是似乎还缺少一个大的动作,让AIO项目摆脱长达十年的阴霾。另外国内的Taobao内核似乎也在11年提交了Buffered IO的BIO补丁,不过好像没有针对用户线程阻塞点的部分?

社区对于AIO的io_submit阻塞问题仍旧停顿,从maillist可以得知,总是有用户对于io_submit有明显的block的抱怨,社区更希望用户能找出block位置被希望在User code上解决。

看起来AIO项目混乱,社区的roadmap和相关计划在整个Kenrel Document是找不到的。即使AIO目前是具备可用性和在“大应用”面前是有性能提升的,但是Normal App还是等等。。。。

9. 吐槽

其实刚开始我是打算写一篇AIO的概览和Linux kernel AIO的全局描述和局部特写,但是写着的时候,资料查着查着就发现AIO这个项目真的有点奇葩,从03年提供了AIO项目并且制订了设计方案,04年IBM的人觉得异步状态机的实现跟已存在的代码不协调并且太复杂,搞出了retry模型,非常高兴的在DB2上演示了性能。后来发现retry模型看起来很美,但是永远存在的block point无法解决弄得IBM的自己都不想整了,Oracle OSS又出来一个接管,然后又觉得retry模型有很多问题,表示retry & exit在他们的一个产品上会有很大性能问题,然后开始弄syslet方案,弄着弄着也崩溃了,留下社区一堆人大眼瞪小眼。

以上对具体问题的解释可能有问题,因为我也是最近才去了解Linux kernel AIO的路径(虽然以前对VFS比较熟悉),对一些遗留历史问题和目前的困境还在探索。对一些FS IO和Buffered IO等问题没有特别深入,这水有点深,文档也太少。

参考

;