专栏名称: zhisheng
Java攻城狮
目录
相关文章推荐
芋道源码  ·  300 秒到 4 秒,如何将 MySQL ... ·  22 小时前  
芋道源码  ·  用 Spring AOP 优化 IN ... ·  2 天前  
芋道源码  ·  MySQL 中 Varchar(50) 和 ... ·  2 天前  
芋道源码  ·  如何快速同步第三方平台数据? ·  3 天前  
芋道源码  ·  我司喜欢40岁程序员 ·  4 天前  
51好读  ›  专栏  ›  zhisheng

深入理解iostat

zhisheng  · 掘金  · Java  · 2018-12-13 13:34

正文

本文转自博客:http://bean-li.github.io/dive-into-iostat/ , 个人觉得非常好,所以分享转载,侵删!

前言

iostat算是比较重要的查看块设备运行状态的工具,相信大多数使用Linux的同学都用过这个工具,或者听说过这个工具。但是对于这个工具,引起的误解也是最多的,大多数人对这个工具处于朦朦胧胧的状态。现在我们由浅到深地介绍这个工具,它输出的含义什么,介绍它的能力边界,介绍关于这个工具的常见误解。

基本用法和输出的基本含义

iostat的用法比较简单,一般来说用法如下:

1iostat -mtx 2

含义是说,每2秒钟采集一组数据:

1-m     Display statistics in megabytes per second.23-t     Print the time for each report displayed. The timestamp format may depend on the value of the S_TIME_FORMAT environment variable (see below).45-x     Display extended statistics.

输出的结果如下所示:

注意,上图是在对sdc这块单盘(RAID卡上的单盘)做4KB的随机写入测试:

1fio --name=randwrite --rw=randwrite --bs=4k --size=20G --runtime=1200 --ioengine=libaio --iodepth=64 --numjobs=1 --rate_iops=5000 --filename=/dev/sdf --direct=1 --group_reporting  

因此上图中只有sdc在忙。

如何阅读iostat的输出,各个参数都是什么含义,反映了磁盘的什么信息?

第一列Device比较容易理解,就是说这一行描述的是哪一个设备。

  • rrqm/s : 每秒合并读操作的次数

  • wrqm/s: 每秒合并写操作的次数

  • r/s :每秒读操作的次数

  • w/s : 每秒写操作的次数

  • rMB/s :每秒读取的MB字节数

  • wMB/s: 每秒写入的MB字节数

  • avgrq-sz:每个IO的平均扇区数,即所有请求的平均大小,以扇区(512字节)为单位

  • avgqu-sz:平均为完成的IO请求数量,即平均意义山的请求队列长度

  • await:平均每个IO所需要的时间,包括在队列等待的时间,也包括磁盘控制器处理本次请求的有效时间。

  • r_wait:每个读操作平均所需要的时间,不仅包括硬盘设备读操作的时间,也包括在内核队列中的时间。

  • w_wait: 每个写操平均所需要的时间,不仅包括硬盘设备写操作的时间,也包括在队列中等待的时间。

  • svctm: 表面看是每个IO请求的服务时间,不包括等待时间,但是实际上,这个指标已经废弃。实际上,iostat工具没有任何一输出项表示的是硬盘设备平均每次IO的时间。

  • %util: 工作时间或者繁忙时间占总时间的百分比

avgqu-sz 和繁忙程度

首先我们用超市购物来比对iostat的输出。我们在超市结账的时候,一般会有很多队可以排,队列的长度,在一定程度上反应了该收银柜台的繁忙程度。那么这个变量是avgqu-sz这个输出反应的,该值越大,表示排队等待处理的io越多。

我们搞4K的随机IO,但是iodepth=1 ,查看下fio的指令和iostat的输出:

1 fio --name=randwrite --rw=randwrite --bs=4k --size=20G --runtime=1200 --ioengine=libaio --iodepth=1 --numjobs=1 --filename=/dev/sdc --direct=1 --group_reporting

同样是4K的随机IO,我们设置iodepth=16, 查看fio的指令和iostat的输出:

1fio --name=randwrite --rw=randwrite --bs=4k --size=20G --runtime=1200 --ioengine=libaio --iodepth=16 --numjobs=1 --filename=/dev/sdc --direct=1 --group_reporting 

注意,内核中有I/O Scheduler队列。我们看到因为avgqu-sz大小不一样,所以一个IO时间(await)就不一样。就好像你在超时排队,有一队没有人,而另一队队伍长度达到16 ,那么很明显,队伍长队为16的更繁忙一些。

avgrq-sz

avgrq-sz这个值反应了用户的IO-Pattern。我们经常关心,用户过来的IO是大IO还是小IO,那么avgrq-sz反应了这个要素。它的含义是说,平均下来,这这段时间内,所有请求的平均大小,单位是扇区,即(512字节)。

上面图中,sdc的avgrq-sz总是8,即8个扇区 = 8*512(Byte) = 4KB,这是因为我们用fio打io的时候,用的bs=4k。

下面我们测试当bs=128k时候的fio指令:

1fio --name=randwrite --rw=randwrite --bs=128k --size=20G --runtime=1200 --ioengine=libaio --iodepth=1 --numjobs=1 --filename=/dev/sdc --direct=1 --group_reporting 

注意sdc的avgrq-sz这列的值,变成了256,即256 个扇区 = 256* 512 Byte = 128KB,等于我们fio测试时,下达的bs = 128k。

注意,这个值也不是为所欲为的,它受内核参数的控制:

1root@node-186:~# cat  /sys/block/sdc/queue/max_sectors_kb 2256

这个值不是最大下发的IO是256KB,即512个扇区。当我们fio对sdc这块盘做测试的时候,如果bs=256k,iostat输出中的avgrq-sz 会变成 512 扇区,但是,如果继续增大bs,比如bs=512k,那么iostat输出中的avgrq-sz不会继续增大,仍然是512,表示512扇区。

1fio --name=randwrite --rw=randwrite --bs=512k --size=20G --runtime=1200 --ioengine=libaio --iodepth=1 --numjobs=1 --filename=/dev/sdc --direct=1 --group_reporting 

注意,本来512KB等于1024个扇区,avgrq-sz应该为1204,但是由于内核的max_sectors_kb控制参数,决定了不可能:

另外一个需要注意也不难理解的现象是,io请求越大,需要消耗的时间就会越长。对于块设备而言,时间分成2个部分:

  • 寻道

  • 读或写操作

注意此处的寻道不能简单地理解成磁盘磁头旋转到指定位置,因为后备块设备可能是RAID,可能是SSD,我们理解写入前的准备动作。准备工作完成之后,写入4K和写入128KB,明显写入128KB的工作量要更大一些,因此很容易理解随机写入128KB给块设备带来的负载要比随机写入4K给块设备带来的负载要高一些。

对比生活中的例子,超时排队的时候,你会首先查看队列的长度来评估下时间,如果队列都差不多长的情况下,你就要关心前面顾客篮子里东西的多少了。如果前面顾客每人手里拿着一两件商品,另一队几乎每一个人都推这满满一车子的商品,你可能知道要排那一队。因为商品越多,处理单个顾客的时间就会越久。IO也是如此。

rrqm/s 和wrqm/s

块设备有相应的调度算法。如果两个IO发生在相邻的数据块时,他们可以合并成1个IO。

这个简单的可以理解为快递员要给一个18层的公司所有员工送快递,每一层都有一些包裹,对于快递员来说,最好的办法是同一楼层相近的位置的包裹一起投递,否则如果不采用这种算法,采用最原始的来一个送一个(即noop算法),那么这个快递员,可能先送了一个包括到18层,又不得不跑到2层送另一个包裹,然后有不得不跑到16层送第三个包裹,然后包到1层送第三个包裹,那么快递员的轨迹是杂乱无章的,也是非常低效的。

Linux常见的调度算法有: noop deadline和cfq。此处不展开了。

1root@node-186:~# cat   /sys/block/sdc/queue/scheduler 2[noop] deadline cfq

类比总结

我们还是以超时购物为例,比如一家三口去购物,各人买各人的东西,最终会汇总到收银台,你固然可以每人各自付各自的,但是也可以汇总一下,把所有购买的东西放在一起,由一个人来完成,也就说,三次收银事件merge成了一次。

至此,我们以超时购物收银为例,介绍了avgqu-sz 类比于队伍的长度,avgrq-sz 类比于每个人购物车里物品的多少,rrqm/s和wrqm/s 类比于将一家购得东西汇总一起,付费一次。还有svctm和%util两个没有介绍。

按照我们的剧情,我们自然而然地可以将svctm类比成收银服务员服务每个客户需要的平均时间,%util类比成收银服务员工作的繁忙程度。

注意这个类比是错误的,就是因为类似的类比,容易让人陷入误区不能自拔。不能简单地将svctm理解成单个IO被块设备处理的有效时间,同时不能理解成%util到了100% ,磁盘工作就饱和了,不能继续提升了,这是两个常见的误区。

svctm和%util是iostat最容易引起误解的两个输出。为了准确地评估块设备的能力,我们希望得到这样一个数值:即一个io从发给块设备层到完成这个io的时间,不包括其他在队列等待的时间。从表面看,svctm就是这个值。实际上并非如此。

Linux下iostat输出的svctm并不具备这方面的含义,这个指标应该非废弃。iostat和sar的man page都有这方面的警告:

1svctm2The  average  service time (in milliseconds) for I/O requests that were issued to the device. Warning! Do not trust this field any more.  This field will be removed in a future sysstat version.

那么iostat输出中的svctm到底是怎么来的,%util又是怎么算出来的,进而iostat的输出的各个字段都是从哪里拿到的信息呢?

iostat输出的数据来源diskstats

iostat数据的来源是Linux操作系统的/proc/diskstats:

注意,procfs中的前三个字段:主设备号、从设备号、设备名。这就不多说了。

从第四个字段开始,介绍的是该设备的相关统计:

  • (rd_ios) : 读操作的次数

  • (rd_merges):合并读操作的次数。如果两个读操作读取相邻的数据块,那么可以被合并成1个。

  • (rd_sectors): 读取的扇区数量

  • (rd_ticks):读操作消耗的时间(以毫秒为单位)。每个读操作从__make_request()开始计时,到end_that_request_last()为止,包括了在队列中等待的时间。

  • (wr_ios):写操作的次数

  • (wr_merges):合并写操作的次数

  • (wr_sectors): 写入的扇区数量

  • (wr_ticks): 写操作消耗的时间(以毫秒为单位)

  • (in_flight): 当前未完成的I/O数量。在I/O请求进入队列时该值加1,在I/O结束时该值减1。 注意:是I/O请求进入队列时,而不是提交给硬盘设备时

  • (io_ticks)该设备用于处理I/O的自然时间(wall-clock time)

  • (time_in_queue): 对字段#10(io_ticks)的加权值

这些字段大多来自内核的如下数据:

1include/linux/genhd.h2struct disk_stats {3        unsigned long sectors[2];       /* READs and WRITEs */4        unsigned long ios[2];5        unsigned long merges[2];6        unsigned long ticks[2];7        unsigned long io_ticks;8        unsigned long time_in_queue;9};

除了in_flight来自:

1part_in_flight(hd), 2static inline int part_in_flight(struct hd_struct *part)3{4        return atomic_read(&part->in_flight[0]) + atomic_read(&part->in_flight[1]);5}

内核相关的代码如下:

 1while ((hd = disk_part_iter_next(&piter))) { 2  cpu = part_stat_lock(); 3  part_round_stats(cpu, hd); 4  part_stat_unlock(); 5  seq_printf(seqf, "%4d %7d %s %lu %lu %llu " 6         "%u %lu %lu %llu %u %u %u %u\n", 7         MAJOR(part_devt(hd)), MINOR(part_devt(hd)), 8         disk_name(gp, hd->partno, buf), 9         part_stat_read(hd, ios[READ]),10         part_stat_read(hd, merges[READ]),11         (unsigned long long)part_stat_read(hd, sectors[READ]),12         jiffies_to_msecs(part_stat_read(hd, ticks[READ])),13         part_stat_read(hd, ios[WRITE]),14         part_stat_read(hd, merges[WRITE]),15         (unsigned long long)part_stat_read(hd, sectors[WRITE]),16         jiffies_to_msecs(part_stat_read(hd, ticks[WRITE])),17         part_in_flight(hd),18         jiffies_to_msecs(part_stat_read(hd, io_ticks)),19         jiffies_to_msecs(part_stat_read(hd, time_in_queue))20      );

io_ticks and time_in_queue

这里面大部分字段都是很容易理解的,稍微难理解的在于io_ticks。初看之下,明明已经有了rd_ticks和wr_ticks 为什么还需一个io_ticks。注意rd_ticks和wr_ticks是把每一个IO消耗时间累加起来,但是硬盘设备一般可以并行处理多个IO,因此,rd_ticks和wr_ticks之和一般会比自然时间(wall-clock time)要大。而io_ticks 不关心队列中有多少个IO在排队,它只关心设备有IO的时间。即不考虑IO有多少,只考虑IO有没有。在实际运算中,in_flight不是0的时候保持计时,而in_flight 等于0的时候,时间不累加到io_ticks。

下一个比较难理解的是time_in_queue这个值,它的计算是当前IO数量(即in_flight的值)乘以自然时间间隔。表面看该变量的名字叫time_in_queue,但是实际上,并不只是在队列中等待的时间。

有人不理解time_in_queue,但是我相信读过小学 听过下面这句话的小朋友都会理解time_in_queue:

因为你上课讲话, 让老师批评你5分钟,班里有50人,50个人你就浪费了全班250分钟。

这段话非常形象地介绍了time_in_queue的计算法则,即自然时间只过去了5分钟,但是对于队列中的所有同学,哦不,所有IO来说,需要加权计算:

 1static void part_round_stats_single(int cpu, struct hd_struct *part, 2                  unsigned long now) 3{ 4  if (now == part->stamp) 5      return; 6 7  /*如果队列不为空,存在in_flight io*/ 8  if (part_in_flight(part)) { 910      /*小学数学老师的算法,now-part->stamp 乘以班级人数,哦不,是乘以队列中等待的io请求个数*/11      __part_stat_add(cpu, part, time_in_queue,12              part_in_flight(part) * (now - part->stamp));1314     /*如实的记录,因为批评调皮学生,浪费了5分钟。io不是空的时间增加now - part->stamp*/15      __part_stat_add(cpu, part, io_ticks, (now - part->stamp));16  }17  part->stamp = now;18}

这个计算的方法很简单:

  • 当请求队列为空的时候:

    • io_ticks不增加

    • time_in_queue不增加

    • part->stamp 更新为now

  • 当请求队列不是空的时候:

    • io_ticks增加, 增加量为 now - part->timestamp

    • time_in_queue增加,增加量为 在队列中IO的个数乘以 (now - part->stamp)

    • part->stamp 更新为now

注意调用part_round_stats_single函数的时机在于:

  • 在新IO请求插入队列(被merge的不算)

  • 完成一个IO请求

空说太过抽象,但是我们还是给出一个例子来介绍io_ticks和time_in_queue的计算:

注意上面总时间是53.90时间内,有3.9秒的自然时间内是有IO的,即IO队列的非空时间为3.9秒。

注意,io_ticks这个字段被iostat用来计算%util,而time_in_queue这个字段被iostat用来计算avgqu-sz,即平均队列长度。

其实不难理解了,队列中不为空的时候占总时间的比例即为 %util

/proc/diskstats中其他数据项的更新

既然我们介绍了io_ticks和time_in_queue,我们也简单介绍下其他字段的获取。

在每个IO结束后,都会调用blk_account_io_done函数,这个函数会负责更新rd_ios/wr_ios、rd_ticks/wr_ticks ,包括会更新in_flight。

 1void blk_account_io_done(struct request *req) 2{ 3    /*    4        * Account IO completion.  flush_rq isn't accounted as a 5        * normal IO on queueing nor completion.  Accounting the 6        * containing request is enough. 7        */ 8    if (blk_do_io_stat(req) && !(req->rq_flags & RQF_FLUSH_SEQ)) { 9            unsigned long duration = jiffies - req->start_time;10            /*从req获取请求类型:R / W*/11            const int rw = rq_data_dir(req);12            struct hd_struct *part;13            int cpu; 1415            cpu = part_stat_lock();16            part = req->part;17            /*更新读或写次数,自加*/18            part_stat_inc(cpu, part, ios[rw]);19            /*将io的存活时间,更新到rd_ticks or wr_ticks*/20            part_stat_add(cpu, part, ticks[rw], duration);21            /*更新io_ticks和time_in_queue*/22            part_round_stats(cpu, part);23            /*对应infight 减 1 */24            part_dec_in_flight(part, rw); 2526            hd_struct_put(part);27            part_stat_unlock();28    }                                                                                                                                              29}

注意part_round_stats会调用上一小节介绍的part_round_stats_single函数:

1void part_round_stats(int cpu, struct hd_struct *part)2{3       /*既要更新分区的统计,也要更新整个块设备的统计*/4        unsigned long now = jiffies;5        if (part->partno)6                part_round_stats_single(cpu, &part_to_disk(part)->part0, now);7        part_round_stats_single(cpu, part, now);8}

读写扇区的个数统计,是在blk_account_io_completion函数中实现的:

 1void blk_account_io_completion(struct request *req, unsigned int bytes)                             { 2        if (blk_do_io_stat(req)) { 3                const int rw = rq_data_dir(req); 4                struct hd_struct *part; 5                int cpu;  6 7                cpu = part_stat_lock(); 8                part = req->part; 9                /*右移9位,相当于除以512字节,即一个扇区的字节数*/10                part_stat_add(cpu, part, sectors[rw], bytes >> 9);11                part_stat_unlock();12        }    13}

关于merge部分的统计,在blk_account_io_start函数中统计:

 1void blk_account_io_start(struct request *rq, bool new_io) 2{ 3        struct hd_struct *part; 4        int rw = rq_data_dir(rq);                                              5        int cpu; 6 7        if (!blk_do_io_stat(rq)) 8                return; 910        cpu = part_stat_lock();1112        if (!new_io) {13                /*注意,merge的IO就不会导致in_flight++*/14                part = rq->part;15                part_stat_inc(cpu, part, merges[rw]);16        } else {17                part = disk_map_sector_rcu(rq->rq_disk, blk_rq_pos(rq));18                if (!hd_struct_try_get(part)) {19                        part = &rq->rq_disk->part0;20                        hd_struct_get(part);21                }22                /*新IO,更新io_ticks and time_in_queue*/23                part_round_stats(cpu, part);24                /*in_flight 加1*/25                part_inc_in_flight(part, rw);26                rq->part = part;27        }2829        part_stat_unlock();30}  

iostat 输出的计算

注意,/proc/diskstats 已经将所有的素材都准备好了,对于iostat程序来说,就是将处理这些数据,给客户展现出更友好,更有意义的数值。事实上,iostat的源码非常的短,它属于sysstat这个开源软件,整个文件大小1619行。

 1int read_sysfs_file_stat(int curr, char *filename, char *dev_name) 2{ 3        FILE *fp;  4        struct io_stats sdev; 5        int i; 6        unsigned int ios_pgr, tot_ticks, rq_ticks, wr_ticks; 7        unsigned long rd_ios, rd_merges_or_rd_sec, wr_ios, wr_merges; 8        unsigned long rd_sec_or_wr_ios, wr_sec, rd_ticks_or_wr_sec; 910        /* Try to read given stat file */11        if ((fp = fopen(filename, "r")) == NULL)12                return 0;1314        i = fscanf(fp, "%lu %lu %lu %lu %lu %lu %lu %u %u %u %u",15                   &rd_ios, &rd_merges_or_rd_sec, &rd_sec_or_wr_ios, &rd_ticks_or_wr_sec,16                   &wr_ios, &wr_merges, &wr_sec, &wr_ticks, &ios_pgr, &tot_ticks, &rq_ticks);1718        if (i == 11) {19                /* Device or partition */20                sdev.rd_ios     = rd_ios;21                sdev.rd_merges  = rd_merges_or_rd_sec;22                sdev.rd_sectors = rd_sec_or_wr_ios;23                sdev.rd_ticks   = (unsigned int) rd_ticks_or_wr_sec;24                sdev.wr_ios     = wr_ios;25                sdev.wr_merges  = wr_merges;                               26                sdev.wr_sectors = wr_sec;27                sdev.wr_ticks   = wr_ticks;28                sdev.ios_pgr    = ios_pgr;29                sdev.tot_ticks  = tot_ticks;30                sdev.rq_ticks   = rq_ticks;31        }32        else if (i == 4) {33                /* Partition without extended statistics */34                sdev.rd_ios     = rd_ios;35                sdev.rd_sectors = rd_merges_or_rd_sec;36                sdev.wr_ios     = rd_sec_or_wr_ios;37                sdev.wr_sectors = rd_ticks_or_wr_sec;38        }39        if ((i == 11) || !DISPLAY_EXTENDED(flags)) {40                /*41                 * In fact, we _don't_ save stats if it's a partition without42                 * extended stats and yet we want to display ext stats.43                 */44                save_stats(dev_name, curr, &sdev, iodev_nr, st_hdr_iodev);45        }4647        fclose(fp);4849        return 1;50} 

数据都采集到了,剩下就是计算了。其中下面几项的计算是非常简单的:

  • rrqm/s

  • wrqm/s

  • r/s

  • w/s

  • rMB/s

  • wMB/s

这几项的计算是非常简单的,就是采样两次,后一次的值减去前一次的值,然后除以时间间隔,得到平均值即可。因为这些/proc/diskstats中对应的值都是累加的,后一次减去前一次,即得到采样时间间隔内的新增量。不赘述。

avgrq-sz的计算

 1/*       rrq/s wrq/s   r/s   w/s  rsec  wsec  rqsz  qusz await r_await w_await svctm %util */ 2        cprintf_f(2, 8, 2, 3                  S_VALUE(ioj->rd_merges, ioi->rd_merges, itv), 4                  S_VALUE(ioj->wr_merges, ioi->wr_merges, itv)); 5        cprintf_f(2, 7, 2, 6                  S_VALUE(ioj->rd_ios, ioi->rd_ios, itv), 7                  S_VALUE(ioj->wr_ios, ioi->wr_ios, itv)); 8        cprintf_f(4, 8, 2, 9                  S_VALUE(ioj->rd_sectors, ioi->rd_sectors, itv) / fctr,10                  S_VALUE(ioj->wr_sectors, ioi->wr_sectors, itv) / fctr,11                  xds.arqsz,  //此处是avgrq-sz12                  S_VALUE(ioj->rq_ticks, ioi->rq_ticks, itv) / 1000.0);//此处是avgqu-sz

注意avgrq-sz来自xds的argsz变量,该变量是通过该函数计算得到的:

 1/*注意sdc中的c指的是current,sdp中的p指的是previous*/ 2void compute_ext_disk_stats(struct stats_disk *sdc, struct stats_disk *sdp, 3                            unsigned long long itv, struct ext_disk_stats *xds) 4{ 5        double tput 6                = ((double) (sdc->nr_ios - sdp->nr_ios)) * HZ / itv; 7 8        xds->util  = S_VALUE(sdp->tot_ticks, sdc->tot_ticks, itv); 9        xds->svctm = tput ? xds->util / tput : 0.0;10        xds->await = (sdc->nr_ios - sdp->nr_ios) ?11                ((sdc->rd_ticks - sdp->rd_ticks) + (sdc->wr_ticks - sdp->wr_ticks)) /12                ((double) (sdc->nr_ios - sdp->nr_ios)) : 0.0;1314        xds->arqsz = (sdc->nr_ios - sdp->nr_ios) ?15                ((sdc->rd_sect - sdp->rd_sect) + (sdc->wr_sect - sdp->wr_sect)) /16                ((double) (sdc->nr_ios - sdp->nr_ios)) : 0.0;17}

注意nr_ios来自如下运算,即读IO和写IO的和





请到「今天看啥」查看全文