Linux IO合并的问题
工作十年,第一次写博客,因为也是最近发现我的记忆力不好了,开始记不住很多东西,而且公司有要求要把做过的东西总结分享,这才发现了总结的好处,可以帮我更加的清楚的认识自己掌握知识的程度。
我遇到了这样一个问题,当我用如下的命令时,并没有产生IO合并
fio --name=read_64k --filename=/dev/sdb --ioengine=libaio --direct=1 --bs=64k --iodepth=32 --size=5G --rw=read --numjobs=1 -group_reporting
read_64k: (g=0): rw=read, bs=(R) 64.0KiB-64.0KiB, (W) 64.0KiB-64.0KiB, (T) 64.0KiB-64.0KiB, ioengine=libaio, iodepth=32
Run status group 0 (all jobs):
READ: bw=169MiB/s (178MB/s), 169MiB/s-169MiB/s (178MB/s-178MB/s), io=5120MiB (5369MB), run=30214-30214msec
Disk stats (read/write):
sdb: ios=81738/0, merge=0/0, ticks=961600/0, in_queue=924779, util=90.48%
按照我的理解,顺序读应该会有多个IO合并成一个IO递交到驱动层。但是无论我把block size改成4k还是512,都没有合并,fio输出的merge 统计依然是0。
然后当我将iodepth设置成33,奇怪的事情发生了
[root@localhost ~]# fio --name=read_64k --filename=/dev/sdb --ioengine=libaio --direct=1 --bs=64k --iodepth=33 --size=5G --rw=read --numjobs=1 -group_reporting
read_64k: (g=0): rw=read, bs=(R) 64.0KiB-64.0KiB, (W) 64.0KiB-64.0KiB, (T) 64.0KiB-64.0KiB, ioengine=libaio, iodepth=33
Run status group 0 (all jobs):
READ: bw=168MiB/s (176MB/s), 168MiB/s-168MiB/s (176MB/s-176MB/s), io=5120MiB (5369MB), run=30445-30445msec
Disk stats (read/write):
sdb: ios=79910/0, merge=1885/0, ticks=974993/0, in_queue=935900, util=88.38%
居然产生了1885个merge。看来内核块设备层对merge的操作和我的理解是有偏差的。
用ftrace跟踪了一下内核代码,梳理了工作的流程,先总结一个流程图,而后是我写了伪代码如下:
io_submit()
{
blk_make_request(bio) {
if (plug_merge(bio)) //尝试bio能否在蓄流表中merge
return;
if (try_sched_bio_merge()) //尝试能否和调度器中的req合并
return;
bio_to_rq(); //将bio转换成 request数据结构
add_rq_to_plug(); //将request添加到蓄流表中
}
blk_flush_plug_list() {
try_sched_insert_request() { //将蓄流表中的io 插入到调度器中
try_insert_merge(); //尝试是否能合并
run_hw_queue() {
if (sched_is_empty()) //如果调度器空了就退出
return;
if (has_buget()) { //判断硬件队列是否有空间
dispatch_request(); //将调度器中的IO下发请求给驱动
remove_request(); // 删除请求
}
}
}
}
}
由于ftrace抓的函数不完全,而不方便查看每一个包的情况,我在try_sched_insert_request函数里面添加了打印,然后又在remove_request 里面添加了打印。
为什么要在这里面添加打印呢,是这样的,try_sched_bio_merge()是执行合并的函数,合并后会调用merge_io_count()进行统计merge。在合并之前会去判断当前是否有发生过merge,也就是rq->last_merge这个变量,如果此变量为空,就不会进行合并。而修改这个变量的地方,正是try_sched_bio_merge()设置,remove_request()置空。
在iodepth=32的情况,这insert和remove两个打印成对出现的,而当iodepth=33时候,发现remove 的打印有时候没有,没有remove,rq->last_merge就不是空,那么下一次IO就会进入到try_sched_bio_merge()走合并流程,因为是顺序读,一定会产生合并。
如果没有remove,那么就说明has_buget()函数返回了false。深入has_buget函数,这个函数最后判断就是当前正在处理的IO个数busy和硬件队列的queue_depth的比较,如果busy 大于 queue_depth,就会返回false。于是,我打印了一下queue_depth,的确是32(其实也不需要打印,可以通过查看scsi对应的硬件队列深度即可)
cat /sys/bus/scsi/devices/1:0:0:0/queue_depth
通过这个命令查看,是32,这就能解释为什么iodepth大于32就会出现IO合并。而这个值是驱动层赋予的,与控制器硬件队列深度是相关联的。
总结
- IO合并并不是我们理解的多个小的连续的IO在调度器中合并,调度器是尽可能的把IO派发给驱动,而只有当驱动硬件队列满了,IO被滞留在调度器当中,才会和下一个IO尝试合并。
- IO合并的地方还包括蓄流表,蓄流表只有在多线程的情况下才会进行合并,因为蓄流表是一个进程才会有一个。