[原写于2012年2月]

有很多系统读写大文件时用的是这个办法:将大文件mmap到内存,然后直接对内存读写。这样就化read/write为memcpy操作,代码开发上很简便。被修改的内存页由kernel负责挑个时间写入硬盘,程序员不用操心。

但是,最近一些使用了taobao kernel(基于redhat6-2.6.32)的机器,上面那些使用mmap的应用变慢了。我们上线查看,才发现mmap文件里有很多脏页,kernel的writeback机制就不停的将这些脏页写往硬盘,结果造成了大量的io(从iostat看,除了几秒的间歇,io util几乎保持在100%),而如果换回2.6.18内核,就没有这个问题(io util不超过20%,而且很稀疏)。

为了简化应用模型,我们做了一个mmap_press程序模拟应用的写操作,内容很简单,就是mmap一块256MB的内存,然后在256MB的范围内随机的写,一次写8个字节,一共写25亿次,在rhel5(kernel-2.6.18)上,这个程序运行只需要374秒,而在我们的内核上,则要805秒才能完成,慢了两倍多。再换upstream的内核,一样慢,这个问题应该是一直有的。

看来自从writeback改成per bdi的以后,脏页写回的力度是大大加强了。加强是有原因的:越快写回脏页,页面新数据丢失的肯能就越少。但问题是,现在writeback的太频繁了,结果消耗了大量io,拖慢了应用。

能不能找个办法通知writeback子系统,让它每隔60秒或两分钟才开始写脏页,这样至少很多相邻的脏页能被合并成一次io,可以变大量小io为几个大io,速度会快很多。于是我们找到了这个参数 /proc/sys/vm/dirty_expire_centisecs ,默认值是3000,即30秒,也就是说,脏页要过了30秒才会被写往硬盘....等一等,这和我们观察到的完全不一样啊!?我们从iostat看到的是io util一直保持在100%,只有几秒的停歇,几秒啊,不是30秒。

多猜无益,writeback子系统可是相当大的一块,于是我们联系了Intel的吴峰光,他第二天就给出了两个patch,我们将其移植到2.6.32内核后(12),效果很明显,writeback不再是不停的制造io,而是5~6秒的集中io以后,就停下来大约30秒(这次符合dirty_expire_centisecs参数的默认值了),然后再开始5~6秒的集中io,如此循环。我们重新跑mmap_press程序,耗费时间是390秒,已经非常接近2.6.18的速度了。

感谢吴峰光同学的帮助。

大概看了一下吴峰光patch的注释:之前writeback在回写完一个文件后,会从头再查找一遍脏页,如果有脏页则继续回写;现在改成,回写到文件尾后,直接停下来,直到脏页expire(也就是30秒后了)再开始从头检查脏页并回写(这是我对patch的解释,肯定有纰漏之处,有兴趣的同学还是直接看一下patch)。原来如此,咱们的mmap操作有大量的随机写,产生了大量分散的脏页,writeback每次从头检查文件都发现脏页,结果每次都要从头开始回写,就这么不停的转着圈的回写,造成io几乎一直保持在100%。

但我还是有一个疑问:应用写内存只是一个内存操作,writeback写脏页只是一个硬盘操作,为什么硬盘操作会拖慢内存操作呢?最后万能的马涛同学给出了答案:应用对页面的写会触发内核的page_mkwrite操作,这个操作里面是会lock_page的,如果page正在被写往硬盘,那这时候它已经被writeback给lock了,page_mkwrite的lock_page会被阻塞,结果应用写内存的操作就顿住了,所以,越是频繁的writeback,越是会拖慢应用对mmap内存的写操作。