深入分析diskstats - ykrocku
背景
内核很多重要子系统均通过proc文件的方式,将自身的一些统计信息输出,方便最终用户查看各子系统的运行状态,这些统计信息被称为metrics。 直接查看metrics并不能获取到有用的信息,一般都是由特定的应用程序(htop/sar/iostat等)每隔一段时间读取相关metrics,并进行相应计算,给出更具用户可读性的输出。 常见的metrics文件有:
- cpu调度统计信息的/proc/stat
- cpu负载统计信息的/proc/loadavg
通用块设备层也有一个重要的统计信息
- /proc/diskstats 内核通过diskstats文件,将通用块设备层的一些重要指标以文件的形式呈现给用户。
因为本文档牵涉到通用块设备层很多细节,建议先了解IO调度器的相关知识。
初探diskstats
首先来看下diskstats里面都有些什么,下面截取的是一个diskstats文件内容:
# cat /proc/diskstats
8 0 sda 8567 1560 140762 3460 0 0 0 0 0 2090 3440
8 1 sda1 8565 1557 140722 3210 0 0 0 0 0 1840 3190
8 16 sdb 8157 1970 140762 2940 0 0 0 0 0 1710 2890
8 17 sdb1 8155 1967 140722 2900 0 0 0 0 0 1670 2850
8 32 sdc 8920 1574 206410 7870 430 0 461 250 0 6820 8120
8 33 sdc1 8918 1571 206370 7840 430 0 461 250 0 6790 8090
8 48 sdd 209703 1628 341966 1318450 3109063 331428 943042901 9728000 0 8943570 11015280
8 49 sdd1 209701 1625 341926 1318200 3109063 331428 943042901 9728000 0 8943320 11015030
虽然如上面提到的,这些数字看上去完全没有规律。不过若想研究内核通用块设备层的统计实现方式,还是得一个一个字段的分析。
简单的说,每一行对应一个块设备,分别有ram0-ram15、loop0-loop7、mtdblock0-mtdblock5,剩下的sdxx就是硬盘和分区了。 这里以sda设备的数据为例,分别列举各字段的意义:
1 |
|
根据内核文档iostats.txt中描述,各字段意义如下:
域 | Value | Quoted | 解释 |
---|---|---|---|
F1 | 8 | major number | 此块设备的主设备号 |
F2 | 0 | minor mumber | 此块设备的次设备号 |
F3 | sda | device name | 此块设备名字 |
F4 | 8567 | reads completed successfully | 成功完成的读请求次数 |
F5 | 1560 | reads merged | 读请求的次数 |
F6 | 140762 | sectors read | 读请求的扇区数总和 |
F7 | 3460 | time spent reading (ms) | 读请求花费的时间总和 |
F8 | 0 | writes completed | 成功完成的写请求次数 |
F9 | 0 | writes merged | 写请求合并的次数 |
F10 | 0 | sectors written | 写请求的扇区数总和 |
F11 | 0 | time spent writing (ms) | 写请求花费的时间总和 |
F12 | 0 | I/Os currently in progress | 次块设备队列中的IO请求数 |
F13 | 2090 | time spent doing I/Os (ms) | 块设备队列非空时间总和 |
F14 | 3440 | weighted time spent doing I/Os (ms) | 块设备队列非空时间加权总和 |
基本上都是数量、时间的累加值,按照读、写分开统计。
流程图
下图是Linux内核通用块设备层IO请求处理的完整流程,如图例所示,所有的统计相关处理均有用不同颜色标注。 在进行深入分析前,请大致浏览图片,对整个流程有一个大致印象。
实现分析
proc入口
在内核代码中grep “diskstats”即可找到定义在block/genhd.c中的diskstats_show
函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
此段代码用seq_printf函数将保存在hd_struct结构体内的统计信息组成了diskstats文件。
数据结构
用到的数据结构都定义在<linux/genhd.h>中,主要有disk_stats和hd_struct两个结构体,意义见注释:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
|
F7/F11 ticks
见下一节
F4/F8 ios
如流程图所示,在每个IO结束后,都会调用blk_account_io_done函数,来对完成的IO进行统计。 blk_account_io_done统计了 ios(F4/F8)
和ticks(F7/F11)
,还处理了in_flight
(后续节有分析)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
|
F5/F9 merges
内核每执行一次Back Merge或Front Merge,都会调用drive_stat_acct。 其实in_flight也是在这个函数中统计的,new_io
参数用来区分是新的IO,如果不是新IO则是在merge的时候调用的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
|
F6/F10 sectors
读写扇区总数是在blk_account_io_completion函数中统计的,如流程图中所示,这个函数在每次IO结束后调用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
F12 in_flight
in_flight这个统计比较特别,因为其他统计都是计算累加值,而它是记录当前队列中IO请求的个数。统计方法则是:
- 新IO请求插入队列(被merge的不算)后加1
- 完成一个IO后减1 实现见上面章节中的blk_account_io_done和drive_stat_acct函数内的注释。
F14 time_in_queue
见下一节。
F13 io_ticks
io_ticks统计块设备请求队列非空的总时间,统计时间点与in_flight相同,统计代码实现在part_round_stats_single函数中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
整个代码实现的逻辑比较简单,在新IO请求插入队列(被merge不算),或完成一个IO请求的时候均执行如下操作:
-
队列为空
- 记下时间stamp = t1
-
队列不为空
- io_ticks[rw] += t2-t1
- time_in_queue += in_flight * (t2-t1)
- 记下时间stamp = t2
下面是一个实际的例子,示例io_ticks和time_in_queue的计算过程:
ID | Time | Ops | in_flight | stamp | gap | io_ticks | time_in_queue |
---|---|---|---|---|---|---|---|
0 | 100.00 | 新IO请求入队 | 0 | 0 | —– | 0 | 0 |
1 | 100.10 | 新IO请求入队 | 1 | 100.00 | 0.10 | 0.10 | 0.10 |
3 | 101.20 | 完成一个IO请求 | 2 | 100.10 | 0.80 | 1.20 | 1.70 |
4 | 103.50 | 完成一个IO请求 | 1 | 100.20 | 1.30 | 2.50 | 3.00 |
5 | 153.50 | 新IO请求入队 | 0 | 103.50 | —– | 2.50 | 3.00 |
6 | 154.50 | 完成一个IO请求 | 1 | 153.50 | 1.00 | 3.50 | 4.00 |
总共时间 54.50s, IO队列非空时间3.50s