内核block层Multi queue多队列的一次优化实践

熟悉block层多队列、nvme的小伙伴应该知道,nvme硬件多通道(或者称为多队列)的优势特别大。普通的ssd硬盘也只有一个通道进行IO数据传输,但是nvme却可以多个硬件队列同时IO读写,性能特别好。谈到这里想到一点,如果是单线程IO读写,只能发挥一个nvme硬件队列的优势,能不能发挥下nvme多个硬件队列的优势呢?

1  nvme 硬件队列繁忙时也容易io wait

先介绍一个知识点,每个CPU都绑定一个唯一的nvme硬件队列,当然这些硬件队列可能会有重复的。我见过的一款性能不错的服务器,nvme1盘有80个硬件队列,这款服务器正好80核,故CPU0~CPU79分别绑定了不同nvme硬件队列,每个nvme硬件队列最多支持1023个IO请求。

曾经做过测试,在脏页回写这种重IO场景,iostat看到的io wait竟然可以达到20ms左右。block层多队列框架,向驱动发送IO请求的一个流程是:submit_bio…->blk_mq_make_request->blk_mq_try_issue_directly-> __blk_mq_try_issue_directly-> __blk_mq_issue_directly,这是把IO请求的相关数据发送给nvme磁盘驱动。然后就等该IO请求传输完成,产生中断执行blk_mq_complete_request->__blk_mq_complete_request->blk_mq_end_request->__blk_mq_end_request,统计IO使用率等数据,唤醒等待IO传输完成而休眠的进程等等。

nvme硬件队列传输IO数据是需要一定时间的,这段时间如果进程疯狂的执行submit_bio发送IO请求,进程所在CPU绑定的nvme硬件队列,只能在nvme驱动里累计这些IO请求。nvme驱动源码没研究过,但是按照经验它应该有一个队列(推测nvme硬件队列各有一个这样的队列),暂存submit_bio发送给nvme驱动的IO请求。测试过的服务器,IO繁忙时一个nvme硬件队列的驱动层队列最多会暂存1023个IO请求(有点像队列深度),这些IO请求只能排队等待传输完成,iostat监控此时io wait达到50ms左右。

因此得出一个结论,nvme也不是万能的,在IO繁忙时nvme硬件队列的驱动层队列暂存了很多IO请求时,照样io wait很高。针对这种情况,可以做优化吗?举个例子,cat test读取test文件,进程运行在CPU0,CPU0绑定的nvme硬件队列编号也是0,CPU1绑定的nvme硬件队列编号是1,其他类推。因为cat test读取过程nvme硬件队列0很繁忙,nvme硬件队列0驱动层队列暂存的IO请求达到1000多,正常情况肯定会io wait偏高。此时能否将cat test进程使用的nvme硬件队列0切换到nvme硬件队列1呢? cat test进程后续读取文件,使用的是新的空闲的nvme硬件队列1,不会再使用nvme硬件队列0,是不是就能降低IO延迟,降低io wait呢?

这就是本文的优化思路,这里称之为“nvme硬件队列繁忙时动态切换”。相关代码详细见GitHub - dongzhiyan-stack/kernel_brainstorming: 内核block层等优化,提升性能。在正式介绍前,需要说明一下,本文需要读者对内核block层多队列源码有个整体了解,建议读者先看一下我写的《linux内核block层Multi queue多队列核心点分析》这篇文章。

2  nvme硬件队列繁忙时动态切换的实现

还是先从submit_bio…->blk_mq_make_request发送IO请求开始。按照内核block层多队列的要求,需要执行blk_mq_sched_get_request->__blk_mq_alloc_request->blk_mq_get_tag为bio分配一个tag。具体细节是,找到当前进程所在cpu,继而找到该cpu绑定的nvme硬件队列,nvme硬件队列用blk_mq_hw_ctx结构体表示。然后,从blk_mq_hw_ctx的成员struct blk_mq_tags *tags分配一个tag,测试的机器发现最多可以分配1023个tag。如果进程已经从nvme硬件队列分配了1023个tag,还要再分配的话,只能休眠等待谁释放tag。

接着,将bio转成request,执行blk_mq_try_issue_directly-> __blk_mq_try_issue_directly-> __blk_mq_issue_directly,把IO请求的相关数据发送给nvme磁盘驱动。等nvme磁盘驱动把该IO请求的数据传输完,产生中断,在中断服务函数里释放刚才从nvme硬件队列blk_mq_hw_ctx分配的tag,函数流程是blk_mq_complete_request->__blk_mq_complete_request->blk_mq_end_request->__blk_mq_end_request-> __blk_mq_end_request->blk_mq_free_request->blk_mq_sched_put_request->blk_mq_finish_request->blk_mq_finish_hctx_request->__blk_mq_finish_request->blk_mq_put_tag。

好的,我觉得nvme硬件队列的tag跟前文提到nvme硬件队列的驱动层队列深度的概念很接近,数量上限都是1023。进程IO传输时,需先为IO请求从nvme硬件队列分配一个tag,然后把IO请求发送到nvme硬件队列的驱动层队列,nvme驱动再从这个队列取出一个个IO请求传输数据,等传输完成释放掉刚才从nvme硬件队列分配的tag。

所以,可以简单的得出一个结论:nvme硬件队列被分配的tag越多,nvme硬件队列的驱动层队列中暂存的IO请求就越多,这个nvme硬件队列就越繁忙,越容器造成明显的io wait。本次的优化就是基于nvme硬件队列tag展开的,下边介绍具体是怎么修改代码的:

首先需要有一个变量能简单的表示nvme硬件队列已经分配的tag数,需在struct blk_mq_hw_ctx新增一个变量queue_transfer_reqs如下:

  1. struct blk_mq_hw_ctx {
  2.     ........
  3.     atomic_t                queue_transfer_reqs;/* 当前硬件队列分配了多少个tag*/
  4.     ........
  5. }

然后在进程从nvme硬件队列分配tag时令queue_transfer_reqs加1,在blk_mq_get_tag函数完成。代码如下,红色代码是新增的。

  1. unsigned int blk_mq_get_tag(struct blk_mq_alloc_data *data)
  2. {
  3.     ........
  4.     tag = __blk_mq_get_tag(data, bt);
  5.     if (tag != -1)//nvme硬件队列分配tag成功
  6.         goto found_tag;
  7.    
  8.     do {
  9.         ........
  10.         io_schedule();//nvme硬件队列分配tag失败
  11.         ........
  12.     }
  13.    
  14. found_tag:
  15.     if(tag != -1 && data->hctx)//硬件队列已经分配的tag数加
  16.         atomic_inc(&data->hctx->queue_transfer_reqs);
  17.        
  18.     return tag + tag_offset;
  19. }

在nvme驱动将IO请求的数据传输完成,产生中断,在最后的回调函数blk_mq_put_tag()中释放从nvme硬件队列分配的tag,其实就是令nvme硬件队列的tag数queue_transfer_reqs减1,如下红色代码:

  1. void blk_mq_put_tag(struct blk_mq_hw_ctx *hctx, struct blk_mq_tags *tags,
  2.                     struct blk_mq_ctx *ctx, unsigned int tag)
  3. {      
  4.     if (!blk_mq_tag_is_reserved(tags, tag)) {
  5.             const int real_tag = tag - tags->nr_reserved_tags;
  6.    
  7.             BUG_ON(real_tag >= tags->nr_tags);                                                                                                                                        
  8.             sbitmap_queue_clear(&tags->bitmap_tags, real_tag, ctx->cpu);
  9.             atomic_dec(&hctx->queue_transfer_reqs);
  10.     } else {
  11.             BUG_ON(tag >= tags->nr_reserved_tags);
  12.             sbitmap_queue_clear(&tags->breserved_tags, tag, ctx->cpu);
  13.             atomic_dec(&hctx->queue_transfer_reqs);
  14.     }              
  15. }

好的,准备工作完成,下来是重头戏:当进程IO读写,进程所在CPU绑定的nvme硬件队列被分配的tag数太多,即nvme硬件队列的驱动层队列暂存的IO请求数太多,则调整进程所在CPU的硬件队列,调整到一个空闲的nvme硬件队列,看下源码实现:

  1. struct request *blk_mq_sched_get_request(struct request_queue *q,
  2.                                          struct bio *bio,
  3.                                          unsigned int op,
  4.                                          struct blk_mq_alloc_data *data)
  5. {
  6.     struct elevator_queue *e = q->elevator;
  7.     struct request *rq;
  8.     const bool is_flush = op & (REQ_FLUSH | REQ_FUA);
  9.     blk_queue_enter_live(q);
  10.     data->q = q;
  11.     if (likely(!data->ctx))
  12.             data->ctx = blk_mq_get_ctx(q);//找到当前进程绑定的软件队列
  13.     if (likely(!data->hctx))//找到当前所在cpu绑定的nvme硬件队列
  14.             data->hctx = blk_mq_map_queue(q, data->ctx->cpu);
  15.    
  16.     //如果当前进程所在cpu绑定的nvme硬件队列被分配的tag数大于阀值,则判定该nvme硬件队列繁忙,需要为该进程找一个空闲的nvme硬件队列
  17.     if(atomic_read(&data->hctx->queue_transfer_reqs) > queue_transfer_threshold){
  18.         unsigned int cpu = data->ctx->cpu;
  19.         printk("%s %d %s cpu:%d hctx_queue_num:%d  queue_transfer_reqs:%d\n",current->comm,current->pid,__func__,data->ctx->cpu,data->hctx->queue_num,atomic_read(&dat
  20. a->hctx->queue_transfer_reqs));
  21.         data->hctx = make_queue_adjust(q,&cpu);
  22.         //如果为进程切换了新的nvme硬件队列,则要根据这个硬件队列原始绑定的cpu,返回它对应的软件队列
  23.         data->ctx = __blk_mq_get_ctx(q,cpu);
  24.     }
  25.     ..................
  26.     //nvme硬件队列分配一个空闲的tag
  27.     rq = __blk_mq_alloc_request(data, op);
  28.     ..................
  29. }

红色还是新增的代码,功能就是当前进程所在cpu绑定的nvme硬件队列被分配的tag数大于阀值queue_transfer_threshold(定义在下边),则判定该nvme硬件队列繁忙,需要为该进程找一个空闲的nvme硬件队列。queue_transfer_threshold初次测试时被赋值20,就是说,如果进程所在cpu绑定的nvme硬件队列被分配的tag数大于20,就要为这个进程找一个空闲的nvme硬件队列。需要说明一点queue_transfer_threshold这个nvme硬件队列繁忙阀值可以通过/sys文件系统echo动态设置。为这个进程找一个空闲的nvme硬件队列在make_queue_adjust()函数实现,如下(函数源码全是新增的):

  1. unsigned long queue_transfer_threshold = 20;
  2. struct blk_mq_hw_ctx *make_queue_adjust(struct request_queue *q,unsigned int *curr_cpu)
  3. {
  4.     struct blk_mq_hw_ctx *hctx,*curr_hctx;
  5.     int find = 0;
  6.     unsigned int cpu = 0;
  7.     //返回CPU绑定的硬件队列
  8.     curr_hctx = blk_mq_map_queue(q,*curr_cpu);
  9.     //遍历每个CPU绑定的硬件队列
  10.     for_each_possible_cpu(cpu){
  11.         hctx = blk_mq_map_queue(q,cpu);
  12.         //如果遍历到的nvme硬件队列被分配的tag数小于阀值,则认为该nvme硬件队列空闲,这就是要找的空闲nvme硬件队列
  13.         if(curr_hctx != hctx  && atomic_read(&hctx->queue_transfer_reqs) < queue_transfer_threshold){                                          
  14.             find = 1;
  15.             printk("%s cpu:%d switch to cpu:%d\n",__func__,*curr_cpu,cpu);
  16.             *curr_cpu = cpu;//记录空闲的nvme硬件队列原始绑定的cpu编号
  17.             break;
  18.         }
  19.     }
  20.     //如果找到空闲的nvme硬件队列则返回,否则返回老的硬件队列
  21.     if(find)
  22.         return hctx;
  23.     else
  24.         return curr_hctx;
  25. }

注释写的比较清楚,就不再具体介绍了。需要提醒一点,blk_mq_get_driver_tag()函数也会从nvme硬件队列分配tag,故也需要把make_queue_adjust()把代码加到该函数里,这里不再列了,详细参考GitHub - dongzhiyan-stack/kernel_brainstorming: 内核block层等优化,提升性能。

3 优化效果测试

好的,本文介绍的优化方法很简单,本质就是:IO传输时,如果进程所在CPU绑定的nvme硬件队列繁忙(繁忙标志是nvme硬件队列被分配的tag数太多或者nmve驱动队列暂存的IO请求太多),就为进程切换一个空闲的nvme硬件队列,达到降低IO延迟、降低io wait的效果。理论上分析应该是每什么问题的,但是实际测试时却遇到大问题,才算遇到了本次优化实践的最大挑战!

首先,为了能快速制造出nvme硬件队列繁忙场景,肯定得想办法使进程疯狂的IO读写,用fio或者dd效果不好。测试下来,用我写的《文件预读readahead内核优化提升文件读取性能》这篇文章介绍的方法,开启一个async_file_read内核线程,疯狂的读取文件。应用程测试代码也是这篇文章介绍的test_read.c。当然,你有其他方法也行,总之能达到进程疯狂IO读写而使nvme硬件队列繁忙的效果就行。

这里我把应用层测试代码test_read.c再列下

  1. #define FILE_SIZE (1024*1024*1)
  2. int main(int argc,char *argv[ ])
  3. {
  4.     int ret = 0;
  5.     unsigned char *p;
  6.     int fd;
  7.     int i,j;
  8.     struct timeval start,stop;
  9.     int read_count = 0;
  10.  
  11. p = (unsigned char*)malloc(FILE_SIZE);
  12. …………
  13. //释放pagecache,避免对测试数据有影响
  14.     system("echo 1 >/proc/sys/vm/drop_caches");
  15.     fd = open("test2",O_RDWR|O_SYNC);
  16. …………
  17.     gettimeofday(&start,0);
  18.     while(1)
  19.     {
  20.         ret = read(fd,p,FILE_SIZE);
  21.         if(ret > 0)
  22.             read_count += ret;
  23.          if(ret <= 0)
  24.             goto err;
  25.         //for循环模拟对读测试的数据做一定处理
  26.         for(i = 0;i < 100;i++)
  27.             for(j = 0;j < 1000;j++);
  28.     }
  29. err:
  30.     gettimeofday(&stop,0);
  31.     printf("dx:%d read_count:%d\n",(stop.tv_sec*1000000+stop.tv_usec) - (start.tv_sec*1000000+start.tv_usec),read_count);
  32.     close(fd);
  33.     if(p)
  34.        free(p);
  35.     return 0;
  36. }
  • [root@localhost my_test]gcc -o test_read test_read.c

读取的文件test2大小是7.1G,在没有使用本文介绍的“nvme硬件队列繁忙时动态切换”优化方法时,就是正常的读文件,./test_read读取完test2文件耗时2.6s左右,iostat看到的io wait 50ms左右。测试多次都是这个数据,比较稳定!

在启用“nvme硬件队列繁忙时动态切换”优化后,./test_read读取完test2文件竟然耗时3.2s以上,一脸懵逼,没有提升IO性能反而拖累了文件读取效率呀!万事有果必有因,重点分析了blk_mq_sched_get_request()函数里添加的printk("%s %d %s cpu:%d hctx_queue_num:%d  queue_transfer_reqs:%d\n",current->comm,current->pid,__func__,data->ctx->cpu,data->hctx->queue_num,atomic_read(&data->hctx->queue_transfer_reqs))调试信息,发现些许问题。

由于日志非常杂乱,这里不再贴原始日志,直接说结论:在./test_read 读取test2文件时,test_read进程运行在cpu0,确实因为cpu0绑定的nvme硬件队列0繁忙,把test_read进程使用的硬件队列0切换到了cpu1绑定的nvme硬件队列1。又因为nvme硬件队列1繁忙,又把test_read进程使用的nvme硬件队列1切换到了nvme硬件队列2……..,因为nvme硬件队列78繁忙,又把test_read进程使用nvme硬件队列切换到了nvme硬件队列79………..一共80个CPU,80个nvme硬件队列。大体是这样情况,实际的切换情况乱七八糟。

内核日志里可以大量看到,为test_read进程切换nvme硬件队列的日志

  • test_read 1123 blk_mq_sched_get_request cpu:0 hctx_queue_num:0 queue_transfer_reqs:21
  • make_queue_adjust cpu:0 switch to  cpu:1
  • test_read 1123 blk_mq_sched_get_request cpu:0 hctx_queue_num:0 queue_transfer_reqs:21
  • make_queue_adjust cpu:0 switch to  cpu:2
  • test_read 1123 blk_mq_sched_get_request cpu:0 hctx_queue_num:0 queue_transfer_reqs:21
  • make_queue_adjust cpu:0 switch to  cpu:3

需注意,判定nvme硬件队列繁忙的标志是硬件队列已经分派的tag数是否大于queue_transfer_threshold变量。queue_transfer_threshold初值20,就是说,如果某个nvme硬件队列已经分配的tag数大于20,说明该nvme硬件队列的驱动层队列暂存的IO请求数大于20,则判定该nvme硬件队列繁忙。当然,queue_transfer_threshold可以通过echo 写/sys文件系统下某个文件,动态设置,最大值1023,最小值1。

内核还会看到这样的打印

  • test_read 1123 blk_mq_sched_get_request cpu:0 hctx_queue_num:0 queue_transfer_reqs:21
  • test_read 1123 blk_mq_sched_get_request cpu:0 hctx_queue_num:0 queue_transfer_reqs:22
  • make_queue_adjust cpu:0 switch to  cpu:1

第一行的打印,说明test_read进程运行的cpu0绑定的nvme硬件队列0的已经分配的tag数大于20了,该nvme硬件队列已经被判定繁忙了,但是接下来并没有打印make_queue_adjust  cpu:0 switch to  cpu:….,为什么呢?因为80个nvme硬件队列已经分配的tag数都大于20,所有的nvme硬件队列都繁忙,找不到一个空闲的nvme硬件队列了!

等test_read进程下次执行blk_mq_sched_get_request(),因为cpu1绑定的nvme硬件队列1的IO请求数据传输完成了,nvme硬件队列1释放了tag。即nvme硬件队列1已经被分配的tag数减1,才腾出空闲的tag。然后test_read进程运行把nvme硬件队列切换到了硬件队列1,因此打印make_queue_adjust cpu:0 switch to  cpu:1。

继续测试,增大queue_transfer_threshold,100,500,800,1000,1022,1023,发现一个规律,queue_transfer_threshold越大,./test_read 读取test2文件耗时越接近最短耗时2.6s。并且,queue_transfer_threshold=1022时,./test_read 读取test2耗时2.7~2.8s;而queue_transfer_threshold=1023时,./test_read 读取test2耗时2.6s,原因是?

仔细分析后,当queue_transfer_threshold=1023,因为nvme硬件队列最多只有1023个空闲tag可分配,blk_mq_sched_get_request()函数里的if(atomic_read(&data->hctx->queue_transfer_reqs) > queue_transfer_threshold)永远不成立(hctx->queue_transfer_reqs最大1023),自然无法执行make_queue_adjust()函数切换nvme硬件队列。此时,相当于关闭了“nvme硬件队列繁忙时动态切换”优化功能,恢复了内核默认功能。真是可笑,queue_transfer_threshold=1023时读取test2文件耗时最短,竟然还是因为关闭了“nvme硬件队列繁忙时动态切换”功能!

4 测试过程遇到的问题深度分析

为什么关闭“nvme硬件队列繁忙时动态切换”优化功能,反而使读取文件耗时最短,而开启了“nvme硬件队列繁忙时动态切换”优化功能,反而增大了文件读取耗时。深层次的原因是什么?这里得先分析下submit_bio…->blk_mq_make_request发送IO请求,在该函数里执行blk_mq_sched_get_request->__blk_mq_alloc_request->blk_mq_get_tag从进程所绑定的nvme硬件队列分配空闲tag。

把blk_mq_get_tag分配tag的源码再列下

  1. unsigned int blk_mq_get_tag(struct blk_mq_alloc_data *data)
  2. {
  3.     ........
  4.     tag = __blk_mq_get_tag(data, bt);
  5.     if (tag != -1)//nvme硬件队列分配tag成功
  6.         goto found_tag;
  7.    
  8.     do {
  9.         ........
  10.         io_schedule();//nvme硬件队列分配tag失败
  11.         ........
  12.     }
  13.    
  14. found_tag:
  15.     if(tag != -1 && data->hctx)//硬件队列已经分配的tag数加
  16.         atomic_inc(&data->hctx->queue_transfer_reqs);
  17.        
  18.     return tag + tag_offset;
  19. }

执行tag = __blk_mq_get_tag(data, bt)就是查询nvme硬件队列是否还有空闲tag,如果已经被分配走了1023个tag,该函数就会返回-1,进程就会执行io_schedule()休眠。等当前nvme硬件队列的驱动层队列传输完成一个IO请求,产生中断并执行中断回调函数,唤醒前边在blk_mq_get_tag –>io_schedule()函数休眠的进程,最后释放该nvme硬件队列的tag。

这里需要提几点:

1 进程从执行blk_mq_get_tag –>io_schedule休眠到被唤醒,实际测试这段时间非常短,只有几百微妙级别,很奇怪。可能原因是,刚休眠就因为nvme硬件队列的驱动层队列暂存的IO请求传输完成了,很快产生中断唤醒这个进程。

2进程被唤醒后,按照实际调试结果大概率会被调度到新的cpu上运行。举个例子,关闭“nvme硬件队列繁忙时动态切换”功能下,前边test_read进程读取test2文件时,默认运行在cpu0,等在cpu0绑定的nvme硬件队列0分配了1023个tag,nvme硬件队列0的tag被掏空。然后test_read进程因为从nvme硬件队列0分配不到tag而执行blk_mq_get_tag –>io_schedule休眠,但是很快被唤醒。被唤醒后test_read进程被调度到了cpu1,这样test_read进程后续就使用nvme硬件队列1;继续,test_read进程把nvme硬件队列1的1023个tag分配完而休眠,被唤醒后调度到cpu2,接着使用nvme硬件队列2……….

就是说,test_read进程把前一个nvme硬件队列的空闲tag分配完,使该nvme硬件队列处于最繁忙的状态。然后因为从这个nvme硬件队列分配不到tag而休眠,等被唤醒后,test_read进程一般被调度到空闲的cpu,然后使用该cpu绑定的空闲nvme硬件队列。这样的话,test_read进程掏空一个nvme硬件队列,接着使用下一个空闲nvme队列,如此会把所有的nvme队列tag都分配掉。

分析下来,block层多队列默认框架好像已经有了“nvme硬件队列繁忙时动态切换”功能呀!似乎根本用不到本文介绍的“nvme硬件队列繁忙时动态切换”优化方案。故开启“nvme硬件队列繁忙时动态切换”功能时,性能并没有提升什么?

更深层次的原因是什么呢?就如前文所说,当“nvme硬件队列繁忙时动态切换”功能关闭,就是内核block层多队列代码没做任何修改。当进程疯狂IO读写,假设它最初运行在cpu0,以非常快的的速度把cpu0绑定的nvme硬件队列0的tag分配完,也就是使nvme硬件队列0的驱动队列被暂存的IO请求占满。然后进程无法再从nvme硬件队列0分配tag,无法向nvme硬件队列0的驱动队列继续派发IO请求,本质就是无法继续使用nvme硬件队里0进行IO传输!

然后进程被迫休眠,等nvme硬件队列0的驱动队列的有一个IO请求传输完成,产生中断,唤醒前边休眠的进程。进程被唤醒后被调度到了cpu1。进程开始使用cpu1绑定的nvme硬件队列1进行IO传输,这时nvme硬件队列0驱动队列暂存的IO请求一个个传输完成,在cpu0上产生中断,cpu0和cpu1完美的异步!然后进程把cpu1的nvme硬件队列1的tag分配完,也就是使nvme硬件队列1的驱动队列被暂存的IO请求占满。接着,进程被迫休眠,很快又唤醒,被调度到了cpu2,进程开始使用cpu2绑定的nvme硬件队里2进行IO传输,这段时间nvme硬件队列1的驱动层队列暂存的IO请求一个个传输完成,在cpu1上产生多次中断,cpu1和cpu2完美异步…….每个cpu之间这样循环。

简单举例总结:进程把前一个cpu绑定的nvme硬件队列IO能力掏空,本质是向该nvme硬件队列的驱动层队列发送的IO请求数达到上限(测试时是1023),不能再向该nvme硬件队列的驱动层队列发送的IO请求了。则进程被调度到下一个空闲cpu,接着把下一个cpu绑定的nvme硬件队列掏空(这段时间上一个cpu绑定的nvme硬件队列的驱动层队列的IO请求被一个个传输完成,然后上一个cpu产生一个个中断),再调度到下下一个空闲的nvme硬件队列……….循环,完美的异步!进程当前运行所在的cpu基本不会受nvme IO传输完成中断的影响。

而“nvme硬件队列繁忙时动态切换”功能开启时,进程始终只运行在一个cpu,无法调度到其他cpu。所有的IO请求传输完成产生中断都会在这个cpu上执行中断服务函数,影响了进程的运行,加大了不利的性能开销,毕竟这个方案只用到了一个cpu。而“nvme硬件队列繁忙时动态切换”功能关闭,是每个cpu都用到,即便进程休眠唤醒切换到空闲的cpu存在耗时,但这个时间很短。这是我分析“nvme硬件队列繁忙时动态切换”功能开启反而导致进程读写文件耗时增大的原因。

最初测试时是读取7G大小的文件,“nvme硬件队列繁忙时动态切换”功能开启读文件耗时反而比关闭多耗时几百毫秒。后续我有测试了读取100M、200M、500M、1G大小的文件,发现“nvme硬件队列繁忙时动态切换”功能开启后,读写文件并没有什么明显的性能提升,耗时跟关闭时差不多,甚至还高于关闭时的耗时。

5 问题思考

主要有几点:

1 “nvme硬件队列繁忙时动态切换”的优化思路,打乱了nvme驱动原始CPU与nvme硬件队列的绑定关系,始终担忧会不会有什么隐患?

2“nvme硬件队列繁忙时动态切换”的优化思路,我始终觉得在读小文件时会发挥出效果的,或者场特殊景会发挥出效果。因为提前在nvme硬件队列接近繁忙时,切换到一个空闲的nvme队列,理论上是能提升性能的!还是后续慢慢研究吧。最后,在 blkio cgroup增加优化的代码,可以执行如下命令,令一个进程开启“nvme硬件队列繁忙时动态切换”功能,默认进程没这个功能。

  • [root@localhost ~]# cd /sys/fs/cgroup/blkio/
  • [root@localhost blkio]# mkdir test/
  • //250:1nvme块设备主次设备号
  • [root@localhost test]# echo "250:1 1" > blkio.throttle.io_multi_queue_adjust
  • [root@localhost test]# echo 进程ID >tasks

具体源码请看GitHub - dongzhiyan-stack/kernel_brainstorming: 内核block层等优化,提升性能 的blk_mq_make_request()和tg_may_dispatch()函数以及struct cftype throtl_files结构体等。

3 使用nvme场景,进程IO读写时不要绑核,否则当进程所在cpu绑定的nvme硬件队列队列繁忙时,进程分配tag失败而休眠,再被唤醒,无法被调度到空闲的cpu,无法使用空闲cpu绑定的空闲nvme硬件队列,推测对性能不利。

以上是本文实际测试的心得总结,如果错误请指正。虽然这次对block层多队列的优化没达到预期效果,但还是学到很多东西的!

你可能感兴趣的:(服务器,linux,block)