使用的电脑做网站的服务器,长沙企业网站建立,开发语言,html5产品展示网站模板如果有个进程正频繁的读写文件#xff0c;此时你vim查看一个新文件#xff0c;将会出现明显卡顿。即便你vim查看的文件只有几十M#xff0c;也可能会出现卡顿。相对的#xff0c;线上经常遇到IO敏感进程偶发IO超时问题。这些进程一次读写的文件数据量很少#xff0c;正常几… 如果有个进程正频繁的读写文件此时你vim查看一个新文件将会出现明显卡顿。即便你vim查看的文件只有几十M也可能会出现卡顿。相对的线上经常遇到IO敏感进程偶发IO超时问题。这些进程一次读写的文件数据量很少正常几十ms就能搞定但是超时一次读写文件竟耗时几百ms为什么会这样出问题的时间点IO流量很大磁盘IO使用率util接近100%磁盘IO带宽占满了IO压力太大。 原来IO敏感进程是受其他进程频繁读写文件影响导致的IO超时怎么解决这个问题呢磁盘选用nvme进程的IO优先级iorenice设置实时优先级可以一定程度缓解磁盘IO压力大场景IO敏感进程的IO超时问题但是还是有问题很好复现磁盘nvme、IO调度算法bfq、启动fio压测(10个线程128k随机写)cat读取200M大小的文件(cat进程的IO优先级设置为实时)耗时竟然会达到800ms多而在IO空闲时只耗时200ms左右
为什么会这样如果你用iostat看下fio压测时的io wait(平均IO延迟)数据发现打印的io wait 达到50ms是家常便饭。而我用systemtap抓取一下nvme盘此时DC耗时(IO请求在磁盘驱动层花费的时间)大于100ms的IO请求竟然是会频繁打印说明fio压测时有很多IO请求在nvme磁盘驱动的耗时都很大。调试显示nvme磁盘驱动队列深度是1024就是说驱动队列最多可以容纳1024个IO请求一个128K大小的IO请求传输完成耗时50us这1024个IO请求传输完成需耗时1024*50us50ms。fio压测时大部分时间nvme磁盘驱动队列都是占满的此时cat读取文件cat进程发送的每个IO请求大概率都排在nvme磁盘驱动队列尾都要等队列前边fio进程的IO请求传输完成。如此cat进程有很多IO请求在磁盘驱动层的耗时都达到50ms左右那怪不得fio压测时cat读取文件慢了很多。 能否改善这种情况呢磁盘nvme、IO优先级设置为实时也没用能否在cat读取文件过程控制nvme磁盘驱动队列的IO请求数不要占满比如nvme磁盘驱动队列的IO请求数控制在100。这样fio压测时因为nvme磁盘驱动队列的IO请求数不超过100此时cat读取文件时cat进程的IO请求即便不幸插入到nvme磁盘驱动队列尾这个IO请求传输完成最大耗时也只有100*50us5ms。如果能达到这种效果IO压力大时IO敏感进程IO超时问题就能得到明显改善了。 按照这个思路目前已经实现了预期效果本文主要介绍设计思路。这个设计思路是在bfq算法基础上实现的核心思想是控制派发给nvme磁盘驱动的IO请求数不超过某个阀值。思路很简单但是开发过程遇到的问题是个血泪史本文基于centos 8.3内核版本4.18.0-240.el8探索下bfq算法详细源码注释见 https://github.com/dongzhiyan-stack/linux-4.18.0-240.el8。
注意本文将IO请求简称rq或者req。另外本文的测试环境是centos 8.3虚拟机。阅读本文前希望读者先看看我写的《linux内核block层Multi queue多队列核心点分析》。这篇文章是针对block层Multi queue(简称blk-mq) 多队列基础知识点总结。
1核心优化思路
先看一次普通的读文件触发的IO派发流程
[ffffb71980cbb6b8] scsi_queue_rq at ffffffffb71d1a51[ffffb71980cbb708] blk_mq_dispatch_rq_list at ffffffffb7009f4c[ffffb71980cbb7d8] blk_mq_do_dispatch_sched at ffffffffb700f4ba[ffffb71980cbb830] __blk_mq_sched_dispatch_requests at ffffffffb700ff99[ffffb71980cbb890] blk_mq_sched_dispatch_requests at ffffffffb7010020[ffffb71980cbb8a0] __blk_mq_run_hw_queue at ffffffffb70076a1[ffffb71980cbb8b8] __blk_mq_delay_run_hw_queue at ffffffffb7007f61[ffffb71980cbb8e0] blk_mq_sched_insert_requests at ffffffffb7010351[ffffb71980cbb918] blk_mq_flush_plug_list at ffffffffb700b4d6[ffffb71980cbb998] blk_flush_plug_list at ffffffffb6fffbe7[ffffb71980cbb9e8] blk_mq_make_request at ffffffffb700ad38[ffffb71980cbba78] generic_make_request at ffffffffb6ffe85f[ffffb71980cbbad0] submit_bio at ffffffffb6ffeadc[ffffb71980cbbb10] ext4_mpage_readpages at ffffffffc081b9a4 [ext4][ffffb71980cbbbf8] read_pages at ffffffffb6e3743b[ffffb71980cbbc70] __do_page_cache_readahead at ffffffffb6e37721[ffffb71980cbbd08] ondemand_readahead at ffffffffb6e37939[ffffb71980cbbd50] generic_file_buffered_read at ffffffffb6e2ce5f[ffffb71980cbbe40] new_sync_read at ffffffffb6ed8841[ffffb71980cbbec8] vfs_read at ffffffffb6edb1c1
可以发现派发IO最后的流程是__blk_mq_sched_dispatch_requests-blk_mq_do_dispatch_sched-blk_mq_dispatch_rq_list也与本次的性能优化有关。看下blk_mq_do_dispatch_sched函数源码
static int blk_mq_do_dispatch_sched(struct blk_mq_hw_ctx *hctx){ struct request_queue *q hctx-queue; struct elevator_queue *e q-elevator; LIST_HEAD(rq_list); int ret 0; do { struct request *rq; //bfq_has_work if (e-type-ops.has_work !e-type-ops.has_work(hctx)) break; if (!list_empty_careful(hctx-dispatch)) { ret -EAGAIN; break; } if (!blk_mq_get_dispatch_budget(hctx)) break; //调用bfq调度器IO派发函数bfq_dispatch_request rq e-type-ops.dispatch_request(hctx); if (!rq) { blk_mq_put_dispatch_budget(hctx); blk_mq_delay_run_hw_queues(q, BLK_MQ_BUDGET_DELAY); break; } list_add(rq-queuelist, rq_list); /*取出rq_list链表上的req派发给磁盘驱动如果因驱动队列繁忙或者nvme硬件繁忙导致派发失败则把rq添加hctx-dispatch等稍后派发遇到rq派发失败返回false退出while循环*/ } while (blk_mq_dispatch_rq_list(q, rq_list, true)); return ret;}
该函数作用是执行bfq_dispatch_request()函数循环从IO调度器队列取出IO请求存入rq_list链表然后取出rq_list链表上的rq执行blk_mq_dispatch_rq_list()派发给磁盘驱动。blk_mq_dispatch_rq_list()函数如果因驱动队列繁忙或者磁盘硬件繁忙导致派发失败则返回false此时blk_mq_do_dispatch_sched()函数退出while循环。当然如果IO调度器队列没IO请求了bfq_dispatch_request返回NULL此时blk_mq_do_dispatch_sched()函数也会退出while循环。把blk_mq_dispatch_rq_list源码简单列下
bool blk_mq_dispatch_rq_list(struct request_queue *q, struct list_head *list, bool got_budget){ struct blk_mq_hw_ctx *hctx; struct request *rq, *nxt; bool no_tag false; int errors, queued; blk_status_t ret BLK_STS_OK; bool no_budget_avail false; ................ errors queued 0; do { struct blk_mq_queue_data bd; rq list_first_entry(list, struct request, queuelist); hctx rq-mq_hctx; ................ list_del_init(rq-queuelist); bd.rq rq; if (list_empty(list)) bd.last true; else { nxt list_first_entry(list, struct request, queuelist); bd.last !blk_mq_get_driver_tag(nxt); } //把rq派发给驱动 ret q-mq_ops-queue_rq(hctx, bd);//scsi_queue_rq 或 nvme_queue_rq //这个if成立应该说明是 驱动队列繁忙 或者nvme硬件繁忙不能再向驱动派发IO因此本次的rq派发失败 if (ret BLK_STS_RESOURCE || ret BLK_STS_DEV_RESOURCE) { if (!list_empty(list)) { //把rq在list链表上的下一个req的tag释放了搞不清楚为什么 nxt list_first_entry(list, struct request, queuelist); blk_mq_put_driver_tag(nxt); } //把派发失败的rq再添加到list链表 list_add(rq-queuelist, list); __blk_mq_requeue_request(rq); break; } ........... //派发rq失败则queued加1 queued; //一直派发list链表上的req直到list链表空 } while (!list_empty(list)); hctx-dispatched[queued_to_index(queued)]; //如果list链表上还有rq说明派发rq时遇到驱动队列或者硬件繁忙rq没有派发成功 if (!list_empty(list)) { ........... spin_lock(hctx-lock); //list上没有派发成功的rq添加到hctx-dispatch链表稍后延迟派发 list_splice_tail_init(list, hctx-dispatch); spin_unlock(hctx-lock); ...................... blk_mq_update_dispatch_busy(hctx, true); return false; } else blk_mq_update_dispatch_busy(hctx, false); //派发rq时遇到驱动队列或者硬件繁忙返回false否则派发正常下边返回true if (ret BLK_STS_RESOURCE || ret BLK_STS_DEV_RESOURCE) return false; return (queued errors) ! 0;}
该函数只是取出list链表上的rq派发给磁盘驱动如果因驱动队列繁忙或者磁盘硬件繁忙导致派发失败则把rq添加hctx-dispatch等稍后派发。本文的IO优化算法是在bfq算法基础上实现的最好先对bfq算法有个了解希望重点看下《内核block层IO调度器—bfq算法之1整体流程介绍》、《内核block层IO调度器—bfq算法之3源码要点总结》、《内核block层IO调度器—bfq算法深入探索2》这3篇文章。
bfq算法把进程传输的IO归为3类in_large_burst型IO、交互式IO、实时性IO。fio这种短时间多个线程派发IO的属于in_large_burst型IO进程偶尔读写一次文件且数据量不大的属于交互式IO进程周期性的读写文件且数据量不大的属于实时性IO。这3种IO模型的对IO时延要求依次增加 bfq算法定义了bfqq-wr_coeff变量这个权重系数来表达这种特性针对这3中IO模型依次是1、30、30* 100。bfqq-wr_coeff越大派发IO的进程绑定的bfqq插入st-active tree(可以理解成IO运行队列)越靠左这样可以更早被bfq调度器调度选中进而更早得到派发该bfqq对应进程的IO保证了低延迟。
本案例的场景是在IO压力大时怎么降低IO敏感进程的时延。怎么模拟这种场景呢fio压测模拟IO压力大然后cat kern读取文件(kern文件几百M)作为IO敏感进程。在开启fio压测下cat kern读取文件观察cat kern耗时。在磁盘空闲时cat kern只耗时不到100ms。在开启fio压测情况cat kern耗时500ms。如果我的IO优化方案生效则需要实现在开启fio压测情况下cat kern耗时小于500ms比如200ms、300ms。这是虚拟机里的测试数据每次不太稳定。
ok具体代码在何处实现呢首先是把IO请求插入bfq IO算法队列执行的bfq_insert_request()-__bfq_insert_request()-bfq_add_request()函数添加如下红色代码
/*高优先级rq*/ #define RQF_HIGH_PRIO ((__force req_flags_t)(1 21)) static void bfq_add_request(struct request *rq){ if (!bfq_bfqq_busy(bfqq)){ bfq_bfqq_handle_idle_busy_switch(bfqd, bfqq, old_wr_coeff,rq, interactive); } .............. if(bfqq-wr_coeff 30){ //设置rq高优先级 rq-rq_flags | RQF_HIGH_PRIO; }}
if(bfqq-wr_coeff 30)成立说明当前IO传输的进程绑定的bfqq拥有高优先级rq属性则执行rq-rq_flags | RQF_HIGH_PRIO对rq设置高优先级rq标志。
这里插一句本文的测试环境是在fio压测情况观察cat kern读取文件的耗时。bfq算法中针对fio这种频繁派发IO的进程fio进程属于burst型IO它的进程的bfqq对应的bfqq-wr_coeff大部分情况是1。而针对cat这种偶尔读取一次文件的进程是交互式IO该进程的bfqq的bfqq-wr_coeff初值是30。显然cat kern读取文件过程cat进程派发的IO大部分拥有高优先级rq属性这是本文的IO性能优化方案的设计思路。
接着是从bfq IO算法队列派发IO请求执行的blk_mq_dispatch_rq_list()源码有删减红色是性能优化添加的代码
static struct request *__bfq_dispatch_request(struct blk_mq_hw_ctx *hctx){ struct bfq_data *bfqd hctx-queue-elevator-elevator_data; struct request *rq NULL; struct bfq_queue *bfqq NULL; int direct_dispatch 0; //不经IO算法队列直接派发的rq if (!list_empty(bfqd-dispatch)) { rq list_first_entry(bfqd-dispatch, struct request,queuelist); list_del_init(rq-queuelist); bfqq RQ_BFQQ(rq); direct_dispatch 1; if (bfqq) { bfqq-dispatched; goto inc_in_driver_start_rq; } goto start_rq; } ..................... bfqq bfq_select_queue(bfqd); if (!bfqq) goto exit; rq bfq_dispatch_rq_from_bfqq(bfqd, bfqq); if (rq) { if(bfqd-queue-high_io_prio_enable) { if(rq-rq_flags RQF_HIGH_PRIO){//高优先级IO //第一次遇到high prio io置1 bfq_high_io_prio_mode启动3s定时器定时到了对bfq_high_io_prio_mode清0 if(bfqd-bfq_high_io_prio_mode 0){ bfqd-bfq_high_io_prio_mode 1; hrtimer_start(bfqd-bfq_high_prio_timer, ms_to_ktime(3000),HRTIMER_MODE_REL); } } else非高优先级IO { if(bfqd-bfq_high_io_prio_mode) { //在 bfq_high_io_prio_mode 非0时间的5s内如果遇到非high prio io并且驱动队列IO个数大于限制则把不派发该IO而是临时添加到bfq_high_prio_tmp_list链表 if((bfqd-rq_in_driver 20) /* (bfqd-bfq_high_prio_tmp_list_rq_count 100)*/){ list_add_tail(rq-queuelist,bfqd-bfq_high_prio_tmp_list);//bfq_high_prio_tmp_list链表上rq的个数加1 bfqd-bfq_high_prio_tmp_list_rq_count ; rq NULL; goto exit1; } } } } /*如果 bfq_high_prio_tmp_list 链表上有rq要派发不执行这里的rq_in_driver在下边的exit那里会执行。当echo 0 /sys/block/sdb/process_high_io_prio 置1再置0后这个if判断就起作用了。没这个判断这里会bfqd-rq_in_driver下边的if里再bfqd-rq_in_driver导致rq_in_driver泄漏*/ if((rq-rq_flags RQF_HIGH_PRIO) || list_empty(bfqd-bfq_high_prio_tmp_list)){inc_in_driver_start_rq: bfqd-rq_in_driver;start_rq: rq-rq_flags | RQF_STARTED; } }exit: //1:如果是高优先级IO该if不成立直接跳过。 2:如果非高优先级IO则把rq添加到bfq_high_prio_tmp_list尾从链表头选一个rq派发 3:如果rq是NULL则也从bfq_high_prio_tmp_list选一个rq派发 if(!direct_dispatch ((rq !(rq-rq_flags RQF_HIGH_PRIO)) || !rq)){ /*如果bfq_high_prio_tmp_list有Io, 则不派发本次的io而添加到bfq_high_prio_tmp_list尾部实际从bfq_high_prio_tmp_list链表头取出一个IO派发。放到 if(bfqd-queue-high_io_prio_enable)外边是为了保证一旦设置high_io_prio_enable为0还能派发残留的在bfq_high_prio_tmp_list上的IO*/ if(!list_empty(bfqd-bfq_high_prio_tmp_list)){ if(rq){ list_add_tail(rq-queuelist,bfqd-bfq_high_prio_tmp_list); bfqd-bfq_high_prio_tmp_list_rq_count ; } rq list_first_entry(bfqd-bfq_high_prio_tmp_list, struct request, queuelist); list_del_init(rq-queuelist); //bfq_high_prio_tmp_list链表上rq的个数减1 bfqd-bfq_high_prio_tmp_list_rq_count --; bfqd-rq_in_driver; rq-rq_flags | RQF_STARTED; } }exit1: .................. return rq;}
该函数中首先执行bfqq bfq_select_queue(bfqd)算法本次派发rq的bfqq然后执行rq bfq_dispatch_rq_from_bfqq(bfqd, bfqq)从bfqq的IO队列取出本次派发的IO请求。后边的就是针对本次性能优化添加的代码。bfqd-queue-high_io_prio_enable是一个使能开关执行echo 1 /sys/block/sdb/process_high_io_prio才会打开本文的性能优化功能。继续如果派发的rq有高优先级属性(即rq-rq_flags RQF_HIGH_PRIO返回true)则bfqd-bfq_high_io_prio_mode 1置1这是进入派发高优先级IO的开始标志。然后执行hrtimer_start(bfqd-bfq_high_prio_timer, ms_to_ktime(3000),HRTIMER_MODE_REL)启动3s定时器3s后在定时器函数里令bfqd-bfq_high_io_prio_mode 0这是派发高优先级IO的结束标志。
ok在第一次遇到派发的rq有高优先级属性后就会令bfqd-bfq_high_io_prio_mode 1置1并进入” 派发高优先级IO”的3s时期。这段时间只有rq有高优先级属性才会会作为__bfq_dispatch_request()返回的rq真正得到机会派发给磁盘驱动。否则普通的rq就要执行list_add_tail(rq-queuelist,bfqd-bfq_high_prio_tmp_list)暂时添加到bfqd-bfq_high_prio_tmp_list链表延迟派发当然前提要有bfqd-rq_in_driver 20成立就是说派发给磁盘驱动但还没传输完成的IO数要达到某个阀值(我在虚拟机里测试的sda机械盘磁盘队列深度是32nvme盘队列深度达到1000多建议这个阀值达到磁盘队列深度的60%以上)。 为什么要这么设计其实就是要在派发给磁盘驱动但还没传输完成的IO数达到磁盘队列深度的某个阀值后(之后再派发IO可能就会把磁盘驱动IO队列占满了)此时正好有进程要派发IO敏感的IO请求(这些IO请求rq标记有RQF_HIGH_PRIO属性)优先派发IO敏感进程的IO延迟派发普通进程的IO(就是把这些rq暂时添加到bfqd-bfq_high_prio_tmp_list链表)。等系统空闲后IO敏感进程的IO都派发完了再从bfqd-bfq_high_prio_tmp_list链表取出延迟派发的IO而继续派发。 简单说在普通进程和IO敏感进程同时派发IO时在普通进程的IO把磁盘驱动IO队列快占满前限制普通进程向磁盘驱动IO队列派发的IO数防止把磁盘驱动IO队列占满。此时呢要优先派发IO敏感进程的IO到磁盘驱动队列的IO。通过这个方法防止在IO压力很大时影响IO敏感进程派发IO的时延。
2 实现IO性能优化效果的曲折过程
开始测试虚拟机centos 8.3系统。先执行echo 1 /sys/block/sdb/process_high_io_prio打开本文的IO性能优化功能。然后启动fio压测同时time cat kern /dev/null读取文件并打印耗时(kern文件大小300M)。没想到竟然一点效果没有以下是测试数据
1echo 1 /sys/block/sdb/process_high_io_prio打开IO性能优化功能开启fio压测cat kern耗时500ms左右偶尔会出现耗时800ms甚至1s2echo 0 /sys/block/sdb/process_high_io_prio关闭IO性能优化功能开启fio压测cat kern耗时500ms左右偶尔会出现耗时800ms甚至1s3echo 1 /sys/block/sdb/process_high_io_prio打开IO性能优化功能关闭fio压测cat kern耗时不到100ms
总结下在磁盘IO空闲时cat kern耗时不到100ms而在fio压测情况下开启和关闭IO性能优化cat kern耗时没有区别。甚至多次测试后发现开启IO性能比关闭IO性能优化cat kern更耗时。这就说明本文的IO性能优化方案不仅没起到作用反而拖了后腿这就需要找下原因了 此时在之前”统计进程派发IO的延迟”功能的帮助下发现开启IO性能优化功能时启动fio压测cat kern读取文件派发IO过程cat进程的id耗时(IO请求在IO队列的耗时)明显偏大 id耗时(IO请求在磁盘驱动层的耗时)也没有缩短。再进一步排查发现在fio压测时当cat进程有IO要派发而插入bfq 的IO算法队列后cat进程的bfqq竟然经常出现过了10ms才得到调度机会就是说fio压测时当cat进程要派发IO时fio一直占着IO派发机会cat进程推迟10ms才得到派发IO机会。 怎么解决这个问题首要目的是降低cat kern进程的延迟就是要让cat进程的来了IO请求后尽快得到调度派发。怎么实现需要增大cat进程的bfqq-wr_coeff这样cat进程绑定的bfqq插入st-active tree(可以理解成IO运行队列)后才能尽可能早的被IO调度器选中进而派发cat进程的IO得到调度延迟的效果。经过繁琐的调试这样调整优化方案
在进程bfqq派发派发IO请求过程因为配额没了而过期失效然后重新加入st-active tree执行的__bfq_requeue_entity()函数中
static void __bfq_requeue_entity(struct bfq_entity *entity){ struct bfq_sched_data *sd entity-sched_data; struct bfq_service_tree *st bfq_entity_service_tree(entity); //如果bfqq-wr_coeff是30说明是交互式io执行到这里说明派发这个进程派发的IO太多了配合消耗完了还没派发完io。此时说明该进程的bfqq需要提升权重提高优先级作为high prio io. struct bfq_queue *bfqq bfq_entity_to_bfqq(entity); if(bfqq bfqq-bfqd-queue-high_io_prio_enable bfqq-wr_coeff 30){ bfqq-wr_coeff 30 * BFQ_HIGH_PRIO_IO_WEIGHT_FACTOR; //置1表示权重变了然后才会在bfq_update_fin_time_enqueue-__bfq_entity_update_weight_prio 里真正提升权重 entity-prio_changed 1; //增大权重提升时间为1.5s bfqq-wr_cur_max_time msecs_to_jiffies(1500); //权重提升时间开始时间为当前时间 bfqq-last_wr_start_finish jiffies; bfqq-entity.completed_size 0; } ............. if (entity-tree) bfq_active_extract(st, entity); bfq_update_fin_time_enqueue(entity, st, false); }
cat进程最初派发IO时被判定为交互式IObfqq-wr_coeff是30。实际测试表明cat进程因为派发IO很多导致的bfqq第一次过期失效是配额耗尽而过期失效。此时cat进程的bfqq是要重新插入st-active tree而等待bfq调度器再次被选中派发IO执行的正是__bfq_requeue_entity()函数在__bfq_requeue_entity()函数中发现cat进程bfqq的bfqq-wr_coeff是30就增大bfqq-wr_coeff为bfqq-wr_coeff 30 * BFQ_HIGH_PRIO_IO_WEIGHT_FACTORBFQ_HIGH_PRIO_IO_WEIGHT_FACTOR是50。 还有一个重点是bfqq-wr_cur_max_time msecs_to_jiffies(1500)这是cat进程的bfqq权重系数增大为30 * BFQ_HIGH_PRIO_IO_WEIGHT_FACTOR的时间期限bfqq-last_wr_start_finish jiffies是cat进程的bfqq权重系数增大为30 * BFQ_HIGH_PRIO_IO_WEIGHT_FACTOR的起始时间。这样设置过后从当前时间起的1.5s内cat进程的bfqq-wr_coeff的都是30 * BFQ_HIGH_PRIO_IO_WEIGHT_FACTOR这样的效果就是这段时间cat进程的bfqq插入st-active tree后能尽可能被bfq调度器选中派发IO大大降低延迟 在插入IO请求函数bfq_add_request()中遇到bfqq-wr_coeff是30 * BFQ_HIGH_PRIO_IO_WEIGHT_FACTOR的进程bfqq才会把该bfqq的IO设置高优先级标志RQF_HIGH_PRIO。这样是为了过滤bfqq-wr_coeff是30的进程的IO不让这种IO被判定为高优先级IO。
#define BFQ_HIGH_PRIO_IO_WEIGHT_FACTOR 50static void bfq_add_request(struct request *rq){ if (!bfq_bfqq_busy(bfqq)){ bfq_bfqq_handle_idle_busy_switch(bfqd, bfqq, old_wr_coeff,rq, interactive); } ............. if(bfqq-wr_coeff 30*BFQ_HIGH_PRIO_IO_WEIGHT_FACTOR){ //设置rq高优先级 rq-rq_flags | RQF_HIGH_PRIO; }}
在cat进程因没有IO请求派发而过期失效加入st-idle tree。然后过了一段时间又来了新的IO请求此时需要执行bfq_add_request()-bfq_bfqq_handle_idle_busy_switch()激活cat进程的bfqq把bfqq插入st-active tree。在这个函数中强制cat进程的bfqq-wr_coeff保持30*BFQ_HIGH_PRIO_IO_WEIGHT_FACTOR不受bfq_bfqq_handle_idle_busy_switch()原生代码的影响具体实现看如下红色代码。
static void bfq_bfqq_handle_idle_busy_switch(struct bfq_data *bfqd, struct bfq_queue *bfqq, int old_wr_coeff, struct request *rq, bool *interactive){ //禁止high prio io进程被判定为rt、interactive 、burst 型io这样下边的bfq_update_bfqq_wr_on_rq_arrival()函数不会修改它的 bfqq-wr_coeff if(bfqq-wr_coeff 30*BFQ_HIGH_PRIO_IO_WEIGHT_FACTOR){ *interactive 0; wr_or_deserves_wr 0; in_burst 0; soft_rt 0; } ................ bfq_update_bfqq_wr_on_rq_arrival(bfqd, bfqq, old_wr_coeff, wr_or_deserves_wr, *interactive, in_burst, soft_rt); ................}
在cat进程的bfqq被bfq调度器选中派发IO后每次执行派发IO执行__bfq_dispatch_request()-bfq_dispatch_rq_from_bfqq()-bfq_update_wr_data()过程都会检查cat进程的bfqq权重提升时间是否到了到了的话就要令bfqq的权重提升时间结束令bfqq-wr_coeff重置为1之后cat进程的bfqq就不再享有低延时派发特性了。在结束进程权重提升bfq_update_wr_data()函数需要添加如下红色代码否则会导致cat进程的bfqq的bfqq-wr_coeff被设置为30*BFQ_HIGH_PRIO_IO_WEIGHT_FACTOR后很短时间就会执行里边的bfq_bfqq_end_wr()令bfqq-wr_coeff重置为1。
static void bfq_update_wr_data(struct bfq_data *bfqd, struct bfq_queue *bfqq){ struct bfq_entity *entity bfqq-entity; if (bfqq-wr_coeff 1) { ............... if (bfq_bfqq_in_large_burst(bfqq)){ bfq_bfqq_end_wr(bfqq); } else if (time_is_before_jiffies(bfqq-last_wr_start_finish bfqq-wr_cur_max_time)) { if (bfqq-wr_cur_max_time ! bfqd-bfq_wr_rt_max_time || time_is_before_jiffies(bfqq-wr_start_at_switch_to_srt bfq_wr_duration(bfqd))) { bfq_bfqq_end_wr(bfqq); } else { switch_back_to_interactive_wr(bfqq, bfqd); bfqq-entity.prio_changed 1; } } if (bfqq-wr_coeff 1 bfqq-wr_cur_max_time ! bfqd-bfq_wr_rt_max_time bfqq-service_from_wr max_service_from_wr bfqq-wr_coeff ! 30 * BFQ_HIGH_PRIO_IO_WEIGHT_FACTOR)//high prio io进程禁止在这里结束权重提升 { bfq_bfqq_end_wr(bfqq); } } if ((entity-weight entity-orig_weight) ! (bfqq-wr_coeff 1)){ __bfq_entity_update_weight_prio(bfq_entity_service_tree(entity), entity, false); }}
在派发IO请求的bfq_dispatch_rq_from_bfqq()函数添加如下代码
static struct request *bfq_dispatch_rq_from_bfqq(struct bfq_data *bfqd, struct bfq_queue *bfqq) { struct request *rq bfqq-next_rq; unsigned long service_to_charge; service_to_charge bfq_serv_to_charge(rq, bfqq); bfq_bfqq_served(bfqq, service_to_charge); bfq_dispatch_remove(bfqd-queue, rq); if (bfqq ! bfqd-in_service_queue) goto return_rq; if(bfqd-queue-high_io_prio_enable){ if(bfqq-wr_coeff 30 * BFQ_HIGH_PRIO_IO_WEIGHT_FACTOR){ //累加bfqq传输完成的rq的数据量,如果bfqq传输数据量太多而超过限制强制令进程bfqq不再有high prio io属性 bfqq-entity.completed_size blk_rq_bytes(rq); if(bfqq-entity.completed_size bfqd-high_prio_io_all_size_limit){ bfq_bfqq_end_wr(bfqq); } } } bfq_update_wr_data(bfqd, bfqq); ...................}
这是令被判定为高优先级IO的进程派发的数据量超过bfqd-high_prio_io_all_size_limit阀值(200M或者300M)后就结束该进程的高优先级IO属性具体是执行bfq_bfqq_end_wr(bfqq)令bfqq-wr_coeff由30 * BFQ_HIGH_PRIO_IO_WEIGHT_FACTOR降低为1这就是普通IO了。这样做是为了防止fio这种频繁数据传输IO的进程被长时间判定为高优先级IO因为fio进程最初派发IO时被判定为交互式IOfqq-wr_coeff 30。然后因配额耗尽而执行__bfq_requeue_entity()重新加入st-active tree时因为bfqq-wr_coeff 是30则fqq-wr_coeff 30* BFQ_HIGH_PRIO_IO_WEIGHT_FACTOR。这样fio进程就被判定为高优先级IO了这个是没办法避免的但是等fio派发IO的数据量超过bfqd-high_prio_io_all_size_limit就强制令fio结束高优先级IO属性。
这样终于实现了IO性能优化效果 echo 1 /sys/block/sdb/process_high_io_prio打开IO性能优化功能开启fio压测cat kern耗时只有200ms左右
1echo 1 /sys/block/sdb/process_high_io_prio打开IO性能优化功能开启fio压测cat kern耗时200ms左右偶尔会出现耗时800m但出现概率低2echo 1 /sys/block/sdb/process_high_io_prio关闭IO性能优化功能开启fio压测cat kern耗时200ms左右但设置cat进程的IO调度算法为RT偶尔会出现耗时800ms但出现概率更高
可以发现本文的性能优化效果比设置IO调度算法为RT更优。这说明本文的IO性能优化算法——降低磁盘驱动队列深度而降低IO敏感进程的IO在磁盘驱动的耗时终于起到了作用因为这是在虚拟机里做的测试性能不太稳定。如果在PC本地测试性能稳定很多但是测试规律跟上边一致。
3其他优化方案
如上方案终于实现了预期效果但是还有还有其他性能优化点。主要是在IO请求插入IO队列的bfq_add_request()函数
#define BFQ_HIGH_PRIO_IO_WEIGHT_FACTOR 50static void bfq_add_request(struct request *rq){ if (!bfq_bfqq_busy(bfqq)){ bfq_bfqq_handle_idle_busy_switch(bfqd, bfqq, old_wr_coeff,rq, interactive); } .............. //如果同一个线程组的进程近期有in_large_burst属性禁止它新创建的线程被判定为交互式io if(bfq_bfqq_in_large_burst(bfqq)){ if(current-tgid ! bfqd-large_burst_process_tgid){ bfqd-large_burst_process_tgid current-tgid; strncpy(bfqd-large_burst_process_name,current-comm,COMM_LEN-1); bfqd-large_burst_process_count 0; }else{ bfqd-large_burst_process_count ; } } if(bfqq-wr_coeff 30*BFQ_HIGH_PRIO_IO_WEIGHT_FACTOR){ //设置rq高优先级 rq-rq_flags | RQF_HIGH_PRIO; }}
把IO请求插入IO队列把进程bfqq激活插入st-active tree执行的bfq_bfqq_handle_idle_busy_switch()函数中添加如下代码
static void bfq_bfqq_handle_idle_busy_switch(struct bfq_data *bfqd, struct bfq_queue *bfqq, int old_wr_coeff, struct request *rq, bool *interactive){ bool soft_rt, in_burst, wr_or_deserves_wr, bfqq_wants_to_preempt, idle_for_long_time bfq_bfqq_idle_for_long_time(bfqd, bfqq), ................... in_burst bfq_bfqq_in_large_burst(bfqq); soft_rt bfqd-bfq_wr_max_softrt_rate 0 !BFQQ_TOTALLY_SEEKY(bfqq) !in_burst time_is_before_jiffies(bfqq-soft_rt_next_start) bfqq-dispatched 0; *interactive !in_burst idle_for_long_time; //如果同一个线程组的进程近期有in_large_burst属性禁止它新创建的线程被判定为交互式io if((bfqd-large_burst_process_count 1) (bfqd-large_burst_process_tgid current-tgid) (strncmp(bfqd-large_burst_process_name,current-comm,COMM_LEN-1) 0)){ *interactive 0; soft_rt 0; in_burst 1; bfq_prevent_high_prio_count; } /*该if成立说明当前进程最近被判定为high prio io。这样等该进程再进程新的IO传输时强制令该进程被判定为 high prio io。否则只能被判断为交互式 io。bfqq-bfqq_list 是NULL说明该进程是新创建的。否则可能该bfqq过期失效而处于st-idle tree现在又派发rq此时该if不成立。*/ if((bfqq-wr_coeff 1) list_empty(bfqq-bfqq_list) (strncmp(bfqd-last_high_prio_io_process,current-comm,COMM_LEN-1)) 0){ bfqq-wr_coeff 30*BFQ_HIGH_PRIO_IO_WEIGHT_FACTOR; } wr_or_deserves_wr bfqd-low_latency (bfqq-wr_coeff 1 || (bfq_bfqq_sync(bfqq) bfqq-bic (*interactive || soft_rt))); //禁止high prio io进程被判定为rt、interactive 、burst 型io这样下边的bfq_update_bfqq_wr_on_rq_arrival()函数不会修改它的 bfqq-wr_coeff if(bfqq-wr_coeff 30*BFQ_HIGH_PRIO_IO_WEIGHT_FACTOR){ *interactive 0; wr_or_deserves_wr 0; in_burst 0; soft_rt 0; //保存最近high prio io进程的名字 strncpy(bfqd-last_high_prio_io_process,current-comm,COMM_LEN-1); } ................ bfq_update_bfqq_wr_on_rq_arrival(bfqd, bfqq, old_wr_coeff, wr_or_deserves_wr, *interactive, in_burst, soft_rt); ................}
在进程绑定的bfqq初始化函数 bfq_init_bfqq()中对bfqq-bfqq_list初始化表示bfqq是新创建的。
static void bfq_init_bfqq(struct bfq_data *bfqd, struct bfq_queue *bfqq, struct bfq_io_cq *bic, pid_t pid, int is_sync)
{ //bfqq创建时对bfqq-bfqq_list初始化 INIT_LIST_HEAD (bfqq-bfqq_list);
}
在bfq_add_request()、bfq_bfqq_handle_idle_busy_switch()中添加的代码的代码主要是两个作用。
1保存最近被判定被高优先级IO的进程名字(比如cat)到bfqd-last_high_prio_io_process。后续如果再有同样进程名字的进程派发IO则立即令进程被判定为高优先级IO。这段代码是bfq_bfqq_handle_idle_busy_switch()函数if((bfqq-wr_coeff 1) list_empty(bfqq-bfqq_list) (strncmp(bfqd-last_high_prio_io_process,current-comm,COMM_LEN-1)) 0) bfqq-wr_coeff 30*BFQ_HIGH_PRIO_IO_WEIGHT_FACTOR这段代码。
注意正常情况一个进程最早开始派发IO时只是被判定为交互式IObfqq-wr_coeff只有30。然后该进程被bfq调度器选中派发IO接着因为配额消耗完而过期失效执行__bfq_requeue_entity()重新加入st-active tree等待被bfq调度器重新调度。此时在__bfq_requeue_entity()函数中因为bfqq-wr_coeff是30才会判定这个进程被高优先级IO。总之这优化点是保证进程一开始派发IO就能被判定为高优先级IO一开始就保证降低IO调度延迟。
2保存最近被判定为in_large_burst型IO的进程名字到bfqd-large_burst_process_name。这样后续再有同样进程名字的新进程派发IO 或者 原本在st-idle tree但来了新的IO而激活加入st-active tree这两种情况进程都会被判定为交互式IObfqq-wr_coeff 赋值30。然后等bfq调度器选中该进程派发IO后该进程因为配额消耗光而过期失效此时是要执行__bfq_requeue_entity()重新加入st-active tree。而在__bfq_requeue_entity()函数中因为bfqq-wr_coeff是30则该进程也会被判定高优先级IO。这样fio压测的进程有可能被判定为高优先级IO进而影响cat 进程派发IO。
解决方案正是bfqd-large_burst_process_name因为fio压测进程会被判定为in_large_burst型IObfqd-large_burst_process_name记录该进程名字fio等后续再有fio压测或者fio进程从原本在st-idle tree但来了新的IO而激活加入st-active tree执行到bfq_bfqq_handle_idle_busy_switch()函数的if((bfqd-large_burst_process_count 1) (bfqd-large_burst_process_tgid current-tgid) (strncmp(bfqd-large_burst_process_name,current-comm,COMM_LEN-1) 0))强制赋值bfqq-wr_coeff为1就是强制作为普通IO没有高优先级属性。这个性能优化点就是避免fio这种IO流量的但时延不敏感的进程影响IO时延敏感进程派发IO。
接着还有一个性能优化点在fio压测时cat 进程读取文件而加入st-active tree即便cat进程被判定为高优先级IO但是也有可能因fio频繁派发IO导致cat进程延迟被bfq调度器选中派发IO。于是加入了高优先级IO进程bfqq在加入st-active tree后超时强制派发机制。代码实现如下
static void __bfq_activate_entity(struct bfq_entity *entity, bool non_blocking_wait_rq){ struct bfq_service_tree *st bfq_entity_service_tree(entity); bool backshifted false; unsigned long long min_vstart; struct bfq_queue *bfqq bfq_entity_to_bfqq(entity); //high prio io的bfqq记录激活加入st-active tree的时间点。在 high_prio_io_schedule_deadline 时间点到期后该bfqq必须被调度到派发rq。bfqq-deadline_list-prev 和 next 必须是LIST_POISON2/LIST_POISON1 说明没有添加到链表上 if((bfqq-deadline_list.prev LIST_POISON2) (bfqq-deadline_list.next LIST_POISON1) (bfqq-wr_coeff 30 * BFQ_HIGH_PRIO_IO_WEIGHT_FACTOR)){ bfqq-high_prio_io_active_time jiffies; list_add_tail(bfqq-deadline_list, bfqq-bfqd-deadline_head); } /* See comments on bfq_fqq_update_budg_for_activation */ if (non_blocking_wait_rq bfq_gt(st-vtime, entity-finish)) { backshifted true; min_vstart entity-finish; } else min_vstart st-vtime; ...............}
如红色代码在cat这种被判定为高优先级IO进程bfqq插入st-active tree时还把bfqq加入bfqd-deadline_head链表。
在bfq调度器选择下一个派发IO的bfqq而执行的bfq_lookup_next_entity()函数中如果bfqd-deadline_head链表上有超时派发IO的bfqq则强制选择这个bfqq作为下次派发IO的bfqq此时不再执行__bfq_lookup_next_entity()从st-active tree选择。代码如下
static struct bfq_entity *bfq_lookup_next_entity(struct bfq_sched_data *sd, bool expiration){ struct bfq_service_tree *st sd-service_tree; struct bfq_service_tree *idle_class_st st (BFQ_IOPRIO_CLASSES - 1); struct bfq_entity *entity NULL; int class_idx 0; struct bfq_queue *bfqq bfq_entity_to_bfqq(sd-next_in_service); struct bfq_data *bfqd bfqq-bfqd; //high prio io的bfqq在加入st-active tree后。high_prio_io_schedule_deadline时间到了必须立即得到调度派发rq。不用遍历链表只有看链表头第一个成员是否超时第一个没超时后边的更不会超时。 if(!list_empty(bfqd-deadline_head)){ bfqq list_first_entry(bfqd-deadline_head, struct bfq_queue,deadline_list); if(time_is_before_jiffies(bfqq-high_prio_io_active_time bfqd-high_prio_io_schedule_deadline)){ entity bfqq-entity; list_del(bfqq-deadline_list); return entity; } }................ entity __bfq_lookup_next_entity(st class_idx,sd-in_service_entity !expiration); return entity;}
bfq算法是很复杂的本文的优化算法也需要持续打磨。本文如有错误请指出