ext4磁盘碎片查看

子团兄最近在测试ext4的bigalloc特性,cluster为256K,放入了16个大文件,每个文件25GB。反馈我说碎片很多,我觉得很奇怪便问是怎么看出来碎片多的,他说是用fsck看的:

#fsck.ext4 -f -y /dev/sdi1
e4fsck 1.42 (29-Nov-2011)
Pass 1: Checking inodes, blocks, and sizes
Pass 2: Checking directory structure
Pass 3: Checking directory connectivity
Pass 4: Checking reference counts
Pass 5: Checking group summary information
/dev/sdi1: 27/7631216 files (59.3% non-contiguous), 104148800/488377984 blocks

看起来"non-contiguous“占到了59.3%,似乎碎片很多的样子。
我翻了一下代码,这个值是在e2fsprogs包里计算出来的:

e2fsck/unix.c

static void show_stats(e2fsck_t ctx)
{
....
    frag_percent_total = ((10000 * (ctx->fs_fragmented +
                    ctx->fs_fragmented_dir))
                  / inodes_used);
....

其实就是用该文件系统中"fragmented“的文件数加上"fragmented”的目录数,再除以一共创建出来的inode数(也就是总文件数加总目录数)。但是哪种文件称为"fragmented“呢,看看扫描extent结构的代码:

e2fsck/pass1.c

static void scan_extent_node(e2fsck_t ctx, struct problem_context *pctx,
                             struct process_block_struct *pb,
                             blk64_t start_block, blk64_t end_block,
                             ext2_extent_handle_t ehandle)
{       
....
                if ((pb->previous_block != 0) &&
                    (pb->previous_block+1 != extent.e_pblk)) {
....
                        pb->fragmented = 1;
                }
....

只要这个extent与上一个extent物理块上不是连上的,这个文件就算是"fragmented“了(目录亦如此)。即使用上bigalloc特性,一个cluster配成256KB,那么一个block group就是4K * 8 * 256K = 8G大小,对于25G大小的文件来说,怎么都不可能连续,因为跨block group了,16个大文件那就算16个"fragmented“,除以inode总数27(这个inode总数包括了文件、目录、甚至journal本身也算一个inode)那就是59.3%了。
这样计算出来的"non-contiguous“只能反映非连续文件占总文件数的比例,不能说明磁盘的碎片多与不多。大文件几乎不可能extent相连一点空都不留,但因此就说碎片严重并不合理,因它是由几个不相连的大块组成的;比如一个25G的文件由4个8G的大块组成,我们不能因为它是"fragmented“的就说碎片很严重,毕竟才4个碎片。换个角度,我如果在该ext4里创建100个4KB的小文件,那non-contiguous就是16/127=12.6%,这并不能表示碎片情况变好了,因为多了这么多小文件,其实磁盘上碎片更多了。

要综合评估磁盘的碎片状况到底是好是坏并不容易,推荐的办法是用filefrag -v看看磁盘里某几个大文件的碎片情况,如果它们的碎片很少,而文件系统里的小文件也不多,那基本可以认为磁盘碎片不严重。当然,最彻底的办法是用dumpe2fs查看每个block group里block bitmap被分配的情况。

rhel6 的软RAID问题

先是彭敏同学报线上服务器出现了kernel panic,我上去一看panic的地方在
md_seq_show()--> mddev_unlock()
里的 md_wakeup_thread(mddev->thread),说是mddev->thread为NULL。用的是基于2.6.32-220的ali_kernel。查了一下,这个问题倒是在2.6.32-279已经fix了,upstream的补丁是"md: Avoid waking up a thread after it has been freed"

于是我让彭敏同学升级内核到2.6.32-279,但是更悲剧的是,279更不靠谱,创建raid10后刚开始mkfs.ext4就panic了....panic的地方更诡异,在drivers/scsi/scsi_lib.c的一个BUG_ON里:
panic

        /*
         * Filesystem requests must transfer data.
         */
        BUG_ON(!req->nr_phys_segments);

        cmd = scsi_get_cmd_from_req(sdev, req);
        if (unlikely(!cmd))
                return BLKPREP_DEFER;

        memset(cmd->cmnd, 0, BLK_MAX_CDB);
        return scsi_init_io(cmd, GFP_ATOMIC);
}

只好顺着代码一点点调试,才发现是raid10在处理io的函数make_request()里错把upstream的补丁直接backport过来,upstream里已经没有BIO_FLUSH和BIO_FUA只有REQ_FLUSH和REQ_FUA了,但是backport的人显然不知道,就直接用REQ_FLUSH来代替BIO_FLUSH,但在2.6.32内核里,这压根是两个不同的值。于是,make_request()在clone request时把request的FLUSH标志给漏掉了,到了scsi层:

static int sd_prep_fn(struct request_queue *q, struct request *rq)
{
        ....

        /*
         * Discard request come in as REQ_TYPE_FS but we turn them into
         * block PC requests to make life easier.
         */
        if (rq->cmd_flags & REQ_DISCARD) {
                ret = sd_setup_discard_cmnd(sdp, rq);
                goto out;
        } else if (rq->cmd_flags & REQ_WRITE_SAME) {
                ret = sd_setup_write_same_cmnd(sdp, rq);
                goto out;
        } else if (rq->cmd_flags & REQ_FLUSH) {
                ret = scsi_setup_flush_cmnd(sdp, rq);
                goto out;
        } else if (rq->cmd_type == REQ_TYPE_BLOCK_PC) {
                ret = scsi_setup_blk_pc_cmnd(sdp, rq);
                goto out;
        } else if (rq->cmd_type != REQ_TYPE_FS) {
                ret = BLKPREP_KILL;
                goto out;
        }
        ret = scsi_setup_fs_cmnd(sdp, rq);

对REQ_FLUSH的判断失效,进不了scsi_setup_flush_cmd而直接走了scsi_setup_fs_cmnd,结果悲剧了。看来升279不是办法,还是把"md: Avoid waking up a thread after it has been freed"的补丁打到220上吧。
感谢彭敏同学对软RAID功能的支持。

把这个279的bug告诉了涛哥
涛哥:upstream把BIO_FLUSH和REQ_FLUSH合并为一个不是Tejun Heo搞的鬼吗?他不是去红帽了吗?他搞了这摊事怎么不backport到rhel6的279上去?
我:....大概他去红帽就只做upstream不用干backport的苦活吧

scsi command的不同路径

本来是在调试一个新的io-scheduler(在基于rhel 2.6.32-279的ali_kernel上),但是出现了一些BUG_ON(),追查了很久,在添加了大堆的trace信息以后,终于在一个多月后的昨天发现了原因。

linux的block层在分配io request以后会对其调用回调函数 elevator_set_req_fn() 做初始化,在request放入每个设备的request queue后还要调用回调函数 elevator_add_req_fn() ,(这两个回调函数都是给io-scheduler来分别实现的,不同的io-scheduler会有各自不同的实现)看起来一个request一般应该经历这两个函数,而我从调试中开始怀疑:会不会有request只经历set_req,而不经历add_req?基于这个假设我翻了好几遍代码,发现当调度器切换的瞬间,新发出的request不走这两个函数。但毕竟是两个函数都不走,不是我怀疑的”只set_req不add_req“。

于是我不得不放弃自己的怀疑,实验了别的重现步骤和错误分析,一个月后还是没有进展。
靠着trace的不断堆砌,疑点终于还是回到了“只set_req不add_req”,这次把各种可能调用到get_request_wait的函数都加上了调试信息,又跑了一个星期,终于发现,确实有io request只走elevator_set_req_fn()而不走elevator_add_req_fn()!它就是scsi command。
当我们用sginfo一类命令查看IO设备的信息时,是会向kernel发送scsi命令的,这些scsi命令一样是走get_request_wait() --> get_request() --> blk_alloc_request()来分配request,也就一样要调用elevator_set_req_fn,但是,它们却是用blk_execute_rq_nowait来将新分配的request放入request queue的,而不是通常用的elv_insert(),而blk_execute_rq_nowait直接以ELEVATOR_INSERT_FRONT或ELEVATOR_INSERT_BACK为参数调用__elv_add_request,把request不经io-scheduler直接放入request queue了,不会调用elevator_add_req_fn()!

void blk_execute_rq_nowait(struct request_queue *q, struct gendisk *bd_disk,
                           struct request *rq, int at_head,
                           rq_end_io_fn *done)
{
        int where = at_head ? ELEVATOR_INSERT_FRONT : ELEVATOR_INSERT_BACK;

        WARN_ON(irqs_disabled());

        rq->rq_disk = bd_disk;
        rq->end_io = done;

        spin_lock_irq(q->queue_lock);

        if (unlikely(blk_queue_dead(q))) {
                rq->errors = -ENXIO;
                if (rq->end_io)
                        rq->end_io(rq, rq->errors);
                spin_unlock_irq(q->queue_lock);
                return;
        }

        __elv_add_request(q, rq, where, 1);
......

所以各个io-scheduler都是要自己处理这种情况的,不能产生“set_req后必然add_req”的假设。

不容易,一个多月才发现scsi命令的奇妙路径,也算是有所收获。

先mkfs后写裸盘

步骤是这样:

1. mkfs.ext4格盘
2. 用fio工具随机写裸盘
3. mount盘为ext4文件系统并开始使用

发现一切使用正常,很多块盘都是这样做的,无一出错,使用了很久dmesg里连WARN都没有。但是一旦换成mkfs.ext3,使用一段时间就有报错了。
这个步骤的顺序肯定是错的,应该先写裸盘再mkfs,但是同事的问题是:为什么ext4不怕而ext3就不行了?

用dumpe2fs查看ext4的feature,嫌疑最大的是uninit_bg,因为带上这个ext4默认的标记后,mkfs.ext4就不会把block group的信息初始化为0,而是等mount后由内核ext4在使用到该block group时再临时初始化,所以在mount前即使block group被其它工具写坏了也不怕。代码见:

e2fsprogs
misc/mke2fs.c
main() --> ext2fs_initialize()

ext2fs_initialize()
{
        for (i = 0; i < fs->group_desc_count; i++) {                                        
                /*
                 * Don't set the BLOCK_UNINIT group for the last group                      
                 * because the block bitmap needs to be padded.                             
                 */
                if (csum_flag) {                                                            
                        if (i != fs->group_desc_count - 1)
                                ext2fs_bg_flags_set(fs, i,
                                                    EXT2_BG_BLOCK_UNINIT);                  
                        ext2fs_bg_flags_set(fs, i, EXT2_BG_INODE_UNINIT);                   
......

其中EXT2_BG_LOCK_UNINT和EXT2_BG_INODE_UNINIT就是用于标记该block group在用时才初始化的:

ext4_read_inode_bitmap()
{
        ext4_lock_group(sb, block_group);
        if (desc->bg_flags & cpu_to_le16(EXT4_BG_INODE_UNINIT)) {
                ext4_init_inode_bitmap(sb, bh, block_group, desc);
                set_bitmap_uptodate(bh);
                set_buffer_uptodate(bh);
                ext4_unlock_group(sb, block_group);
                unlock_buffer(bh);
                return bh;
        }                                                                                   
        ext4_unlock_group(sb, block_group);                                             k
......

ext4_init_inode_bitmap即是把inode bitmap全部清0

做个实验确认一下:

1. mkfs.ext4 -O ^uninit_bg /dev/sdc
2. fio跑32K的随机写,十分钟
3. mount -t ext4 /dev/sdc /test/
4. for i in `seq 1 9000`; do mkdir /test/dawu$i; ./falloc -p /test/dawu$i/1 -o 0 -l 100M ; done
(之所以在根目录下创建这么多目录,是因为ext4会把根目录下的目录均匀到不同的block group去,有助于问题重现)

dmesg里会有错误:
[ 6808.192101] EXT4-fs error (device sdc): ext4_mb_generate_buddy: EXT4-fs: group 7416: 25800 clusters in bitmap, 32768 in gd
[ 6808.192978] EXT4-fs error (device sdc): ext4_mb_generate_buddy: EXT4-fs: group 7417: 25304 clusters in bitmap, 32768 in gd
[ 6808.193635] EXT4-fs error (device sdc): ext4_mb_generate_buddy: EXT4-fs: group 7418: 25304 clusters in bitmap, 32768 in gd
....
如果带上uninit_bg参数格盘,同样的步骤,没有任何错。
uninit_bg本来是用来加快mkfs的速度的,没想到歪打正着,还不怕少量随机写裸盘的影响,不过还是不建议这样做,万一哪天随机写把super block写坏了,那什么文件系统都没辙了。
感谢舞爷的刨根问底,我又长见识了。

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的运维同学又渡过了一个难眠的夜晚,给大家添麻烦了,实在抱歉!

1 2 3 4 7