在上一篇《块设备内核参数max_segments和max_sectors_kb解析》介绍了max_sectors_kb这个参数对device mapper设备和软raid设备的影响,实际在测试时,发现另外一个奇怪的现象:一块物理盘,在这个盘上创建一个device mapper设备,然后对这个设备使用direct IO进行随机大块写或者顺序大块写(随机读和顺序读没这个问题),块大小要大于dm设备的max_sectors_kb,但是小于dm底层设备(这里是物理磁盘)的max_sectors_kb,测试结果发现在3.2内核里落到物理盘上的io并没有合并,但是在3.10内核下落到物理盘上的io确是合并了(合并后的大小为用户发的io块大小)。具体现象下面以例子进行说明。
1.不同内核下的表现
这里选取3.2.57内核和3.10.11内核作为对比。
1.1 3.2.57内核下测试
物理盘sdg,在其上创建的10G的device mapper设备dmtest1,对应的设备为dm-71
1)默认的参数设置
1 | # cat /sys/block/sdg/queue/max_sectors_kb |
使用fio测试,配置如下
1 | [global] |
然后iostat查看io状态
可以看到dm设备和sdg上的io大小都是256KB。
2)修改dm设备max_sectors_kb参数
1 | # echo 64 > /sys/block/dm-71/queue/max_sectors_kb |
同样使用fio测试,iostat查看
只调整了dm设备的max_sectors_kb参数,没有调整物理设备的(还是512),期望的结果是dm设备上平均io大小是64kb,然后物理设备上市256kb,但是结果落到物理设备上的io并没有合并。
1.2 3.10.11内核下测试
物理盘sdg,在其上创建的10G的device mapper设备dmtest1,对应的设备为dm-65
1)默认的参数设置
1 | # cat /sys/block/sdg/queue/max_sectors_kb |
2)修改dm设备max_sectors_kb参数
1 | # echo 64 > /sys/block/dm-65/queue/max_sectors_kb |
同样的fio测试,iostat结果
结果dm设备上io变成64KB,而最后落到物理设备上确是合并后的结果,也就是用户发的256KB。跟期望的结果一致。
那么在这两个内核下到底是什么地方导致了结果的不同呢?
2.使用systemtap分析
首先简单介绍一下direct io时的io调用路径,这里以write为例。
1 | sys_write系统调用 --> vfs_write() --> do_sync_write() --> blkdev_aio_write() --> __generic_file_aio_write() --> generic_file_direct_write() --> blkdev_direct_IO() --> __blockdev_direct_IO() --> dio_bio_submit() -> submit_bio() --> generic_make_request() --> 会调用q->make_request_fn() --> blk_queue_bio(),在初始化scsi磁盘时注册的,这个函数处理io合并的(包含各个调度算法的调用) --> 最后会触发调用scsi_request_fn(),这个也是在初始化scsi磁盘的时候注册的,有多个地方触发scsi_request_fn --> scsi_dispatch_cmd下发到底层设备 |
然后使用systemtap在关键函数调用处探测,为了方便统计,fio块大小设置为32KB,device mapper的max_sectors_kb设置为16。
2.1 3.2.57内核下分析
1 | # echo 16 > /sys/block/dm-71/queue/max_sectors_kb |
1 | [generic_file_direct_write] size:32768 |
可以看到在调用blk_queue_bio之后就立即调用了scsi_request_fn
把scsi_request_fn的函数调用栈打印出来,可以看到确实是由blk_queue_bio触发的scsi_request_fn。
1 | 0xffffffffa000798a : scsi_request_fn+0x35/0x51e [scsi_mod] |
为了对比,下面看看3.10.11内核下的stap结果。
1 | =========================== |
2.2 3.10.11内核下分析
1 | # echo 16 > /sys/block/dm-65/queue/max_sectors_kb |
1 | [generic_file_direct_write] size:32768 |
可以看到第一次blk_queue_bio后(也就是刚好16kb时)并没有立即调用scsi_request_fn,而是在第二次(也就是第二个16kb,合并起来就是用户发的32kb)blk_queue_bio之后再触发的scsi_request_fn,而且下面列出scsi_request_fn的函数调用栈,可以看出并不是blk_queue_bio触发的scsi_request_fn。
1 | 0xffffffffa00070b6 : scsi_request_fn+0x39/0x4f4 [scsi_mod] |
3.分析blk_queue_bio代码
从上面的分析看出问题出在blk_queue_bio这个函数,下面就看看差别在哪。
3.2.57内核中blk_queue_bio
可以看到跟plug=current->plug这个有关,plug是blk_plug结构。
blk_plug允许构建一个缓存碎片IO的请求队列,能够保持io碎片一段时间,这样就能将顺序请求合并成一个大的请求。 合并后请求批量从per-task链表移动到设备请求队列,减少了设备请求队列锁竞争, 从而提高了效率。
blk_plug的使用很简单:
a)设置该线程开启请求合并模式 blk_start_plug
b)关闭线程请求合并 blk_finish_plug
那么上面描述的问题会不会与blk_start_plug()有关呢?
3.2.57内核中在generic_file_aio_write和generic_file_aio_read中都有调用blk_start_plug()
实际上在测试时的io不是由generic_file_aio_write去调用__generic_file_aio_write的,而是在blkdev_aio_write中调用的,
这条io路径的调用函数栈如下,在这条路径上并没有调用blk_start_plug,所以blk_queue_bio中就没有进入if (plug)的条件语句中。
1 | 0xffffffff811993d2 : generic_make_request+0x90/0xcf [kernel] |
而3.10.11内核中在generic_file_aio_write和generic_file_aio_read中都没有调用这个start plug的函数了,
而是在do_blockdev_direct_IO函数中调用的。
打印blk_start_plug的函数栈,可见确实是在do_blockdev_direct_IO中调用的。
1 | 0xffffffff811ae2a6 : blk_start_plug+0x1d/0x3b [kernel] |
4.内核patch变更
后来google了一下,从3.6-rc3版本就修复了这个问题,把blk_start_plug从generic_file_aio_write和generic_file_aio_read中移到了do_blockdev_direct_IO。
对应的patch“block: remove plugging at buffered write time” http://www.kernelhub.org/?p=2&msg=16336
关于这个patch的讨论参见 http://www.linuxhorizon.com/9-linux/aae0da2b1c65f3ef.htm#.U3ofDvmSzNd
5.结论
从上面的分析得出,blk_plug对io的合并会有影响,在使用device mapper或者软raid时,如果内核低于3.6,就会出现上面描述的问题:上层设备的max_sectors_kb影响了底层设备的io合并。所以建议在条件允许的情况下换成相对较新的内核使用。