08 2012存档

首先感谢大家对原文的关注,能和大家一起交流内核技术我非常的荣幸,这里试着回答大家的提问。


超然台上仙:有点不太理解请教一下, “看,这样写完一个文件后,每个page都被makr_page_accessed了两次,也就是说都成了AC TIVE。在2.6.18 kernel里,确实是这样;” 这说明这些page应该是active的,可是后面的修改, “fix这个问题的方案非常简单,就是不要让第14个page有特权,而是让它在被mark_page_a ccessed之前就一直呆在pagevec,别去全局lru链表。” 这样这些page不都成REFERENCED的了么

答:是的,按我的patch的方案,这14个page在被write了两遍以后,都是REFERENCED了。
可能有人要说,文件写了两遍,应该page都是ACTIVE呀。道理上说是这样,但是,pagevec的引入已经造成了“两遍写之后page都是REFERENCED”这个即成事实(当然,还有不均匀的问题),所以我第一次在社区发出这个patch的第一版时,就有人回信说干脆把pagevec去掉得了,但毕竟去pagevec牵扯太多,可能带来的副作用也不好评估,于是最后社区还是决定先收我这个patch,至少先把这14个page弄“平等”,大家都是REFERENCED总好过“前13个REFERENCED后一个ACTIVE“。
等以后哪位内核牛人决定了最终去掉pagevec,我这个patch也就没有存在的必要了。到了那个时候,再也没有不平等的page,而文件在连续的写过(或读过)两遍之后,page们就都是ACTIVE了。


超然台上仙:2.6.18 kernel也同样使用了pagevec,__grab_cache_page中代码意思与32版本是一样的,似乎与文中描述不符

答:你看得非常仔细,我去翻了一下code,发现在upstream的2.6.18内核里,generic_file_buffered_write的逻辑是:

__grab_cache_page()
prepare_write (对应2.6.32内核里的write_begin)
copy content into page
commit_write (对应2.6.32内核里的write_end)
mark_page_accessed(page)

在__grab_cache_page里确实是把page先放入了pagevec,所以,一样会出现page冷热不均的情况(我的patch里有复现这个问题的方法,大家可以试一试)。
但是,redhat的2.6.18是打了补丁的(我上文的2.6.18都是指redhat的),打过之后,__grab_cache_page变成了grab_cache_page_write_begin,这里面就没有用pagevec,而是直接加入全局lru链表了。很可能redhat已经发现了pagevec会带来副作用。
奇怪的是,redhat如果知道了pagevec引入的这个问题,为什么在新的rhel6里没有fix?....这个就不得而知了。


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

答:我才发现,确实如你所说!如果用户是用2K的buffer来write且用多进程,那么即使是我的patch也不能完全避免page冷热不均。只是在hadoop上大文件是一次写成,一个page不会被两次mark_page_accessed,也就不会触发而已。
Bergwolf同学果然洞察玄机,拜服了!

(谨以此文感谢淘宝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,这才是对性能有影响的关键。

故人

| | Comments (0) | TrackBacks (0)
无意中在网上找到这个采访视频:


这个之前在SUN就职的张玉昆先生就是05年我去SUN面试实习生时,面我的面试官。

一开始看视频的时候,还不太确认,毕竟事隔7年,哪还有那么明确的印象,但是一听他说话,想起来了:没错,这语气、这口音,就是他。

当时我没面上SUN的实习生,转去别的地方了。时光荏苒,SUN被收购了,张玉昆先生也自己出来创业了。大家都还过得不错,挺好。


7年了....那些有过一面之缘的朋友们,你们现在过得还好吗?
早上子团来询问我 kernel.taobao.org 的那个wiki是怎么搭的,我于是帮他拷了一个Mediawiki。Mediawiki的安装很方便,所以出来安装提示页面后,我就让子团自己填配置了。

过了一会儿。

子团:为什么我配置完成后,打开首页,只有一朵菊花....

我听着觉得怎么这么搞笑。打开看了一下,嘿,我怎么从来没注意到mediawiki的这个logo还真是一朵菊花咧。

mediawiki


整个下午,我一想起子团的话就觉得乐不可支。

关于存档

This page is an archive of entries from 08 2012 listed from newest to oldest.

06 2012 is the previous archive.

09 2012 is the next archive.

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