Linux 页缓存命中率

最近一次 Linux 性能回退,最终发现是页缓存命中率变化造成的:旧系统的缓存效果很好,新系统却很差。那么,如何直接测量页缓存命中率?

像下面这样的工具怎么样?

# ./cachestat 1
Counting cache functions... Output every 1 seconds.
    HITS   MISSES  DIRTIES    RATIO   BUFFERS_MB   CACHE_MB
     210      869        0    19.5%            2        209
     444     1413        0    23.9%            8        210
     471     1399        0    25.2%           12        211
     403     1507        3    21.1%           18        211
     967     1853        3    34.3%           24        212
     422     1397        0    23.2%           30        212
[...]

它不只显示缓冲区和页缓存的大小,也显示活动统计。我已将 cachestat 加入 GitHub 上的 perf-tools 工具集。

更完整的示例

下面先给出示例输出,再说明产生它的工作负载:

# ./cachestat -t
Counting cache functions... Output every 1 seconds.
TIME         HITS   MISSES  DIRTIES    RATIO   BUFFERS_MB   CACHE_MB
08:28:57      415        0        0   100.0%            1        191
08:28:58      411        0        0   100.0%            1        191
08:28:59      362       97        0    78.9%            0          8
08:29:00      411        0        0   100.0%            0          9
08:29:01      775    20489        0     3.6%            0         89
08:29:02      411        0        0   100.0%            0         89
08:29:03     6069        0        0   100.0%            0         89
08:29:04    15249        0        0   100.0%            0         89
08:29:05      411        0        0   100.0%            0         89
08:29:06      411        0        0   100.0%            0         89
08:29:07      411        0        3   100.0%            0         89
[...]

我使用 -t 选项加入 TIME 列,以便说明输出。工作负载为:

# echo 1 > /proc/sys/vm/drop_caches; sleep 2; cksum 80m; sleep 2; cksum 80m

在08:28:58,第一条命令清除了页缓存。CACHE_MB(页缓存大小)从191 MB 降为8 MB,体现了这一变化。

等待2秒后,在08:29:01对名为 80m 的80 MB 文件执行 cksum,总共导致约20,400次未命中(MISSES 列),页缓存增大80 MB。每页为4 KB,因此20k × 4k = 80 MB。未缓存读取期间,命中率降到3.6%。

再等待2秒后,08:29:03第二次执行 cksum。这次完全命中缓存,统计跨越了两行输出。

工作原理

我想知道 Linux 内核内置的 ftrace 能否测量缓存活动,因为 ftrace 函数性能分析能在内核中高效计数。系统的缓存活动频率可能很高,因此必须认真考虑任何插桩的开销。

ftrace 函数性能分析虽然开销较低,但能力也有限。它可以按 CPU 统计内核函数调用,并显示平均延迟。这与 perf-tools 中 funccount 使用的是同一机制。例如,我无法使用高级过滤器匹配函数参数或返回值。只有仅凭内核函数调用频率推断缓存活动时,这种方式才能奏效。

对于我研究的内核(3.2和3.13),我分析以下四个内核函数以测量缓存活动:

  • mark_page_accessed():测量缓存访问。
  • mark_buffer_dirty():测量缓存写入。
  • add_to_page_cache_lru():测量页面加入。
  • account_page_dirtied():测量页面变脏。

mark_page_accessed() 显示总缓存访问次数,add_to_page_cache_lru() 显示缓存插入次数。add_to_page_cache_locked() 也能显示插入,甚至包含一个跟踪点,但在较新的内核上不会触发。

我一度以为这两个函数已经足够:假定插入就是未命中,那么已有未命中次数和总访问次数,便可算出命中次数。

问题在于,写入也会发生访问和插入,并将缓存数据标记为脏。因此另外两个函数帮助区分这些情况。请记住,我只能使用函数调用频率。mark_buffer_dirty() 用于识别哪些访问来自写入,account_page_dirtied() 用于识别哪些插入来自写入。

你的内核版本中,这些函数可能已经改名,或逻辑不同,脚本便无法原样工作。我希望少于四个函数就能实现,以提高可维护性,但在已测试的工作负载中,没有找到有效的更小函数集合。

如果 cachestat 在我使用的内核版本上过于频繁地失效,我可能会改写为使用支持过滤的 SystemTap 或 perf_events。

注意事项

缓存活动插桩确实有开销,该工具可能使目标系统慢约2%。如果对缓存进行压力测试,开销可能更高。它还使用内核函数动态跟踪,具体内核版本下可能导致内核冻结或 panic。使用前应先测试。

这些统计也应视为尽力而为的估计。某些不常见工作负载不能被这四个内核函数正确匹配,其出现频率可能带来误差。应用于目标场景前,应通过已知工作负载测试,建立可信度。

最初的问题

遇到前面提到的 Linux 性能回退时,我还没有 cachestat。我们发现磁盘 I/O 频率很高,于是我调查原因,逐步追溯到了缓存未命中。我使用自定义 ftrace 和 perf_events 命令,测量内核函数频率及其调用栈。

虽然完成了排查,但我希望下次有更好的方法,于是开发了 cachestat。

其他方法

我发现人们通常采用以下几种方法研究 Linux 页缓存命中率:

A) 使用 iostat(1) 监控磁盘读取,研究页缓存未命中频率,并假定这些读取来自缓存未命中,而不是例如 O_DIRECT。实际上,未命中频率通常比命中率更重要,因为未命中与应用的性能痛点成正比。同时使用 free(1) 查看缓存大小。

B) 清空页缓存(echo 1 > /proc/sys/vm/drop_caches),测量性能变差了多少!我很喜欢这种负向实验,但用它了解缓存使用情况,代价当然很高。

C) 使用 sar(1) 研究 minor fault 和 major fault。我认为这行不通,例如对于常规 I/O。

D) 使用 SystemTap 的 cache-hit-rate.stp 脚本。它在“Linux page cache hit ratio”的互联网搜索中排第二。脚本在调用栈较高的 VFS 接口处跟踪缓存访问,因此能看到任何文件系统或存储设备的读取;缓存未命中通过磁盘 I/O 测量。它也会遗漏部分工作负载类型(相关页面的 Lessons 一节提及了一些),而且把比值称为 rate。

原本我会先尝试 SystemTap 方法,但它可能遗漏 mmap 读取以及其他内核来源。例如,下方 mark_page_accessed()(一次缓存读取)的调用栈表明,我们是通过 write() 系统调用到达这里的:

          dd-30425 [000] 6788093.150288: mark_page_accessed: (mark_page_accessed+0x0/0x60)
          dd-30425 [000] 6788093.150291:
 => __getblk
 => __bread
 => ext3_get_branch
 => ext3_get_blocks_handle
 => ext3_get_block
 => __block_write_begin
 => ext3_write_begin
 => generic_perform_write
 => generic_file_buffered_write
 => __generic_file_aio_write
 => generic_file_aio_write
 => do_sync_write
 => vfs_write
 => sys_write
 => system_call_fastpath

这里读取的是文件系统元数据。示例通过我的 kprobe 工具使用 ftrace,工具属于 perf-tools。

我更倾向于修改内核,对页缓存活动插桩,例如选择:

E) 应用 Keiichi 的 pagecache monitoring 内核补丁。它提供缓存插桩跟踪点和功能很强的工具,不仅能得到系统级比值,还能按进程、按文件观察。我希望它进入主线。

F) 开发另一个内核补丁,向 /proc/meminfo 加入缓存命中、未命中统计。

此外,还有我实际排查该问题时采用的方法:通过 ftrace 和 perf_events 动态跟踪文件系统和磁盘 I/O 函数。

pcstat

如果你对页缓存活动感兴趣,也应该看看 Amy Tobey 的 pcstat。它使用 mincore(或 fincore),查看文件有多少内容存在于页缓存中,十分好用。

结论

希望未来内核能通过 /proc 或跟踪点,提供简单的页缓存活动测量方式。目前,我有适用于自己内核版本的 cachestat。它当前的实现比较脆弱,其他版本可能需要修改后才能正常工作,因此最大的价值也许是展示:投入一点努力,就可以实现什么。


原文:Linux Page Cache Hit Ratio
作者:Brendan Gregg,2014年12月31日。本文保留原文历史内核范围3.2/3.13;示例输出来自原文,不是本次实测。原文权利归原权利人所有,转载依据用户对本批文章的明确授权;工具自身许可证须发布前核对。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容