熟悉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如下:
- struct blk_mq_hw_ctx {
- ........
- atomic_t queue_transfer_reqs;/* 当前硬件队列分配了多少个tag*/
- ........
- }
然后在进程从nvme硬件队列分配tag时令queue_transfer_reqs加1,在blk_mq_get_tag函数完成。代码如下,红色代码是新增的。
- unsigned int blk_mq_get_tag(struct blk_mq_alloc_data *data)
- {
- ........
- tag = __blk_mq_get_tag(data, bt);
- if (tag != -1)//从nvme硬件队列分配tag成功
- goto found_tag;
- do {
- ........
- io_schedule();//从nvme硬件队列分配tag失败
- ........
- }
- found_tag:
- if(tag != -1 && data->hctx)//硬件队列已经分配的tag数加1
- atomic_inc(&data->hctx->queue_transfer_reqs);
- return tag + tag_offset;
- }
在nvme驱动将IO请求的数据传输完成,产生中断,在最后的回调函数blk_mq_put_tag()中释放从nvme硬件队列分配的tag,其实就是令nvme硬件队列的tag数queue_transfer_reqs减1,如下红色代码:
- void blk_mq_put_tag(struct blk_mq_hw_ctx *hctx, struct blk_mq_tags *tags,
- struct blk_mq_ctx *ctx, unsigned int tag)
- {
- if (!blk_mq_tag_is_reserved(tags, tag)) {
- const int real_tag = tag - tags->nr_reserved_tags;
- BUG_ON(real_tag >= tags->nr_tags);
- sbitmap_queue_clear(&tags->bitmap_tags, real_tag, ctx->cpu);
- atomic_dec(&hctx->queue_transfer_reqs);
- } else {
- BUG_ON(tag >= tags->nr_reserved_tags);
- sbitmap_queue_clear(&tags->breserved_tags, tag, ctx->cpu);
- atomic_dec(&hctx->queue_transfer_reqs);
- }
- }
好的,准备工作完成,下来是重头戏:当进程IO读写,进程所在CPU绑定的nvme硬件队列被分配的tag数太多,即nvme硬件队列的驱动层队列暂存的IO请求数太多,则调整进程所在CPU的硬件队列,调整到一个空闲的nvme硬件队列,看下源码实现:
- struct request *blk_mq_sched_get_request(struct request_queue *q,
- struct bio *bio,
- unsigned int op,
- struct blk_mq_alloc_data *data)
- {
- struct elevator_queue *e = q->elevator;
- struct request *rq;
- const bool is_flush = op & (REQ_FLUSH | REQ_FUA);
- blk_queue_enter_live(q);
- data->q = q;
- if (likely(!data->ctx))
- data->ctx = blk_mq_get_ctx(q);//找到当前进程绑定的软件队列
- if (likely(!data->hctx))//找到当前所在cpu绑定的nvme硬件队列
- data->hctx = blk_mq_map_queue(q, data->ctx->cpu);
- //如果当前进程所在cpu绑定的nvme硬件队列被分配的tag数大于阀值,则判定该nvme硬件队列繁忙,需要为该进程找一个空闲的nvme硬件队列
- if(atomic_read(&data->hctx->queue_transfer_reqs) > queue_transfer_threshold){
- unsigned int cpu = data->ctx->cpu;
- 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
- a->hctx->queue_transfer_reqs));
- data->hctx = make_queue_adjust(q,&cpu);
- //如果为进程切换了新的nvme硬件队列,则要根据这个硬件队列原始绑定的cpu,返回它对应的软件队列
- data->ctx = __blk_mq_get_ctx(q,cpu);
- }
- ..................
- //从nvme硬件队列分配一个空闲的tag
- rq = __blk_mq_alloc_request(data, op);
- ..................
- }
红色还是新增的代码,功能就是当前进程所在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()函数实现,如下(函数源码全是新增的):
- unsigned long queue_transfer_threshold = 20;
- struct blk_mq_hw_ctx *make_queue_adjust(struct request_queue *q,unsigned int *curr_cpu)
- {
- struct blk_mq_hw_ctx *hctx,*curr_hctx;
- int find = 0;
- unsigned int cpu = 0;
- //返回CPU绑定的硬件队列
- curr_hctx = blk_mq_map_queue(q,*curr_cpu);
- //遍历每个CPU绑定的硬件队列
- for_each_possible_cpu(cpu){
- hctx = blk_mq_map_queue(q,cpu);
- //如果遍历到的nvme硬件队列被分配的tag数小于阀值,则认为该nvme硬件队列空闲,这就是要找的空闲nvme硬件队列
- if(curr_hctx != hctx && atomic_read(&hctx->queue_transfer_reqs) < queue_transfer_threshold){
- find = 1;
- printk("%s cpu:%d switch to cpu:%d\n",__func__,*curr_cpu,cpu);
- *curr_cpu = cpu;//记录空闲的nvme硬件队列原始绑定的cpu编号
- break;
- }
- }
- //如果找到空闲的nvme硬件队列则返回,否则返回老的硬件队列
- if(find)
- return hctx;
- else
- return curr_hctx;
- }
注释写的比较清楚,就不再具体介绍了。需要提醒一点,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再列下
- #define FILE_SIZE (1024*1024*1)
- int main(int argc,char *argv[ ])
- {
- int ret = 0;
- unsigned char *p;
- int fd;
- int i,j;
- struct timeval start,stop;
- int read_count = 0;
- p = (unsigned char*)malloc(FILE_SIZE);
- …………
- //释放pagecache,避免对测试数据有影响
- system("echo 1 >/proc/sys/vm/drop_caches");
- fd = open("test2",O_RDWR|O_SYNC);
- …………
- gettimeofday(&start,0);
- while(1)
- {
- ret = read(fd,p,FILE_SIZE);
- if(ret > 0)
- read_count += ret;
- if(ret <= 0)
- goto err;
- //for循环模拟对读测试的数据做一定处理
- for(i = 0;i < 100;i++)
- for(j = 0;j < 1000;j++);
- }
- err:
- gettimeofday(&stop,0);
- printf("dx:%d read_count:%d\n",(stop.tv_sec*1000000+stop.tv_usec) - (start.tv_sec*1000000+start.tv_usec),read_count);
- close(fd);
- if(p)
- free(p);
- return 0;
- }
- [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的源码再列下
- unsigned int blk_mq_get_tag(struct blk_mq_alloc_data *data)
- {
- ........
- tag = __blk_mq_get_tag(data, bt);
- if (tag != -1)//从nvme硬件队列分配tag成功
- goto found_tag;
- do {
- ........
- io_schedule();//从nvme硬件队列分配tag失败
- ........
- }
- found_tag:
- if(tag != -1 && data->hctx)//硬件队列已经分配的tag数加1
- atomic_inc(&data->hctx->queue_transfer_reqs);
- return tag + tag_offset;
- }
执行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:1是nvme块设备主次设备号
- [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层多队列的优化没达到预期效果,但还是学到很多东西的!