这一章在做什么
回答一个问题:应用等文件系统等了多久,等在了哪里。文件系统拿内存做缓存(page cache),再加上预读和写回,把存储设备的慢挡在应用外面,所以磁盘上的数字常常和应用的感受对不上。这一章教你从应用这一层量延迟:看完整的分布而不是平均值,揪出慢的那几次,分清是缓存没命中、同步写在等盘,还是后台的写回和别的进程。工具从 free、strace 一直到 ext4dist、ext4slower。
什么时候翻这一章
- 服务读写文件偶尔很慢,不知道算不算盘的问题先在应用这一层(或文件系统这一层)量延迟,算出它占请求时间的比例。占比很小,就别在存储上花时间。§8.5.2§8.6.14场景手册 →
- 平均读延迟不高,p99 却很难看缓存命中和未命中混在一起,分布是两个峰;平均值落在两峰中间,什么也说明不了。看直方图,盯住慢的那一峰。§8.6.13§8.6.18场景手册 →
- ARM 板写 eMMC 或 SD 卡,一写日志就卡多半是同步写(O_SYNC、每条一次 fsync)在等闪存;也可能是脏页攒得太多,写的进程被拉去一起写回。§8.3.6§8.3.7§7.6.1场景手册 →
- free 里几乎没有空闲内存,buff/cache 占了一大半这是正常的:闲着的内存被拿去做文件缓存,应用要用时内核会收回来。该看的是 available。§8.3.2§8.6.2场景手册 →
- iostat 里盘一阵一阵地忙,应用却不慢后台写回、预读、别的进程都会产生没人在等的磁盘 IO。逻辑 IO 和物理 IO 对不上是常态。§8.3.12场景手册 →
- 自己做的读写测试,数字好得不像真的测试文件比内存小,测到的是缓存,不是盘。§8.5.8§8.7.1场景手册 →
page cache 的回收、换页和 dirty 系列参数在第 7 章(内存);盘本身的延迟、利用率、iostat 在第 9 章。一条常见的排查路线是:先在这一章确认应用确实在等文件系统,再往第 9 章走。
先用中文过一遍
page cache:命中和未命中差两三个数量级
Linux 把读过的文件内容留在内存里,叫页缓存(page cache)。下次再读同一块,内核在内存里就找到了,几微秒返回,这是命中(cache hit);找不到才去存储设备读,要等几百微秒到几毫秒,这是未命中(cache miss),读上来的数据顺手放进缓存(§8.2.2)。缓存会慢慢长到占满闲着的内存,这是故意的:内存闲着也是闲着,应用要用时内核再从缓存里回收(§8.3.2)。所以 free 里 free 一栏很小、buff/cache 很大,一般不用担心,看 available 就行。
两种结果的耗时差了两三个数量级,混在一起就是一个双峰分布(bimodal distribution):左边一个峰是命中,右边一个峰是未命中。这时平均值落在两峰之间的低谷,几乎没有哪一次请求真的花了那么久;中位数也一样,命中率高时它只反映命中那一峰(§8.6.18)。书里因此坚持看完整的分布(直方图、热图),或者把慢的操作逐条列出来(§8.5.2)。命中率 99% 时,平均值几乎全由命中决定,那 1% 的慢请求被淹没在里面,而它们恰恰是用户在抱怨的。
写回和同步写:write() 返回不等于落盘
写的一侧默认是写回缓存(write-back caching):write() 把数据拷进内核的页缓存、把这些页标成脏页(dirty page),就返回了,应用只花了拷内存的时间。真正写到盘上是之后的事,由内核的 flusher 线程在后台做:脏页放了一段时间(默认约 30 秒),脏页总量超过阈值,或者有人调了 sync、fsync,都会触发写回(§8.4.3)。代价是可靠性:掉电时,还没写下去的脏页就丢了(§8.3.6)。
不能丢数据的程序用同步写(synchronous write):文件用 O_SYNC / O_DSYNC 打开,每次 write() 都等数据和必要的元数据落盘才返回;或者照常写,到一个检查点再调一次 fsync(),把之前的写一起提交(§8.3.7)。后者通常快得多,因为一批写只等一次盘。嵌入式板子上这件事特别要紧:慢的闪存上每等一次都可能是毫秒级,一个每条日志都 fsync 的线程,吞吐就被钉死在每秒几百条甚至更少。
oflag=dsync 后每次约 1.6 毫秒,差了一百多倍(见下面「动手」)。预读和直接 IO:两种绕开等待的办法
顺序读一个大文件,按说每一块第一次读都该未命中。实际上很快,因为文件系统发现偏移量一个接一个,就提前把后面的几块读进缓存,这叫预取(prefetch),Linux 上一般叫预读(read-ahead)(§8.3.4)。猜对了,应用的读全是命中;猜错了,白读的数据占着缓存、占着盘的带宽。随机读没法预读,这也是随机读比顺序读慢得多的原因之一(§8.3.3)。
另一头是直接 IO(direct I/O):用 O_DIRECT 打开文件,读写都绕过页缓存(§8.3.8)。数据库这类自己管缓存的程序用它,免得同一份数据在两处各缓存一遍。它还常被拿来做测试,量的是盘而不是缓存。注意它不是同步写:O_DIRECT 不承诺 O_SYNC 那样的落盘保证,而且对缓冲区地址、IO 大小有对齐要求,不对齐可能直接返回 EINVAL。
逻辑 IO 和物理 IO:应用看到的不是盘看到的
应用发给文件系统的读写叫逻辑 IO(logical I/O),文件系统发给盘的叫物理 IO(physical I/O)。两者常常对不上(§8.3.12):有的缩水了(命中缓存就没有物理 IO,多次改同一块只写回一次),有的膨胀了(改几个字节要读写整块,再加一份日志),有的是间接的(预读、后台写回),有的跟你的应用根本无关(别的进程、备份、RAID 重建)。这就是书里不推荐只盯着磁盘排查的原因(§8.5.1):盘上 50 毫秒的写,可能没有任何请求在等;应用卡住的那次读,在盘上可能根本不存在(它在等一把锁,或者在等同一个文件上的 fsync)。
| 英文 | 中文 | 一句话 |
|---|---|---|
| page cache | 页缓存 | 内核拿内存缓存文件内容,大小随空闲内存伸缩;free 里算在 buff/cache §8.4.3 术语页 |
| cache hit ratio | 缓存命中率 | 读请求里在缓存中找到的比例;理想情况接近 100% §8.6.12 术语页 |
| prefetch / read-ahead | 预取 / 预读 | 发现顺序读,就提前把后面的数据读进缓存 §8.3.4 |
| write-back caching | 写回缓存 | 写进内存就算完成,稍后异步写到盘上;快,但掉电会丢 §8.3.6 术语页 |
| dirty page / flushing | 脏页 / 刷写 | 改过还没写下去的页;flusher 线程负责把它们写回 §8.4.3 |
| synchronous write | 同步写 | O_SYNC 打开或调 fsync,等数据落盘才返回 §8.3.7 术语页 |
| direct I/O | 直接 IO | O_DIRECT,绕过页缓存;不等于同步写 §8.3.8 术语页 |
| logical / physical I/O | 逻辑 IO / 物理 IO | 应用发给文件系统的 / 文件系统发给盘的,常常对不上 §8.3.12 术语页 |
| VFS | 虚拟文件系统 | 内核里各种文件系统共用的一层接口,也是量延迟的一个统一位置 §8.4.2 术语页 |
| dentry cache / inode cache | 目录项缓存 / inode 缓存 | 缓存路径查找结果和文件属性,在 slabtop 里看得到 §8.4.3 |
| journaling | 日志 | 先把改动记进日志再落到原处,崩溃后能快速恢复;同步写入日志 §8.4.4 |
| working set size (WSS) | 工作集大小 | 实际被访问的数据量;装得进内存,测的就是缓存 §8.5.8 术语页 |
| bimodal distribution | 双峰分布 | 命中一个峰、未命中一个峰,平均值落在中间 §8.6.18 术语页 |
| transaction cost | 事务里的文件系统耗时 | 一个请求里阻塞在文件系统上的时间占比,决定优化它最多能快多少 §8.5.2 |
方法:按什么顺序查
书里建议先用这几种方法,顺序是:延迟分析、性能监控、负载特征、微基准测试、静态性能调优(§8.5)。核心是第一个:从应用这一侧量文件系统操作的延迟,而不是先看盘。落到一次排查上,本站整理成下面六步。前两步决定要不要继续查文件系统;要 root 的 BPF 工具从第二步开始出场,没有它们时每一步都给了退路。
- 文件系统占了请求时间的多少?
在应用里给读、写、
fsync计时,或者对一个线程短时间跑strace -c/strace -T粗看。占比 = 一个请求里阻塞在文件系统上的时间 ÷ 请求总时间。占九成,消掉它最多能快近十倍;只占百分之一,就去查别处。§8.5.2 - 延迟的分布长什么样?
ext4dist-bpfcc 10 1(要 root)按操作类型给出直方图。只有左边一个微秒级的峰,说明读几乎全命中,文件系统不是问题;右边有一个毫秒级的峰,再看它占多少。没有 BPF 时,在应用里自己按 2 的幂分桶计数,就是同一张图(见下面第二个实验)。§8.6.13§8.6.18 - 慢的那几次是什么操作、哪个文件、谁发的?
ext4slower-bpfcc 1列出超过 1 毫秒的操作。类型是S(fsync)的多,是同步写在等盘;R的多,是读未命中。只有 strace 时用strace -T -e trace=read,write,fsync -p PID,但要记住它会把每次调用拖慢一大截。§8.6.14§8.6.7 - 施加的负载是什么样的?
谁在读写哪些文件:
opensnoop-bpfcc、filetop-bpfcc -C;读写比例、同步写有多少、顺序还是随机。/proc/PID/io里rchar是读了多少(逻辑),read_bytes是真正从存储层取了多少(物理),两者一比就知道命中了多少。常常能找到根本不该做的读写(反复打开同一个配置文件、调试日志没关)。§8.5.3§8.6.10§8.6.11 - 缓存够不够、写回压不压得住?
free -m看 buff/cache 和 available;cachestat-bpfcc 1看命中率;grep -E 'Dirty|Writeback' /proc/meminfo看脏页有多少。命中率低,先比一比工作集和可用内存:应用自己的内存占得太多,留给缓存的就少。§8.6.2§8.6.12§8.5.8 - 配置有没有问题?
mount看文件系统类型和挂载选项(relatime/noatime、有没有sync);df -h看是不是快满了;程序是O_SYNC逐条写还是攒一批fsync。这些都排除了,延迟确实来自盘,就到第 9 章去看盘本身。§8.5.5§8.6.1§8.3.16
工具速查
| 想知道 | 敲什么 | 看哪里 | 节 |
|---|---|---|---|
| 缓存占了多少内存 | free -mfree -mw | buff/cache 大是正常的;available 才是应用还能拿到多少;-w 把 buffers 和 cache 分开 | §8.6.2 |
| 脏页和写回 | grep -E 'Dirty|Writeback' /proc/meminfovmstat 1 | Dirty 长期很大、写回时 bo 一阵猛冲;vmstat 的 buff、cache 两列是缓存大小 | §8.6.4 |
| 挂载选项 | mount | 文件系统类型;relatime 还是 noatime;有没有 sync 这种让所有写都变同步的选项 | §8.6.1 |
| 单次系统调用多慢 | strace -T -e trace=read,write,fsync -p PID | 行尾尖括号里的秒数;它本身会拖慢被跟踪的进程,只短时间用 | §8.6.7 |
| 读了多少、走没走盘 | cat /proc/PID/io | rchar 是逻辑读的字节,read_bytes 是从存储层取的;后者远小于前者说明大多命中 | §8.3.12 |
| 打开了哪些文件 | opensnoop-bpfcc -Topensnoop-bpfcc -x | 同一个文件被反复打开;-x 只看打开失败的,找缺失的配置和库 | §8.6.10 |
| 哪个文件读写最多 | filetop-bpfcc -C | 按读字节排序的文件列表;-C 不清屏,输出留在终端里 | §8.6.11 |
| 文件访问事件(不用 BPF) | fatrace | 每行一个事件:O 打开、R 读、W 写、C 关闭;忙的系统上输出极多,开销不小 | §8.6.8 |
| page cache 命中率 | cachestat-bpfcc 1 | HITRATIO 接近 100% 才好;DIRTIES 是写;它靠 kprobe,换内核版本可能失效 | §8.6.12 |
| ext4 延迟分布 | ext4dist-bpfcc 10 1 | 每种操作一张直方图;读出现两个峰,左边命中、右边未命中;XFS 用 xfsdist | §8.6.13 |
| 慢操作逐条列出 | ext4slower-bpfcc 1 | 默认阈值 10 毫秒,参数改阈值;T 列 R 读、W 写、O 打开、S 同步 | §8.6.14 |
| 目录项和 inode 缓存 | slabtop -osar -v 1 | dentry、ext4_inode_cache 这几个 slab 的大小;sar -v 的 dentunusd | §8.6.6 |
free、vmstat、slabtop 在 procps 包里,sar 在 sysstat,strace 和 fatrace 各是一个同名包。带 -bpfcc 后缀的是 Debian bpfcc-tools 包里的 BCC 工具,和 fatrace、slabtop 一样都要 root。工具名链到本站的工具卡:free、strace、fatrace、opensnoop、filetop、cachestat、ext4dist、ext4slower、slabtop。
动手:在自己的机器上试
page cache:读走缓存,写先进内存
用一个 128 MB 的文件看三件事:缓存怎么长出来、冷读和热读差多少、同步写比普通写慢多少。全程不需要 root:丢掉某个文件的缓存用的是 dd 的 iflag=nocache(底下是 posix_fadvise 的 DONTNEED),它丢不掉还没写下去的脏页,所以前面要先 sync 这个文件。
# 在自己的目录里做,文件总量控制在两三百 MB 以内 $ mkdir -p ~/fslab && cd ~/fslab $ free -m $ dd if=/dev/urandom of=f1 bs=1M count=128 status=none $ free -m # 先把 f1 落盘,再请内核把它的页从缓存里丢掉 $ sync f1 $ dd if=f1 iflag=nocache count=0 status=none $ free -m # 冷读、热读、绕过缓存各一次 $ dd if=f1 of=/dev/null bs=1M $ dd if=f1 of=/dev/null bs=1M $ dd if=f1 of=/dev/null bs=1M iflag=direct
本站在 WSL2 上看到的(free 只留了 buff/cache 一列,dd 只留最后一行):
buff/cache(MB) 写之前 1867 写完 f1 1998 丢掉 f1 的缓存后 1867 冷读 134217728 bytes copied, 0.142748 s, 940 MB/s 热读 134217728 bytes copied, 0.0316084 s, 4.2 GB/s 直接 134217728 bytes copied, 0.0531463 s, 2.5 GB/s
- 写完 f1,buff/cache 正好多了约 130 MB:刚写的数据留在了缓存里,free 一栏跟着少了。丢掉它的缓存,又回到原来的数。
- 热读比冷读快四倍多。这个差距在 WSL2 上偏小:虚拟盘背后还有 Windows 的文件缓存,冷读其实也是从宿主的内存里拿的;第二次重跑冷读,本站看到的是 1.6 GB/s。在真的 eMMC 或机械盘上,冷读会慢一两个数量级。
- 绕过缓存的直接读(
iflag=direct)比冷读还快,也是同一个原因,外加它用 1 MB 的大块直接下发。这一组数在 WSL2 上只看相对关系,不代表真机。
再看写。下面三条命令都写 2000 个 4 KB 的块:
$ dd if=/dev/zero of=f2 bs=4k count=2000 # 每次写都等落盘(相当于 O_DSYNC) $ dd if=/dev/zero of=f2 bs=4k count=2000 oflag=dsync # 照常写,最后 fsync 一次 $ dd if=/dev/zero of=f2 bs=4k count=2000 conv=fsync # 看脏页什么时候被写回:写 100 MB,每 5 秒看一次 Dirty $ dd if=/dev/zero of=f3 bs=1M count=100 status=none $ for i in $(seq 0 8); do grep '^Dirty:' /proc/meminfo; sleep 5; done $ rm -f f1 f2 f3
普通写 8192000 bytes copied, 0.0206156 s, 397 MB/s dsync 8192000 bytes copied, 3.19233 s, 2.6 MB/s 末尾 fsync 8192000 bytes copied, 0.0162681 s, 504 MB/s Dirty(每 5 秒) 104700 103804 107076 107292 98696 99104 105156 9112 11608 (kB)
- 每次都等落盘,2000 次写花了 3.2 秒,平均每次 1.6 毫秒;普通写平均每次 10 微秒。同样的数据,差了一百五十倍。
- 最后统一
fsync一次,和普通写差不多快:8 MB 一起交给盘,只等一次。这就是「攒一批再提交」的道理。 - 100 MB 脏页在内存里停了 30 多秒,然后一下子被写回。这和
vm.dirty_expire_centisecs的默认值 3000(30 秒)对得上;本机读到dirty_writeback_centisecs是 1500(内核的默认值是 500,即 5 秒,本机为什么不同本站没有查),flusher 每 15 秒才醒来检查一次,过期的脏页要等到它下一次醒来,大概因此看到的是 35 秒左右;你的机器上可能不同。
- WSL2
- 本站实测过上面每一条命令(Debian 13,coreutils 9.7),不需要 root。WSL2 的虚拟盘有宿主缓存垫着,冷读和同步写的绝对值都偏乐观,只看相对关系。
- 服务器
- 别在生产机的业务盘上跑写测试。要冷缓存可以用 root 写
/proc/sys/vm/drop_caches(会清掉整台机器的缓存,影响线上服务),只丢一个文件就用上面的iflag=nocache。 - ARM 板
- busybox 有
free和dd,但iflag=nocache、oflag=dsync这些选项是 GNU dd 的,busybox 支持哪些看你的版本和裁剪;板子上有 root 时直接echo 3 > /proc/sys/vm/drop_caches。/proc/meminfo的 Dirty、Writeback 一般都在。在 eMMC、SD 卡上跑 dsync 那一条,每次写的耗时会明显比 WSL2 长,正好量一量你的板子同步写一次要多久;写测试会磨损闪存,量少一点。
从应用这一层量延迟:亲眼看见双峰
下面这个小程序随机读一个文件 4 KB 一块,给每次 pread 计时,最后按 2 的幂分桶打印。它就是方法第一步说的「在应用里计时」,也是没有 BPF 时 ext4dist 的替身。先猜:文件只有一半在缓存里时,平均值会是多少?
// lat.c:随机读一个文件 N 次(每次 4 KB),给每次 pread 计时,打印对数直方图
#define _GNU_SOURCE
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>
#include <sys/stat.h>
static long long now_ns(void) {
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return ts.tv_sec * 1000000000LL + ts.tv_nsec;
}
static int cmp(const void *a, const void *b) {
long long x = *(const long long *)a, y = *(const long long *)b;
return (x > y) - (x < y);
}
int main(int argc, char **argv) {
if (argc < 3) { fprintf(stderr, "usage: %s FILE N\n", argv[0]); return 1; }
int fd = open(argv[1], O_RDONLY);
if (fd < 0) { perror("open"); return 1; }
int n = atoi(argv[2]);
struct stat st; fstat(fd, &st);
long blocks = st.st_size / 4096;
posix_fadvise(fd, 0, 0, POSIX_FADV_RANDOM); // 关掉预读,每次未命中只读这一块
static char buf[4096];
long long *t = malloc(sizeof(long long) * n), sum = 0;
int hist[32] = {0};
srand(42); // 固定种子:每次跑读的是同一串偏移
for (int i = 0; i < n; i++) {
off_t off = (off_t)(rand() % blocks) * 4096;
long long t0 = now_ns();
if (pread(fd, buf, sizeof buf, off) != sizeof buf) { perror("pread"); return 1; }
t[i] = (now_ns() - t0) / 1000; // 微秒
sum += t[i];
int b = 0; while (b < 31 && (2LL << b) <= t[i]) b++; // 落在 [2^b, 2^(b+1)) 这一桶
hist[b]++;
}
for (int b = 0; b < 32; b++)
if (hist[b]) printf("%6lld -> %-6lld us : %d\n", b ? 1LL << b : 0, (2LL << b) - 1, hist[b]);
qsort(t, n, sizeof *t, cmp);
printf("avg %lld us, p50 %lld us, p99 %lld us\n", sum / n, t[n / 2], t[n * 99 / 100]);
return 0;
}
$ gcc -O2 -Wall lat.c -o lat $ dd if=/dev/urandom of=f1 bs=1M count=128 status=none # 全冷:丢掉 f1 的缓存再读 $ sync f1; dd if=f1 iflag=nocache count=0 status=none $ ./lat f1 4000 # 全热:同一串偏移再读一遍,刚才读过的块都在缓存里 $ ./lat f1 4000 # 一半热:丢掉缓存,只把前 64 MB 读进来,再随机读整个文件 $ dd if=f1 iflag=nocache count=0 status=none $ dd if=f1 of=/dev/null bs=1M count=64 status=none $ ./lat f1 4000
本站在 WSL2 上的结果。全冷是 avg 164 us, p50 160 us,全热是 avg 1 us, p50 1 us;一半热的完整输出:
0 -> 1 us : 721
2 -> 3 us : 1104
4 -> 7 us : 216
8 -> 15 us : 120
16 -> 31 us : 17
64 -> 127 us : 162
128 -> 255 us : 1630
256 -> 511 us : 25
512 -> 1023 us : 3
1024 -> 2047 us : 2
avg 74 us, p50 5 us, p99 245 us
- 两个峰:几微秒的是命中,一两百微秒的是未命中,32 到 63 微秒这一桶一次也没有。平均值 74 微秒正好落在空档附近,没有哪一次读真的花了这么久;p50 是 5 微秒,只说了命中那一峰。上面「先用中文过一遍」的直方图就是这组数。
- 全冷那次也有两百多次读很快:随机偏移会重复,第二次读到同一块就命中了。命中率不只看文件有多少在缓存里,还看访问模式。
- 想先确认缓存里到底有多少页,可以用 Linux 6.5 加的
cachestat系统调用问内核,本站在 WSL2 的 6.6 内核上用syscall(451, …)调通了:只读前 64 MB 后报告 f1 有 16640 页(约 65 MB)在缓存里,多出来的一点是 dd 顺序读时的预读。f1 是自己建的文件;对别人的只读文件,打过 2025 年安全补丁的内核不给结果。
最后看 strace 自己的代价。把文件全部读进缓存,分别直接跑和在 strace 下跑:
$ dd if=f1 of=/dev/null bs=1M status=none $ ./lat f1 4000 | tail -1 $ strace -o /dev/null -e trace=pread64 ./lat f1 4000 | tail -1 $ rm -f f1
直接跑 avg 0 us, p50 1 us, p99 1 us
strace 下 avg 141 us, p50 133 us, p99 219 us
- 同样是全命中的读,在 strace 下每次从 1 微秒变成 130 多微秒,比 WSL2 上的未命中还慢。strace 用 ptrace 在每次系统调用进出时把进程停下来,程序自己量到的时间也被拉长了(§8.6.7)。
- 所以
strace -T看到的绝对值不可信,尤其是本来很快的调用;它适合短时间看「调了什么、哪几次特别慢」。量延迟要用开销低的办法:应用自己计时,或者 BPF 工具。
- WSL2
- 本站实测过:gcc 14、内核 6.6;strace 用的是 Debian 的 strace 6.13(装
strace包)。跟踪自己启动的程序不需要 root。未命中那一峰的位置是 WSL2 虚拟盘的,不代表真机。 - 服务器
- 有 root 就直接
ext4dist-bpfcc 10 1、ext4slower-bpfcc 1看真实服务(本站没有 root,没有实测)。别对线上的高频进程长时间挂 strace。 - ARM 板
- lat.c 只用 POSIX 接口,交叉编译就能跑,最适合 BPF 用不了的板子;读的文件放在 eMMC 或 SD 卡上,未命中那一峰会比 WSL2 靠右得多。strace 很多 BSP 里有(Buildroot、Yocto 都能选上),开销问题一样。cachestat 系统调用要内核 6.5 以上,看你的 BSP 内核版本。
动手:模拟器
先猜:命中率 90%,命中 2 微秒,未命中 1 毫秒(慢一点的闪存),平均延迟是多少?有多少次读的耗时接近这个平均值?再把命中率调到 99%,平均值降了多少,那 1% 的慢请求还在不在?
缓存命中率与延迟分布
每次「读」先按命中率决定命中还是未命中,再按各自的延迟抽一个耗时(围绕设定值上下浮动)。下面是对数刻度的直方图(柱高按次数的平方根画,矮的峰也看得见):浅色是命中,橙色是未命中;实线是样本的平均值,虚线是按公式算的平均值。延迟的预设是示意值,不是哪块盘的实测。
- 默认设置读 1000 次:平均值落在哪?「落在平均值 ±30% 内的读」是多少?再把未命中换成机械盘,平均值翻了几倍,两个峰本身变了吗?
- 命中率从 90% 调到 99%、再到 99.9%:p50 几乎不动,平均值一路下降。到 99.9% 时读 1000 次,p99 落在哪一峰?这就是为什么高命中率时平均值和中位数都看不见那几次慢读,要看 p99、p99.9 或者逐条列慢操作。
- 把命中延迟从 2 微秒改成 10 微秒(比如每次读还要多走一层加密或校验):命中率 99% 时,它对平均值的影响和「未命中从 1 毫秒变成 8 毫秒」比,哪个大?
成书之后
查询某个文件的缓存状态,有了正式的系统调用
- 书里
- cachestat 工具靠 kprobe 跟踪内核函数,换内核版本就可能失效;作者希望将来有跟踪点或 /proc 统计,让它变稳定(§8.6.12)。
- 今天
- Linux 6.5(2023 年)加入了
cachestat()系统调用:给一个文件和一段范围,返回其中在缓存里的页数、脏页数、正在写回的页数、被逐出的页数和最近被逐出的页数,不需要 root。2025 年初修了一个安全问题(CVE-2025-21691)之后,打过补丁的内核要求调用者是文件的属主或有写权限才给出结果,和 mincore 的规矩一样。它回答的是「这个文件有多少在缓存里」,不是全系统的命中率,所以不能完全替代 cachestat 工具。本站在 WSL2 的 6.6 内核上用syscall(451, …)调用过。
BPF 里能拿到文件路径了:bpf_d_path
- 书里
- 跟踪 read(2) 只拿得到文件描述符,分不清是文件、套接字还是 /proc;作者提到一个即将到来的 BPF 辅助函数,名字写作 get_fd_path(),能把描述符换成路径(§8.6.15)。
- 今天
- 最后进入内核的名字是
bpf_d_path(),随 Linux 5.10 合入,作用是由内核里的路径结构得到完整路径。你的内核和 BPF 工具都够新时,按路径区分文件系统读写方便多了;内核是 5.10 以前的(不少板子的 BSP 内核还很老),还得用书里说的其他办法。
分节地图
这一章还是那个套路:背景(§8.1–8.3)、架构(§8.4)、方法(§8.5)、工具(§8.6)、实验和调优(§8.7–8.8)。上手要读的是概念里讲缓存、预读、写回、同步写、逻辑与物理 IO 的几节,架构里的 Linux 缓存,方法里的延迟分析和微基准测试,工具里的 free 和三个 BPF 工具,最后是讲双峰的可视化。各种文件系统的细节、ZFS 的调优用到再查或者跳过。
- File system performance often matters more…约 300 词开篇为什么文件系统比盘更贴近应用;学习目标和六个部分。
- Terminology约 300 词术语本章的 IO 只算直接读写的操作,open、close 不算。
- Models约 100 词模型一句引子。
- File System Interfaces约 100 词文件系统的接口把文件系统当黑盒,只量各种操作的延迟。
- File System Cache约 100 词文件系统缓存命中、未命中、预热;写也会先缓冲。
- Second-Level Cache约 100 词二级缓存闪存做的第二级缓存,主要是 ZFS 的做法,一般用不到。
- Concepts约 100 词概念一句引子。
- File System Latency约 300 词文件系统延迟首要指标;哪些情况下应用并不直接等它。
- Caching约 200 词缓存缓存吃掉空闲内存为什么是正常的;几种缓存。
- Random vs. Sequential I/O约 200 词随机与顺序 IO按偏移量区分;碎片让顺序读变成随机读。
- Prefetch约 300 词预取顺序读怎么被提前读进缓存;猜错的代价。
- Read-Ahead约 100 词预读Linux 的叫法和 readahead 系统调用。
- Write-Back Caching约 200 词写回缓存写进内存就返回,稍后再刷;掉电的风险。
- Synchronous Writes约 200 词同步写逐次同步写和用 fsync 成批提交的区别。
- Raw and Direct I/O约 200 词裸 IO 与直接 IO绕过文件系统和只绕过缓存;对齐和 EINVAL。
- Non-Blocking I/O约 200 词非阻塞 IOO_NONBLOCK、aio 和 io_uring;普通文件上未必生效。
- Memory-Mapped Files约 200 词内存映射文件mmap 省的是系统调用,省不掉等盘。
- Metadata约 200 词元数据逻辑元数据和物理元数据;stat 密集的负载。
- Logical vs. Physical I/O约 700 词逻辑 IO 与物理 IO应用的 IO 和盘的 IO 为什么对不上,五类原因。
- Operations Are Not Equal约 200 词操作各不相同只看每秒操作数说明不了什么。
- Special File Systems约 100 词特殊文件系统tmpfs、/proc、/sys 这些不落盘的文件系统。
- Access Timestamps约 100 词访问时间戳读文件也会引起写;怎么关掉见调优。
- Capacity约 100 词容量文件系统快满时为什么变慢。
- Architecture约 100 词架构这一部分的引子。
- File System I/O Stack约 100 词文件系统 IO 栈从系统调用到设备的几条路径。
- VFS约 100 词VFS各文件系统共用的接口,也是统一的观测点。
- File System Caches约 900 词文件系统的几种缓存buffer cache、page cache、dentry、inode 缓存;脏页何时写回。
- File System Features约 500 词文件系统特性块与 extent、日志、写时复制、擦洗。
- File System Types约 2000 词文件系统类型FFS 的历史,ext3、ext4、XFS、ZFS、btrfs 的性能特性。选文件系统时查。
- Volumes and Pools约 400 词卷与存储池LVM 和存储池;RAID 重建会拖慢很久。
- Methodology约 100 词方法方法总表和作者建议的起手顺序。
- Disk Analysis约 100 词只看磁盘只盯着盘的老办法为什么不够了。
- Latency Analysis约 600 词延迟分析在哪一层量、怎么呈现;请求里文件系统耗时的占比。
- Workload Characterization约 600 词负载特征描述文件系统负载的几项属性和一张问题清单。
- Performance Monitoring约 200 词性能监控长期盯操作速率和延迟两个指标。
- Static Performance Tuning约 200 词静态性能调优一张检查配置的问题清单。
- Cache Tuning约 100 词缓存调优几种缓存都可以查看、往往可以调。
- Workload Separation约 100 词负载分离日志和数据放在不同的盘上。
- Micro-Benchmarking约 400 词微基准测试工作集大小决定测的是缓存还是盘。
- Observability Tools约 100 词观测工具工具总表:传统工具和 BPF 工具。
- mount约 100 词mount挂载选项;relatime 怎么减少时间戳写入。
- free约 100 词freebuff/cache 和 available 两列怎么读。
- top约 100 词top头部也有缓存大小。
- vmstat约 100 词vmstatbuff 和 cache 两列。
- sar约 100 词sar-v 看目录项和 inode,-r 看缓存大小。
- slabtop约 100 词slabtop哪些 slab 是文件系统的缓存。
- strace约 200 词strace在系统调用层计时;开销大,数字会被拉长。
- fatrace约 100 词fatrace基于 fanotify 的文件访问事件;忙时输出太多。
- LatencyTOP约 100 词LatencyTOP作者说它久未更新、要少见的内核选项,用 BPF 工具代替。
- opensnoop约 200 词opensnoop跟踪文件打开,找配置、日志和缺失的文件。
- filetop约 200 词filetop按文件排的 top,找意外忙碌的文件。
- cachestat约 200 词cachestatpage cache 命中率;命中率低时调什么。
- ext4dist (xfs, zfs, btrfs, nfs)约 200 词ext4dist 等延迟分布工具按操作类型画延迟直方图,读出双峰。
- ext4slower (xfs, zfs, btrfs, nfs)约 200 词ext4slower 等慢操作工具逐条列出超过阈值的操作,看出写后同步的模式。
- bpftrace约 800 词bpftrace单行命令、跟踪 read 的坑、VFS 延迟按类型拆分。要自己写工具时查。
- Other Tools约 200 词其他工具别处讲过的十几个 BPF 工具,df、inotify,ZFS 的工具。
- Visualizations约 400 词可视化文件系统延迟为什么是双峰,热图怎么读。
- Experimentation约 100 词实验测试时让 iostat 一直开着,确认盘上有没有动静。
- Ad Hoc约 100 词临时测试用 dd 粗测顺序读写;写的数字是进内存的速度。
- Micro-Benchmark Tools约 500 词微基准测试工具Bonnie、fio、FileBench;作者推荐 fio。
- Cache Flushing约 100 词清空缓存drop_caches 的三个值,测试前让缓存回到冷状态。
- Tuning约 100 词调优最大的收益来自去掉不必要的工作。
- Application Calls约 200 词应用的调用fsync 成批提交;posix_fadvise 和 madvise 给缓存提示。
- ext4约 300 词ext4挂载选项、tune2fs、/sys/fs/ext4 和 e2fsck;noatime。
- ZFS约 200 词ZFSZFS 的属性调优;不用 ZFS 的读者可以跳过。
- Exercises约 300 词习题习题;动手部分本页已经给了替代。
词与短语
只收先读和用到再查两档的节。斜体那一行是原句里连续的几个词,在电子书里搜它就能找到那一句。
§0 开篇
§8.2.2 File System Cache
§8.3.1 File System Latency
§8.3.4 Prefetch
§8.3.6 Write-Back Caching
§8.3.9 Non-Blocking I/O
§8.3.10 Memory-Mapped Files
§8.3.12 Logical vs. Physical I/O
§8.3.13 Operations Are Not Equal
§8.4.3 File System Caches
§8.4.5 File System Types
§8.4.6 Volumes and Pools
§8.5.1 Disk Analysis
§8.5.2 Latency Analysis
§8.5.7 Workload Separation
§8.5.8 Micro-Benchmarking
§8.6.7 strace
§8.6.8 fatrace
§8.6.12 cachestat
§8.6.18 Visualizations
§8.7 Experimentation
§8.8 Tuning
诊断题:换成你来查
场景和输出都是本站编的。先自己判断,再点选项看解释。
场景:一块 ARM 板上的采集程序每收到一条数据就往 eMMC 上的日志文件写一行,文件是 O_SYNC 打开的。数据一密集,采集线程就积压。你用 strace 看了一会儿:
write(5, ..., 96) = 96 <0.011820> write(5, ..., 112) = 112 <0.009347> write(5, ..., 88) = 88 <0.014105>
每次写一百来字节却要等十毫秒左右,时间全花在等闪存落盘上:线程每秒最多写一百条上下。攒一批再 fsync,一批只等一次盘,这是书里说的成批提交(§8.3.7、§8.8.1)。mmap 省掉的是系统调用的开销,等盘的时间一点没少(§8.3.10);O_SYNC 的写根本不经过脏页的积累,调 dirty 参数没有用;换大容量的盘也不改变每次同步写的延迟。strace 本身也会拉长每次调用,但这里的十毫秒远大于它的开销。
场景:一台 16 GB 的服务器跑你的 C++ 文件服务,同事看到下面的 free 输出,担心内存快用完了,想每小时执行一次 echo 3 > /proc/sys/vm/drop_caches。
total used free shared buff/cache available Mem: 15890 3490 410 25 12360 12400
page cache 会长到占满空闲内存,这是设计如此(§8.3.2)。判断还剩多少看 available:它算上了能马上回收的缓存(§8.6.2)。定时清缓存,等于每小时把这台文件服务的命中率打回零,接下来一阵子的读全要等盘,延迟分布右边那一峰会突然变高。drop_caches 适合做基准测试前让缓存回到冷状态(§8.7.3),不是日常维护手段。
场景:服务每个请求读一小块文件,监控上读延迟的平均值是 0.85 毫秒,团队觉得「盘有点慢」。你用 ext4dist 看了 10 秒的读:
usecs count 0 -> 1 8120 2 -> 3 1530 4 -> 7 210 (中间几桶都是 0) 2048 -> 4095 400 4096 -> 8191 900 8192 -> 16383 250
左峰 9860 次在几微秒内,右峰 1550 次在 2 到 16 毫秒:典型的命中加未命中(§8.6.13)。没有一次读真的花了 0.85 毫秒。两条路:提高命中率(工作集和可用内存比一比,看应用是不是把内存占得太多),或者降低未命中的代价(这才轮到盘,到第 9 章查)。下一步用 ext4slower-bpfcc 1 看慢的是哪些文件、哪些偏移,有没有本来可以顺序读的随机读。
场景:iostat 显示数据盘每隔三十来秒写延迟就冲到 80 毫秒、利用率接近 100%,持续一两秒。服务的 p99 延迟一直平稳,ext4slower-bpfcc 10 跑了几分钟没有输出。
time w/s w_await %util 10:00:01 12 0.9 1.1 10:00:31 840 82.4 96.0 10:00:32 15 1.2 1.5
书里专门拿这种情况举例:后台刷写在磁盘统计上看着吓人,可是没有应用在等它(§8.3.1、§8.3.12)。应用这一层没有超过 10 毫秒的操作,说明请求没被它拖累。把过期时间调短会让写更碎、更频繁,还少了同一页多次修改只写一次的好处;改 O_DIRECT 是大改,而且让每次写都直接等盘。只有当读请求排在写回后面、ext4slower 开始出现慢读时,才需要动手。
场景:一块 1 GB 内存的 ARM 板,厂家说 eMMC 顺序写大约 40 MB/s。你在板子上测了一下:
$ dd if=/dev/zero of=test bs=1M count=200 209715200 bytes (210 MB, 200 MiB) copied, 1.1 s, 191 MB/s
默认的写是写回缓存,dd 报告的是把数据拷进内存的速度(§8.7.1)。工作集比内存小时,测的是缓存而不是盘(§8.5.8)。conv=fsync 让 dd 在结束前 fsync 一次,时间里就包括了真正写到 eMMC 的部分;测的同时另开一个终端看 /proc/meminfo 的 Dirty,或者有 iostat 就开着,确认数据确实到了盘上。换 urandom 会把生成随机数的 CPU 开销混进来,eMMC 一般也不压缩数据。
值得停下来的句子
For this reason, representing the distribution as a single value—such as a mean, mode, or median—is misleading.— §8.6.18。本站译:因此,用一个数——平均值、众数或中位数——来代表这个分布,会误导人。
这句话前面是理由:文件系统延迟本来就是两个峰,一个是缓存命中,一个是去存储设备。监控面板上最常见的恰恰是一个平均值。读者自己的服务大概率也是这样:缓存命中率九成以上,平均值被几微秒的命中拉得很好看,用户抱怨的却是那几次几十毫秒的读。看到「平均延迟正常」时,先问一句分布是什么样的,上面的模拟器把这件事演了一遍。
From the disk device-level statistics, this looks alarming; however, no application is waiting on these to complete.— §8.3.1。本站译:从磁盘设备层的统计看,这很吓人;可是并没有哪个应用在等它们完成。
「这」指后台刷写已写数据时突发的高延迟磁盘 IO。它说的是这一章的立足点:衡量好坏的是应用等了多久,不是盘有多忙。反过来也成立:盘很闲,应用也可能在等文件系统(等锁、等同一个文件上的 fsync)。所以排查从应用那一层的延迟开始,确认确实在等,再往下走到盘。
带走的几条
一句话:从应用这一层量文件系统延迟、看整个分布,分清命中、未命中和同步写,确认应用真在等了,再去看盘。
一个请求里等文件系统的时间占多少。占九成,值得深挖;占百分之一,去查别处。
§8.5.2命中和未命中差两三个数量级,平均值落在两峰之间。用直方图、p99 或逐条的慢操作(ext4dist、ext4slower)。
§8.6.18闲内存拿去做缓存,应用要用时会被收回。看 available,别定时 drop_caches。
§8.3.2普通写只是进了内存,脏页默认放约 30 秒才写回,掉电会丢。要保证落盘用 fsync 或 O_SYNC,代价是每次都等盘。
§8.3.6慢闪存上每次 fsync 都可能是毫秒级。逐条同步写会钉死吞吐,攒一批再 fsync,批的大小按能丢多少定。
§8.3.7后台写回、预读、别的进程都会让盘忙。逻辑 IO 和物理 IO 对不上是常态,以应用这一层为准。
§8.3.12文件比内存小,测的是缓存。测盘要用 direct IO 或把 fsync 算进去,同时看着盘上有没有 IO。
§8.5.8