开局先上一张图,某用户反馈即使在空载的情况下也会偶尔出现上图所示,明明没什么IO但是看到的%util已经接近100%了,而传统意义上大多数同学会拿util的值来判断这个磁盘是否已经达到了性能瓶颈。所以经常会有客户怀疑是我们云盘有性能问题。
但是实际上只要压测一下云盘就可以知道性能是达标的,那为什么会出现这个问题呢,util到底是什么?可以代表什么呢?从iostat的源码中摘出如下逻辑:
我们iostat计算util时的tot_ticks其实就是这里的io_ticks,所以计算方法可以总结为:
util=[Δio_ticks/Δt]
所以下一步我们要搞清楚的是这个io_tick是怎么来的才能理清楚这个公式是什么意思。
从代码中可以看到iostat计算的所有数据都来自 /proc/diskstats , 所以我们应该先搞清楚这里面都有些什么东西:
以下解释来自( https://www.kernel.org/doc/Documentation/iostats.txt):
- (rd_ios)读操作的次数。
- (rd_merges)合并读操作的次数。如果两个读操作读取相邻的数据块时,可以被合并成一个,以提高效率。合并的操作通常是I/O scheduler(也叫elevator)负责的。
- (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)。 请注意io_ticks与rd_ticks(字段#4)和wr_ticks(字段#8)的区别,rd_ticks和wr_ticks是把每一个I/O所消耗的时间累加在一起,因为硬盘设备通常可以并行处理多个I/O,所以rd_ticks和wr_ticks往往会比自然时间大。而io_ticks表示该设备有I/O(即非空闲)的时间,不考虑I/O有多少,只考虑有没有。在实际计算时,字段#9(in_flight)不为零的时候io_ticks保持计时,字段#9(in_flight)为零的时候io_ticks停止计时。
- (time_in_queue)对字段#10(io_ticks)的加权值。字段#10(io_ticks)是自然时间,不考虑当前有几个I/O,而time_in_queue是用当前的I/O数量(即字段#9 in-flight)乘以自然时间。虽然该字段的名称是time_in_queue,但并不真的只是在队列中的时间,其中还包含了硬盘处理I/O的时间。iostat在计算avgqu-sz时会用到这个字段。
上面大部分字段都是直译的很容易理解的,稍微难理解的在于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。
搞清楚这个再回去看上面的公式就很清楚了,util指的是采样周期内in_flight不为0的时间相对于总时间的占空比。
但是这又带来了另外一个问题,util这个指标本意是用来表示磁盘当前的负载或者说磁盘性能的使用率,按照上面的定义计算的方法显然是有问题的,例如我有个盘处理1个I/O需要的时间是1ms,有能力并行处理10个I/O(有10个IO队列),一共有1000个I/O需要处理,如果我每次合并I/O数都是10那么总处理时间应该是100ms,util计算结果100/1000=10%,但是如果我把它串行起来一次只下发一个,那么总完成时间就是1000ms,util计算结果就1000/1000=100%,所以这个值在一定意义上取决于内核如何合并优化IO请求或者也取决于上层应用读写的离散程度,从这个角度来说util已经没有什么实际意义了。
另一种更直观的场景,我上层发起IO的对象肯定是不唯一的,依然假设处理一条IO的时间为100ms,磁盘有10个队列,0-0.1s 在第一个队列中有IO,0.1-0.2s在第二个队列中有IO,依此类推如图,这样的话整个1s中in_flight始终不为0,所以计算得到的util = 100%,但是这显然不符合时间情况。 PS:svctm:已被废弃的指标,没什么意义,svctm=[util/tput]
貌似一般这种问题到这里就可以收尾了,但是奈何客户有大量对比环境发现有一些是相对正常的,而有一些环境是明显有上述异常的且都是同版本系统空载情况下看到的,这样以来就不好解释了因为变化的因子只有云盘和后端的虚拟化环境了。
想要进一步排查的话就得知道到底是谁在更新io_ticks,我们看一下头问genhd.h,/proc/diskstats里的值主要在disk_stats这个结构体里维护,另有部分来自hd_struct:
我们读/proc/diskstats是由block/genhd.c中的diskstats_show处理的:
1
struct disk_stats { unsigned long sectors[2]; /* READs and WRITEs */ unsigned long ios[2]; unsigned long merges[2]; unsigned long ticks[2]; unsigned long io_ticks; unsigned long time_in_queue; };
上面这些字段都是在内核处理IO请求的过程中实时更新的,目前发现主要在以下几个流中更新:part_round_stats
blk_account_io_start() 和blk_account_io_done()
磁盘分区被remove之后依然有IO落到这个分区,这个场景不太清楚,看起来一般不会走到这里~
所以正常应该是在IO done的时候在更新一次。从上面看,io_ticks是实际影响util和svctm的核心指标,重新总结下我们遇到的问题:
- 表现为只要有IO,util就非常容易打到90%以上甚至100%
- 可以从截图中看到,裸盘的IO ticks明显比分区vda1大很多,而系统盘本来就只有一个分区,读写都是基于文件系统也就是vda1,所以理论上vda和vda1的ticks应该相等才对(至少系统盘挂载上来之后计数就应该相等了)
- 内核版本3.10.0-957-21.3
从上面分析看影响io_ticks计数可能的因素:
● 内核跨版本差异
● 磁盘多队列处理
● 磁盘调度算法
● IO merge
后面实际验证中发现这是特定版本的问题,触发条件为内核版本3.10.0-957-21.3以及磁盘队列大于等于2时触发。所以实际只需要升级下内核就可以解决了。
PS:
额外的场景:complete中断被绑定到了比较繁忙的CPU上,实际表现为同等IO负载下,util比正常环境大很多,但是非持续100%,可以修改中断亲和性
echo 40 > /proc/irq/51/smp_affinity
echo 40 > /proc/irq/52/smp_affinity