rhel6再次发现jbd2的bug

从上次修复jbd2的bug后到现在,已经四个月了,突然昨天DB的机器又出现了panic,用netoops抓到的信息如下:
jbd2 panic
居然又栽在jbd2上了,panic的位置和上次一样是

J_ASSERT(journal->j_running_transaction != NULL);

考虑到是运行很长时间的DB机器才出现这个问题,疑点还是落到tid上,由文卿同学找到了现象最符合的upstream的补丁:

commit deeeaf13b291420fe4a4a52606b9fc9128387340
Author: Theodore Ts'o 
Date:   Sun May 1 18:16:26 2011 -0400

    jbd2: fix fsync() tid wraparound bug
    
    If an application program does not make any changes to the indirect
    blocks or extent tree, i_datasync_tid will not get updated.  If there
    are enough commits (i.e., 2**31) such that tid_geq()'s calculations
    wrap, and there isn't a currently active transaction at the time of
    the fdatasync() call, this can end up triggering a BUG_ON in
    fs/jbd2/commit.c:
    
    	J_ASSERT(journal->j_running_transaction != NULL);
    
    It's pretty rare that this can happen, since it requires the use of
    fdatasync() plus *very* frequent and excessive use of fsync().  But
    with the right workload, it can.
    
    We fix this by replacing the use of tid_geq() with an equality test,
    since there's only one valid transaction id that we is valid for us to
    wait until it is commited: namely, the currently running transaction
    (if it exists).
    
    Signed-off-by: "Theodore Ts'o" 

diff --git a/fs/jbd2/journal.c b/fs/jbd2/journal.c
index e0ec3db..8c10004 100644
--- a/fs/jbd2/journal.c
+++ b/fs/jbd2/journal.c
@@ -479,9 +479,12 @@ int __jbd2_log_space_left(journal_t *journal)
 int __jbd2_log_start_commit(journal_t *journal, tid_t target)
 {
 	/*
-	 * Are we already doing a recent enough commit?
+	 * The only transaction we can possibly wait upon is the
+	 * currently running transaction (if it exists).  Otherwise,
+	 * the target tid must be an old one.
 	 */
-	if (!tid_geq(journal->j_commit_request, target)) {
+	if (journal->j_running_transaction &&
+	    journal->j_running_transaction->t_tid == target) {
 		/*
 		 * We want a new commit: OK, mark the request and wakeup the
 		 * commit thread.  We do _not_ do the commit ourselves.
@@ -493,7 +496,14 @@ int __jbd2_log_start_commit(journal_t *journal, tid_t target)
 			  journal->j_commit_sequence);
 		wake_up(&journal->j_wait_commit);
 		return 1;
-	}
+	} else if (!tid_geq(journal->j_commit_request, target))
+		/* This should never happen, but if it does, preserve
+		   the evidence before kjournald goes into a loop and
+		   increments j_commit_sequence beyond all recognition. */
+		WARN(1, "jbd: bad log_start_commit: %u %u %u %u\n",
+		     journal->j_commit_request, journal->j_commit_sequence,
+		     target, journal->j_running_transaction ? 
+		     journal->j_running_transaction->t_tid : 0);
 	return 0;
 }

原来,做fdatasync()时,会调用 ext4_sync_file():

fs/ext4/fsync.c
ext4_sync_file()

        commit_tid = datasync ? ei->i_datasync_tid : ei->i_sync_tid;                        
        printk("datasync:%lu, sync:%lu\n", ei->i_datasync_tid, ei->i_sync_tid);             
        if (jbd2_log_start_commit(journal, commit_tid)) {                                
		......

直接把i_datasync_tid赋给target,然后调用__jbd2_log_start_commit(),由于i_datasync_tid只在新创建inode或者ext4_iget()时更新,而数据库在打开某些文件后几乎再不关闭,而且一直是覆盖写,extent树不变,故而i_datasync_tid始终不更新,保持一个很小的值,而j_commit_request却随着jbd2日志的不断提交而不停增长,最后,它俩的差值越来越大,直至差值超过int型上限,tid_geq判断出错。

真是此恨绵绵,上次通过给i_datasync_tid赋正确的初值避免了“0比2177452108更晚”这一逻辑错误,修复了,但现在又是由于j_commit_request和target之间差值太大(大于了2的31次方)再次造成判断错误,所以又触发了BUG_ON。这次明显Ted哥也抓狂了,折腾tid_geq这破逻辑干嘛,直接判断j_running_transaction是不是NULL得了....

重现方法由伯瑜发现:打开一个文件,不停的覆盖写和fdatasync();另外启动一个进程打开另一个文件不停覆盖写和fsync()。重现时间需要三个多月,我们在线下修改了部分内核代码才加速复现的。
上次线上临时修复这个问题是umount ext4文件系统后用专用工具直接将tid置为0,这次其实临时方案更简单:umount后再mount就行了,因为这样会让i_datasync_tid更新为最新的tid,避免了它们之间差值不断变大。当然,文卿同学正在用kprobe的机制写了个hotfix的内核模块,这个才是正解。

DB的运维同学又渡过了一个难眠的夜晚,给大家添麻烦了,实在抱歉!

ext4突然拔盘

测试ali-kernel中发现,如果进程正在对一个ext4文件系统做普通的写入:

dd if=/dev/zero of=/test/ok bs=1M count=1048576

突然拔掉盘,由于io-error-guard的作用,能迅速把ext4变为read-only,dd进程直接返回Input/Output error然后退出,但是如果接着umount,那么umount命令就一直hang住了。
跟了一下发现是文件系统发现io错误开始清除inode,ext4_destroy_inode() --> ext4_ioend_wait()判断i_ioend_count总是不为0,所以一直等待。没啥新鲜的,加trace跟踪i_ioend_count,原来ext4_free_io_end是负责将此计数减一的,但是调用ext4_free_io_end的有很多函数,看上去ext4_end_io_buffer_write()应该是调用者(名字叫buffer_write嘛,普通的写入就是这个),查了半天,这个函数根本不调用ext4_free_io_end(),那谁在调,又查,才发现是ext4_end_Io_buffer_write()在结尾时调用

queue_work(wq, &io_end->work);

让工作队列来负责下面的事(这个work_queue,最容易看漏了),实际的工作交给ext4_end_aio_dio_work()来做了,你看这函数名字,又是aio又是dio的,我的写入既不用aio也不用dio,却非得走到它这里来,太误导了,还是upstream的名字改得对,就叫ext4_end_io_work()得了。然后就一眼看出ext4_end_aio_dio_work()里有蹊跷了,里面有个逻辑,如果io出现错误无法进行,居然不调用ext4_free_io_end而直接返回了,那肯定会造成计数无法变为0,ext4_ioend_wait当然就总等着了。
再看upstream,唉,很遗憾,已经被Jan Kara修复了,patch在这里

还要感谢鸿蒙(邹巍)同学对我的大力支持,千里迢迢寄过来了Sandybridge服务器,一机在手,调试不愁。能亲手插拔硬盘,才能发现这样的问题,以后才能多多方便大家。

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 的开发将主要集中在已有问题的解决,性能的调优和代码的重构上。暂时不会有新的供用户使用的新特性。

ext4会对机械硬盘做trim吗

今天@子团来问ext4对trim支持相关的问题:trim的语义是将设备某一部分的内容全置为0,如果ext4下层是机械硬盘,ext4也会给它发trim,频繁的将硬盘上的数据置0吗?
我翻了一会儿rhel 2.6.32-279 kernel的代码,找到了jbd2完成时调用trim的地方:

static void release_blocks_on_commit(journal_t *journal, transaction_t *txn)       
{                                                                                  
    struct super_block *sb = journal->j_private;                                   
    struct ext4_buddy e4b;                                                         
    struct ext4_group_info *db;                                                    
    int err, count = 0, count2 = 0;                                                
    struct ext4_free_data *entry;                                                  
    struct list_head *l, *ltmp;                                                    
                                                                                   
    list_for_each_safe(l, ltmp, &txn->t_private_list) {                            
        entry = list_entry(l, struct ext4_free_data, list);                        
                                                                                   
        mb_debug(1, "gonna free %u blocks in group %u (0x%p):",                    
             entry->count, entry->group, entry);                                   
                                                                                   
        if (test_opt(sb, DISCARD))                                                 
            ext4_issue_discard(sb, entry->group,                                   
                       entry->start_cluster, entry->count);                        
......

如果mount时设置了DISCARD(mount -t ext4 -o discard),则调用ext4_issue_discard,给设备发一个DISCARD,看来不管什么设备,这个调用是绕不开的,继续看:

ext4_issue_discard
    --> sb_issue_discard
        --> blkdev_issue_discard
int blkdev_issue_discard(struct block_device *bdev, sector_t sector,
        sector_t nr_sects, gfp_t gfp_mask, int flags)
{
    DECLARE_COMPLETION_ONSTACK(wait);
    struct request_queue *q = bdev_get_queue(bdev);
    int type = (1 << BIO_RW) | (1 << BIO_RW_DISCARD);
    unsigned int max_discard_sectors;
    struct bio_batch bb;
    struct bio *bio;
    int ret = 0;

    /*
     * DEPRECATED support for DISCARD_FL_BARRIER which will
     * fail with -EOPNOTSUPP (due to implicit BIO_RW_BARRIER)
     */ 
    if (flags & DISCARD_FL_BARRIER)
        type = DISCARD_BARRIER;

    if (!q)
        return -ENXIO;

    if (!blk_queue_discard(q))
        return -EOPNOTSUPP;
......

看来block层的代码如果发现该设备不支持DISCARD(blk_queue_discard(q)),则直接返回-EOPNOTSUPP,不会做任何其它操作。
所以,不用担心,普通机械硬盘也可以使用discard参数来mount ext4,kernel直接忽略discard请求,不会对硬盘造成额外的物理损伤。

ext4在aio dio下的问题

[原写于2013年1月]

皓庭同学在给一台测试机做压力的时候出现了kernel panic:

<4>[ 223.202997] RIP: 0010:[] [] iput+0x69/0x70
......
<4>[ 223.210475] [] ext4_free_io_end+0x2d/0x40 [ext4]
<4>[ 223.210864] [] ext4_end_aio_dio_work+0xac/0xf0 [ext4]
<4>[ 223.211277] [] ? ext4_end_aio_dio_work+0x0/0xf0 [ext4]
<4>[ 223.211693] [] worker_thread+0x170/0x2a0
<4>[ 223.212033] [] ? autoremove_wake_function+0x0/0x40
<4>[ 223.212425] [] ? worker_thread+0x0/0x2a0
<4>[ 223.227238] [] kthread+0x96/0xa0
<4>[ 223.241935] [] child_rip+0xa/0x20
<4>[ 223.256518] [] ? kthread+0x0/0xa0
<4>[ 223.271085] [] ? child_rip+0x0/0x20

重现的环境和步骤都很简单:只要在redhat linux-2.6.32-279.14.1 kernel上进一个使用ext4的目录一跑:

stress --cpu 2 --io 2 --vm 1 --vm-bytes 128M --hdd 2 --timeout 1d

几分钟后就出现。
这个stress工具果然是神器。
由于重现步骤简单,我也没有太多需要绞尽脑汁的猜测和bug重现工作,就是加trace_printk一点一点调试,最后找到了原因:
stress测试工具是用DIRECT_IO发起的aio(异步IO)请求,在ext4里对于DIRECT_IO请求,都会用ext4_init_io_end初始化一个ext4_io_end_t结构,初始化时调用igrab对该inode里面的i_count就行递增;

struct inode *igrab(struct inode *inode)
{
spin_lock(&inode_lock);
if (!(inode->i_state & (I_FREEING|I_CLEAR|I_WILL_FREE)))
__iget(inode);
else {
/*
* Handle the case where s_op->clear_inode is not been
* called yet, and somebody is calling igrab
* while the inode is getting freed.
*/
printk("%s: %lx\n", __func__, inode->i_state);
inode = NULL;
}
spin_unlock(&inode_lock);
return inode;
}

这些aio结束后,会调用ext4_free_io_end进而调用iput把inode的i_count递减,等到i_count递减到0时,就用iput_final把该inode置为I_CLEAR,意思是该inode可被清除了(当内存不够的时候会把这种I_CLEAR的inode占的内存空间回收)。
但是,如果一个文件已经被rm了,即已经调用了generic_delete_inode,那么inode就已经置上了I_FREEING,此时再调用igrab时,由于第一个判断无效,inode里的i_count就不会被加1了,i_count会一直保持0!等到aio结束后,iput被调用了多次,第一次触发iput_final把inode置上I_CLEAR,第二次就坏了,触发BUG_ON

void iput(struct inode *inode)
{
if (inode) {
BUG_ON(inode->i_state == I_CLEAR);

if (atomic_dec_and_lock(&inode->i_count, &inode_lock))
iput_final(inode);
}
}

因为一般对同一个inode,调了iput_final就不能再调iput了,因为你已经final了、没了。
说来说去,就是igrab似乎不该做那个判断,或者,干脆不要用igrab,用别的计数方式。翻了一下upstream,得,又已经被fix了,commit f7ad6d2e9201a6e1c9ee6530a291452eb695feb8, Ted两年前就已经fix了,抛弃了igrab改为自己管理计数。这个fix backport到rhel-2.6.32-279 kernel还需要一些修改,好在有文卿帮我review代码。

还要感谢皓庭同学帮忙找到了这么快速的重现方法,以及余峰老哥推荐的stress这个生猛工具。

1 2 3