io调度器问题追查

linux内核里,所有的io都由bio结构来管理,然后bio又合并成request(个别设备会直接处理bio),最后request丢进设备队列里。一个设备能允许最多一次性丢进去多少request,由 /sys/block/sdx/queue/nr_requests 参数决定,默认值是128。
后来,io也有了cgroup,这就意味着我可以设定两个进程A和B到两个group,group1的weight是1000,group2的weight是500,那么如果两个进程都在使劲读文件,那B进程的带宽(或iops)只能是A进程的一半,但是,有例外,如果B进程采用aio,一次发出128个io请求,一下子把设备能处理的128个request缓冲都用完,那A进程就取不到新的request也就无法发出io了,于是,cgroup按比例分配就失效了,高优先级的A进程被低优先级的B进程“抢占”了资源。为了解决这个问题,Tejun Heo提了一个patch系列,办法就是让每个group都可以有nr_requests个请求(针对每个group和一个queue之间,建立一个request_list),这样,如果一个设备由被来自n个group的进程访问,它实际上可以分配(n * nr_requests)个请求(因为有n个request_list),这样group与group之间就没有了竞争,优先级反转的bug就这么fix了。这个patch系列我在做flashcache改造的时候一起打进了阿里kernel(基于redhat-2.6.32)。

故事还没有说完。
最近在rhel 2.6.32-279 kernel上开发io scheduler相关的东西,发现一个诡异想象:我正在使用cfq调度器的cgroup功能,io正哗啦啦的往下发,这时猛然把调度器切换到deadline,会有一部分cfq时发下去的io在调度起切换以后才从设备返回,然后,由于此时调度起已经变了,最后由deadline调度器处理这写返回的io。有点说不通,一个调度起发出的request让另一个调度起来做完成处理,万一request里有cfq特有的指针,deadline岂不悲剧了。于是仔细看了一下调度器切换的代码。

elevator_switch
    --> elv_quiesce_start
        --> blk_drain_queue

函数名很清楚,切换新调度器之前要把老调度器上的request都抽干(drain_queue),看看函数实现:

void blk_drain_queue(struct request_queue *q, bool drain_all)
{   
    while (true) {
        bool drain = false;
        int i;

        spin_lock_irq(q->queue_lock);

        elv_drain_elevator(q);
        if (drain_all)
            blk_throtl_drain(q);

        __blk_run_queue(q);
        
        drain |= q->root_rl.elvpriv;
        
......

        spin_unlock_irq(q->queue_lock);

        if (!drain)
            break;
        msleep(10);
    }
}

__blk_run_queue是把queue里的request都发往设备,难道不等这些request返回就直接切还调度器吗?“等request全部返回”这个功能是这个函数里的哪个语句完成的?又用trace_printk跟踪了一下,哦,是这句

drain |= q->root_rl.elvpriv;

当kernel分配request时调用 rl.elvpriv++,释放request是调用 rl.elvpriv--,elvpriv如果等于0说明所有发出去的request都返回了。这不挺对的吗?哦,request_list现在不只一个了,它是多个!也就是说,如果进程A放在cgroup的group1里,它在group1里申请的request都算在group1的rl.elvpriv里,而blk_drain_queue里却判断root group的root_rl的elvpriv,当然不对了。
再倒回去看看upstream的代码,原来Tejun Heo早就有补丁 https://lkml.org/lkml/2012/6/4/552 ,是我打补丁打漏了......唉,backport也是个高风险高要求的苦活啊。

iothrottle测试报告

[原写于2013年2月]

如果一台服务器上跑很多应用,对资源的隔离当然是不言而喻的,但是很多时候,你即使在一台机器上就跑一个应用,也可能需要资源控制。比如,你的服务器是提供在线查询服务的,数据放在硬盘上,部分热数据在内存中,一切顺利,但是,某天你在这台服务器上scp数据文件过来时,发现磁盘被拷文件的操作压得喘不过气,以至于查询服务都受到了影响,那这时该怎么办?当然,我们可以用scp -l 来限制拷贝的带宽,避免对磁盘的巨大压力,但如果同步数据不是用的scp,是用户自己的某个工具或者是自己写的某个程序,那么还需要去改程序吗?难道每个用户开发的程序都要带上控制带宽和资源使用率的功能?

现在比较建议的解决办法是用cgroup来隔离资源,而到io层面,常用的就是iothrottle。iothrottle已经在公司的生产环境中使用,最近运维考虑扩大使用范围,所以针对运维和DBA的一些疑问,我做了一些iothrottle的测试,不是什么高深的测试方法,但是确实能回答使用者的一些问题,测试报告见

kernel.taobao.org

希望能给大家一些帮助,和信心

块设备层死锁

[原写于2012年12月]

​把flashcache改为request-based后,虽然IO数量可以按比例控制了,但是作为“珍稀”资源的cache(通常是昂贵的固态硬盘或更昂贵的fusionio卡),也需要按例分配不同进程以不同的cache空间。近一个月我一直在忙着加这个功能。
新代码写完了,压力测试二十多个小时,出现了一个死锁:

<4>[11957.888102] [] ? nmi+0x20/0x30
<4>[11957.897016] [] ? _spin_lock_irqsave+0x2f/0x40
<4>[11957.906089] <> [] ? flashcache_clean_set+0x9f/0x600 [flashcache]
<4>[11957.915610] [] ? dm_io_async_bvec+0xbc/0xf0 [flashcache]
<4>[11957.925377] [] ? flashcache_io_callback+0x0/0x4a0 [flashcache]
<4>[11957.935318] [] ? flashcache_read_miss+0x9e/0x150 [flashcache]
<4>[11957.945378] [] ? flashcache_read+0x138/0x330 [flashcache]
<4>[11957.955511] [] ? flashcache_mk_rq+0x165/0x1d0 [flashcache]
<4>[11957.965677] [] ? dm_request+0x5d/0x210 [dm_mod]
<4>[11957.975911] [] ? generic_make_request+0x261/0x530

应该是有进程拿到了request_queue的锁 queue_lock 却再没有释放。
由于出现了“flashcache”的字样,有很大嫌疑是我新加的代码有问题,但是不管怎样,二十多个小时的重现步骤太慢了,得找到一个快点的,于是花了几天尝试不同的压测脚本(一边尝试,一边把我的cubieboard刷成了android,我的天,android比linaro好用多了,而且是刷在cubieboard自带的NAND里,可以不用SD卡,刷机所需image见这里),终于把重现步骤缩短为2个小时左右,接下来把我加的代码全回退,依然有死锁——看来是redhat-2.6.32内核的问题了。

我在内核报死锁的地方watchdog_overflow_callback()函数里调用panic()之前加了一句show_state(),即把死锁时所有进程的stack信息统统输出到dmesg里去(再依靠netoops把消息传到另外一台机器上,因为出现死锁的本机已经登不上去了)。
跑了不到两个小时,死锁出现了,我在netoops吐出来的一堆stack信息里一个个找,看谁像是那个拿到锁不还的人,发现一个可疑的:cfq_get_cfqg()里调用blk_init_rl()函数。blk_init_rl函数里面要做一个内存分配,但是传给kmalloc_node的gfp_mask却是GFP_KERNEL,带上GFP_KERNEL后在分配不到内存时进程会去睡眠直到别的进程释放了一些可用内存,blk_init_rl()已经拿到了queue_lock,现在却一睡不醒(系统内存紧张,迟迟没有可用内存),当然别的进程来取queue_lock就等疯了。
看来不是我加的代码造成的,也与flashcache没有关系,是block层的问题。

这种情况一般有两种修复办法:一是在分配内存之前把queue_lock释放掉,分配后再重新拿锁;或者,把分配内存的gfp_mask设为GFP_ATOMIC,就是没内存就直接返回NULL,不睡。
不过我看了一下upstream代码,这个问题已经修复了。参见Tejun Heo的两个patch,12,第二个patch的头几个改动才是把GFP_KERNEL改成GFP_ATOMIC。
没想到netoops加show_state()调试死锁这么方便,当然,难的是找到快速重现bug的办法。