和之前所抱怨的一样,xperf(Windows Performance Toolkit)的文档依旧相当简略。在摘要表中使用的列名可能极其微妙,而且我从未见过任何介绍它们的官方文档。不过,我曾和 xperf 的作者进行交流,也自己使用 xperf 很多次,并做过一些实验,这篇文章中将分享更多收获,这次聚焦于“磁盘使用”(Disk Usage)摘要表。
提示: 本文于 2015 年 9 月更新,加入了有关 UIforETW、WPA,以及新列的内容。
The Old New Thing 的一篇文章提供了有关磁盘 I/O 和文件 I/O 跟踪的另一种视图,并解释了哪些开销被忽略(如反恶意软件扫描),另外一篇文章则讨论了 system 进程。
之前与本篇相关的系列文章如下:

本文默认你使用 UIforETW 来记录跟踪,并且安装了随 UIforETW 一同提供的 WPA(Windows Performance Analyzer)启动配置文件。你可以在“Settings”对话框中点击“Copy startup profiles”来完成此操作。另一个默认假设是,你使用 WPA 10 打开跟踪文件——当你在 UIforETW 的“Traces”列表中双击一个跟踪文件时,就会默认使用 WPA 10。
什么时候该使用磁盘使用视图
当我在做性能分析时,我通常不会从“磁盘使用”开始,因为它往往并没有直接的作用。磁盘 100% 占用本身并不一定是坏事,这可能意味着磁盘正在严重抖动(thrashing)并导致应用程序挂起,也可能是操作系统在空闲时将已修改的内存页懒散地写回磁盘。
一般来说,只有当我发现某个卡顿(hang)时才会着手查看磁盘 I/O。通常这是一个空闲 CPU 卡顿,此时关键线程在等待某次磁盘 I/O 操作(例如 ReadFile 或 CreateFile)的返回。偶尔也会是忙碌 CPU 卡顿,CPU 一直在忙等待(busy wait),直到某个 I/O 完成。但总的来说,我通常要先在别处看到线索,比如说“嘿,我们都在等磁盘 I/O!”然后再去看磁盘使用情况。
如何查看
在 WPA 左侧的 Graph Explorer 视图中,你可以双击 Storage 小节打开默认的磁盘使用图表。

然后,你可以点击下图中五个按钮中的第一个(显示摘要表和图表)或者第三个(只显示摘要表),来显示对应的摘要表。你可以使用 “View Editor” 来调整需要显示的列,也可以右键点击任意列来启用/禁用列,或者拖动列来更改它们的顺序。特别注意:对列进行重新排序(尤其是橙色分割线左侧的分组列)对于找出问题非常关键。

展开 Graph Explorer 中的 Storage 项目,你会看到一个名为 Disk Usage 的条目,进一步展开你会发现大约十四个不同的图表供选择。这些图表都能以不同的方式展示磁盘使用情况,但其对应的摘要表提供的列基本相同,总计约 30 个。默认情况下,这些列的顺序不尽相同,有时同一个列会以 Sum 或 Average 的形式重复出现,但总的来说,这些磁盘使用图表关联的摘要表其本质是相同的。
先来点儿炫酷的图表!
在深入枯燥的技术细节之前,我想先分享一下我在 WPA 中最喜欢的一个图表——“Disk Usage Disk Offset”图表。该图在 x 轴上以时间为坐标(与往常一样),在 y 轴上则以磁盘操作的偏移量(disk offset)为坐标。对于传统的机械硬盘来说,这能大致体现磁头的移动情况:

我觉得这真是太酷了!良好或糟糕的磁盘访问模式可以很直观地呈现出来。
图中竖直的红线代表 flush 操作,对磁盘性能而言可谓是致命一击。如图中选中的区域所示:某应用程序在不断地进行写入然后立即 flush,而每一次 flush 都会强制写入 C:\\\\$LogFile
(图底部紫色点),这就意味着相应的寻道开销。
并非所有文件 I/O 都会落到磁盘上
理解“磁盘 I/O”与“文件 I/O”的区别至关重要。在 xperf 的术语里,每当有人调用 ReadFile 等文件 I/O API,就会记一次“File I/O”。然而“Disk I/O”只有在磁盘真正开始工作时才会记录。实际上,Windows 的磁盘缓存在很多时候都能把数据缓存到内存中,从而避免实际的磁盘 I/O。这也是为什么你绝对不会嫌内存太多的原因——我的工作机有 64 GB 内存,其中往往有 45 GB 都是缓存数据,这就让系统运行得相当流畅。
同样需要注意的是,在 Windows 7 中,光驱(光盘)的 I/O 并不会被记录到跟踪中,这确实有点遗憾。
和往常一样,这些列都可以根据需要配置成 count、average、max、min、sum 等统计方式。只需要在 WPA 的 View Editor 中调整 Aggregation 类型即可。
说完这些,下面按照类别来介绍各列的含义。
说明“谁”在进行 I/O 的列
- Process 进行 I/O 的进程名称和进程 ID。很多写操作会显示为 “System”,因为 WriteFile 调用会很快返回,系统进程会在后台执行真正的写操作。对于读操作,如果是系统进行预读(read-ahead),则也可能归因于 “System” 进程。
- Process Name 进程的名称,不带进程 ID;这样可以把同名的进程归到一起。
- Thread ID 正在执行 I/O 操作的线程 ID。
- IO Init Stack 如果在开启 xperf 跟踪时使用了 DiskReadInit、DiskWriteInit 或 DiskFlushInit 选项,那么对应的调用栈就会显示在这里。这些选项可在 UIforETW 的“Extra kernel stackwalks”里配置。当你想准确追踪磁盘 I/O 来源时,它非常有用。然而对于读操作,我更常用的是空闲线程分析来定位磁盘 I/O 的调用栈。
说明“做了什么 I/O,以及在哪里做”的列
- Path Tree 显示被访问文件路径的树状视图。将它拖到橙色分割线左侧后,你可以像普通树形控件那样展开它,从而查看部分路径或子目录下的汇总统计。
- Path Name 显示被访问文件的完整路径。如果将它放在橙色分割线左侧,则可以按文件名(非分层方式)进行分组。
- File Extension 显示被访问文件的扩展名,如果你想按扩展名(如 .dll 或 .jpg)进行分组以发现瓶颈,可能会很有帮助。
- Disk 磁盘编号,方便将其放在最左侧进行按磁盘分组。大多数机器只有一个磁盘,所以一般会显示为 0。
- Min Offset Offset(偏移量)是从磁盘开头(0)到磁盘末端之间的一个十六进制数,表示磁盘访问的位置。你可以把它理解成机械硬盘的磁头位置,用于查看磁盘的寻道情况。该列显示当前汇总行中所有操作的最小偏移量。
- Max Offset 显示当前汇总行中所有操作的最大偏移量。对于单个操作,Max Offset 通常是 Min Offset + Size - 1。通过比较 Min Offset 和 Max Offset,你大致能看出访问区域是否紧密,对于机械硬盘来说,这对性能影响很大。
- IO Type 记录当前 I/O 是否为读取(Read)、写入(Write)还是刷新(Flush)。
说明 I/O 性能特征的列
请注意,单位为“秒”(s)的列代表时间点,而单位为“微秒”(μs)的列则代表持续时长。
- Init Time (s) I/O 请求发起的时间。
- Complete Time (s) I/O 请求完成的时间。
- Disk Service Time (μs) 从磁盘角度看,执行该读或写操作所消耗的时间。假设一个磁盘一次只能处理一个 I/O 或一小段时间只能记给一个 I/O,因此这些时间的总和一般不会大幅超过分析区间的总时长;稍有超出是因为可能有一个 I/O 在分析区间开始之前就已发起,但在分析区间内完成。
- IO Time (μs) 从发起 I/O 的线程角度看,执行读或写操作所需的时间。大致等同于从 Init Time (s) 到 Complete Time (s) 的差值。IO Time 绝不可能小于 Disk Service Time,但如果有多个线程同时执行磁盘 I/O,那么 IO Time 可能比 Disk Service Time 大很多。对于写操作来说,API 在数据真正写入磁盘前就已经返回,所以 IO Time 会被设置为 Disk Service Time,这可能会高估线程在磁盘 I/O 上的实际时间。
- Size 该行汇总的所有操作读/写的字节数,这是衡量工作量的一个指标。
- Count 该行汇总的操作次数。在很多情况下,这可能比 Size 更能体现“工作量”。磁盘最有效的方式是少量大规模读写——通常小于 1 MB 的操作都可能比较低效,因此大量小操作 可能 很低效。
- Priority I/O 优先级。大多数 I/O 都是 Normal 优先级,但像 SearchProtocolHost.exe 之类的进程,有些 I/O 会使用 Very Low 优先级来避免干扰前台任务。其他优先级也有可能出现。I/O 优先级和线程优先级是相互独立的。
- IACA #IOs Init After Completed After ——用来统计某些状态,但含义不够明确。
- IACB #IOs Init After Completed Before ——这个度量的是有多少 I/O 在比当前 I/O 提交得更晚却更早完成了。这是一种优化,可以减少磁头移动从而提升吞吐,但会影响某些请求的延迟。
- IBCB #IOs Init Before Completed Before ——参见后面对相关计数的说明。
- QD/C Queue Depth at Complete Time ——当前 I/O 完成时,队列中还有多少个排队的 I/O。
- QD/I Queue Depth at Init Time ——当前 I/O 提交时,队列中已有多少 I/O 在排队。深队列往往意味着磁盘已经饱和,可能 会拖慢你的磁盘 I/O。
- Annotation 这个列在大多数(或所有)WPA 表里都能使用,方便在分析过程中添加备注。可参考这篇文章了解更多细节。
其他杂项列
- Source 我只见过 “Original” 或 “Unknown”,不清楚它具体意味着什么。
- Boosted 不明所以。
- Table 表的名称。
- Thread Activity Tag 不清楚。
- Trace # 显示当前可视化的是哪一个加载的跟踪文件。
示例摘要表
要想在摘要表中更有效地分析数据,你需要根据实际问题来重新排列列。下面是一些示例的截图。如果你有特别钟爱的排列方式,可以把它们添加到 WPA 里,下次快速使用。
如下图,将分组依次设为“磁盘号”和“读/写类型”,再统计其 I/O 次数和大小,以获得某些高层的整体视图。

这里是 Windows Live Photo Gallery 启动 时的一些磁盘 I/O,按照进程和 Path Tree 分组,并按 Init Time 排序。这可以让我们发现,WLPG 在启动过程中会在同一个文件里频繁进行随机小范围读取,这就是它能花上一分钟甚至更久才启动完毕的原因。

其他很多实用的列排序/分组方式都可以尝试,找出最适合你的分析需求即可。
学会自给自足
缺少全面的官方文档确实令人沮丧,但有时也能找到一些绕过它的方法。当我写这篇文章时,就找了一个磁盘 I/O 很多的跟踪,然后在 WPA 里尝试各种列和排列。将某列拖到最左侧,就能自动得到在该跟踪中出现的所有可能值,通常这已经足以帮助我写出相应的解释。建议你也大胆尝试移动这些列,通过这种方式既能调查性能问题,也能摸清那些未知列的真实含义。
有时你会怀疑某些列之间存在数学关系,比如 Init Time、Complete Time 和 IO Time 之间。想测试这些猜想时,你可以选中几百行数据,右键点击后使用 “Copy Selection”,然后粘贴到 Excel 里做计算,非常高效。
关于 brucedawson
我是 Google 的一名程序员,主要工作是做优化和可靠性开发。让代码跑得快 10 倍比什么都开心,除非能同时消灭大量 bug。我也玩单轮车、打冰球(ice hockey)和轮椅冰球(sled hockey),还会玩杂耍(juggle)。有时我会纠结这个博客究竟该不该叫 randomutf-8。了解更多可看我在 Twitter 上的2010 年代回顾。
以上翻译仅供参考,原文信息请以英文原版为准。
原文链接:https://randomascii.wordpress.com/2012/11/21/the-lost-xperf-documentationdisk-usage/
2