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,这才是对性能有影响的关键。


相关文章

分类

18 Comments

赞啊,当时我们Kbuild运行2.6.32内核上很不稳定,你们终于找到原因啦~

DongHao Author Profile Page said:

不知您所说的“不稳定”也是指IO速率吗?

有点不太理解请教一下:
“看,这样写完一个文件后,每个page都被makr_page_accessed了两次,也就是说都成了ACTIVE。在2.6.18 kernel里,确实是这样;”

这说明这些page应该是active的,可是后面的修改:

“fix这个问题的方案非常简单,就是不要让第14个page有特权,而是让它在被mark_page_accessed之前就一直呆在pagevec,别去全局lru链表。”

这样这些page不都成REFERENCED的了么

Liang xie said:

对了,歪个楼,请教下你们现在hadoop线上系统的readahead大小一般设置多大?

casualfish said:

@Liang xie readahead大小与底层设备的io速度有关,系统默认的是128KB,可以调整/sys/class/bdi//read_ahead_kb进而调整预读大小.

Liang xie said:

@casualfish,呵呵这两点我倒是知道,因为在DB领域这个经常会系统初始化阶段就直接调小到16/32k。 我其实是想知道对于hadoop场景一般是啥推荐经验值,因为我完全没hadoop经验。。。

DongHao Author Profile Page said:

@Liang xie,DB对大文件是随机读,所以readahead没有意义;hadoop则刚好相反,典型的顺序写顺序读,readahead很有用,所以,尽可以调大,当然,别到4MB,8MB那么夸张就行。

Bergwolf said:

帅锅,IIUC这个问题的根源是pagevec破坏了active page的公平性,以及readahead window遇到已经存在的page就停下。看了下你的patch,如果在page 14两次mark_page_accessed()之间,有别的page需要加入到LRU,那page 14还是会被置成active。可以想象的场景是,你的test case在多进程并发写的场景下,vm还是出现少量独立的active page的情况。

DongHao Author Profile Page said:

Bergwolf同学洞察玄机,拜服了!

卢 琪 said:

弱弱的问一下, 楼主怎么监测系统此时此刻预读了多少K的?systemtap? blktrace?
我也有这方面的需求,找了半天没找到方法,我比较菜啦,
楼主赐教啊

DongHao Author Profile Page said:

我是用的blktrace,当然,调试是通过在kernel代码里加trace_printk....

卢 琪 said:

blktrace 能直接得到 系统预读了多少k么? 还是有什么间接的方式?上周五看到你说用blktrace 发现预读不正常,我就开始琢磨这东西,到现在也没发现blktrace有能看系统预读多少的功能,难道理解歪了?

DongHao Author Profile Page said:

@卢琪,blktrace不能直接看出预读了多少,是通过它提供的信息,再分析出来的。如果一个blktrace或者调试工具就能看透一切,我们这些工程师就要失业了 _

JayPerter said:

看了patch的邮件,重现的方法,能这么简洁,很棒。依据的是hadoop的顺序读特点吧,排除了hadoop的运行环境依赖,直接转为IO读写。

高强 said:

预读碰到uptodate的页为什么会停下来啊 我在2.6.32的代码里好像没发现这个逻辑

awp47 said:

"看,这样写完一个文件后,每个page都被makr_page_accessed了两次"中的"makr_page_accessed"===>"mark_page_accessed"

DongHao Author Profile Page said:

@awp47 非常感谢!已经改过来了

DongHao Author Profile Page said:

@高强 ,感谢您的提问,是我的说法有问题,已经补充在文章后面了

留言:

关于文章

This page contains a single entry by DongHao published on 08 30, 2012 4:15 PM.

故人 was the previous entry in this blog.

hadoop集群上捉到linux kernel bug一个 (答读者提问) is the next entry in this blog.

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