驱蚊器喵的翻译平台

Can you hear the gravity?

  1. 1. 原始磁盘统计信息
  2. 2. 分析数据: iostat
  3. 3. 解释结果
    1. 3.1. aqu-sz:平均队列大小
    2. 3.2. r_await / w_await: 服务时间
    3. 3.3. rareq-sz 和 wareq-sz:平均请求大小
    4. 3.4. %util: 利用率
  4. 4. 常见错误
  5. 5. 总结

原文地址:https://brian-candler.medium.com/interpreting-prometheus-metrics-for-linux-disk-i-o-utilization-4db53dfedcfc
原文标题:Interpreting Prometheus metrics for Linux disk I/O utilization
原文作者:Brian Candler
原文写于:2021/01/29

译者:驱蚊器喵#ΦωΦ
翻译水平有限,有不通顺的语句,请见谅。


Prometheus 是一个指标收集系统,它的 node_exporter 暴露了丰富的系统指标。

在本文中,我将对磁盘 I/O 的各个指标进行讲解。这些指标提供了很多磁盘的关键信息,包括关于你的磁盘性能、磁盘忙碌程度以及你的应用程序正在经历的 I/O 延迟。

Grafana 有很多 node_exporter 的面板,但并不是所有的面板都能正确标注统计信息。因此,本文的内容是很值得你了解的。

原始磁盘统计信息

在 Linux 系统中,node_exporter/proc/diskstats 读取磁盘指标。这个文件的内容格式在内核文档中写有;前三列标注设备,随后的字段是详细数据。

每一个 prometheus 指标都直接对应于该文件中的一个字段,在 NewDiskstatsCollector代码中可以找到。所以理解 Prometheus 指标的第一步是将指标与它们对应的内核源数据相匹配。

为了方便理解,我将内核文档放在这里,并在每个字段中添加了对应的 prometheus 指标名称。

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
48
49
50
51
node_disk_reads_completed_total (field 1)
这是成功完成的读取数量.

node_disk_reads_merged_total (field 2)
node_disk_writes_merged_total (field 6)
node_disk_discards_merged_total (field 13)
为了提高效率,相邻的读和写可能会被合并. 因此,两个 4K 的读可能会变成一个 8K 的读,然后再交给磁盘处理, 因此它将被计入(和队列)作为一次 I/O 操作. 这个字段让你知道合并的次数有多少.

node_disk_read_bytes_total (field 3)
这是成功读取的字节总数。

node_disk_read_time_seconds_total (field 4)
这是所有读取所花费的总秒数(是从 __make_request() 到 end_that_request_last() 测量的)。


node_disk_writes_completed_total (field 5)
这是成功完成写入的总数。

node_disk_written_bytes_total (field 7)
这是成功完成写入的总字节数.

node_disk_write_time_seconds_total (field 8)
这是所有写操作所花费的总秒数(是从 __make_request() 到 end_that_request_last() 测量的)。

node_disk_io_now (字段 9)
唯一应该归零的字段。随着请求的增加递增,并在请求结束后递减。

node_disk_io_time_seconds_total (field 10)
I/O 操作所花的秒数。
只要字段 9 不为 0,该字段就会增加。

node_disk_io_time_weighted_seconds_total (field 11)
做 I/O 时花费的加权秒数。
这个字段在每次 I/O 开始、I/O 完成、I/O 合并或读取这些统计数据时,该字段都会被递增。
递增的方式是正在进行的 I/O 数量(字段9)乘以自该字段最后一次更新以来花在 I/O 上的秒数。
这可以提供一个简单的方式来衡量 I/O 完成时间和可能积压的数据。

node_disk_discards_completed_total (field 12)
这是成功完成的丢弃总数量。

node_disk_discarded_sectors_total (field 14)
这是成功丢弃的扇区总数。

node_disk_discard_time_seconds_total (field 15)
这是所有丢弃所花费的总秒数(是从 __make_request() 到 end_that_request_last() 测量的).

node_disk_flush_requests_total (field 16)
成功完成的刷新请求总数。

node_disk_flush_requests_time_seconds_total (field 17)
所有刷新请求所花费的总秒数。

node_exporter 暴露了由内核返回的原始数据,除了几个进行了缩放数据。(如果内核返回的是扇区数,node_exporter 将其乘以扇区大小,成为字节数;如果内核报告的时间是毫秒,node_exporter 将其乘以 0.001,成为秒数。我已经调整了上面的描述来匹配)

注意:旧的内核不会返回所有显示的字段。有些系统可能只返回字段 1-11 的数据;有些则只返回字段 1-15 。

分析数据: iostat

我们已经得到了以计数器和累积时间形式的原始数据。我们如何从这些数据解释,在数据采样的每个时间点之间发生了什么?

为了回答这个问题,我首先要看看一个核心的 Linux 工具,iostat(在 Ubuntu 中,iostatsysstat 包的一部分)。它解释了相同的底层磁盘统计 diskstats,看看它是如何做到的,这会很有启发。

通常运行 iostat 的方法是:iostat 5 -x.(-x Display extended statistics. 展示扩展信息)

这意味着:每 5 秒钟对统计资料进行采样,并以扩展形式显示结果。下面是一些输出的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
avg-cpu:  %user   %nice %system %iowait  %steal   %idle
2.77 2.56 5.33 0.20 0.00 89.14

Device r/s w/s rkB/s wkB/s rrqm/s wrqm/s %rrqm %wrqm r_await w_await aqu-sz rareq-sz wareq-sz svctm %util
loop0 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
loop1 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
loop2 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
loop3 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
loop4 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
loop5 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
loop6 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
loop7 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
sda 0.40 51.20 2.40 683.20 0.00 0.60 0.00 1.16 0.00 0.11 0.01 6.00 13.34 0.08 0.40
dm-0 0.40 4.80 2.40 19.20 0.00 0.00 0.00 0.00 0.00 0.00 0.00 6.00 4.00 0.00 0.00
dm-1 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
dm-2 0.40 46.60 0.00 664.00 0.00 0.00 0.00 0.00 4.00 0.09 0.01 0.00 14.25 0.09 0.40
zd0 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00

你将看到的第一组统计数据是系统启动后整个时间的平均值(可能是几个月或几年)。在这之后,你会得到涵盖每个 5 秒钟周期的数据。

逐一查看这些列,以下是 iostat(1) 手册中对它们的描述。

  • r/s: 设备每秒完成的读取请求数(合并后).
  • w/s: 设备每秒完成的写入请求数(合并后).
  • rkB/s: 每秒从设备上读取的 KB[^1] 数
  • wkB/s: 每秒写到设备上的 KB[^1] 数。
  • rrqm/s: (字母含义,read requests queue merge) 每秒合并到设备上的读取请求数
  • wrqm/s: (字母含义,write requests queue merge) 每秒合并到设备上的写入请求数.
  • %rrqm: 读取请求在发送到设备前被合并的百分比.
  • %wrqm: 写入请求在发送到设备前被合并的百分比.
  • r_await: 向设备发出的读请求被送达的平均时间(以毫秒计)。这包括请求在队列中花费的时间和处理的时间.
  • w_await: 向设备发出的写请求被送达的平均时间(以毫秒计)。这包括请求在队列中花费的时间和处理的时间。
  • aqu-sz/avgqu-sz: (字母含义,average queue - size) 发给设备的请求平均队列长度.
  • rareq-sz: (字母含义,read average request - size) 向设备发出的读取请求的平均大小(以 KB 为单位).
  • wareq-sz: (字母含义,write average request - size) 向设备发出的写请求的平均大小(以 KB 为单位).
  • svctm: (字母含义,service time) 向设备发出的I/O请求的平均服务时间. 因其不可靠,已从当前版本的 iostat删除.
  • %util: 向设备发出I/O请求的时间的百分比(设备的带宽利用率)。当这个值接近 100% 时,设备就会出现饱和,因为设备是以串行方式提供请求的。但对于并行服务请求的设备,如 RAID 阵列和现代 SSD,这个数字并不反映其性能极限。

这些数据是如何从 /proc/diskstats 得出的,因此相应的 Prometheus 查询也是什么?答案就在源代码中,我在这里做了转译。

  • r/s: 字段 1 的增加率.
    rate(node_disk_reads_completed_total[*])
    单位: 速率(每秒操作数)
  • w/s: 字段 5 的增加率.
    rate(node_disk_writes_completed_total[*])
    单位: 速率(每秒操作数)
  • rkB/s:
    rate(node_disk_read_bytes_total[*])
    单位: 字节/秒
  • wkB/s:
    rate(node_disk_written_bytes_total[*])
    单位: 字节/秒
  • rrqm/s:
    rate(node_disk_reads_merged_total[*])
    单位: 速率(每秒操作数)
  • wrqm/s:
    rate(node_disk_writes_merged_total[*])
    单位: 速率(每秒操作数)
  • %rrqm[^2]:
    rate(node_disk_reads_merged_total[*]) / (rate(node_disk_reads_merged_total[*] + rate(node_disk_reads_completed_total[*]))
    单位: 没有度量单位(0-1分)
  • %wrqm[^2]:
    rate(node_disk_writes_merged_total[*]) / (rate(node_disk_writes_merged_total[*] + rate(node_disk_writes_completed_total[*]))
    单位: 没有度量单位(0-1分)
  • r_await[^2]:
    rate(node_disk_read_time_seconds_total[*]) / rate(node_disk_reads_completed_total[*])
    单位: 秒
  • w_await[^2]:
    rate(node_disk_write_time_seconds_total[*]) / rate(node_disk_writes_completed_total[*])
    单位: 秒
  • aqu-sz:
    rate(node_disk_io_time_weighted_seconds_total[*])
    单位: 没有度量单位(排队操作的数量)
  • rareq-sz[^2]:
    rate(node_disk_read_bytes_total[*]) / rate(node_disk_reads_completed_total[*])
    单位: 字节
  • wareq-sz[^2]:
    rate(node_disk_written_bytes_total[*]) / rate(node_disk_writes_completed_total[*])
    单位: 字节
  • %util:
    rate(node_disk_io_time_seconds_total[*])
    单位: 没有度量单位(分数0-1)

我使用[*]来代表在这些查询中使用的时间范围。如何选择合适的时间范围,可能是另一篇文章的主题,但现在,选择一个至少是你的采样间隔两倍的范围即可。也就是说,如果你以 1 分钟的间隔取样,时间范围必须至少是[2m]才能成功地计算出比率。在Grafana 7.2 或更高版本中,使用 [$__rate_interval],并确保你在 Grafana 数据源中设置了正确的采样率

解释结果

除了明显的吞吐量数字,每秒的操作数和每秒写入/读取的字节数,还有几个指标特别重要。

aqu-sz:平均队列大小

这表明等待服务的平均操作数量。请注意,一些设备,如 SSD,需要有多个未处理的请求,以达到最大的吞吐量:例如,一个有 8 个内部控制器通道的 SSD 只有在至少有 8 个未处理的并发请求时才能达到全吞吐量。这在某种程度上也适用于硬盘,如果有多个未处理的请求,硬盘就能优化其磁头在盘面上的移动。

这个值是通过获取 node_disk_io_time_weighted_seconds_total 的增加率来精确计算的。

这与 node_disk_io_now 不同,node_disk_io_now 给出的是瞬时队列深度。假设你每 5 秒轮询一次node_exporter;那么 node_disk_io_now 给你的是在采样瞬间在队列中的项目数量。在不同的毫秒时刻这个值会有很大的变化,所以采样值可能是非常嘈杂的。这就是 node_disk_io_time_weighted_seconds_total 指标存在的原因;通过将队列深度乘以队列处于该深度的总时间,并将其相加,通过该指标的增加率得到特定时间段的平均队列深度。

r_await / w_await: 服务时间

这些是分别为每个读请求和写请求提供服务所需的平均时间。如果这些值变得很高,那么说明应用程序正在经历 I/O 延迟,因为在排队等待其他请求。

rareq-sz 和 wareq-sz:平均请求大小

这些对于理解 I/O 模式很有用。应用程序可能混合了小的传输(如 4 KB)或大的传输(如 512 KB);平均数让你了解哪种传输占主导地位。较大的传输更有效率,特别是对于机械硬盘。

%util: 利用率

如果这个值低于 100%,那么设备在某些时间是完全空闲的。这意味着肯定有空余的容量。

然而,如果这个值是 100%,也不一定意味着设备已经饱和了。正如前面所解释的,一些设备需要多个待处理的 I/O 请求来提供其全部吞吐量。因此,一个可以处理(比如)8 个并发请求的设备,在只有 1 或 2 个并发请求的情况下,仍然可以显示 100% 的利用率。在这种情况下,它仍然有很多东西可以提供。你需要看一下吞吐量(kB/s),每秒的操作数,以及队列深度和服务时间,以衡量它被使用的程度。

常见错误

Prometheus 有很多现成的 Grafana 仪表盘,但很多都把这些统计数据弄错了。要么是使用了错误的查询,要么是更常见的标注了错误的图形或是显示错误单位。

作为一个例子,我将以 1860 仪表盘 “Node Exporter Full” 为例。我一直在使用它,并强烈推荐它。

在写这篇文章的时候,其中一个面板被标记为 “磁盘 I/O 加权”,并显示单位为毫秒。

PromQL 查询是:

1
rate(node_disk_io_time_weighted_seconds_total{instance="$node",job="$job"}[5m])

你可能认出这是在查询 “aqu-sz”。问题是什么呢?

首先,Y 轴被标记为 “时间”,并且有时间单位。这是不对的。

原始数据的度量单位是(队列深度*秒),所以它的尺寸是 “秒”。但是,你在这个度量中采取的是一个增加率。因此,其单位是 “秒/秒”,是无维度的。这个值代表了采样区间内的平均队列深度,这只是一个数字。

其次,该图形名为 “磁盘 I/O 加权”。现在,基础指标不是 I/O 操作的数量,而是 I/O 时间(由队列深度加权)。应该说这个图形是 “磁盘 I/O 时间加权” 吗?不是的,因为当你对其进行加权的时候,你已经得到了队列深度。所以一个更好的名字应该是 “平均队列深度”(或大小)。

一旦做了这些修正,该图就更有意义了:它显示了每个数据点所涵盖的时间间隔内未完成的平均 I/O 操作数。

同样,还有一张标有 “I/O 操作的所需时间” 的图表 - 同样,Y 轴以秒为单位标示。

显示的数值范围在 0s 和 1s 之间。它的 PromQL 查询如下:

1
rate(node_disk_io_time_seconds_total{instance="$node",job="$job"}[5m])

事实上,这就是之前显示的 “%util” 的查询。因为这是一个变化率,显示的值实际上是每秒 I/O 活动的秒数。所以它最好显示为一个分数或百分比,而不是时间。再一次,对轴的图例和单位做一个简单的改变就能使它更清晰。

另一张标有 “磁盘 R/W 时间” 的图表显示了以下指标:

1
2
rate(node_disk_read_time_seconds_total{...}[5m])
rate(node_disk_write_time_seconds_total{...}[5m])

不幸的是,这些指标并不是很有意义。尽管被标记为秒,但它们实际上又是无尺寸的(每秒的秒数)。如果你的磁盘每秒进行 100 次读取操作,每次需要 20ms,那么显示的数值是 “2秒”,但这只是意味着平均有2次读取操作在同时进行。

更有用的是使用 r_awaitw_await 的查询:

1
2
3
rate(node_disk_read_time_seconds_total{...}[5m]) / rate(node_disk_reads_completed_total{...}[5m])

rate(node_disk_write_time_seconds_total{...}[5m]) / rate(node_disk_writes_completed_total{...}[5m])

这些查询的维度是 “秒”,分别给出了读和写请求的平均服务时间,在这个例子中,图表会显示 “20ms”,而这更有趣。

更新:仪表板的作者已经大方地接受了这些建议并且更新了图表。谢谢你,Ricardo!

总结

希望这篇文章能帮助你理解 node_exporter 的磁盘指标,这些指标与来自 /proc/diskstats 的原始内核数据的关系,如何用它们创建有用的 Prometheus 查询,以及如何解释结果。

[^1] 严格来说,这是以 KB(1024 B)为单位。磁盘传输是以整个扇区为单位,也就是 512 B 或 4096 B,所以这是磁盘传输速度的传统单位。然而,尴尬的是,磁盘存储容量通常是以 10 次方为单位(例如,1 MB = 1,000,000 B),网络传输速度也是如此(例如,1 Mb/s = 1,000,000 b/s)。
Prometheus 通过转换为字节来回避这个问题。你如何缩放你的图表由你决定;Grafana 支持两种选择,对 10 的幂数称之为 “SI”,对 2 的幂数称之为 “IEC”。

[^2] 在 iostat 中,当分母为 0 时,即设备空闲时,程序逻辑将强制答案为 0。用所示的 Prometheus 查询,你最终会得到 NaN 或无穷大。

本文作者 : meow
This blog is under a CC BY-NC-SA 4.0 Unported License
本文链接 : https://translation.meow.page/post/interpreting-prometheus-metrics-for-linux-disk-i-o-utilization/

本文最后更新于 天前,文中所描述的信息可能已发生改变