正文
本文转自博客: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
当请求队列不是空的时候:
注意调用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的和