写mmap内存变慢的原因

有很多系统读写大文件时用的是这个办法:将大文件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内存的写操作。


=== 2012.2.29 ===

Bergwolf:
nitpicky一下:) writeback的时候page只是会被mark成PG_WRITEBACK,而不是被lock住。所以page_mkwrite的等待很可能是在wait_for_page_writeback上而不是lock_page上。

RobinDong: 
我看了一下upstream的代码,以ext4文件系统的mmap为例,writeback时,write_cache_pages里先lock_page(mm/page-writeback.c 2241行),再调用ext4_writepage来写单个页面,ext4_writepage里调用block_write_full_page来把页面mark成PG_WRITEBACK。我这里看来,是先lock后mark。
另外,在我们这边的2.6.32老内核里,__block_page_mkwrite里没有调用wait_on_page_writeback,这个是去年五月份才加进去的。所以,2.6.32 mmap变慢的原因应该还是lock_page的争抢。

Bergwolf:
page_mkwirte应该只在page第一次被写的时候调用,线上应用的page应该都是在内存中了,为什么你们认为page_mkwrite和writeback有竞争呢?你的测试代码是新建一个sparse文件,有试过对一个已经在内存中的dense文件测试吗?

RobinDong:
我们用的就是已经在内存中的dense文件,之前我也以为只有page第一次进入内存时才有do_page_fault才有page_mkwrite,但是马涛哥颠覆了我的世界观——writeback完成后的page,在被写的时候就会调用page_mkwrite——代码路径我们今早也找到了。
在write_cache_pages里,lock_page以后会调用clear_page_dirty_for_io(page),然后一路

--> clear_page_dirty_for_io(page)
--> page_mkclean(page)
--> page_mkclean_file(page)
--> page_mkclean_one(page)

下来,一直到page_mkclean_one(page),里面会做pte_wrprotect(entry),也就是把pte置为“写保护”,这样做是故意的,当应用写这个page时,就会因为碰了“写保护”页面而触发do_page_fault,即使这个page已经是在内存中了。

-->do_page_fault
--> handle_mm_fault
--> handle_pte_fault

handle_pte_fault这段代码:

        if (flags & FAULT_FLAG_WRITE) {
                if (!pte_write(entry))
                        return do_wp_page(mm, vma, address,
                                        pte, pmd, ptl, entry);
                entry = pte_mkdirty(entry);
        }

如果用户是在尝试写页面(FAULT_FLAG_WRITE),且pte是写保护的(!pte_write(entry)),那么就调用do_wp_page,而do_wp_page里面对可写且共享的vma里的page,会依次调用page_mkwrite。


相关文章

分类

24 Comments

谢良 said:

Orz,这个blog解释的来龙去脉好清楚,连我这种kernel小白也能看的相当流畅,呵呵:)

不知道Fengguang当初是怎么能那么快就定位出这个细节并fix的?

Ps:patch的链接外面的人访问不了貌似?

passenger said:

????一下, 您所描述的???}在Debian 6 (2.6.32)下也???l生???

DongHao Author Profile Page said:

@passenger, 我没有测过debian,但是考虑到Fengguang同学的patch还没有进upstream,我猜测debian 6的2.6.32很可能也有同样的问题。您可以用mmap_stress测一测,看看io util是不是一直很高。

壮壮粑粑 said:

我想起去年测过这个问题,不同的是当时我是写了一个内核模块来测的。大规模的mmap,应该是搜索应用吧?呵呵

DongHao Author Profile Page said:

@壮壮粑粑, 搜索是比较典型的用mmap的应用,但是不只,还有很多应用也是如此 :)

DongHao Author Profile Page said:

@谢良,Fengguang对writeback肯定是了如指掌,又有可以快速复现问题的程序,所以应该是比较快fix的,但是社区不见得会收这个patch,我估计社区会说:“如果你不想让writeback发生,那就直接用共享内存或ramfs好了,干嘛偏要用硬盘文件的mmap?“。

另外,那个patch之前确实外部无法访问,我改了链接,现在应该可以了。

bregwolf said:

nitpicky一下:)

writeback的时候page只是会被mark成PG_WRITEBACK,而不是被lock住。所以page_mkwrite的等待很可能是在wait_for_page_writeback上而不是lock_page上。

DongHao Author Profile Page said:

欢迎 @Bergwolf (http://weibo.com/bergwolf)哥来访 !,可惜不是坐在一个办公室没法当面交流,但我有很多话要说,这样吧,我把你的comment附到正文上去,再在正文里回复。当然,如果你还发现了漏洞,欢迎继续comment :)

bergwolf said:

@DongHao 恩,是我没有看仔细。我原本认为在writeback的时候lock_page的时间相比非常短,所以我觉得在lock_page上冲突的概率比较小。但是我没有注意到你的testcase是写一个256MB的文件。对小文件的密集随机写,lock_page冲突还是很可能的。

另外我还有一个疑问,page_mkwirte应该只在page第一次被写的时候调用,线上应用的page应该都是在内存中了,为什么你们的认为page_mkwrite和writeback有竞争呢?你的测试代码是新建一个sparse文件,有试过对一个已经在内存中的dense文件测试吗?

DongHao Author Profile Page said:

@Bergwolf,“page_mkwirte应该只在page第一次被写的时候调用,线上应用的page应该都是在内存中了,为什么你们的认为page_mkwrite和writeback有竞争呢”,你问到点子上了,这也是我之前的问题,我测试用的就是一直在内存中的dense文件,既然都在内存中,怎么会反复的调用page_mkwrite?马涛哥的回答是:只要page不是dirty,在写page时就会触发page_mkwrite。说实话,我的世界观被颠覆了,我一直以为只有page不在内存中时,才会触发do_page_fault,然后page_mkwrite。

我翻了一下代码,暂时还没找到马涛哥所说的逻辑对应的代码,我再继续找找,找到了再分享一下。

sunzixun said:

dirty_writeback_centisecs 有试过么 ?per bdi thread会去schedule()。

Bergwolf said:

@DongHao 我的世界观也被颠覆了:)赞万能的马涛哥,学习了,多谢分享~~

DongHao Author Profile Page said:

@sunzixun,这个参数我们试过,dirty_writeback_* 相关的几乎所有参数我们都试过,但是如果没有峰光的patch,这些参数的调整都解决不了问题。因为一旦writeback开始转着圈的扫描文件脏页,它就不会理睬这些参数,“转圈”这个是没参数配的。

sunzixun said:

pte_wrprotect 这一招比较意思啊。。

lz 开始对 dirty_expire_centisecs
理解有误,它是说最后的情况,而不是仅有的

sunzixun said:

我明白你的情况了 。谢谢

DongHao Author Profile Page said:

@sunzixun, "dirty_expire_centisecs 理解有误,它是说最后的情况,而不是仅有的",噢,请问正确的理解是?

yt said:

楼主,能否更新下链接,不能用?

DongHao Author Profile Page said:

@yt,你指的是哪个链接?我试了一下,都是好的呀

casualfish said:

看来2.6.32内核中引进的per-bdi-writeback引发的性能回退还是很普遍的,我在最近的工作中也遇到产品写性能下降的问题,最终也定位到这里:http://hi.baidu.com/casualfish/item/8bd452a6463a83268919d39e
正如你所说,“转圈”写回是问题的根源,吴峰光的io限流功能patch(3.2内核中已经merge了这个patch)只是减小了flush的频率,也没有完全解决这个问题。
“如果你不想让writeback发生,那就直接用共享内存或ramfs好了,干嘛偏要用硬盘文件的mmap?",不只是mmap,频繁的写入和修改文件同样会引发性能的问题,这点在目前只能通过调整用户空间的文件写入策略来规避了。

casualfish said:

lz说的去年五月份加进去的功能应该是stable pages,Ted Ts'o merge了这个功能:http://thread.gmane.org/gmane.linux.kernel.mm/62764/focus=63383,
lwn上有对应的介绍:http://lwn.net/Articles/486311/
有意思的是,提出这个patch引起性能回退的也是Ted Ts'o:http://thread.gmane.org/gmane.linux.file-systems/62226
个人觉得还是callback的实现模式比较好,不用block进程而且比较轻量。

frostwatcher said:

我这也有个类似的内存映射的问题请教您。在redhat 2.6.18内核4G物理内存环境下,用mmap映射一块内存空间做bloom过滤,一个bloom查找会将这块内存中22个bit位置为1。开始映射一个1G大小的内存10万次查找耗时很少,但当将映射内存变为2G后,同样进行10万次查找耗时却明显增加,查看io util为100%,我在更高内存的机器上(16G内存)测试得出同样的结论:只要映射的内存变大则耗时就会急剧增加,请帮忙分析下原因,谢谢!

DongHao Author Profile Page said:

io util 100%,那我猜就只能是writeback在写硬盘了,你可以试一试

echo 0 > /proc/sys/vm/flush_mmap_pages

这是rhel5的一个trick,设成0以后,mmap的内存即使脏了也不会回写。这样做有一点风险,就是如果突然宕机,内存里的新数据就丢了。你可以写个crontab,在系统压力小的时候做一下sync。

还有一个办法就是把你要改的那些bit凑在一起(而不是分布在mmap内存的不同地方),这样回写就是连续写磁盘,压力也会好很多。

ruochen said:

我想问下,你们都是在ext3下测试的么? rhel6默认都是ext4了吧,ext4有同样问题么?

DongHao Author Profile Page said:

这是writeback的机制造成的,跟文件系统没有关系,如果writeback回写得太频繁,你用什么文件系统都有一样的问题

留言:

关于文章

This page contains a single entry by DongHao published on 02 27, 2012 4:17 PM.

ext4 bigalloc + inline_data的fsck速度 was the previous entry in this blog.

办公室里 is the next entry in this blog.

Find recent content on the main index or look in the 存档 to find all content.