tpps: 一个可做cgroup资源隔离的高效IO调度器

(本文里说的“资源隔离”主要是指cgroup根据blkio.weight的值来按比例调配io的带宽和IOPS,不包括io-throttle即blkio.throttle.xxx的一系列配置,因为linux的io-throttle机制不依赖于IO调度器)

由于现在的SSD越来越快也越来越便宜,如何高效的利用SSD就变得十分重要。高效利用SSD有两个办法,一个是加大应用程序发出的io深度(比如用aio),对于无法加大io深度的一些重要应用(比如mysql数据库),则可以用第二个办法:在一个高速块设备上跑多个应用实例。
现在问题来了,既然跑多个实例,就要避免它们互相干扰,比如一个mysql由于压力大把io带宽都占了,这对另一个mysql实例就不太公平。现在常用的办法当然是用cgroup来做io资源的隔离(参考这篇),但是,有个尴尬的事情,目前只有cfq这一个io调度器是支持cgroup的,而cfq调度器在高速设备上表现却不尽人意。
deadline调度器准备了两个队列,一个用来处理写请求,一个用来处理读请求,然后根据io快要到期的时间(即deadline)来选择哪个io该发出去了,这个选择同时也要考虑“读比写优先”、“尽可能连续发射io”等约束,但是不管怎么说,既然把io的响应时间做为了该调度算法的第一要素,那deadline调度器的io延时性就是相对最好的(即response time很短),它也是SSD设备的推荐调度器。问题在于,“两个队列”,这结构太简单了,自然也无法支持cgroup这样复杂的特性,所以,虽然deadline调度器很快,RT很短,但是做不了资源隔离。
再来看cfq,cfq最早就是基于磁盘来做优化的,它的算法尽可能保证io的连续性,而不保证io的及时响应(所以,在对磁盘做高io压测的时候,cfq调度器有时会制造出长达几秒的io响应来),比如,对于单个io,它会做一个小小的等待,看有没有和这个io连上的下一个io到来,如果有,可以一起发出去,以形成连续的io流,但是,这个”小小的等待“,就大大延长了io响应时间。cfq调度器为cgroup中的每个group单独创建一个"struct cfq_queue“实例,然后以各group的weight所占的权重来决定改发哪个group的io,所以cfq是支持资源隔离的。我讲的这些还没有碰到cfq的皮毛,它四千多行的代码有很多复杂和技巧性的算法和数据结构,我估计只有fusionio的李少华能把它说清楚,我这里就不班门弄斧了。
介绍IO调度器的文章,可参考这篇

总的来说,deadline适合SSD设备,但是它不支持资源隔离;cfq支持资源隔离,但是在SSD上跑延时又太差,且代码复杂,不易改造。(noop铁定不支持资源隔离,而anticipator跟deadline代码相差不大,这两个都出局了)最终,经过我、高阳伯瑜三个人的讨论,得出决定:如果要方便应用方的使用,最好是做一个新的、代码非常简单的新IO调度器。这样,应用方如果是用硬盘,那就选择cfq;如果是用SSD,就选择deadline;如果既用了高速设备,又想做资源隔离,那就选这个新调度器。既方便应用方灵活选择,又方便我们自己维护。(如果把新调度器做进deadline,后期的维护和backport就会越来越吃力)。新调度器的原理很简单:通过类似cfq的xxx_queue结构来记录各group的信息,针对每个group都创建一个list,存放从该group到来的io,然后在dispatch io时,用该设备的io request容量(即nr_request)减去已经发出但还没有处理完成的io数(即rq_in_driver),得出的就是该设备还可处理的io数(即下面代码中的quota),然后根据这个“可处理io数”和各group的权重,算出各group的list上可以dispatch的io数,最后,就是按照这些数去list上取io,把它们发出去了。

static int tpps_dispatch_requests(struct request_queue *q, int force)
{
        struct tpps_data *tppd = q->elevator->elevator_data;
        struct tpps_group *tppg, *group_n;
        struct tpps_queue *tppq;
        struct list_head *next;
        int count = 0, total = 0, ret;
        int quota, grp_quota;

        if (unlikely(force))
                return tpps_forced_dispatch(tppd);

        if (!tppd->total_weight)
                return 0;

        quota = q->nr_requests - tppd->rq_in_driver;
        if (quota < MIN_DISPATCH_RQ)
                return 0;

        list_for_each_entry_safe(tppg, group_n, &tppd->group_list, tppd_node) {
                if (!tppg->nr_tppq)
                        continue;
                tpps_update_group_weight(tppg);
                grp_quota = (quota * tppg->weight / tppd->total_weight) -
                                tppg->rq_in_driver;
......

这样,就是按cgroup的权重比例来发出io了,而且,不像cfq一个队列一个队列的处理,这个新调度器是拿到quota后从每个group list里都取一定量的io,所以也还保证了一定的“公平性”,同时,quota是按照设备的处理能力算出来的,所以也能保证尽可能把设备打满。简单、按比例并发处理(准确的说,是公平处理),所以我们给这个新io调度器起名字叫“Tiny Parallel Proportion Scheduler",简称tpps。代码已经合并到了ali_kernel,patch链接是1,2,3,4,5,6

另外申明一下,这个调度器并不复杂,也不高端,讲求的是简单好用,所以如果有哪位内核高手看到这个调度器后发表不满:“这么简单也好意思拿出来说?!”,那大可不必。我们就是做出来解决问题的,问题解决就ok,不必做得那么高深。我写这篇文章也只是方便有兴趣的DBA或者SA来试用和提建议,不是要说明这个调度器有多牛逼。

想要使用tpps调度器需要使用ali_kernel:

git clone git@github.com:alibaba/ali_kernel.git
git checkout dev
#编译并重启内核

机器重启后

modprobe tpps-iosched
#再用 cat /sys/block/sdX/queue/scheduler 可以看到多个io调度器,其中有tpps
echo tpps > /sys/block/sdX/queue/scheduler

我用一个快速设备测试了一下cfq和tpps对cgroup的支持,fio测试脚本如下:

[global]
direct=1
ioengine=libaio
runtime=20
bs=4k
rw=randwrite
iodepth=1024
filename=/dev/sdX
numjobs=4

[test1]
cgroup=test1
cgroup_weight=1000

[test2]
cgroup=test2
cgroup_weight=800

[test3]
cgroup=test3
cgroup_weight=600

[test4]
cgroup=test4
cgroup_weight=400

测试结果:

各个cgroup的IOPS

io调度器 test1 test2 test3 test4
cfq 6311 5334 3983 2569
tpps 8592 8141 7152 5767

各个cgroup的平均RT(Response Time)单位:毫秒

io调度器 test1 test2 test3 test4
cfq 161 221 241 398
tpps 114 127 141 177

各个cgroup的最大RT(Response Time)单位:毫秒

io调度器 test1 test2 test3 test4
cfq 312 387 445 741
tpps 173 262 260 322

从测试看来,tpps既能实现资源隔离,同样的设备和io压力下性能也略高于cfq。
重要提示:对tpps来说,只有当设备被压满时才会有不同cgroup的不同iops比例出现,所以,测试时压力一定要大。

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请求,不会对硬盘造成额外的物理损伤。