当前位置: 首页 > news >正文

一次降低进程IO延迟的性能优化实践——基于block层bfq调度器

如果有个进程正频繁的读写文件,此时你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,就是说驱动队列最多可以容纳1024IO请求,一个128K大小的IO请求传输完成耗时50us,这1024IO请求传输完成需耗时1024*50us=50ms。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*50us=5ms。如果能达到这种效果,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多队列核心点分析》。这篇文章是针对blockMulti 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函数源码

  1. static int blk_mq_do_dispatch_sched(struct blk_mq_hw_ctx *hctx)
  2. {
  3.     struct request_queue *q = hctx->queue;
  4.     struct elevator_queue *e = q->elevator;
  5.     LIST_HEAD(rq_list);
  6.     int ret = 0;
  7.     do {
  8.         struct request *rq;
  9.         //bfq_has_work
  10.         if (e->type->ops.has_work && !e->type->ops.has_work(hctx))
  11.             break;
  12.         if (!list_empty_careful(&hctx->dispatch)) {
  13.             ret = -EAGAIN;
  14.             break;
  15.         }
  16.         if (!blk_mq_get_dispatch_budget(hctx))
  17.             break;
  18.         //调用bfq调度器IO派发函数bfq_dispatch_request
  19.         rq = e->type->ops.dispatch_request(hctx);
  20.         if (!rq) {
  21.             blk_mq_put_dispatch_budget(hctx);
  22.             blk_mq_delay_run_hw_queues(q, BLK_MQ_BUDGET_DELAY);
  23.             break;
  24.         }
  25.         list_add(&rq->queuelist, &rq_list);
  26.     /*取出rq_list链表上的req派发给磁盘驱动,如果因驱动队列繁忙或者nvme硬件繁忙导致派发失败,则把rq添加hctx->dispatch等稍后派发遇到rq派发失败返回false,退出while循环*/
  27.     } while (blk_mq_dispatch_rq_list(q, &rq_list, true));
  28.     return ret;
  29. }

该函数作用是:执行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源码简单列下:

  1. bool blk_mq_dispatch_rq_list(struct request_queue *q, struct list_head *list,
  2.                  bool got_budget)
  3. {
  4.     struct blk_mq_hw_ctx *hctx;
  5.     struct request *rq, *nxt;
  6.     bool no_tag = false;
  7.     int errors, queued;
  8.     blk_status_t ret = BLK_STS_OK;
  9.     bool no_budget_avail = false;
  10.     ................
  11.     errors = queued = 0;
  12.     do {
  13.         struct blk_mq_queue_data bd;
  14.         rq = list_first_entry(list, struct request, queuelist);
  15.         hctx = rq->mq_hctx;
  16.         ................
  17.         list_del_init(&rq->queuelist);
  18.         bd.rq = rq;
  19.         if (list_empty(list))
  20.             bd.last = true;
  21.         else {
  22.             nxt = list_first_entry(list, struct request, queuelist);
  23.             bd.last = !blk_mq_get_driver_tag(nxt);
  24.         }
  25.         //rq派发给驱动
  26.         ret = q->mq_ops->queue_rq(hctx, &bd);//scsi_queue_rq nvme_queue_rq
  27.         //这个if成立应该说明是 驱动队列繁忙 或者nvme硬件繁忙,不能再向驱动派发IO,因此本次的rq派发失败
  28.         if (ret == BLK_STS_RESOURCE || ret == BLK_STS_DEV_RESOURCE) {
  29.             if (!list_empty(list)) {
  30.                 //rqlist链表上的下一个reqtag释放了,搞不清楚为什么
  31.                 nxt = list_first_entry(list, struct request, queuelist);
  32.                 blk_mq_put_driver_tag(nxt);
  33.             }
  34.             //把派发失败的rq再添加到list链表
  35.             list_add(&rq->queuelist, list);
  36.             __blk_mq_requeue_request(rq);
  37.             break;
  38.         }
  39.         ...........
  40.         //派发rq失败则queued1
  41.         queued++;
  42.     //一直派发list链表上的req直到list链表空
  43.     } while (!list_empty(list));
  44.     hctx->dispatched[queued_to_index(queued)]++;
  45.     //如果list链表上还有rq,说明派发rq时遇到驱动队列或者硬件繁忙,rq没有派发成功
  46.     if (!list_empty(list)) {
  47.         ...........
  48.         spin_lock(&hctx->lock);
  49.         //list上没有派发成功的rq添加到hctx->dispatch链表,稍后延迟派发
  50.         list_splice_tail_init(list, &hctx->dispatch);
  51.         spin_unlock(&hctx->lock);
  52.         ......................
  53.         blk_mq_update_dispatch_busy(hctx, true);
  54.         return false;
  55.     } else
  56.         blk_mq_update_dispatch_busy(hctx, false);
  57.     //派发rq时遇到驱动队列或者硬件繁忙,返回false,否则派发正常下边返回true
  58.     if (ret == BLK_STS_RESOURCE || ret == BLK_STS_DEV_RESOURCE)
  59.         return false;
  60.    
  61.     return (queued + errors) != 0;
  62. }

该函数只是取出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()函数,添加如下红色代码:

  1. /*高优先级rq*/
  2. #define RQF_HIGH_PRIO           ((__force req_flags_t)(1 << 21))
  3. static void bfq_add_request(struct request *rq)
  4. {
  5.     if (!bfq_bfqq_busy(bfqq)){
  6.         bfq_bfqq_handle_idle_busy_switch(bfqd, bfqq, old_wr_coeff,rq, &interactive);
  7.     }
  8.     ..............
  9.     if(bfqq->wr_coeff == 30){
  10.        //设置rq高优先级
  11.        rq->rq_flags |= RQF_HIGH_PRIO;
  12.     }
  13. }

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(),源码有删减,红色是性能优化添加的代码:

  1. static struct request *__bfq_dispatch_request(struct blk_mq_hw_ctx *hctx)
  2. {
  3.     struct bfq_data *bfqd = hctx->queue->elevator->elevator_data;
  4.     struct request *rq = NULL;
  5.     struct bfq_queue *bfqq = NULL;
  6.    int direct_dispatch = 0;
  7.     //不经IO算法队列,直接派发的rq
  8.     if (!list_empty(&bfqd->dispatch)) {
  9.         rq = list_first_entry(&bfqd->dispatch, struct request,queuelist);
  10.         list_del_init(&rq->queuelist);
  11.         bfqq = RQ_BFQQ(rq);
  12.         direct_dispatch = 1;
  13.         if (bfqq) {
  14.                 bfqq->dispatched++;
  15.                 goto inc_in_driver_start_rq;
  16.         }
  17.         goto start_rq;
  18.     }
  19.     .....................
  20.     bfqq = bfq_select_queue(bfqd);
  21.     if (!bfqq)
  22.         goto exit;
  23.     rq = bfq_dispatch_rq_from_bfqq(bfqd, bfqq);
  24.     if (rq) {
  25.         if(bfqd->queue->high_io_prio_enable)
  26.         {
  27.             if(rq->rq_flags & RQF_HIGH_PRIO){//高优先级IO
  28.                 //第一次遇到high prio io,置1 bfq_high_io_prio_mode,启动3s定时器,定时到了对bfq_high_io_prio_mode0
  29.                 if(bfqd->bfq_high_io_prio_mode == 0){
  30.                     bfqd->bfq_high_io_prio_mode = 1;
  31.                     hrtimer_start(&bfqd->bfq_high_prio_timer, ms_to_ktime(3000),HRTIMER_MODE_REL);
  32.                 }
  33.             }
  34.             else非高优先级IO
  35.             {
  36.                if(bfqd->bfq_high_io_prio_mode)
  37.                {
  38.                    // bfq_high_io_prio_mode 0时间的5s内,如果遇到非high prio io,并且驱动队列IO个数大于限制,则把不派发该IO,而是临时添加到bfq_high_prio_tmp_list链表
  39.                    if((bfqd->rq_in_driver >= 20) /*&& (bfqd->bfq_high_prio_tmp_list_rq_count < 100)*/){
  40.                     list_add_tail(&rq->queuelist,&bfqd->bfq_high_prio_tmp_list);
  41. //bfq_high_prio_tmp_list链表上rq的个数加1
  42.                     bfqd->bfq_high_prio_tmp_list_rq_count ++;
  43.                     rq = NULL;
  44.                     goto exit1;
  45.                    }
  46.                }
  47.             }
  48.         }
  49.        /*如果 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泄漏*/
  50.         if((rq->rq_flags & RQF_HIGH_PRIO) || list_empty(&bfqd->bfq_high_prio_tmp_list)){
  51. inc_in_driver_start_rq:
  52.         bfqd->rq_in_driver++;
  53. start_rq:
  54.         rq->rq_flags |= RQF_STARTED;
  55.         }
  56.     }
  57. exit:
  58.     //1:如果是高优先级IOif不成立,直接跳过。 2:如果非高优先级IO,则把rq添加到bfq_high_prio_tmp_list尾,从链表头选一个rq派发 3:如果rqNULL,则也从bfq_high_prio_tmp_list选一个rq派发
  59.     if(!direct_dispatch && ((rq && !(rq->rq_flags & RQF_HIGH_PRIO)) || !rq)){
  60.        /*如果bfq_high_prio_tmp_listIo, 则不派发本次的io而添加到bfq_high_prio_tmp_list尾部,实际从bfq_high_prio_tmp_list链表头取出一个IO派发。放到 if(bfqd->queue->high_io_prio_enable)外边是为了保证一旦设置high_io_prio_enable0,还能派发残留的在bfq_high_prio_tmp_list上的IO*/
  61.          if(!list_empty(&bfqd->bfq_high_prio_tmp_list)){
  62.             if(rq){
  63.                 list_add_tail(&rq->queuelist,&bfqd->bfq_high_prio_tmp_list);
  64.                 bfqd->bfq_high_prio_tmp_list_rq_count ++;
  65.             }
  66.             rq = list_first_entry(&bfqd->bfq_high_prio_tmp_list, struct request, queuelist);
  67.             list_del_init(&rq->queuelist);
  68.             //bfq_high_prio_tmp_list链表上rq的个数减1
  69.             bfqd->bfq_high_prio_tmp_list_rq_count --;
  70.             bfqd->rq_in_driver++;
  71.             rq->rq_flags |= RQF_STARTED;
  72.         }
  73.     }
  74. exit1:
  75.     ..................
  76.     return rq;
  77. }

该函数中,首先执行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机械盘磁盘队列深度是32,nvme盘队列深度达到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)。没想到,竟然一点效果没有!以下是测试数据

  • 1:echo 1 >/sys/block/sdb/process_high_io_prio打开IO性能优化功能,开启fio压测,cat kern耗时500ms左右,偶尔会出现耗时800ms甚至1s
  • 2:echo 0 >/sys/block/sdb/process_high_io_prio关闭IO性能优化功能,开启fio压测,cat kern耗时500ms左右,偶尔会出现耗时800ms甚至1s
  • 3:echo 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()函数中:

  1. static void __bfq_requeue_entity(struct bfq_entity *entity)
  2. {
  3.     struct bfq_sched_data *sd = entity->sched_data;
  4.     struct bfq_service_tree *st = bfq_entity_service_tree(entity);
  5.     //如果bfqq->wr_coeff30说明是交互式io,执行到这里说明派发这个进程派发的IO太多了,配合消耗完了还没派发完io。此时说明该进程的bfqq需要提升权重,提高优先级,作为high prio io.
  6.     struct bfq_queue *bfqq = bfq_entity_to_bfqq(entity);
  7.     if(bfqq && bfqq->bfqd->queue->high_io_prio_enable && bfqq->wr_coeff == 30){
  8.         bfqq->wr_coeff = 30 * BFQ_HIGH_PRIO_IO_WEIGHT_FACTOR;
  9.         //1表示权重变了,然后才会在bfq_update_fin_time_enqueue->__bfq_entity_update_weight_prio 里真正提升权重
  10.         entity->prio_changed = 1;
  11.         //增大权重提升时间为1.5s
  12.         bfqq->wr_cur_max_time = msecs_to_jiffies(1500);
  13.         //权重提升时间开始时间为当前时间
  14.         bfqq->last_wr_start_finish = jiffies;
  15.         bfqq->entity.completed_size = 0;
  16.     }
  17.     .............
  18.     if (entity->tree)
  19.         bfq_active_extract(st, entity);
  20.     bfq_update_fin_time_enqueue(entity, st, false);
  21. }

cat进程最初派发IO时被判定为交互式IO,bfqq->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_FACTOR,BFQ_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。

  1. #define BFQ_HIGH_PRIO_IO_WEIGHT_FACTOR  50
  2. static void bfq_add_request(struct request *rq)
  3. {
  4.     if (!bfq_bfqq_busy(bfqq)){
  5.         bfq_bfqq_handle_idle_busy_switch(bfqd, bfqq, old_wr_coeff,rq, &interactive);
  6.     }
  7.     .............
  8.     if(bfqq->wr_coeff == 30*BFQ_HIGH_PRIO_IO_WEIGHT_FACTOR){
  9.        //设置rq高优先级
  10.        rq->rq_flags |= RQF_HIGH_PRIO;
  11.     }
  12. }

在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()原生代码的影响,具体实现看如下红色代码。

  1. static void bfq_bfqq_handle_idle_busy_switch(struct bfq_data *bfqd,
  2.                          struct bfq_queue *bfqq,
  3.                          int old_wr_coeff,
  4.                          struct request *rq,
  5.                          bool *interactive)
  6. {
  7.     //禁止high prio io进程被判定为rtinteractive burst io,这样下边的bfq_update_bfqq_wr_on_rq_arrival()函数不会修改它的 bfqq->wr_coeff
  8.     if(bfqq->wr_coeff == 30*BFQ_HIGH_PRIO_IO_WEIGHT_FACTOR){
  9.         *interactive = 0;
  10.         wr_or_deserves_wr = 0;
  11.         in_burst = 0;
  12.         soft_rt = 0;
  13.     }
  14.     ................
  15.     bfq_update_bfqq_wr_on_rq_arrival(bfqd, bfqq,
  16.                              old_wr_coeff,
  17.                              wr_or_deserves_wr,
  18.                              *interactive,
  19.                              in_burst,
  20.                              soft_rt);
  21.     ................
  22. }

在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。

  1. static void bfq_update_wr_data(struct bfq_data *bfqd, struct bfq_queue *bfqq)
  2. {
  3.     struct bfq_entity *entity = &bfqq->entity;
  4.     if (bfqq->wr_coeff > 1) {
  5.         ...............
  6.         if (bfq_bfqq_in_large_burst(bfqq)){
  7.             bfq_bfqq_end_wr(bfqq);
  8.         }
  9.         else if (time_is_before_jiffies(bfqq->last_wr_start_finish +
  10.                         bfqq->wr_cur_max_time)) {
  11.             if (bfqq->wr_cur_max_time != bfqd->bfq_wr_rt_max_time ||
  12.             time_is_before_jiffies(bfqq->wr_start_at_switch_to_srt +
  13.                            bfq_wr_duration(bfqd)))
  14.             {
  15.                 bfq_bfqq_end_wr(bfqq);
  16.             }
  17.             else {
  18.                 switch_back_to_interactive_wr(bfqq, bfqd);
  19.                 bfqq->entity.prio_changed = 1;
  20.             }
  21.         }
  22.         if (bfqq->wr_coeff > 1 &&
  23.             bfqq->wr_cur_max_time != bfqd->bfq_wr_rt_max_time &&
  24.             bfqq->service_from_wr > max_service_from_wr &&
  25.             bfqq->wr_coeff != 30 * BFQ_HIGH_PRIO_IO_WEIGHT_FACTOR)//high prio io进程禁止在这里结束权重提升
  26.         {
  27.             bfq_bfqq_end_wr(bfqq);
  28.         }
  29.     }
  30.     if ((entity->weight > entity->orig_weight) != (bfqq->wr_coeff > 1)){
  31.         __bfq_entity_update_weight_prio(bfq_entity_service_tree(entity),
  32.                         entity, false);
  33.     }
  34. }

在派发IO请求的bfq_dispatch_rq_from_bfqq()函数添加如下代码:

  1. static struct request *bfq_dispatch_rq_from_bfqq(struct bfq_data *bfqd,
  2.                                                  struct bfq_queue *bfqq)                                                                                                                                        
  3. {
  4.     struct request *rq = bfqq->next_rq;
  5.     unsigned long service_to_charge;
  6.     service_to_charge = bfq_serv_to_charge(rq, bfqq);
  7.     bfq_bfqq_served(bfqq, service_to_charge);
  8.     bfq_dispatch_remove(bfqd->queue, rq);
  9.     if (bfqq != bfqd->in_service_queue)
  10.             goto return_rq;
  11.     if(bfqd->queue->high_io_prio_enable){
  12.         if(bfqq->wr_coeff == 30 * BFQ_HIGH_PRIO_IO_WEIGHT_FACTOR){
  13.             //累加bfqq传输完成的rq的数据量,如果bfqq传输数据量太多而超过限制,强制令进程bfqq不再有high prio io属性
  14.             bfqq->entity.completed_size += blk_rq_bytes(rq);
  15.             if(bfqq->entity.completed_size > bfqd->high_prio_io_all_size_limit){
  16.                 bfq_bfqq_end_wr(bfqq);
  17.             }
  18.         }
  19.     }
  20.     bfq_update_wr_data(bfqd, bfqq);
  21.     ...................
  22. }

这是令被判定为高优先级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时,被判定为交互式IO,fqq->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左右:

  • 1:echo 1 >/sys/block/sdb/process_high_io_prio打开IO性能优化功能,开启fio压测,cat kern耗时200ms左右,偶尔会出现耗时800m,但出现概率低
  • 2:echo 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()函数:

  1. #define BFQ_HIGH_PRIO_IO_WEIGHT_FACTOR  50
  2. static void bfq_add_request(struct request *rq)
  3. {
  4.     if (!bfq_bfqq_busy(bfqq)){
  5.         bfq_bfqq_handle_idle_busy_switch(bfqd, bfqq, old_wr_coeff,rq, &interactive);
  6.     }
  7.     ..............
  8.     //如果同一个线程组的进程近期有in_large_burst属性,禁止它新创建的线程被判定为交互式io
  9.     if(bfq_bfqq_in_large_burst(bfqq)){
  10.         if(current->tgid != bfqd->large_burst_process_tgid){
  11.             bfqd->large_burst_process_tgid = current->tgid;
  12.             strncpy(bfqd->large_burst_process_name,current->comm,COMM_LEN-1);
  13.             bfqd->large_burst_process_count = 0;
  14.         }else{
  15.             bfqd->large_burst_process_count ++;
  16.         }
  17.     }
  18.     if(bfqq->wr_coeff == 30*BFQ_HIGH_PRIO_IO_WEIGHT_FACTOR){
  19.        //设置rq高优先级
  20.        rq->rq_flags |= RQF_HIGH_PRIO;
  21.     }
  22. }

把IO请求插入IO队列,把进程bfqq激活插入st->active tree执行的bfq_bfqq_handle_idle_busy_switch()函数中,添加如下代码:

  1. static void bfq_bfqq_handle_idle_busy_switch(struct bfq_data *bfqd,
  2.                          struct bfq_queue *bfqq,
  3.                          int old_wr_coeff,
  4.                          struct request *rq,
  5.                          bool *interactive)
  6. {
  7.     bool soft_rt, in_burst, wr_or_deserves_wr,
  8.         bfqq_wants_to_preempt,
  9.         idle_for_long_time = bfq_bfqq_idle_for_long_time(bfqd, bfqq),
  10.     ...................
  11.     in_burst = bfq_bfqq_in_large_burst(bfqq);
  12.     soft_rt = bfqd->bfq_wr_max_softrt_rate > 0 &&
  13.         !BFQQ_TOTALLY_SEEKY(bfqq) &&
  14.         !in_burst &&
  15.         time_is_before_jiffies(bfqq->soft_rt_next_start) &&
  16.         bfqq->dispatched == 0;
  17.     *interactive = !in_burst && idle_for_long_time;
  18.     //如果同一个线程组的进程近期有in_large_burst属性,禁止它新创建的线程被判定为交互式io
  19.     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)){
  20.         *interactive = 0;
  21.         soft_rt = 0;
  22.         in_burst = 1;
  23.         bfq_prevent_high_prio_count++;
  24.     }
  25.     /*if成立,说明当前进程最近被判定为high prio io。这样等该进程再进程新的IO传输时,强制令该进程被判定为 high prio io。否则,只能被判断为交互式 iobfqq->bfqq_list NULL说明该进程是新创建的。否则可能该bfqq过期失效而处于st->idle tree,现在又派发rq,此时该if不成立。*/
  26.     if((bfqq->wr_coeff == 1) && list_empty(&bfqq->bfqq_list) && (strncmp(bfqd->last_high_prio_io_process,current->comm,COMM_LEN-1)) == 0){
  27.             bfqq->wr_coeff = 30*BFQ_HIGH_PRIO_IO_WEIGHT_FACTOR;
  28.     }
  29.     wr_or_deserves_wr = bfqd->low_latency &&
  30.         (bfqq->wr_coeff > 1 ||
  31.          (bfq_bfqq_sync(bfqq) &&
  32.           bfqq->bic && (*interactive || soft_rt)));
  33.          
  34.     //禁止high prio io进程被判定为rtinteractive burst io,这样下边的bfq_update_bfqq_wr_on_rq_arrival()函数不会修改它的 bfqq->wr_coeff
  35.     if(bfqq->wr_coeff == 30*BFQ_HIGH_PRIO_IO_WEIGHT_FACTOR){
  36.         *interactive = 0;
  37.         wr_or_deserves_wr = 0;
  38.         in_burst = 0;
  39.         soft_rt = 0;
  40.         //保存最近high prio io进程的名字
  41.         strncpy(bfqd->last_high_prio_io_process,current->comm,COMM_LEN-1);
  42.     }
  43.     ................
  44.     bfq_update_bfqq_wr_on_rq_arrival(bfqd, bfqq,
  45.                              old_wr_coeff,
  46.                              wr_or_deserves_wr,
  47.                              *interactive,
  48.                              in_burst,
  49.                              soft_rt);
  50.     ................
  51. }

在进程绑定的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时,只是被判定为交互式IO,bfqq->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,这两种情况进程都会被判定为交互式IO,bfqq->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型IO,bfqd->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后超时强制派发机制。代码实现如下:

  1. static void __bfq_activate_entity(struct bfq_entity *entity,
  2.                                   bool non_blocking_wait_rq)
  3. {
  4.     struct bfq_service_tree *st = bfq_entity_service_tree(entity);
  5.     bool backshifted = false;
  6.     unsigned long long min_vstart;
  7.     struct bfq_queue *bfqq = bfq_entity_to_bfqq(entity);
  8.     //high prio iobfqq,记录激活加入st->active tree的时间点。在 high_prio_io_schedule_deadline 时间点到期后,该bfqq必须被调度到派发rqbfqq->deadline_list->prev next 必须是LIST_POISON2/LIST_POISON1 ,说明没有添加到链表上
  9.     if((bfqq->deadline_list.prev == LIST_POISON2) && (bfqq->deadline_list.next == LIST_POISON1) && (bfqq->wr_coeff == 30 * BFQ_HIGH_PRIO_IO_WEIGHT_FACTOR)){
  10.        bfqq->high_prio_io_active_time = jiffies;
  11.        list_add_tail(&bfqq->deadline_list, &bfqq->bfqd->deadline_head);                                                                                                                                    
  12.     }
  13.     /* See comments on bfq_fqq_update_budg_for_activation */
  14.     if (non_blocking_wait_rq && bfq_gt(st->vtime, entity->finish)) {
  15.             backshifted = true;
  16.             min_vstart = entity->finish;
  17.     } else
  18.             min_vstart = st->vtime;
  19.     ...............
  20. }

如红色代码,在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选择。代码如下:

  1. static struct bfq_entity *bfq_lookup_next_entity(struct bfq_sched_data *sd,
  2.                                                  bool expiration)
  3. {
  4.     struct bfq_service_tree *st = sd->service_tree;
  5.     struct bfq_service_tree *idle_class_st = st + (BFQ_IOPRIO_CLASSES - 1);
  6.     struct bfq_entity *entity = NULL;
  7.     int class_idx = 0;
  8.     struct bfq_queue *bfqq = bfq_entity_to_bfqq(sd->next_in_service);
  9.     struct bfq_data *bfqd = bfqq->bfqd;
  10.     //high prio iobfqq在加入st->active tree后。high_prio_io_schedule_deadline时间到了,必须立即得到调度派发rq。不用遍历链表,只有看链表头第一个成员是否超时,第一个没超时,后边的更不会超时。
  11.     if(!list_empty(&bfqd->deadline_head)){
  12.         bfqq = list_first_entry(&bfqd->deadline_head, struct bfq_queue,deadline_list);
  13.         if(time_is_before_jiffies(bfqq->high_prio_io_active_time + bfqd->high_prio_io_schedule_deadline)){
  14.             entity = &bfqq->entity;
  15.             list_del(&bfqq->deadline_list);
  16.             return entity;
  17.         }
  18.     }
  19. ................
  20.     entity = __bfq_lookup_next_entity(st + class_idx,sd->in_service_entity &&!expiration);
  21.     return entity;
  22. }

bfq算法是很复杂的,本文的优化算法也需要持续打磨。本文如有错误请指出!

相关文章:

一次降低进程IO延迟的性能优化实践——基于block层bfq调度器

如果有个进程正频繁的读写文件&#xff0c;此时你vim查看一个新文件&#xff0c;将会出现明显卡顿。即便你vim查看的文件只有几十M&#xff0c;也可能会出现卡顿。相对的&#xff0c;线上经常遇到IO敏感进程偶发IO超时问题。这些进程一次读写的文件数据量很少&#xff0c;正常几…...

C语言易错知识点十(指针(the final))

❀❀❀ 文章由不准备秃的大伟原创 ❀❀❀ ♪♪♪ 若有转载&#xff0c;请联系博主哦~ ♪♪♪ ❤❤❤ 致力学好编程的宝藏博主&#xff0c;代码兴国&#xff01;❤❤❤ 许久不见&#xff0c;甚是想念&#xff0c;真的是时间时间&#xff0c;你慢些吧&#xff0c;不能再让头发变秃…...

React 18 新增的钩子函数

React 18 引入了一些新的钩子函数&#xff0c;用于处理一些常见的场景和问题。以下是 React 18 中引入的一些新钩子函数以及它们的代码示例和使用场景&#xff1a; useTransition&#xff1a; 代码示例&#xff1a;import { useTransition } from react;function MyComponent()…...

安装与部署Hadoop

一、前置安装准备1、机器2、java3、创建hadoop用户 二、安装Hadoop三、环境配置1、workers2、hadoop-env.sh3、core-site.xml4、hdfs-site.xml5、linux中Hadoop环境变量 四、启动hadoop五、验证 一、前置安装准备 1、机器 主机名ip服务node1192.168.233.100NameNode、DataNod…...

MySQL 8.0 InnoDB Tablespaces之General Tablespaces(通用表空间/一般表空间)

文章目录 MySQL 8.0 InnoDB Tablespaces之General Tablespaces&#xff08;通用表空间/一般表空间&#xff09;General tablespaces&#xff08;通用表空间/一般表空间&#xff09;通用表空间的功能通用表空间的限制 创建通用表空间&#xff08;一般表空间&#xff09;创建语法…...

循环生成对抗网络(CycleGAN)

一、说明 循环生成对抗网络&#xff08;CycleGAN&#xff09;是一种训练深度卷积神经网络以执行图像到图像翻译任务的方法。网络使用不成对的数据集学习输入和输出图像之间的映射。 二、基本介绍 CycleGAN 是图像到图像的翻译模型&#xff0c;就像Pix2Pix一样。Pix2Pix模型面临…...

数组--53.最大子数组和/medium

53.最大子数组和 1、题目2、题目分析3、解题步骤4、复杂度最优解代码示例5、抽象与扩展 1、题目 给你一个整数数组 nums &#xff0c;请你找出一个具有最大和的连续子数组&#xff08;子数组最少包含一个元素&#xff09;&#xff0c;返回其最大和。 子数组 是数组中的一个连…...

centos 编译安装 python 和 openssl

安装环境&#xff1a; centos 7.9 &#xff1a; python 3.10.5 和 openssl 3.0.12 centos 6.10 &#xff1a; python 3.10.5 和 openssl 1.1.1 两个环境都能安装成功&#xff0c;可以正常使用。 安装 openssl 下载地址 下载后解压&#xff0c;进入到解压目录 执行&#xf…...

【nodejs】前后端身份认证

前后端身份认证 一、web开发模式 服务器渲染&#xff0c;前后端分离。 不同开发模式下的身份认证&#xff1a; 服务端渲染推荐使用Session认证机制前后端分离推荐使用JWT认证机制 二、session认证机制 1.HTTP协议的无状态性 了解HTTP协议的无状态性是进一步学习Session认…...

数据结构【线性表篇】(三)

数据结构【线性表篇】(三&#xff09; 文章目录 数据结构【线性表篇】(三&#xff09;前言为什么突然想学算法了&#xff1f;为什么选择码蹄集作为刷题软件&#xff1f; 目录一、双链表二、循环链表三、静态链表 结语 前言 为什么突然想学算法了&#xff1f; > 用较为“官方…...

Python装饰器的专业解释

装饰器&#xff0c;其实是用到了闭包的原理来进行操作的。 单个装饰器&#xff1a; 以下是一个简单的例子&#xff1a; def outer(func):print("OUTER enter ...")def wrapper(*args, **kwargs):print("调用之前......")result func(*args, **kwargs)p…...

vue3框架笔记

Vue Vue 是一个渐进式的前端开发框架&#xff0c;很容易上手。Vue 目前的版本是 3.x&#xff0c;但是公司中也有很多使用的是 Vue2。Vue3 的 API 可以向下兼容 2&#xff0c;Vue3 中新增了很多新的写法。我们课程主要以 Vue3 为主 官网 我们学习 Vue 需要转变思想&#xff0…...

pytest --collectonly 收集测试案例

pytest --collectonly 是一条命令行指令&#xff0c;用于在运行 pytest 测试时仅收集测试项而不执行它们。它会显示出所有可用的测试项列表&#xff0c;包括测试模块、测试类和测试函数&#xff0c;但不会执行任何实际的测试代码。 这个命令对于查看项目中的测试结构和确保所有…...

dev express 15.2图表绘制性能问题(dotnet绘图表)

dev express 15.2 绘制曲线 前端代码 <dxc:ChartControl Grid.Row"1"><dxc:XYDiagram2D EnableAxisXNavigation"True"><dxc:LineSeries2D x:Name"series" CrosshairLabelPattern"{}{A} : {V:F2}"/></dxc:XYDi…...

WorkPlus:领先的IM即时通讯软件,打造高效沟通协作新时代

在当今快节奏的商业环境中&#xff0c;高效沟通和协作是企业成功的关键。而IM即时通讯软件作为实现高效沟通的利器&#xff0c;成为了现代企业不可或缺的一部分。作为一款领先的IM即时通讯软件&#xff0c;WorkPlus以其卓越的性能和独特的功能&#xff0c;助力企业打造高效沟通…...

学习SpringCloud微服务

SpringCloud 微服务单体框架微服务框架SpringCloud微服务拆分微服务差分原则拆分商品服务拆分购物车服务拆分用户服务拆分交易服务拆分支付服务服务调用RestTemplate远程调用 微服务拆分总结 服务治理注册中心Nacos注册中心服务注册服务发现 OpenFeign实现远程调用快速入门引入…...

WPF 显示气泡提示框

气泡提示框应用举例 有时候在我们开发的软件经常会遇到需要提示用户的地方&#xff0c;为了让用户更直观&#xff0c;快速了解提示信息&#xff0c;使用简洁、好看又方便的气泡提示框显得更加方便&#xff0c;更具人性化。如下面例子&#xff1a;(当用户未输入账号时&#xff0…...

L1-062:幸运彩票

题目描述 彩票的号码有 6 位数字&#xff0c;若一张彩票的前 3 位上的数之和等于后 3 位上的数之和&#xff0c;则称这张彩票是幸运的。本题就请你判断给定的彩票是不是幸运的。 输入格式&#xff1a; 输入在第一行中给出一个正整数 N&#xff08;≤ 100&#xff09;。随后 N 行…...

python+vue高校体育器材管理信息系统5us4g

优秀的高校体育馆场地预订系统能够更有效管理体育馆场地预订业务规范&#xff0c;帮助管理者更加有效管理场地的使用&#xff0c;有效提高场地使用效率&#xff0c;可以帮助提高克服人工管理带来的错误等不利因素&#xff0c;所以一个优秀的高校体育馆场地预订系统能够带来很大…...

10 款顶级的免费U盘数据恢复软件(2024 年 更新)

你曾经遇到过U盘无法访问的情况吗&#xff1f;现在我们教你如何恢复数据。 在信息时代&#xff0c;数据丢失往往会造成巨大的困扰。而USB闪存驱动器作为我们常用的数据存储设备&#xff0c;其重要性不言而喻。但是&#xff0c;U盘也可能会出现各种问题&#xff0c;如无法访问、…...

C# json 转匿名对象及C#关键字的处理

调用第三方接口&#xff0c;返回的json字符串&#xff0c;为了方便使用转为C#匿名对象&#xff1a; /// <summary>/// json转为匿名对象/// </summary>/// <typeparam name"T"></typeparam>/// <param name"json"></para…...

关于彻底通过外网,自动批量下载Python的pip依赖包后到企业内网重安装的步骤-比单个包的要方便多了。

关于彻底通过外网&#xff0c;自动批量下载Python包后到企业内网重安装的步骤 前言&#xff1a; 哎&#xff0c;在本人的前面的博客中&#xff0c;分享的方法可能是不通用的。因为在一次实践中发现它不能总是通用且麻烦。所以本次记录分享一个更方便快速的方式。 上期前言&am…...

Oracle T4-4小型机上配置Ldom部署rac

Ldom控制域配置 (两台主机一样&#xff0c;以hydb1为例) roothydb1 # ldm add-vds primary-vds0 primary roothydb1 # ldm add-vcc port-range5000-5100 primary-vcc0 primary roothydb1 # ldm add-vsw net-devigb0 primary-vsw0 primary roothydb1 # ldm add-vsw net-devixgbe…...

【2023Hadoop大数据技术应用期末复习】填空题题型整理

大数据的 4V 特征包含&#xff08;&#xff09;&#xff08;&#xff09;&#xff08;&#xff09;&#xff08;&#xff09; 答案&#xff1a;大量、多样、高速、价值Hadoop 三大组件包含&#xff08;&#xff09;&#xff08;&#xff09;&#xff08;&#xff09; 答案&…...

劫持 PE 文件:新建节表并插入指定 DLL 文件

PE格式简介 PE(Portable Executable)格式&#xff0c;是微软Win32环境可移植可执行文件(如exe、dll、vxd、sys和vdm等)的标准文件格式。PE格式衍生于早期建立在VAX(R)VMS(R)上的COFF(Common Object File Format)文件格式。 Portable 是指对于不同的Windows版本和不同的CPU类型上…...

HTTP分数排行榜

HTTP分数排行榜 介绍一、创建数据库二、创建PHP脚本三、上传下载分数四、测试 介绍 Unity中向服务器发送用户名和得分&#xff0c;并存入数据库&#xff0c;再讲数据库中的得分按照降序的方式下载到Unity中。 一、创建数据库 首先&#xff0c;我们要在MySQL数据库中建立一个…...

Android 实现 Slots 游戏旋转效果

文章目录 前言一、效果展示二、代码实现1.UI布局2.SlotAdapter2.SlotsActivity 总结 前言 slots游戏&#xff1a; Slots游戏是一种极具流行度的赌博和娱乐形式&#xff0c;通常被称为老虎机或水果机。它们在赌场、线上游戏平台和手机应用中广泛存在。一般这类游戏都使用Unity…...

AI产品经理 - 如何做一款软硬协同AI产品

【背景】从0做一款软硬协同的AI产品&#xff0c;以智能医药保温箱 1.以智能医药保温箱 2.调研定义市场方向 地点&#xff1a;医药、实验室 场景&#xff1a;长宽高/装箱/运输/实验室 3.需求挖掘 4.如何进行软硬件AI产品工作 软硬件产品设计&#xff1a;功能/硬件外观设计、…...

拒绝采样(算法)总结

先说说什么是拒绝采样算法&#xff1a;就类似于数学上的求阴影面积的方法&#xff0c;直接求求不出来&#xff0c;就用大面积 - 小面积 阴影面积的办法。 所谓拒绝 和 采样 &#xff1a;就像是撒豆子计个数&#xff0c;计算概率问题一样&#xff0c;大桶里面套小桶&#xff0c…...

分布式数据库事务故障恢复的原理与实践

关系数据库中的事务故障恢复并不是一个新问题&#xff0c;自70年代关系数据库诞生之后就一直伴随着数据库技术的发展&#xff0c;并且在分布式数据库的场景下又遇到了一些新的问题。本文将会就事务故障恢复这个问题&#xff0c;分别讲述单机数据库、分布式数据库中遇到的问题和…...

Spark中的数据加载与保存

Apache Spark是一个强大的分布式计算框架&#xff0c;用于处理大规模数据。在Spark中&#xff0c;数据加载与保存是数据处理流程的关键步骤之一。本文将深入探讨Spark中数据加载与保存的基本概念和常见操作&#xff0c;包括加载不同数据源、保存数据到不同格式以及性能优化等方…...

2023-12-20 LeetCode每日一题(判别首字母缩略词)

2023-12-20每日一题 一、题目编号 2828. 判别首字母缩略词二、题目链接 点击跳转到题目位置 三、题目描述 给你一个字符串数组 words 和一个字符串 s &#xff0c;请你判断 s 是不是 words 的 首字母缩略词 。 如果可以按顺序串联 words 中每个字符串的第一个字符形成字符…...

C# 事件(Event)

C# 事件&#xff08;Event&#xff09; C# 事件&#xff08;Event&#xff09;通过事件使用委托声明事件&#xff08;Event&#xff09;实例 C# 事件&#xff08;Event&#xff09; 事件&#xff08;Event&#xff09; 基本上说是一个用户操作&#xff0c;如按键、点击、鼠标移…...

2312d,d的sql构建器

原文 项目 该项目在我工作项目中广泛使用,它允许自动处理联接方式动态构建SQL语句. 还会自动直接按表示数据库行结构序化.它在dconf2022在线演讲中介绍了:建模一切. 刚刚添加了对sqlite的支持.该API还不稳定,但仍非常有用.这是按需构建,所以虽然有个计划外表,但满足了我的需要…...

以太网二层交换机实验

实验目的&#xff1a; &#xff08;1&#xff09;理解二层交换机的原理及工作方式&#xff1b; &#xff08;2&#xff09;利用交换机组建小型交换式局域网。 实验器材&#xff1a; Cisco packet 实验内容&#xff1a; 本实验可用一台主机去ping另一台主机&#xff0c;并…...

启封涂料行业ERP需求分析和方案分享

涂料制造业是一个庞大而繁荣的行业 它广泛用于建筑、汽车、电子、基础设施和消费品。涂料行业生产不同的涂料&#xff0c;如装饰涂料、工业涂料、汽车涂料和防护涂料。除此之外&#xff0c;对涂料出口的需求不断增长&#xff0c;这增加了增长和扩张的机会。近年来&#xff0c;…...

华为ensp网络设计期末测试题-复盘

网络拓扑图 地址分配表 vlan端口分配表 需求 The device is running!<Huawei>sys Enter system view, return user view with CtrlZ. [Huawei]un in en Info: Information center is disabled. [Huawei]sys S1 [S1]vlan 99 [S1-vlan99]vlan 100 [S1-vlan100]des IT [S1-…...

Dockerfile: WORKDIR vs VOLUME

WORKDIR WORKDIR指令为Dockerfile中的任何RUN、CMD、ENTRYPOINT、COPY和ADD指令设置工作目录。 如果WORKDIR不存在&#xff0c;它将被创建&#xff0c;即使它没有在任何后续Dockerfile指令中使用。 语法 : WORKDIR dirpath WORKDIR指令可以在Dockerfile中多次使用。如果提供了…...

spring ioc源码-refresh();

主要作用是刷新应用上下文 Override public void refresh() throws BeansException, IllegalStateException {synchronized (this.startupShutdownMonitor) {// 启动刷新的性能跟踪步骤StartupStep contextRefresh this.applicationStartup.start("spring.context.refre…...

使用递归实现深拷贝

文章目录 为什么要使用递归什么深拷贝具体实现基础实现处理 函数处理 Symbol处理 Set处理 Map处理 循环引用 结语-源码 为什么要使用递归什么深拷贝 我们知道在 JavaScript 中可以通过使用JSON序列化来完成深拷贝&#xff0c;但是这种方法存在一些缺陷&#xff0c;比如对于函数…...

工程(十七)——自己数据集跑R2live

博主创建了一个科研互助群Q&#xff1a;772356582&#xff0c;欢迎大家加入讨论。 r2live是比较早的算法&#xff0c;编译过程有很多问题&#xff0c;通过以下两个博客可以解决 编译R2LIVE问题&解决方法-CSDN博客 r2live process has died 问题解决了_required process …...

【python高级用法】迭代器、生成器、装饰器、闭包

迭代器 可迭代对象&#xff1a;可以使用for循环来遍历的&#xff0c;可以使用isinstance()来测试。 迭代器&#xff1a;同时实现了__iter__()方法和__next__()方法&#xff0c;可以使用isinstance()方法来测试是否是迭代器对象 from collections.abc import Iterable, Iterat…...

Nx市工业数据洞察:Flask、MySQL、Echarts的可视化之旅

Nx市工业数据洞察&#xff1a;Flask、MySQL、Echarts的可视化之旅 背景数据集来源技术选型功能介绍创新点总结 背景 随着工业化的不断发展&#xff0c;Nx市工业数据的收集和分析变得愈发重要。本博客将介绍如何利用Flask、MySQL和Echarts等技术&#xff0c;从统计局获取的数据…...

关于正态分布

目录 1.正态分布是什么2.正态分布有什么用途3.如何确定数据服从正态分布 本文简单介绍正态分布的基本概念和用途。 1.正态分布是什么 正态分布&#xff0c;也称为高斯分布&#xff0c;是由德国数学家卡尔弗里德里希高斯在研究测量误差时提出的。他发现许多自然现象和统计数据…...

每日一练(编程题-C/C++)

目录 CSDN每日一练1. 2023/2/27- 一维数组的最大子数组和(类型&#xff1a;数组 难度&#xff1a;中等)2. 2023/4/7 - 小艺照镜子(类型&#xff1a;字符串 难度&#xff1a;困难)3. 2023/4/14 - 最近的回文数(难度&#xff1a;中等)4. 2023/2/1-蛇形矩阵(难度&#xff1a;困难)…...

Unity UnityWebRequest 在Mac上使用报CommectionError

今天是想把前两天写的Demo拿到Mac上打个IPA的完事我发现 在运行时释放游戏资源的时候UnityWebRequest返回的结果不是Success 查看Log发现是 req.result 是CommectionError error是 Cannot connect to destination host 代码如下&#xff1a; UnityWebRequest req UnityWebRequ…...

WorkPlus为企业打造私有化部署IM解决方案

在移动数字化时代&#xff0c;企业面临着如何全面掌控业务和生态的挑战。企业微信、钉钉、飞书、Teams等应用虽然提供了部分解决方案&#xff0c;但无法满足企业的私有化部署需求。此时&#xff0c;WorkPlus作为安全专属的移动数字化平台&#xff0c;被誉为移动应用的“航空母舰…...

QT上位机开发(抽奖软件)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 用抽奖软件抽奖&#xff0c;是一种很常见的抽奖方式。特别是写这篇文章的时候&#xff0c;正好处于2023年12月31日&#xff0c;也是一年中最后一天…...

雨课堂作业整理

第一次作业 1.下列序列是图序列的是&#xff08; &#xff09; A.1&#xff0c;2&#xff0c;2&#xff0c;3&#xff0c;4&#xff0c;4&#xff0c;5 B.1&#xff0c;1&#xff0c;2&#xff0c;2&#xff0c;4&#xff0c;6&#xff0c;6 C.0&#xff0c;0&#xff0c;2&am…...

C#/WPF 只允许一个实例程序运行并将已运行程序置顶

使用用互斥量(System.Threading.Mutex)&#xff1a; 同步基元&#xff0c;它只向一个线程授予对共享资源的独占访问权。在程序启动时候&#xff0c;请求一个互斥体&#xff0c;如果能获取对指定互斥的访问权&#xff0c;就职运行一个实例。 实例代码&#xff1a; /// <…...