我最近注意到,我的 Windows 7 家用笔记本电脑运行速度比平时要慢。系统似乎在磁盘 I/O 上花了很多时间。当我查看任务管理器时,我发现笔记本电脑的缓存磁盘数据量大约只有平时的 五分之一,但问题的原因并不明显。
我的工作之一是性能专家——要调查各种日常或深层次的性能问题。所以当我意识到我的笔记本出现了严重且系统性的问题时,我集中精力,拿出所有专业工具进行调查,最终找到了罪魁祸首。
你即将读到的这个故事完全真实。唯一被改动的是一些缓存、可用和空闲内存的精确数值,以保护“无辜者”。
提前剧透:我从 Costco 买的那台 Western Digital My Book Live 备份驱动器是罪魁祸首。
再多剧透一点:在我发表这篇文章九天之后,Western Digital 联系了我,并非常认真地对待这个问题。大约四个月后,他们发布了修复补丁。这个故事的结局很美好,而且你可以通过博客来改变世界。
缓存的意义
想要理解这个谜团,首先得理解 Windows 磁盘缓存的重要性。在计算机里拥有大量内存是很重要的,不仅能让你同时运行很多程序,而不必动用页面文件,而且还能让更多的数据缓存到内存里。硬盘由于依赖物理旋转盘片,是计算机里速度最慢的组件之一,而笔记本硬盘则是硬盘中最慢的类型。Windows 会把硬盘中的数据缓存到内存中,以便能瞬间访问这些数据,而不是每次都要花上几毫秒来读取。缓存的数据越多,你对磁盘的访问就越少,计算机速度也就越快。正因为如此,Windows 的磁盘缓存几乎可以说“内存越多越好”。我工作的机器有 32 GiB(GiB 指二进制 1024 的幂的单位),这意味着我通常能缓存超过 24 GiB 的磁盘数据,这让我能在不同的程序和不同的项目之间切换时,最大程度地减少磁盘 I/O。
从 Windows Vista 开始,Windows 引入了一些只为将磁盘缓存填满而生的系统机制,它会根据日志判断你下一步最可能需要哪些数据,然后提前加载。比如 SuperFetch 会监控你的使用模式,在你启动应用程序时提前预取数据。系统空闲时也会进行预取。
这些功能在 Windows Vista 刚推出时让人们感到困惑。用户抱怨 Vista 占用大量内存,很“笨重”,因为在任务管理器里看到几乎没有空闲内存。然而事实证明,这其实是好事。空闲内存完全是浪费,一点用都没有。一台运行良好的 Windows 机器应该几乎没有空闲内存,而有大量被用来做磁盘缓存的内存。缓存的内存是有用的,因为它能让系统运行得更快,而且当需要时,缓存内存可以随时转变成空闲内存来供程序分配。
健康的机器
当 Windows 正常运行时,任务管理器(通过右键点击任务栏来打开)在物理内存统计信息中应该显示类似右图的内容:大量的 cached(缓存)内存,大量的 available(可用)内存,以及很少的 free(空闲)内存。如果 available 内存很低(低于几千 MiB),就值得考虑增加更多内存,以便 Windows 有更多空间来运行程序并缓存数据。如果 free 内存很高(高于几百 MiB),那么要么 Windows 还没来得及往磁盘缓存中填充数据,要么就是哪里出了问题。
不健康的机器
在我的笔记本上,任务管理器的“性能”选项卡看起来是这样的:
上图中紫色圈出的区域显示:我有很多可用内存,但磁盘缓存比预期要小(对于一台 8 GiB 的机器来说),而且空闲内存明显偏高。我的 8 GiB 笔记本通常能缓存 4-5 GiB 的数据,但我注意到问题发生时,它只能缓存 1-2 GiB 左右的数据。
查看内存使用历史,则能看到为什么缓存内存低、空闲内存高。
上图中红色圈起的部分显示了三次短暂的内存使用峰值。每一处峰值都对应了一个瞬时的 4 GiB 内存分配。Windows 为满足其中一次分配,把 4 GiB 有用数据从文件缓存中腾空,最后就变成了浪费的空闲内存。
任务管理器帮助我了解了问题的大概结构,但并不能找到罪魁祸首。它的“峰值工作集(Peak Working Set)”一列里没有显示任何可能导致这类峰值的进程。这时候我就要使用最喜欢的系统分析工具了。
用 xperf 进行调查
我使用了我所知最强大的性能调查工具:ETW tracing(也就是 xperf),来自 Windows Performance Toolkit(WPT)。这是微软免费提供的一套工具,可以从 CPU 使用率、磁盘 I/O、内存分配等各个角度来分析 Windows 的性能。xperf 使用起来并不容易,但对于熟练用户来说,它能提供对 Windows 性能几乎无限的洞察力。
我决定使用新的 Windows Performance Recorder UI(wprui)来记录追踪数据。我勾选了所有和内存分配相关的选项,并让它把日志记录到内存中(目前我会使用 UIforETW,在“Settings”对话框中勾选“VirtualAlloc stacks always”,在“Extra kernel flags”里选“POOL”,并在“Extra kernel stackwalks”里选“PoolAlloc”)。在这种模式下,wprui 或 UIforETW 会持续监控你的系统,将详细的性能信息写入到内存中的循环缓冲区里。当你发现性能问题时,点击“Save”按钮,就能把缓冲区保存到磁盘上。也就是说,它是一种“事后”分析器,你可以随时记录最后约 30 秒的系统活动。我一般会让这种内存记录模式一直开着,这样我随时可以捕捉罕见的性能问题,而不用去想如何复现。当下图截图时,我已经让 wprui 连续运行了快一周,但它的“Time”字段每天会重置。
抓个正着
内存峰值大多出现在随机时刻,很容易错过,所以我一直没能捕捉到想要的追踪记录。不过我最后发现,每次我的笔记本从待机恢复时,都会出现一次内存峰值。这样一来就简单多了。只要在 wprui 开始记录后,让笔记本休眠并恢复,登录后立刻点击“Save”就行。搞定!然后我把记录加载到 WPA(Windows Performance Analyzer)里,查看和内存相关的异常。这一步有时候需要耐心,因为你并不知道应该看哪里,也不知道具体要找什么,但有时当你看到正确的图表时,问题会非常明显。看看下面这张图,能看到那个极其明显的内存峰值吗?
到目前为止,我们看到的还只是跟任务管理器类似的信息,但 xperf 的强大之处在于可以深入得多。这个追踪记录包含所有池(pool)内存分配事件的调用栈。只要选中合适的时间范围(从峰值中段到峰值前),我们就能得到一个汇总表。通过合理设置汇总表列,按大小排序,再深入到 AIFO 类型(即“在选定时间范围内分配,但未在范围内释放”),我们很快就找到了那个导致问题的 4,294,967,296 字节的分配(也就是 4 GiB)。而且我们还拿到了调用栈。
最初看到这个调用栈时,我以为是 Windows 的 bug。因为我最近才装了一些 Windows 更新,我猜可能是哪个补丁导致了这个问题。结果我猜错了,但需要再深入一些才能明白。
从调用栈可以看到,FstubAllocateDiskInformation 这个函数做了两次 ExAllocatePoolWithTag 调用,其中一次分配了大量内存。现在是时候看看 FstubAllocateDiskInformation 做了什么。
真相在代码里
要查看 FstubAllocateDiskInformation 这段代码,最简单的方法是使用 Local Kernel Debugging。通过它可以让 windbg 获取内核的内存快照,而无需真正中断内核执行。它对调试功能有局限,比如不能设置断点或者单步调试,但查看反汇编已经够用了,而且比在两台机器之间用电缆做实时内核调试更简单。
很显然,这里需要用 uf
命令来反汇编此函数。下面是反汇编后的结果,我稍作了格式化,并用粗体标出了几个关键的指令,并加上了一些注释:
lkd> uf ntkrnlmp!FstubAllocateDiskInformation
nt!FstubAllocateDiskInformation:
mov qword ptr [rsp+8],rbx
mov qword ptr [rsp+10h],rbp
mov qword ptr [rsp+18h],rsi
push rdi
sub rsp,20h
mov rsi,rdx
mov rbp,rcx
mov r8d,42747346h ; pool tag of FstB
mov edx,40h ; allocation size
xor ecx,ecx ; flags
call nt!ExAllocatePoolWithTag
test rax,rax
je Exit1nt!FstubAllocateDiskInformation+0x35:
lea rdx,[rax+10h]
mov rcx,rbp
mov rbx,rax
call nt!FstubGetDiskGeometry
test eax,eax
js Exit2nt!FstubAllocateDiskInformation+0x4c:
mov ecx,dword ptr [rbx+24h]
test ecx,ecx ; compare SectorSize to zero
je Exit3nt!FstubAllocateDiskInformation+0x57:
mov rax,qword ptr [rbx+28h]
test rax,rax
je Exit3nt!FstubAllocateDiskInformation+0x64:
xor edx,edx
mov dword ptr [rbx+8],ecx
mov r8d,42747346h ; pool tag of FstB
div rax,rcx
mov rdx,rcx ; SectorSize/allocation size
mov ecx,4 ; flags
mov qword ptr [rbx+38h],rax
mov qword ptr [rbx],rbp
call nt!ExAllocatePoolWithTag
test rax,rax
je Exit4
你不需要仔细理解所有代码才能跟上后面的逻辑。代码中有两次调用 ExAllocatePoolWithTag
,一次是 0x40 字节,另一次则是可变大小。两次调用都使用了 pool tag 42747346h
(对应 “FstB”)。要做完整分析还要查看输入参数类型及其布局,我也去 reactos 网站上看了他们的源码版本,以确认我对这段汇编的解读没错。关键行可以这样概括:
FstubGetDiskGeometry(DiskInformation);
...
if (DiskInformation->SectorSize != 0) ExAllocatePoolWithTag(DiskInformation->SectorSize);
换句话说,FstubAllocateDiskInformation
调用了 FstubGetDiskGeometry
(参见 FstubGetDiskGeometry),然后如果 SectorSize
不为 0,就分配 SectorSize
字节的内存。
我稍微停顿了一下,去 验证 主硬盘的扇区大小:
C:>fsutil fsinfo ntfsinfo c:
NTFS Volume Serial Number : 0x00b8457cb8457168
Version : 3.1
Number Sectors : 0x0000000048ad8ae2
Total Clusters : 0x000000000915b15c
Free Clusters : 0x00000000016ac4a5
Total Reserved : 0x0000000000000780
Bytes Per Sector : 512
Bytes Per Physical Sector : 512
...
随后,我猛地醒悟到几周前,我购买并使用了一个备份驱动器,当我第一次使用它时,它安装了一个驱动。这个驱动正是导致所有问题的根源。这正好和我开始出现性能问题的时间相吻合,也和我安装那些 Windows 更新的时间差不多。
问题解决!
我打开设备管理器,找到了问题驱动,把它卸载,然后甚至不用重启电脑,我的笔记本就恢复正常了。无论是休眠还是恢复,都不再出现内存峰值,我的磁盘缓存也不会再被清空,倍感欣慰。现在我这台笔记本有 8 GiB 内存的真正表现了,而不再像只有 4 GiB 那样。
我并不是驱动开发专家——我的领域在用户态——但我能想象大概发生了什么。这个驱动好像在处理 IOCTL_DISK_GET_DRIVE_GEOMETRY 请求时,要返回一个 DISK_GEOMETRY 结构。但如果某些信息不可用,该怎么做呢?或许可以返回一个错误代码,也可能填一个全是零的结构,但这个驱动却把所有字段——包括 BytesPerSector
——都设成了 0xFFFFFFFF
。由于 SectorSize
是无符号数,加上内存分配会向上取整,所以最终导致分配了 4 GiB 内存。这个错误不算太难犯,但 Western Digital 的开发者们确实应该注意到它带来的严重问题。
Windows 内核只检查了 BytesPerSector
是否等于 0,却没有检查它是否在某个合理的范围(也可参见 KB2510009)内。也就是说,内核少了一道保护,否则本可以避免此问题。
如果你拥有这台设备,我建议你把它退掉。不过,这可能还不够。我在离那个备份驱动器很远、并且没有连接它的情况下,也会出现这个问题。所以很可能需要打开设备管理器,手动卸载驱动。这个我也不完全肯定。
我还建议你联系 Western Digital,要求他们放出修复补丁。
后记
发现问题后,我原本只想把这个驱动器退掉,但在那之前,我特意确保我机器上装了最新的驱动,以及 NAS 固件上的所有更新,以保证测试结果准确。随后我清空了 NAS 里的数据,并格式化了硬盘,“Full Factory Restore” 选项会安全地擦除磁盘,这对于退回一个曾经存过我整机备份的硬盘至关重要。然后,我卸载了驱动,确认问题解决。
再一次,ETW 让我用科学的方式调查并解决了一个神秘问题。我希望能有更多的开发者运用“基于证据的方法”来调查性能问题,而不是依靠猜测。
我同情那些普通消费者,他们可能会很随意地买下这个设备,也不太可能想到把性能问题和新买的备份硬盘联系起来。
我通过一些私人渠道联系了微软,他们确认相关团队已经知晓这个问题。我在 2012 年 8 月 29 日通过普通的支持渠道联系了 Western Digital,但他们一直没回复——后来他们的一位工程师看到了这篇文章,并在 2012 年 9 月 7 日(也就是九天之后)通过技术支持渠道回复了我。
其他故事
如果你喜欢这篇文章,也许你也会喜欢我用 xperf 进行的其他取证式性能调查:
- Xperf and Visual Studio: The Case of the Breakpoint Hangs
- PowerPoint Poor Performance Problem(以及 Xperf for Excess CPU Consumption 这篇更详细的教程,它探讨了同一个问题)
- Faster Fractals Through Better Scheduling —— 调查并优化 Fractal eXtreme 中的一些低效点
- Xperf Wait Analysis–Finding Idle Time —— 分析 Visual Studio 中三秒卡顿的教程
- Windows Live Photo Gallery—Poor Performance Peculiarities —— 调查并解决放大图像时出现的短暂卡顿
- Fixing another Photo Gallery performance bug —— 调查并解决启动时的卡顿问题
如果你想学着自己用 xperf 做调查,可参考 ETW central。
关于 brucedawson
我是 Google 的一名程序员,主要工作是做优化和可靠性开发。让代码跑得快 10 倍比什么都开心,除非能同时消灭大量 bug。我也玩单轮车、打冰球(ice hockey)和轮椅冰球(sled hockey),还会玩杂耍(juggle)。有时我会纠结这个博客究竟该不该叫 randomutf-8。了解更多可看我在 Twitter 上的2010 年代回顾。
以上翻译仅供参考,原文信息请以英文原版为准。
原文链接:https://randomascii.wordpress.com/2012/09/04/windows-slowdown-investigated-and-identified/