hadoop集群上捉到linux kernel bug一个
(谨以此文感谢淘宝hadoop运维团队柯旻、刘毅、沈洪等同学对我的帮助和支持)
5月份和公司的hadoop团队一起研究在hadoop集群上的linux kernel优化,主要起因是hadoop集群测试发现rhel6的2.6.32内核跑hadoop应用比rhel5的2.6.18慢,测试方法是跑terasort。
通过blktrace查看2.6.32内核的io情况,发现一个奇怪的现象:明明系统的readahead大小设的是1MB,但是对hadoop中间文件file.out的每次readahead只读52K就停下来了。而在2.6.18内核上没有这个问题。对于像hadoop这样的“大读大写”的环境,readahead对性能的帮助很大,所以,如果readahead莫名其妙的停下来,对性能就是一种损伤。
52K,多奇怪,正好是13个4K的page,内核里有啥地方对13个page有特殊处理呢?....想不出来,于是漫长的debug开始了。
先看了一下readahead的实现,发现只要在预读过程中发现有一个page已经在内存中,就会停下来,也就是说,这个巨大的1.1G的file.out文件,每隔13个page就会有一个page在内存中,再说白一点:page1~page13不在内存中,page14在,page15~page27不在内存中,page28在....依此类推。
怎么会形成这个样子?一般来说,如果内存充足,一个文件在第一遍被读(或者被写)以后,对应的page会被放入lru链表里,且每个page会被置为REFERENCED;如果再读一遍这个文件,则page会被进一步置为ACTIVE。当系统内存不足时,OS会去回收cache里的page,哪些page先被回收呢?当然是只被读过一遍的那些REFERENCED的page了,因为它没有ACTIVE的page那么“热”。
从这个角度分析,hadoop里这个1.1G的file.out对应的page很可能有的是REFERENCED,有的是ACTIVE,不均匀,所以当系统回收page后,file.out有些page还留在内存,有些就不在了。发生这种情况有可能是有的进程反复读了file.out文件的某些部分,造成有些page热,有些page冷。问了hadoop团队的达人们,确实不存在这样的应用,hadoop基本就是顺序读顺序写,很规整的。也是,哪个应用会每隔13个page读一个page呢?而且,2.6.18跑同样的terasort不就没这个问题嘛,应该不是应用而是kernel的问题。
不得已,debug进入最艰苦的阶段:在kernel代码里加trace_printk一步步跟踪每个page的状态的变化。搞了3天,终于找到了原因:确实是2.6.32 kernel在把page置为REFERENCED和ACTIVE时逻辑不够严密。
我们在write时,实际的kernel代码走到了generic_perform_write(),里面会找到write对应的那个page,然后
write_begin
mark_page_accessed(page)
copy content into page
write_end
其中mark_page_accessed就是把page置为REFERENCED(对已经REFERENCED的置为ACTIVE)。
好,假设现在用户的write是每次写2K,那么对同一个page,会调用两次generic_perform_write():
write_begin
mark_page_accessed(page)
copy content into page
write_end
write_begin
mark_page_accessed(page)
copy content into page
write_end
看,这样写完一个文件后,每个page都被mark_page_accessed了两次,也就是说都成了ACTIVE。在2.6.18 kernel里,确实是这样;但在2.6.32 kernel里,不是这样!因为有人嫌每个page挨个儿进全局lru链表太麻烦了,改为用一个pagevec先把page存着(这个pagevec是放在cpu的cacheline里的,所以访问速度理论上来说比内存快),存满14个再一起放入全局lru链表。但是这个“优化”有漏洞:page先进的是pagevec而不是lru,结果mark_page_accessed就不再把它置为ACTIVE了:
void mark_page_accessed(struct page *page)
{
if (!PageActive(page) && !PageUnevictable(page) &&
PageReferenced(page) && PageLRU(page)) {
activate_page(page);
ClearPageReferenced(page);
} else if (!PageReferenced(page)) {
SetPageReferenced(page);
}
}
PageLRU(page)这时返回的是false,所以不会activate_page,结果前13个page虽然经历了两次mark_page_accessed,但都只是置为REFERENCED,而第14个page进pagevec时,pagevec就满了,这14个page都通通放入全局lru链表,这时再mark_page_accessed第14个page,第14个page就变成ACTIVE了!大家都是平等被访问,第14个page却总被置成”更热“。
fix这个问题的方案非常简单,就是不要让第14个page有特权,而是让它在被mark_page_accessed之前就一直呆在pagevec,别去全局lru链表。我已经发了一个patch到社区,响应还不错。
我们自己已经把这个补丁收入了淘宝kernel,在hadoop上再跑terasort,2.6.32终于快过了2.6.18内核,快了5%左右。
这个bug前后定位了两个星期。如果我们对kernel代码更熟悉一些,一看到14这个数字就想到pagevec是存14个page的,那问题定位就快多了。所以,还是要勤学多练啊 :)
------ 2013.1.11 ------
高强:预读碰到uptodate的页为什么会停下来啊 我在2.6.32的代码里好像没发现这个逻辑
答:感谢高强同学的提问,“readahead碰到已经在内存中的page会停下来”这个说法是有问题的,不是停下来,而是一个连续的IO被拆成了两个IO,中间正好隔一个page。代码路径:
page_cache_sync_readahead()
--> ondemand_readahead()
--> ra_submit()
--> __do_page_cache_readahead()
在__do_page_cache_readahead()里把要readahead的页放入page_pool里,如果此时有个page已经在radix_tree里了,那么page_pool里放的连续页就断开了一个page,当调用readpages来处理page_pool时,就没办法用一个io_submit来处理所有页,只能分为两个IO,这才是对性能有影响的关键。
相关文章
- 解block层死锁 - 12 19, 2012
- EMC USD组 招聘 - 09 24, 2012
- hadoop与午睡 - 06 11, 2012
赞啊,当时我们Kbuild运行2.6.32内核上很不稳定,你们终于找到原因啦~
不知您所说的“不稳定”也是指IO速率吗?
有点不太理解请教一下:
“看,这样写完一个文件后,每个page都被makr_page_accessed了两次,也就是说都成了ACTIVE。在2.6.18 kernel里,确实是这样;”
这说明这些page应该是active的,可是后面的修改:
“fix这个问题的方案非常简单,就是不要让第14个page有特权,而是让它在被mark_page_accessed之前就一直呆在pagevec,别去全局lru链表。”
这样这些page不都成REFERENCED的了么
对了,歪个楼,请教下你们现在hadoop线上系统的readahead大小一般设置多大?
@Liang xie readahead大小与底层设备的io速度有关,系统默认的是128KB,可以调整/sys/class/bdi//read_ahead_kb进而调整预读大小.
@casualfish,呵呵这两点我倒是知道,因为在DB领域这个经常会系统初始化阶段就直接调小到16/32k。 我其实是想知道对于hadoop场景一般是啥推荐经验值,因为我完全没hadoop经验。。。
@Liang xie,DB对大文件是随机读,所以readahead没有意义;hadoop则刚好相反,典型的顺序写顺序读,readahead很有用,所以,尽可以调大,当然,别到4MB,8MB那么夸张就行。
帅锅,IIUC这个问题的根源是pagevec破坏了active page的公平性,以及readahead window遇到已经存在的page就停下。看了下你的patch,如果在page 14两次mark_page_accessed()之间,有别的page需要加入到LRU,那page 14还是会被置成active。可以想象的场景是,你的test case在多进程并发写的场景下,vm还是出现少量独立的active page的情况。
Bergwolf同学洞察玄机,拜服了!
弱弱的问一下, 楼主怎么监测系统此时此刻预读了多少K的?systemtap? blktrace?
我也有这方面的需求,找了半天没找到方法,我比较菜啦,
楼主赐教啊
我是用的blktrace,当然,调试是通过在kernel代码里加trace_printk....
blktrace 能直接得到 系统预读了多少k么? 还是有什么间接的方式?上周五看到你说用blktrace 发现预读不正常,我就开始琢磨这东西,到现在也没发现blktrace有能看系统预读多少的功能,难道理解歪了?
@卢琪,blktrace不能直接看出预读了多少,是通过它提供的信息,再分析出来的。如果一个blktrace或者调试工具就能看透一切,我们这些工程师就要失业了 _
看了patch的邮件,重现的方法,能这么简洁,很棒。依据的是hadoop的顺序读特点吧,排除了hadoop的运行环境依赖,直接转为IO读写。
预读碰到uptodate的页为什么会停下来啊 我在2.6.32的代码里好像没发现这个逻辑
"看,这样写完一个文件后,每个page都被makr_page_accessed了两次"中的"makr_page_accessed"===>"mark_page_accessed"
@awp47 非常感谢!已经改过来了
@高强 ,感谢您的提问,是我的说法有问题,已经补充在文章后面了