02 2012存档

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

先前我以为对文件系统来说,读写文件时跑得快才是关键,mkfs和fsck的速度不用操心。直到几天前,线上的运维同学反映:集群里某台(或者悲剧一点,某几台)机器如果宕机了,如果不能迅速重启,可能会对其它服务器带来额外的压力,因为外部流量还在,而“重启”过程包括了对突然断电的磁盘做fsck,所以,fsck的速度也很重要。

于是我找了个台式机(没办法,我们组的开发机服务器没有上T的硬盘),上面配了coly (@淘伯松)自己买的 2T 硬盘,测了一下ext4如果带上 nojournal + bigalloc + inline_data 后fsck的速度。

硬盘: 希捷Barracuda LP 2TB 5900转 32MB(ST32000542AS)
CPU:  Core 2 Duo E8400 3.00GHz 1333MHz FSB (2 cores)
内存: 2GB 800MHz DDR2
mkfs命令:mke2fs -m 0 -C $CLUSTER_SIZE -I 4096 -O ^has_journal,^resize_inode,^uninit_bg,extent,meta_bg,flex_bg,bigalloc,inline_data $DEVICE

用工具 dir_tree 程序创建树状目录结构和文件:

./dir_tree -m /test/ -d 7 -l 4 -n 5 -f 5 -S 64m -s 24576 -t cd
./dir_tree -m /test/ -d 7 -l 4 -n 5 -f 5 -S 64m -s 24576 -t cf

7个总目录,4层目录结构,每个目录下有5个子目录,每个文件64MB(这是某种程度上模仿hadoop的线上环境),总共5470个目录,21875个文件,大约占掉1.4T空间

用不同的cluster size来分别格盘并创建文件,看fsck -f运行的时间。测试结果如下:

cluster size (KB) NR of inode (inode table) time (seconds)
4         122093568         5484
64         30517408         1366
128         15258704         682
256         7629352 339
512         3815136 168
1024         1907352 84

中间这一列“NR of inode“就是mkfs完成后默认的总inode数,反映了inode table的大小。

从测试结果看,随着cluster size越来越大,fsck的速度越来越快。原因是大cluster占用的元数据更少——更少的block group,更少的inode table。
普通的ext4(就是不带bigalloc+inline_data)要90多分钟,而如果用1MB大小的cluster(bigalloc),则不到2分钟就检查结束了。

需求大概是这样:在一个linux系统上,想跑多个不同应用,这些应用由不同的运维来操作,为了避免互相干扰,希望运维只能看见自己的文件,而看不见别的应用的文件信息。一个常用解决办法就是干脆装多个虚拟机,但是,虚拟机对我们来说偏“重”,比如,多个应用公用的一些动态链接库(比如 libc.so)和配置文件(比如 hosts.conf)就复制了多份,如果原先一个系统在运行时系统文件占了500M的cache,那么现在装了4台虚拟机,就有2G的cache被重复占用了。

怎样才能让系统文件只占一份cache呢?我们首先想到这么个主意:把linux系统装到ext4上,然后做4个snapshot("快照“),这4个snapshot分别mount到4个目录,4个运维chroot到这4个目录里,然后就自己干自己的,干扰不到别人的文件。由于ext4 snapshot的实现机制是让同一个物理block被映射到不同的文件系统里,所以我们觉得,这一个4k的物理block应该就只占4k的cache。

(也许有人要说,这么费劲干嘛,直接把系统常用的动态链接库做4个软链接出来,给4个运维用不就行了?这样做有两个问题,第一,动态链接库以及各种系统文件很多,不可能一一做软链接;第二,也是关键的一点,如果其中一位运维错误操作,例如覆盖写了某个系统文件,那么其他的运维就歇菜了,因为软链接实际指向的是同一个实际文件。)

于是开始考察ext4的snapshot。ext4目前是没有snapshot功能的,但是Amir Goldstein已经开发好了对应的patch(https://github.com/amir73il/ext4-snapshots/),但是目前还没有被收入mainline。粗略看了一下,Amir的patch目前只支持readonly的snapshot,于是我发邮件问“如果改成writable snapshot,代码量大不大?“,Amir回帖表示代码量不大;另外还有别人回帖,推荐不用ext4而是用device mapper提供的thin provision的internal snapshot(http://kernelnewbies.org/Linux_3.2http://lwn.net/Articles/465740/),这样就不用依赖于某一个文件系统(就是如果咱们以后不用ext4了,也可以继续做snapshot)。

鉴于ext4 snapshot不支持writable snapshot,且有7000行的改动之多,且目前都没有进mainline的计划,而device mapper的thin provision已经进了3.2 kernel,且只有5000行改动,且支持writable snapshot,所以。。。转而又考察thin provision。考察基本顺利,做snapshot没有问题,snapshot写入没问题,最后chroot然后编译kernel测试速度也没问题,但是,最后发现一个郁闷的事情:这些snapshot被mount以后,公用的文件在不同的文件系统里各自都要占一份cache,也就是说,明明是一个4k物理block,mount到4个不同的文件系统,就占4 x 4k的内存cache了!

难道是device mapper的问题?于是再试了一下ext4的snapshot,甚至btrfs的snapshot,都一样!这就是vfs的特性:只要是inode不同,即使这些inode指向的是同一个物理block,那么它们的cache都是各自独有的,不共享。

我把这个事儿告诉了coly(@淘伯松

coly: (石化片刻)唉,我们之前想漏了,snapshot根本不能解决这个问题。。。太郁闷了,测了快两周才发现

我: 想开一点吧,还好是测出来的,而不是上线了才发现——到时候运维找过来“你们的这个方案好像不省内存啊”,然后还得解释,还得回滚,就更被动更狼狈了

coly: 噢,你这样一说,我舒坦多了

最后还是马涛同学(@淘伯瑜)给出了一个方案——overlay fs(http://ovlfs.sourceforge.net/),能把两个目录(甭管是什么文件系统的两个目录,是目录就行)“叠合”成一个文件系统,而这个新文件系统的inode其实还是原来目录里的那个,但是视图已经是“叠合”后的了。

比如,有两个目录,其中一个目录dir1有两个文件,是:

./ab  (ino:14)
./cd  (ino:16)

另一个目录dir2有三个文件,是:

./apple  (ino:23)
./banana (ino:27)
./lemon  (ino:31)

最后用

mount -t overlayfs overlayfs -olowerdir=/dir1,upperdir=/dir2 /test/

建立的新文件系统/test/里看上去是这样:

./ab     (ino:14)
./cd     (ino:16)
./apple  (ino:23)
./banana (ino:27)
./lemon  (ino:31)

注意,inode还是那些inode,但是他们“凑一块儿了”,而且,这个新文件系统是可写的,即使覆盖写了某个文件,也只影响upperdir(例子里的dir2)的内容,而lowerdir(例子里的dir1)没有任何影响。这样,我们就可以把linux系统根目录当成lowerdir,而每个运维自己的系统当成 upperdir ,某个运维的错误操作就不会影响其他人了。

感谢马涛同学的推荐,目前这个还没有进mainline的overlay fs非常契合我们的应用。

关于存档

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

01 2012 is the previous archive.

04 2012 is the next archive.

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