Ext4 Workshop 2013 总结

作者:刘峥

2013年04月17日,Ext4 社区的开发者举行了一个讨论会。主要讨论目前 Ext4 文件系统中出现的问题及解决方法,以及后续的开发计划。本文章汇总了本次会议的内容。

在这次会议中,主要讨论了如下内容(每个话题中讨论的内容包括但不限于话题本身):

  1. write stall 问题
  2. 3.10 merge window 的相关情况
  3. 关于 Ext4 stable/long-term tree 的维护
  4. extent tree layout 修改
  5. bigalloc 特性的改进
  6. Online Fsck 特性的开发
  7. data=guarded 和文件系统延迟问题的优化
  8. 不同挂载参数的选择
  9. Ext4 针对不同设备的优化

与会人员:

  • Ted Ts'o (Ext4/JBD2 Maintainer, Google)
  • Jan Kara (EXT2/3/JBD Maintainer, SUSE)
  • Eric Sandeen (Red Hat)
  • Mingming Cao (Oracle)
  • Tao Ma (Tao Bao)
  • Lukas Cerner (Red Hat)
  • Zheng Liu (Tao Bao)
  • Darrick Wong (Oracle)
  • Mel Gorman (SUSE)
  • Daniel Phillips

1. write stall 问题

这一问题最初是由 MM 内核开发者 Mel Gorman 报告的,他在测试内存管理的补丁时,发现 Ext4 文件系统的交互响应比较差,延迟很高。通过观察发现是由于 do_get_write_access() 函数中会阻塞在 lock_buffer() 上,造成延迟较大。

Ted Ts'o 提出了两种解决方案:

1) 在调用 do_get_write_access() 时避免 lock_buffer;

2) 整体修改 JBD2,自己管理 buffer_head,从而降低延迟。

由于第二种方案太过复杂,短期内很难完成,所以目前打算先优化 do_get_write_access() 函数来降低延迟。同时,在刷新 metadata 时,通过加入 REQ_* 标记来避免优先级反转问题,提高响应速度。

 

2. 3.10 merge window 的相关情况

最近马上要开始新一轮的 merge window 了,所以 Ted 说明了一下目前的进展,他目前还在进行各种测试。Jan Kara 说他通过 xfstests 测试发现 data=journal 模式下第68项测试失败。Ted 则抱怨了一下 xfstests 没有支持 local group,造成每次测试都要手工指定测试项目。

Lukas 讨论了他关于优化 invalidatepage() 的补丁,Jan 表示会 review 代码。Ted 则说这个 merge window 可以考虑把 vfs 层的补丁先合并进来,下个 merge window 再解决 Ext4 专门的补丁。

Jan 则讨论了他关于优化 Ext4 writepage 路径的补丁,Ted 的建议是留到下一个 merge window 再合并。

 

3. 关于 Ext4 stable/long-term tree 的维护

Ted 对于 stable/long-term tree 的维护工作的态度是,目前他没有时间和精力来单独维护一个这样的 tree。目前,对于 Ext4 文件系统来说,如果需要使用较为稳定的版本,可以的选择是 linux stable tree。因为目前所有有意义的补丁会被抄送给 stable@vger.kernel.org,由 stable tree 对应的维护者来进行维护。当然,如果一些补丁不能够简单的直接打到 stable tree 上,则该补丁将不会包含在相应的 stable tree 中。

 

4. extent tree layout 修改

目前 Ext4 中 extent 存储的物理磁盘块的长度是 48 位,所以,可以将其扩展为 64 位以支持更大的磁盘分区。Ted 表示如果可以扩展到 64 位当然是最好的,但是这是一个长期的工作,目前还没有明确的需求需要搞这么大的文件系统。

目前 Ted 认为对于 Ext4 文件系统更为重要的事情是如下几个:

  • 解决 unwritten extent conversion 的问题,从而彻底实现 dioread_nolock;
  • 对 extent tree/map_blocks 函数进行重构;
  • mballoc 代码的重构和改进,包括 buddy bitmap 被很快回收的问题;

 

5. bigalloc 特性的改进

目前淘宝在使用 bigalloc 的过程中发现该特性还有一些不足,比如在使用 cluster (默认64k) 作为磁盘分配的单位时,对于小文件的存储,空间开销偏大。所以淘宝的想法是希望能够某几个 block group 按照 block size 来分配文件,剩下的 block group 按照 cluster size 来分配。这样可以尽量减少小文件对磁盘空间的占用。Ted 给出的建议是可以考虑指定一个参数,在格式化的时候,指定前面多少个 block group 通过 block size 来分配。这样做相对比较简单;另外一种更为简单的方法是将磁盘分成两个分区,分别按 block size 和 cluster size 来进行格式化,从而解决这一问题。

 

6. Online Fsck 特性的开发

这一需求也是淘宝目前遇到的实际问题。由于数据库等系统需要尽可能的在线提供服务,所以我们希望能够尽可能的缩短宕机后的启动时间。所以,如果可以实现 online fsck 的功能,则可以尽可能的缩短系统启动的时间,同时保证文件系统不被破坏。目前已知的可以进行 online fsck 的文件系统是 zfs, ffs, btrfs。Ted 给出的建议是可以考虑在分配磁盘块的时候在相应的 block group 上添加相应的标记,表明这个 block group 正在进行磁盘块的分配,在出现宕机后,这个 block group 的数据只进行读操作,所有写操作都会被映射到其他的 block group 中。这样可以尽可能的保证文件系统的一致性。

 

7. data=guarded 和文件系统延迟问题的优化

data=guarded 模式最早是 Chris Mason 针对 ext3 fsync 延迟过大提出的一种日志模式。但是后来由于 ext4 中引入延迟分配 (delalloc) 特性而没有再继续开发,也没有被合并进 upstream kernel。这次提出这个话题,主要是由于淘宝在部署 ext4 的过程中遇到了很多延迟问题。在讨论延迟问题的过程中,Ted 提出了一个长远的改进计划,即通过使用 extent status tree 在内存中进行磁盘块的分配,在 end_io 中再创建日志并修改元数据提交。这样可以极大地缩短日志的持有时间,从而尽可能的避免由于日志提交造成文件系统操作的延迟。同时 Ted 还建议减少延迟的方法是尽可能提前将文件预分配出来,同时尽量将文件的元信息保存在内存中。

 

8. 不同挂载参数的选择

这个话题主要是 Red Hat 和 SUSE 的开发者提出的,目的是减少 Ext4 测试环节的压力,同时由于 Linux 发行版陆续使用 Ext4 内核模块来处理 Ext2/3 文件系统,需要在内核中添加检查代码来确保 Ext2/3 文件系统在使用 Ext4 内核模块时的正确性。

其中比较重要的几个挂载参数是是:

  • indirect-based/extent-based 文件的支持;
  • async_journal_commit参数(该参数将被标记为危险,可能造成数据丢失);
  • data=journal 与 delalloc 的互斥问题

 

9. Ext4 针对不同设备的优化

这一话题主要是围绕目前文件系统开发的优化方向来进行讨论。由于 ARM 处理器在服务器领域的使用,文件系统即需要考虑高端硬件设备的优化,同时还要兼顾这类低端设备的优化。针对高端设备,文件系统可以多使用些资源(如内存资源)来获取更好的性能;同时针对 ARM 这类内存较小的设备,可以减小资源的使用,同时提供尽可能好的性能。

Ted 表示目前 Google 针对 e2fsck/mke2fs 工具是有一些优化的,并且已经放到了 upstream 的 e2fsprogs 中,但是并没有在内核代码中针对不同设备进行优化的想法。同时 Ted 表示目前可能更需要考虑针对 eMMC 设备的优化问题。

 

根据本次 Ext4 workshop,后续 Ext4 文件系统的开发主要包括:

  • extent tree 代码重构
  • map_blocks 函数代码重构
  • do_get_write_access() 优化,解决 write stall 问题(目前 Ted 已经将补丁发出并合并进ext4 dev 分支)
  • 使用 extent status tree 来优化磁盘块分配,减小延迟
  • 修改 extent tree layout 支持更大的磁盘
  • mballoc 代码的重构和优化

关于此前使用 extent status tree 实现 range locking 的问题。由于 Jan Kara 已经开始在 VFS 层实现一套通用的 range locking,因此目前与 Ted, Jan 讨论的结果是暂时先不在 Ext4 中单独实现一个 range locking 了。

未来一段时间,Ext4 的开发将主要集中在已有问题的解决,性能的调优和代码的重构上。暂时不会有新的供用户使用的新特性。

hadoop集群上捉到kernel bug一个

[原写于2012年8月]

(谨以此文感谢淘宝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都被makr_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的,那问题定位就快多了。所以,还是要勤学多练啊 🙂

后记:

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

超然台上仙:有点不太理解请教一下, “看,这样写完一个文件后,每个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了。

zc19881024的来信:首先,2.6.18 kernel也同样使用了pagevec,__grab_cache_page中代码意思与32版本是一样的,似乎与文中描述不符
其次,patch修改顺序之后,按照文中的意思仍然没有解决前13个page只设置为REFERENCED的问题,这仍然有可能导致缓存替换时被换出。

答:zc19881024同学非常仔细,我去翻了一下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?....这个就不得而知了。
zc19881024的第二个问题如上回答。

netoops启动时报 "XX is a slave device, aborting"

[原写于2012年6月]

为了监控上线的新内核,我们把google的netoops backport到了自己的内核,生产上如有kernel panic,会将panic的栈信息发送到日志服务器,方便调试和修复。
前天,洪川同学报告说以前线上的netoops都是把bond的slave网口作为发消息的dev,新上线2.6.32-220内核后,启动netoops失败,系统报:

”eth0 is a slave device, aborting."

找了一下从 2.6.32-131 到 2.6.32-220 的redhat的变动,发现了王聪同学的这个patch:

commit 0c1ad04aecb975f2a2014e1bc5a2fa23923ecbd9
Author: WANG Cong
Date: Thu Jun 9 00:28:13 2011 -0700

netpoll: prevent netpoll setup on slave devices

In commit 8d8fc29d02a33e4bd5f4fa47823c1fd386346093
(netpoll: disable netpoll when enslave a device), we automatically
disable netpoll when the underlying device is being enslaved,
we also need to prevent people from setuping netpoll on
devices that are already enslaved.

Signed-off-by: WANG Cong
Signed-off-by: David S. Miller

diff --git a/net/core/netpoll.c b/net/core/netpoll.c
index 2d7d6d4..42ea4b0 100644
--- a/net/core/netpoll.c
+++ b/net/core/netpoll.c
@@ -792,6 +792,12 @@ int netpoll_setup(struct netpoll *np)
return -ENODEV;
}

+ if (ndev->master) {
+ printk(KERN_ERR "%s: %s is a slave device, aborting.\n",
+ np->name, np->dev_name);
+ return -EBUSY;
+ }
+
if (!netif_running(ndev)) {
unsigned long atmost, atleast;

从此,netpoll就无法使用slave设备了(netoops用的就是netpoll),不过我奇怪为什么以前可以现在又不行了,所以发邮件问了王聪同学为何现在不能使用slave设备,回答是:

“因为slave设备没有IP地址,http://wangcong.org/blog/archives/1657”

而且王同学在redhat搞netconsole也遇到了同样的问题,只能改用master网口。我们的netoops也只能遵循同样的规则,改用 bond0做dev

BUG_ON exit_mmap

[原写于2012年6月]

上个月,生产服务器上报来了内核bug:

------------[ cut here ]------------
kernel BUG at mm/mmap.c:2352!
invalid opcode: 0000 [#1] SMP
last sysfs file: /sys/devices/system/cpu/cpu23/cache/index2/shared_cpu_map
CPU 13
....
[] mmput+0x65/0x100
[] exit_mm+0x105/0x140
[] do_exit+0x1ad/0x840
[] ? __sigqueue_free+0x40/0x50
[] do_group_exit+0x41/0xb0
[] get_signal_to_deliver+0x1e8/0x430
[] do_notify_resume+0xf4/0x8a0
[] ? security_task_kill+0x16/0x20
[] ? recalc_sigpending+0x32/0x80
[] ? sigprocmask+0x75/0xf0
[] ? sys_rt_sigprocmask+0x81/0x100
[] ? sigprocmask+0x75/0xf0
[] int_signal+0x12/0x17

原因就是 exit_mmap 函数最后一行的BUG_ON被触发了(我们用的是 2.6.32 内核)

void exit_mmap(struct mm_struct *mm)
{
....
BUG_ON(mm->nr_ptes > (FIRST_USER_ADDRESS+PMD_SIZE-1)>>PMD_SHIFT);
}

[ 这个 (FIRST_USER_ADDRESS+PMD_SIZE-1)>>PMD_SHIFT 其实就是0 ]

从代码直观能想到的就是进程在退出的时候pte没有释放对,或者nr_ptes计数漏了,导致最后一步nr_ptes没有变成0。
想归想,这个非常难重现。但是很快,好运来了,同事在开发cgroup的过程中无意中也触发了了这个BUG_ON,触发的方法是在 __mem_cgroup_uncharge_common 函数里加了一对 down_read/up_read (除了这一对,没有任何别的操作)。于是我开始想象:这一对 down_read/up_read 没有做任何与page或者pte相关的事情,却引起 nr_ptes计算出错,那八成是这一对锁的添加触发了原来的某个race condition,最终导致 nr_ptes 没有计算对。
于是,我沿着这条思路,顺着 exit_mmap 一路 trace_printk ,终于发现是在 page_remove_rmap() 前后nr_ptes开始不对,调用路线还挺深(花了不少力气才找到):

--> exit_mmap
--> unmap_vmas
--> unmap_page_range
--> zap_pud_range
--> zap_pmd_range
--> zap_pte_range
--> page_remove_rmap

为什么是 page_remove_rmap ? 又花了两天才发现,原来exit_mmap里用到了 tlb_gather_mmu/tlb_finish_mmu,就是把要清空页表项的page暂存在一个 struct mmu_gather 里,然后在tlb_finish_mmu时统一清空页表,以提高性能。2.6.32内核里的这个 struct mmu_gather 是per cpu变量(每个cpu一个),这就意味着在 tlb_gahter_mmu 和 tlb_finish_mmu 之间进程不能切换CPU,否则拿到的 struct mmu_gather就可能是另一个CPU上的变量,那就彻底不对了(upstream为了支持抢占,已经将mmu_gather改为 stack argument)。糟糕之处就在于,page_remove_rmap调用了mem_cgroup_uncharge_page进而调用了__mem_cgroup_uncharge_common,而我们在里面加了一个down_read,而down_read里有一句might_sleep(),结果,进程睡眠了,等他醒过来,可能已经身处另一个CPU,于是mmu_gather拿错了,于是nr_ptes不对了....(其实我挺好奇:为什么down_read里要加一句might_sleep?求高手解答)

花了三天辛辛苦苦的找,最后发现不是race condition,就是自己加的down_read造成的问题。
但是毕竟,生产报来的bug是没有这一更改的,那就是另有原因,还得查。

上周刘峥同学给了个链接 https://lkml.org/lkml/2012/2/15/322 ,看来redhat也遇到不少exit_mmap BUG_ON(),不过他们发现是Transparent Huge Page造成的,咱们报bug的生产服务器上并没有用到THP,还不是一个问题。不过,我总体感觉,进程退出的代码路径里,这个 BUG_ON(mm->nr_ptes > 0) 是最后一关,结果成了bug触发的汇集处,很多别处的甚至不是mm相关的代码错误都可能触发这个BUG_ON,比如这个 https://lkml.org/lkml/2012/5/25/553

后来,生产服务器从 2.6.32-131 升到 2.6.32-220 后,再没报过这个BUG_ON,至于终极的原因....可能是mm部分的bug fix,也可能是某个驱动的升级,这个,只有redhat知道了