一次 read() 调用花了 200 毫秒,但
iostat 显示磁盘平均延迟只有 0.3
毫秒——延迟到底丢在了哪一层?这个问题在存储系统调优里反复出现,但很少有人能给出准确答案。原因很简单:从应用发起
I/O
到数据真正从介质返回,中间至少经过七层软件栈,每一层都可能引入排队、锁竞争、上下文切换和中断延迟。只看平均值更是灾难——平均延迟
0.5 毫秒的系统,P99 可能已经飙到 50 毫秒。
本文从存储 I/O
路径的逐层分解开始,分析各层延迟的分布特征和放大机制,然后聚焦尾延迟(Tail
Latency)、延迟热图(Latency
Heatmap)两个核心分析方法,最后用
biolatency、ext4dist、fileslower、ext4slower
四个 BCC/bpftrace 工具做全链路延迟追踪实战。所有命令在 Linux
5.15+(Ubuntu 22.04)上验证,内核源码引用基于 Linux
6.1。
一、全链路延迟的工程意义
1.1 延迟与吞吐的区别
吞吐量(Throughput)回答的是”每秒能搬多少数据”,延迟(Latency)回答的是”一次操作要等多久”。高吞吐不等于低延迟——一条高速公路可以每小时通过一万辆车,但如果入口收费站排队半小时,单车延迟依然很高。
在存储场景里,吞吐优化和延迟优化经常冲突。批量合并 I/O 能提高吞吐,但会让先到达的请求等待后到达的请求一起下发,增加延迟。I/O 调度器为了提高吞吐做请求排序,但排序本身引入延迟。理解这个矛盾是做存储调优的前提。
1.2 为什么要做全链路分析
只看块设备层延迟(iostat 的
await)不够,原因有三:
- 应用感知的延迟包含了所有软件层的开销。 文件系统的日志提交、Page Cache 的回写抖动、VFS 的锁竞争都不会反映在块设备层的统计里。
- 延迟可能在中间层被放大。 一个 4 KB 的
read()到达文件系统后可能触发元数据读取,变成多个块设备 I/O,每个 I/O 的延迟累加后远超单次块设备延迟。 - 排队延迟只有在请求入口才能观测。
块设备层的
await包含了设备端排队时间,但不包含在块设备层入口的排队时间——在高负载下,后者可能远大于前者。
1.3 延迟分析的核心目标
延迟分析不是为了得出一个数字,而是为了回答三个问题:
- 延迟花在哪一层? 逐层分解,找到延迟贡献最大的层次。
- 延迟是稳定的还是波动的? 看分布,而不只看均值。
- 什么操作触发了异常延迟? 追踪具体的慢操作,关联到文件、进程、I/O 类型。
二、存储 I/O 路径分解
2.1 七层模型
一次存储 I/O 从用户态到硬件,经过以下层次:
应用层(Application)
│
├── 用户态库(libc / io_uring library)
│
├── 系统调用(syscall: read / write / pread / pwrite / io_uring_enter)
│
├── 虚拟文件系统(VFS, Virtual File System)
│
├── 具体文件系统(FS: ext4 / XFS / Btrfs)
│ ├── 日志子系统(Journal / Log)
│ └── 元数据管理
│
├── 页缓存(Page Cache)
│
├── 块设备层(Block Layer: bio → request → blk-mq)
│ ├── I/O 调度器(mq-deadline / BFQ / kyber / none)
│ └── 请求合并(plug / merge)
│
├── 设备驱动(Device Driver: NVMe / SCSI / virtio-blk)
│
└── 硬件(Hardware: SSD Controller / HDD / NVMe SSD)
每一层都有自己的延迟特征。下面逐层拆解。
2.2 应用层
应用层的延迟包括:
- 用户态缓冲区管理。
stdio的缓冲 I/O(fread/fwrite)在用户态维护一个缓冲区,只有缓冲区满或显式fflush时才发起系统调用。缓冲区拷贝本身耗时极短(纳秒级),但缓冲策略影响系统调用的频率。 - 序列化与编码。 应用把内存中的数据结构序列化为字节流再写入,序列化本身的 CPU 开销可能不亚于 I/O 开销。
- 锁竞争。 多线程应用访问同一个文件描述符时,应用层的互斥锁可能引入排队延迟。
应用层延迟的特点是:与 I/O
栈无关,只能通过应用层的追踪工具(strace、应用内埋点)观测。
2.3 系统调用层
read() / write()
系统调用本身的开销包括:
- 用户态→内核态切换。 在 x86_64
上,
syscall指令加上内核入口的寄存器保存大约需要 50-100 纳秒。启用 KPTI(Kernel Page Table Isolation,内核页表隔离)后,TLB 刷新会额外增加 100-200 纳秒。 - 参数校验。 内核检查文件描述符有效性、缓冲区地址可访问性。
copy_from_user/copy_to_user。 在内核和用户空间之间拷贝数据,耗时与数据量成正比。4 KB 拷贝大约需要 200-500 纳秒。
io_uring 通过共享环形缓冲区(Submission
Queue / Completion
Queue)和内核轮询(IORING_SETUP_SQPOLL)减少系统调用开销,在高频小
I/O 场景下可以把系统调用层延迟从微秒级降到接近零。
2.4 VFS 层
VFS(Virtual File System,虚拟文件系统)是内核中所有文件系统的统一接口层。它的延迟来源包括:
- 路径查找(Path Lookup)。
open()要逐级解析路径名,每级目录查找涉及dentry缓存查询和可能的磁盘读取。深路径(10 级以上)在dentry缓存未命中时可能引入显著延迟。 - inode 锁。 VFS 对 inode
加读写信号量(
i_rwsem),写操作之间互斥,读写操作之间也互斥。多线程并发写同一个文件时,i_rwsem成为瓶颈。 - 文件系统回调。 VFS 通过
file_operations和inode_operations回调具体文件系统,回调函数的执行时间取决于具体文件系统的实现。
2.5 文件系统层
以 ext4 为例,文件系统层的延迟来源:
- 元数据查找。 把文件偏移量翻译成磁盘块地址需要查找 extent 树。对于大文件,extent 树可能有多层,每层查找是一次内存操作(如果元数据已缓存)或一次磁盘读取(如果未缓存)。
- 日志提交(Journal Commit)。 ext4
的日志(jbd2)默认每 5
秒做一次提交,将事务从内存写入日志区域。如果应用调用
fsync(),会触发立即提交,延迟取决于日志写入的块设备延迟。 - 空间分配。
写入新数据时需要分配磁盘块。ext4
的多块分配器(
mballoc)在空闲空间充足时速度很快,但在碎片化严重时可能需要扫描大量位图。 - 延迟分配刷写。 ext4
默认使用延迟分配(Delayed Allocation),
write()时只在 Page Cache 中标记脏页,不立即分配磁盘块。真正的分配和写入发生在回写(Writeback)时,回写路径的延迟不会体现在write()的延迟里,但会体现在fsync()和后台 I/O 的延迟里。
2.6 页缓存层
页缓存(Page Cache)是读写延迟的关键分水岭:
- 缓存命中。 读操作如果命中 Page Cache,延迟只有内存拷贝的开销——4 KB 数据大约 200-500 纳秒。这是最快的路径。
- 缓存未命中。 读操作未命中 Page Cache 时,需要从块设备读取数据。Page Cache 会发起预读(Readahead),默认预读窗口最大 128 KB。预读可以把后续读请求变成缓存命中,但第一次读取的延迟无法避免。
- 脏页回写压力。 当脏页比例超过
dirty_ratio(默认 20%)时,写操作会被阻塞,等待回写完成后才能继续。在内存紧张时,这个阻塞可能持续数百毫秒甚至秒级。 - 内存回收。 系统内存不足时,内核回收 Page Cache 的过程涉及 LRU 链表操作和磁盘写入(如果回收脏页),这会给 I/O 路径引入不可预测的延迟。
2.7 块设备层
块设备层的延迟组成:
- 请求分配。 从 blk-mq 的标签集(Tag Set)分配一个请求。如果标签耗尽(队列满),请求会被阻塞,等待标签释放。NVMe 设备默认队列深度为 1023,高并发下可能出现标签等待。
- I/O 调度器排队。
mq-deadline调度器为读请求维护一个截止时间(默认 500 毫秒),超时的请求优先派发。调度器的排队时间在低负载下接近零,高负载下可能到达毫秒级。 - 请求合并(Merge)。 相邻的
bio会被合并成一个更大的request,减少驱动交互次数。合并操作本身的延迟很小(微秒级),但被合并的bio需要等待合并窗口关闭(plug结束)才能下发。 - 派发到硬件。 将
request交给设备驱动的queue_rq回调。在 NVMe 路径上,这是一次 MMIO 写入门铃寄存器(Doorbell Register),耗时约 1-2 微秒。
2.8 设备驱动与硬件层
- 驱动层。 NVMe 驱动将
request转换为 NVMe 命令(Submission Queue Entry),写入设备的提交队列。中断处理或轮询模式处理完成队列。驱动层开销在微秒级。 - 硬件层。 SSD 内部的延迟取决于闪存介质的读写特性:SLC 读 25 微秒、TLC 读 75 微秒、QLC 读 120 微秒;写入需要先擦除再编程,擦除延迟在毫秒级。SSD 控制器的 FTL(Flash Translation Layer,闪存转换层)还引入地址翻译和垃圾回收(GC)延迟——GC 期间延迟可能飙升到数十毫秒。HDD 的延迟主要来自寻道时间(3-10 毫秒)和旋转等待(平均 2-4 毫秒,取决于转速)。
三、各层延迟分布特征
3.1 各层典型延迟量级
下表总结了各层在典型负载下的延迟量级。数据来自公开文献和实测经验,具体数值随硬件、内核版本和负载变化。
┌─────────────────────────┬───────────────────┬───────────────────────────────────┐
│ 层次 │ 典型延迟量级 │ 延迟来源 │
├─────────────────────────┼───────────────────┼───────────────────────────────────┤
│ 应用层用户态缓冲 │ 10-100 ns │ 内存拷贝 │
│ 系统调用(不含 KPTI) │ 50-100 ns │ 模式切换、寄存器保存 │
│ 系统调用(含 KPTI) │ 150-300 ns │ TLB 刷新 │
│ VFS 路径查找(缓存命中)│ 200-500 ns │ dentry 查找 │
│ VFS 路径查找(缓存未中)│ 100 us - 10 ms │ 磁盘读取目录块 │
│ Page Cache 命中读 │ 200-500 ns │ 内存拷贝 │
│ 文件系统元数据(已缓存)│ 1-10 us │ extent 树查找 │
│ 文件系统 fsync(SSD) │ 50-500 us │ 日志提交 + 设备 flush │
│ 文件系统 fsync(HDD) │ 5-15 ms │ 日志提交 + 寻道 + 旋转 │
│ 块设备层(低负载) │ 1-5 us │ bio 分配、合并、派发 │
│ 块设备层(高负载排队) │ 100 us - 10 ms │ 标签等待、调度器排队 │
│ NVMe SSD 4K 随机读 │ 70-120 us │ 闪存读取 + FTL 翻译 │
│ NVMe SSD 4K 随机写 │ 10-30 us │ 写入缓冲区(不含 GC) │
│ NVMe SSD GC 期间 │ 1-50 ms │ 内部数据搬迁 │
│ SATA SSD 4K 随机读 │ 100-300 us │ 闪存读取 + SATA 协议开销 │
│ HDD 4K 随机读 │ 3-15 ms │ 寻道 + 旋转等待 + 传输 │
│ HDD 顺序读 │ 20-50 us/扇区 │ 无寻道,仅传输 │
└─────────────────────────┴───────────────────┴───────────────────────────────────┘
3.2 延迟放大机制
一次应用层 I/O 的延迟不是各层延迟的简单相加——多种机制会把延迟放大:
I/O 扩增(I/O Amplification)。 应用写 4 KB 数据,文件系统可能需要更新 extent 树、inode 表、块位图、组描述符,产生多个额外的块设备 I/O。极端情况下(如 COW 文件系统的快照场景),一次应用写可能扩增为 5-10 次块设备写。
队列级联(Queue Cascading)。 每一层都有自己的队列——文件系统的工作队列、块设备层的调度队列、设备驱动的提交队列。高负载时,请求在每层队列都排队等待,总延迟是各层排队延迟的累加。
锁竞争。 写操作在多个层次持有锁:VFS 的
i_rwsem、ext4 的 jbd2
事务锁、块设备层的标签位图锁。当多个线程竞争同一把锁时,延迟从微秒级跳到毫秒级。
中断合并(Interrupt Coalescing)。 NVMe 控制器和驱动可以配置中断合并——把多个完成事件攒在一起用一个中断通知 CPU。中断合并提高吞吐但增加单次 I/O 的延迟。
3.3 各层延迟的可观测性
| 层次 | 主要观测工具 | 观测粒度 |
|---|---|---|
| 应用层 | strace -T、应用内埋点 |
单次系统调用 |
| 系统调用层 | strace -T、perf trace |
单次系统调用 |
| VFS 层 | funclatency vfs_read(bpftrace) |
函数级 |
| 文件系统层 | ext4dist、ext4slower、funclatency |
操作类型级 |
| 页缓存层 | cachestat、funclatency filemap_fault |
命中率、缺页延迟 |
| 块设备层 | biolatency、biosnoop、blktrace |
单个 bio/request |
| 设备驱动层 | nvme-cli smart-log、iostat |
设备级聚合 |
| 硬件层 | S.M.A.R.T.、NVMe 日志页 | 设备级聚合 |
四、尾延迟分析
4.1 为什么平均延迟会骗人
假设一个存储系统处理 10000 个请求,其中 9990 个耗时 0.1 毫秒,10 个耗时 100 毫秒。平均延迟是 0.2 毫秒——看起来很好。但那 10 个请求的用户等了 100 毫秒,体验极差。更要命的是:在分布式系统里,一次上层请求可能扇出到多个存储节点,只要有一个节点返回慢,整个请求就慢。扇出度为 100 时,单节点 P99 的延迟就变成了用户体验的中位延迟。
这就是尾延迟(Tail Latency)问题。Jeff Dean 在 2013 年的论文 “The Tail at Scale” 里系统阐述了这个问题:在大规模分布式系统中,高百分位延迟(P99、P99.9)比平均延迟更能决定用户体验。
4.2 百分位延迟的含义
延迟百分位(Percentile)的定义:P99 延迟是指 99% 的请求的延迟低于这个值。换句话说,只有 1% 的请求延迟高于 P99。
常用的百分位及其含义:
- P50(中位数)。 一半请求比这快,一半比这慢。比均值更能反映”典型”延迟。
- P90。 10% 的请求比这慢。初步发现延迟异常的门槛。
- P99。 1% 的请求比这慢。在线服务通常用 P99 作为 SLO(Service Level Objective,服务等级目标)指标。
- P99.9(P999)。 0.1% 的请求比这慢。高扇出系统需要关注这个级别。
- P99.99(P9999)。 万分之一的请求比这慢。极端场景,通常只有大规模系统才需要关注。
4.3 尾延迟的常见成因
在存储系统中,尾延迟的来源可以分为三类:
硬件层面: - SSD 垃圾回收(GC):GC 期间内部数据搬迁,读写延迟可能从 100 微秒跳到 10-50 毫秒。 - SSD 磨损均衡(Wear Leveling):后台数据迁移引入额外延迟。 - HDD 热校准(Thermal Recalibration):温度变化导致磁头重新校准,暂停 I/O 数百毫秒。 - NVMe 控制器内部排队:设备队列满时,新请求排队等待。
内核层面: - 脏页回写风暴(Writeback
Storm):脏页比例达到阈值,写操作被阻塞。 - 内存回收(Memory
Reclaim):直接回收路径上可能触发同步 I/O。 - jbd2
日志提交:fsync()
等待日志事务提交,如果事务较大,等待时间较长。 -
i_rwsem
锁竞争:多线程并发写同一文件,排队等锁。 - cgroup I/O
限流:cgroup 的 I/O 带宽限制触发节流。
应用层面: - 日志轮转(Log
Rotation):日志文件关闭和创建新文件时的
fsync() 开销。 -
数据库检查点(Checkpoint):数据库定期将内存数据刷到磁盘,产生突发
I/O。 - 压缩/编码:CPU 密集的压缩操作延迟了 I/O 提交。
4.4 尾延迟分析方法
分析尾延迟的步骤:
- 确认问题存在。
用直方图工具(
biolatency)或百分位统计确认 P99/P999 远高于 P50。 - 定位问题层次。
在不同层次分别采集延迟分布:应用层(
strace -T)、文件系统层(ext4dist)、块设备层(biolatency)。对比各层的 P99,找到延迟跳变最大的层。 - 追踪慢操作。
用阈值工具(
fileslower、ext4slower、biosnoop)捕获超过阈值的单次操作,关联到进程、文件和 I/O 类型。 - 关联外部事件。
把延迟尖峰和系统事件对齐:是不是
GC?是不是回写?是不是内存压力?
dmesg、vmstat、sar的时间线对齐很重要。
五、延迟热图
5.1 什么是延迟热图
延迟热图(Latency Heatmap)是 Brendan Gregg 推广的一种延迟可视化方法。它把延迟分布随时间的变化画成一张二维图:
- X 轴:时间。
- Y 轴:延迟值(通常用对数刻度)。
- 颜色深浅:该时间段内、该延迟范围内的请求数量。颜色越深,请求越多。
延迟热图示意(Latency Heatmap)
延迟
(ms)
64 │ ░░░
32 │ ░░░░░███░░░
16 │ ░░░░░████████░░░░░
8 │ ░░░░████████████████░░░
4 │ ░░░░░██████████████████████░░
2 │ ░░░░░████████████████████████████░░░░
1 │░░░░░███████████████████████████████████████░░
0.5 │████████████████████████████████████████████████
0.25│████████████████████████████████████████████████
└────────────────────────────────────────────────
00:00 00:05 00:10 00:15 00:20 00:25 时间
颜色:░ 少量请求 █ 大量请求
图释:大部分请求集中在 0.25-1 ms(深色带),
但在 00:10-00:15 期间出现延迟尖峰(达到 32-64 ms),
与该时段 SSD GC 事件吻合。
5.2 延迟热图的优势
相比直方图和百分位时间序列,延迟热图有三个优势:
- 同时看到分布和趋势。 直方图丢失了时间信息,百分位时间序列丢失了分布形状。延迟热图两者兼顾。
- 直观发现双峰分布。 如果延迟分布有两个峰(比如缓存命中和缓存未命中),热图上会出现两条水平亮带,一眼就能看出来。用均值或 P99 根本看不到双峰。
- 定位延迟异常的时间窗口。 延迟尖峰在热图上表现为竖直方向的颜色突变,可以精确定位到哪个时间段发生了异常。
5.3 用 biolatency 生成热图数据
biolatency 工具(来自 BCC/bpftrace
项目)可以输出延迟直方图数据。配合定时采集和后处理脚本,可以生成延迟热图。
采集方法:每秒输出一次直方图,持续 60 秒:
# 每秒输出一次块设备 I/O 延迟直方图,持续 60 秒
# 需要 root 权限,依赖 bcc-tools 包
biolatency-bpfcc -m -T 1 60输出格式(删减版):
Tracing block device I/O... Hit Ctrl-C to end.
14:23:01
msecs : count distribution
0 -> 1 : 1542 |****************************************|
2 -> 3 : 87 |** |
4 -> 7 : 23 | |
8 -> 15 : 5 | |
16 -> 31 : 1 | |
14:23:02
msecs : count distribution
0 -> 1 : 1489 |****************************************|
2 -> 3 : 95 |** |
4 -> 7 : 31 | |
8 -> 15 : 12 | |
16 -> 31 : 8 | |
32 -> 63 : 3 | |
Brendan Gregg 的 trace2heatmap.pl
脚本可以将这类输出转换为 SVG 热图。流程:
# 1. 采集数据(每秒一个直方图,持续 300 秒)
biolatency-bpfcc -m -T 1 300 > bio_latency_raw.txt
# 2. 解析为热图输入格式(需要自行编写解析脚本或使用 trace2heatmap)
# trace2heatmap 期望的输入格式:每行一个 "时间戳 延迟桶 计数"
# 具体解析逻辑取决于 biolatency 的输出格式
# 3. 生成 SVG 热图
# trace2heatmap.pl --title="Block I/O Latency Heatmap" < parsed_data.txt > heatmap.svg5.4 解读延迟热图的常见模式
单峰带状分布。 正常状态——大部分请求集中在一个窄延迟带内,颜色均匀。这说明延迟稳定,没有明显的干扰源。
双峰分布。 两条平行的亮带——通常意味着两种不同的 I/O 路径。典型场景:Page Cache 命中(低延迟带)和缓存未命中(高延迟带)。
周期性尖峰。 每隔固定时间出现一次延迟突变——通常是定时任务导致的,比如 ext4 的 5 秒日志提交、30 秒脏页回写、cron 任务、数据库检查点。
渐进式延迟上升。 延迟随时间缓慢上升——通常是资源逐渐耗尽的信号:磁盘空间不足导致分配变慢、SSD 磨损导致 GC 频率增加、内存压力导致 Page Cache 收缩。
突发式延迟跳变。 延迟在某个时刻突然飙升——可能是 SSD GC 触发、RAID 重建开始、突发大 I/O 负载挤占队列。
六、biolatency 与 ext4dist
6.1 biolatency:块设备层延迟分布
biolatency 是 BCC
工具集中最常用的块设备延迟分析工具。它在块设备层的入口(blk_account_io_start)和完成回调(blk_account_io_done)之间测量延迟,输出直方图。
基本用法:
# 以毫秒为单位显示延迟直方图
biolatency-bpfcc -m
# 以微秒为单位显示,更适合 NVMe SSD
biolatency-bpfcc
# 按磁盘分组显示
biolatency-bpfcc -D
# 按操作类型(读/写)分组
biolatency-bpfcc -F
# 每 5 秒输出一次,持续 30 秒
biolatency-bpfcc -m 5 6示例输出(NVMe SSD,4K 随机读负载,经删减):
Tracing block device I/O... Hit Ctrl-C to end.
usecs : count distribution
0 -> 1 : 0 | |
2 -> 3 : 0 | |
4 -> 7 : 0 | |
8 -> 15 : 12 | |
16 -> 31 : 45 |* |
32 -> 63 : 287 |******** |
64 -> 127 : 1423 |****************************************|
128 -> 255 : 356 |********** |
256 -> 511 : 28 | |
512 -> 1023 : 5 | |
1024 -> 2047 : 2 | |
解读:大部分请求(1423 个)延迟在 64-127 微秒——这是 NVMe SSD 4K 随机读的典型延迟。少量请求(2 个)延迟超过 1 毫秒,这些可能是 GC 或内部排队导致的尾延迟。
6.2 biolatency 按磁盘分组
多磁盘系统中,需要区分各磁盘的延迟分布:
biolatency-bpfcc -D -m输出(经删减):
disk = 'nvme0n1'
msecs : count distribution
0 -> 1 : 2156 |****************************************|
2 -> 3 : 34 | |
4 -> 7 : 8 | |
disk = 'sda'
msecs : count distribution
0 -> 1 : 89 |**** |
2 -> 3 : 156 |******* |
4 -> 7 : 834 |****************************************|
8 -> 15 : 423 |******************** |
16 -> 31 : 67 |*** |
对比清晰:nvme0n1(NVMe SSD)的延迟集中在
0-1 毫秒,而 sda(HDD)的延迟集中在 4-15
毫秒。
6.3 ext4dist:文件系统操作延迟分布
ext4dist 跟踪 ext4
文件系统的四种操作(read、write、open、fsync)的延迟分布。它挂载在
ext4 的内核函数入口和返回点,测量函数执行时间。
# 默认显示所有操作的延迟分布
ext4dist-bpfcc
# 每 10 秒输出一次
ext4dist-bpfcc 10
# 只看特定进程
ext4dist-bpfcc -p $(pidof mysqld)示例输出(数据库负载下,经删减):
Tracing ext4 operation latency... Hit Ctrl-C to end.
operation = 'read'
usecs : count distribution
0 -> 1 : 4521 |****************************************|
2 -> 3 : 1203 |********** |
4 -> 7 : 345 |*** |
8 -> 15 : 89 | |
16 -> 31 : 23 | |
32 -> 63 : 5 | |
64 -> 127 : 234 |** |
128 -> 255 : 567 |***** |
operation = 'write'
usecs : count distribution
0 -> 1 : 0 | |
2 -> 3 : 3456 |****************************************|
4 -> 7 : 2341 |*************************** |
8 -> 15 : 567 |****** |
16 -> 31 : 123 |* |
operation = 'fsync'
usecs : count distribution
0 -> 1 : 0 | |
2 -> 3 : 0 | |
4 -> 7 : 0 | |
8 -> 15 : 0 | |
16 -> 31 : 0 | |
32 -> 63 : 12 | |
64 -> 127 : 45 |* |
128 -> 255 : 234 |***** |
256 -> 511 : 1567 |****************************************|
512 -> 1023 : 423 |*********** |
1024 -> 2047 : 34 | |
2048 -> 4095 : 5 | |
解读要点:
- read 操作呈双峰分布。 第一个峰在 0-7 微秒(Page Cache 命中),第二个峰在 128-255 微秒(缓存未命中,需要块设备 I/O)。这是典型的混合负载特征。
- write 操作延迟很低。 集中在 2-15
微秒——因为 ext4 默认使用延迟分配,
write()只写入 Page Cache,不等待磁盘。 - fsync 操作延迟最高。 峰值在 256-511 微秒,这是日志提交加设备 flush 的延迟。少数超过 2 毫秒的 fsync 可能是日志空间不足等待回收导致的。
6.4 biolatency 与 ext4dist 的对比使用
两个工具测量不同层次的延迟:
| 对比维度 | biolatency | ext4dist |
|---|---|---|
| 测量层次 | 块设备层(bio 提交到完成) | 文件系统层(ext4 函数入口到返回) |
| 包含 Page Cache | 不包含——只有 Cache 未命中的 I/O 才到块设备层 | 包含——Cache 命中的读操作也会被记录 |
| 操作类型 | 读/写(按块设备) | read/write/open/fsync(按文件操作语义) |
| 适用场景 | 判断设备本身的延迟是否正常 | 判断文件系统操作的延迟是否正常 |
| 延迟差异 | 反映纯设备延迟 + 块设备层排队 | 反映文件系统处理 + 可能的设备延迟 |
如果 ext4dist 的 read 延迟远高于
biolatency,说明延迟主要在文件系统层(元数据查找、锁等待)。如果两者接近,说明延迟主要在设备端。
七、fileslower 与 ext4slower
7.1 fileslower:追踪慢文件操作
fileslower 追踪 VFS
层的文件读写操作,只输出延迟超过阈值的操作。它挂载在
vfs_read() 和 vfs_write()
上,适用于所有文件系统。
# 追踪延迟超过 10 毫秒的文件操作
fileslower-bpfcc 10
# 追踪延迟超过 1 毫秒的操作(更灵敏,输出更多)
fileslower-bpfcc 1输出格式:
Tracing sync read/writes slower than 10 ms
TIME(s) COMM TID D BYTES LAT(ms) FILENAME
0.000 mysqld 12345 R 16384 14.52 ibdata1
0.023 mysqld 12346 R 16384 11.34 ib_logfile0
1.345 python3 23456 W 65536 23.78 output.csv
2.567 rsync 34567 R 131072 45.12 backup.tar.gz
5.891 jbd2/sda1-8 890 W 4096 67.23 [journal]
每行包含:时间戳、进程名、线程 ID、方向(R=读 / W=写)、字节数、延迟(毫秒)、文件名。这让你能直接看到是哪个进程在读写哪个文件时遇到了慢操作。
7.2 ext4slower:追踪 ext4 慢操作
ext4slower 专门针对 ext4
文件系统,追踪四种操作(read、write、open、fsync)中延迟超过阈值的操作。
# 追踪延迟超过 10 毫秒的 ext4 操作
ext4slower-bpfcc 10
# 追踪延迟超过 1 毫秒的操作
ext4slower-bpfcc 1
# 只追踪特定进程
ext4slower-bpfcc -p $(pidof postgres) 1输出格式:
Tracing ext4 operations slower than 10 ms
TIME COMM PID T BYTES OFF_KB LAT(ms) FILENAME
14:23:01 postgres 5678 S 0 0 34.56 base/16384/16385
14:23:01 postgres 5679 R 8192 1024 12.34 base/16384/16389
14:23:03 postgres 5680 W 8192 2048 15.67 base/16384/16390
14:23:05 jbd2/nvme0n1-8 890 S 0 0 45.23 [journal]
T 列表示操作类型:R=read、W=write、O=open、S=fsync。
7.3 fileslower 与 ext4slower 的选择
- 如果你不确定慢操作发生在哪个文件系统上,用
fileslower——它在 VFS 层工作,适用于所有文件系统。 - 如果你已经确定问题在 ext4 上,用
ext4slower——它能提供更详细的信息(文件偏移量、精确的操作类型)。 - 对 XFS 有对应的
xfsslower,对 Btrfs 有btrfsslower。
7.4 实战:定位数据库 fsync 延迟抖动
场景:PostgreSQL 的提交延迟偶尔飙升到 50 毫秒以上,平均延迟只有 0.5 毫秒。怀疑是存储延迟问题。
第一步,用 ext4slower 抓慢操作:
ext4slower-bpfcc -p $(pidof postgres) 10观察到大量 fsync 操作延迟超过 30 毫秒,集中在
pg_wal 目录下的 WAL 文件上。
第二步,用 biolatency 确认块设备层延迟:
biolatency-bpfcc -D -m 5 12发现 nvme0n1 的延迟分布偶尔出现 16-63
毫秒的请求,和 ext4slower
观察到的时间窗口吻合。
第三步,检查 NVMe 设备的 SMART 日志:
nvme smart-log /dev/nvme0n1关注 percentage_used(磨损百分比)和
unsafe_shutdowns 字段。如果
percentage_used 接近 100%,SSD 的 GC
频率会显著增加,导致延迟尖峰。
结论:延迟抖动的根因是 SSD GC。短期缓解方案:增加 SSD 的预留空间(Over-Provisioning),降低 GC 频率。长期方案:更换磨损较少的 SSD。
八、全链路延迟追踪实战
8.1 场景描述
一个在线服务报告读取延迟偶尔超过 200 毫秒。服务架构:应用层(Go 语言)→ ext4 文件系统 → NVMe SSD。正常情况下 P50 延迟 0.2 毫秒,但 P99 达到 50 毫秒,P99.9 偶尔超过 200 毫秒。目标是找到 P99.9 延迟的根因并优化。
测试环境: - CPU:Intel Xeon Gold 6348(IceLake),28 核 - 内存:256 GB DDR4 3200 - 存储:Intel P5510 NVMe SSD,3.84 TB - OS:Ubuntu 22.04,内核 5.15.0 - 文件系统:ext4,挂载参数
noatime,nobarrier- 负载:4 KB 随机读为主,QPS 约 50000
8.2 第一步:确认延迟分布
# 块设备层延迟分布,每 5 秒输出一次
biolatency-bpfcc -m -D 5观察结果(经删减):
disk = 'nvme0n1'
msecs : count distribution
0 -> 1 : 248923 |****************************************|
2 -> 3 : 1234 | |
4 -> 7 : 456 | |
8 -> 15 : 78 | |
16 -> 31 : 23 | |
32 -> 63 : 5 | |
64 -> 127 : 2 | |
128 -> 255 : 1 | |
块设备层确实有少量请求延迟超过 100 毫秒,但数量极少。大部分延迟应该不在块设备层。
8.3 第二步:文件系统层延迟
ext4dist-bpfcc 5operation = 'read'
usecs : count distribution
0 -> 1 : 198456 |****************************************|
2 -> 3 : 42345 |********* |
4 -> 7 : 5678 |* |
8 -> 15 : 1234 | |
16 -> 31 : 456 | |
32 -> 63 : 123 | |
64 -> 127 : 1456 | |
128 -> 255 : 2345 | |
256 -> 511 : 345 | |
512 -> 1023 : 56 | |
1024 -> 2047 : 12 | |
文件系统层的 read 延迟分布也呈双峰:主峰在 0-7 微秒(Page Cache 命中),次峰在 64-255 微秒(Cache 未命中)。少量超过 1 毫秒的读操作——这些可能是元数据未缓存的情况。
但文件系统层的最高延迟是 2 毫秒级——远低于应用报告的 200 毫秒。延迟不在文件系统层,也不在块设备层。
8.4 第三步:系统调用层
# 追踪应用进程的 read 系统调用延迟
strace -p $(pidof myapp) -e trace=read -T -c% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- --------
85.23 1.234567 5 246913 read
7.12 0.103456 234 442 read
5.43 0.078901 5678 14 read
2.22 0.032198 160990 2 read
------ ----------- ----------- --------- --------- --------
100.00 1.449122 247371 total
关键发现:有 2 次 read() 调用延迟约 160
毫秒。但块设备和文件系统层都没有这么高的延迟——延迟在哪里?
8.5 第四步:排查内存回收
# 检查是否有直接内存回收
grep -c "direct reclaim" /proc/vmstat
cat /proc/vmstat | grep -E "allocstall|pgmajfault|pgscan_direct"allocstall_dma 0
allocstall_dma32 0
allocstall_normal 45
allocstall_movable 12
pgmajfault 234
pgscan_direct 89012
allocstall_normal=45 和
pgscan_direct=89012
说明发生了直接内存回收(Direct Reclaim)。直接回收在
read() 的路径上同步执行——内核为了给新的 Page
Cache
页腾出空间,需要先回收旧页面,如果旧页面是脏页,还需要等待写回完成。
用 bpftrace 确认直接回收和延迟尖峰的关联:
# 追踪直接内存回收的耗时
bpftrace -e '
kprobe:__alloc_pages_direct_reclaim {
@start[tid] = nsecs;
}
kretprobe:__alloc_pages_direct_reclaim /@start[tid]/ {
$lat = (nsecs - @start[tid]) / 1000000;
if ($lat > 10) {
printf("direct reclaim: %d ms, comm=%s\n", $lat, comm);
}
@latency = hist((nsecs - @start[tid]) / 1000);
delete(@start[tid]);
}
'输出确认:直接回收的延迟峰值达到 150-200 毫秒,和应用层观察到的延迟尖峰一致。
8.6 第五步:根因与修复
根因:系统内存 256 GB,应用数据集约 300 GB。Page Cache 无法装下全部数据,频繁的缓存淘汰触发直接内存回收。回收路径上如果遇到脏页,需要同步写回,延迟达到数百毫秒。
修复方案:
# 1. 降低脏页比例,减少直接回收遇到脏页的概率
sysctl -w vm.dirty_ratio=5
sysctl -w vm.dirty_background_ratio=2
# 2. 增加最小空闲内存水位,提前触发后台回收
sysctl -w vm.min_free_kbytes=1048576
# 3. 对读密集型负载,考虑使用 Direct I/O 绕过 Page Cache
# 在应用层使用 O_DIRECT 标志打开文件效果验证:调整后 P99.9 从 200 毫秒降到 5
毫秒。直接回收次数(allocstall_normal)降为零。
8.7 全链路延迟追踪的方法论总结
全链路延迟追踪流程:
应用报告延迟异常
│
▼
┌─────────────────────┐
│ biolatency 确认 │
│ 块设备层延迟分布 │
└────────┬────────────┘
│
┌─────────┴─────────┐
│ │
块设备层异常 块设备层正常
│ │
▼ ▼
┌────────────────┐ ┌─────────────────┐
│ NVMe SMART 日志│ │ ext4dist 确认 │
│ iostat 详细分析│ │ 文件系统层延迟 │
└────────────────┘ └────────┬────────┘
│
┌─────────┴─────────┐
│ │
文件系统异常 文件系统正常
│ │
▼ ▼
┌────────────────┐ ┌─────────────────┐
│ ext4slower 追踪│ │ strace -T 确认 │
│ 慢操作详情 │ │ 系统调用层延迟 │
└────────────────┘ └────────┬────────┘
│
┌────────┴────────┐
│ │
syscall 异常 syscall 正常
│ │
▼ ▼
┌──────────────┐ ┌──────────────┐
│ 内存回收? │ │ 应用层锁? │
│ 锁竞争? │ │ CPU 调度? │
│ cgroup 限流?│ │ 网络延迟? │
└──────────────┘ └──────────────┘
关键原则:
- 自底向上排查。 先看块设备层——如果设备本身延迟正常,问题一定在软件栈。
- 看分布而非均值。 每一层都看直方图,不看平均值。平均值会掩盖尾延迟问题。
- 用阈值工具追踪个案。
直方图告诉你”有多少请求慢了”,阈值工具(
fileslower、ext4slower)告诉你”哪个请求、读哪个文件、是什么操作”。 - 关联时间线。
把延迟尖峰和系统事件对齐——
vmstat的内存回收、iostat的 I/O 突增、dmesg的硬件报警。
九、延迟优化优先级指南
9.1 延迟优化的一般原则
延迟优化不是”把所有层的延迟都压到最低”,而是”找到贡献最大的层,集中优化”。根据阿姆达尔定律(Amdahl’s Law),优化一个占总延迟 80% 的组件,收益远大于优化一个占 5% 的组件。
9.2 各层优化手段与优先级
┌──────┬──────────────────────────┬────────────────────────────┬──────────────┐
│ 优先 │ 优化方向 │ 具体手段 │ 预期收益 │
│ 级 │ │ │ │
├──────┼──────────────────────────┼────────────────────────────┼──────────────┤
│ P0 │ 消除不必要的 I/O │ 增大缓存、合并小 I/O、 │ 10x-100x │
│ │ │ 减少 fsync 频率 │ │
├──────┼──────────────────────────┼────────────────────────────┼──────────────┤
│ P1 │ 消除排队延迟 │ 降低并发度、分离读写队列、 │ 2x-10x │
│ │ │ 增加设备数量分散负载 │ │
├──────┼──────────────────────────┼────────────────────────────┼──────────────┤
│ P2 │ 减少锁竞争 │ 分离文件、减少共享、 │ 2x-5x │
│ │ │ 使用 io_uring │ │
├──────┼──────────────────────────┼────────────────────────────┼──────────────┤
│ P3 │ 优化设备端延迟 │ 升级 SSD、增加 OP 空间、 │ 1.5x-3x │
│ │ │ 启用 NVMe 多队列 │ │
├──────┼──────────────────────────┼────────────────────────────┼──────────────┤
│ P4 │ 调整内核参数 │ 调度器选择、readahead 大小、│ 1.2x-2x │
│ │ │ dirty ratio、min_free_kB │ │
├──────┼──────────────────────────┼────────────────────────────┼──────────────┤
│ P5 │ 减少软件栈开销 │ Direct I/O、io_uring、 │ 1.1x-1.5x │
│ │ │ 绕过 VFS │ │
└──────┴──────────────────────────┴────────────────────────────┴──────────────┘
9.3 尾延迟专项优化
针对 P99/P999 延迟,优化策略和平均延迟优化不同:
隔离干扰源。 把延迟敏感的 I/O 和后台 I/O
分到不同的设备或 I/O 优先级类。用 ionice
降低后台任务的 I/O 优先级,用 cgroup v2 的
io.latency 控制器保障前台任务的延迟。
# 用 cgroup v2 io.latency 控制器设置延迟目标
# 前台任务组:目标延迟 1 毫秒
echo "259:0 target=1000" > /sys/fs/cgroup/foreground/io.latency
# 后台任务组:不设限制
echo "259:0 target=0" > /sys/fs/cgroup/background/io.latency消除脏页回写抖动。 降低
dirty_ratio 和
dirty_background_ratio,让回写更早开始、更频繁进行,避免积累大量脏页后的突发写入。
sysctl -w vm.dirty_background_ratio=2
sysctl -w vm.dirty_ratio=5
sysctl -w vm.dirty_expire_centisecs=500
sysctl -w vm.dirty_writeback_centisecs=100避免直接内存回收。 增大
min_free_kbytes,提前触发后台回收(kswapd),避免在
I/O 路径上同步回收内存。
控制 SSD GC 影响。 保持 SSD 的空闲空间在 20% 以上(通过文件系统预留或 SSD 固件的 OP 设置),降低 GC 频率和 GC 期间的延迟。
使用 NVMe 轮询模式。
对于延迟极度敏感的场景,用 io_uring 的
IORING_SETUP_IOPOLL 模式让 CPU
主动轮询完成事件,避免中断延迟(约 2-5 微秒)。代价是 CPU
占用增加。
9.4 延迟监控指标体系
生产环境需要持续监控的延迟指标:
| 指标 | 采集工具 | 采集频率 | 告警阈值(参考) |
|---|---|---|---|
| 块设备 P99 延迟 | biolatency 定时采集 |
每 10 秒 | 设备标称延迟的 10 倍 |
| fsync P99 延迟 | ext4dist 或应用埋点 |
每 10 秒 | SLO 要求的 50% |
| 直接回收次数 | /proc/vmstat allocstall |
每 5 秒 | 任何非零值 |
| 脏页比例 | /proc/meminfo Dirty/MemTotal |
每 5 秒 | dirty_background_ratio 的 80% |
| I/O 等待时间 | iostat await |
每 5 秒 | 基线的 3 倍 |
| NVMe 设备温度 | nvme smart-log |
每 60 秒 | 厂商规定的上限减 10 度 |
十、参考文献
书籍
- Gregg, Brendan. Systems Performance: Enterprise and the Cloud, 2nd Edition. Addison-Wesley, 2020. 第 9 章(Disks)和第 16 章(Case Studies)详细讨论了存储延迟分析方法。
- Gregg, Brendan. BPF Performance Tools.
Addison-Wesley, 2019. 第 9 章(Disk I/O)包含
biolatency、biosnoop、ext4dist、ext4slower等工具的详细说明和案例。
论文与演讲
- Dean, Jeff and Barroso, Luiz Andre. “The Tail at Scale.” Communications of the ACM, Vol. 56, No. 2, 2013. 系统分析了大规模分布式系统中尾延迟的成因和缓解策略。
- Gregg, Brendan. “Latency Heat Maps.” 2010. 延迟热图方法论的原始描述,包括可视化方法和解读指南。
- Axboe, Jens. “Efficient I/O with io_uring.” Linux Plumbers Conference, 2019. io_uring 的设计与延迟优势。
内核源码
- Linux 6.1,
block/blk-mq.c,blk_mq_submit_bio()——块设备层 bio 提交入口。 - Linux 6.1,
fs/ext4/file.c,ext4_file_read_iter()——ext4 读操作入口。 - Linux 6.1,
fs/jbd2/journal.c,jbd2_journal_commit_transaction()——jbd2 日志事务提交流程。 - Linux 6.1,
mm/page-writeback.c,balance_dirty_pages()——脏页回写节流逻辑。 - Linux 6.1,
mm/vmscan.c,__alloc_pages_direct_reclaim()——直接内存回收路径。
工具文档
- BCC (BPF Compiler Collection)
项目文档——
biolatency、ext4dist、ext4slower、fileslower等工具的使用说明和源码。 - bpftrace 参考指南——bpftrace 语法和内核探针的使用方法。
iostat(1)手册页——块设备性能统计工具。vmstat(8)手册页——虚拟内存统计工具,可观测内存回收行为。
NVMe 与 SSD
- NVM Express Base Specification, Revision 2.0. 第 5.15 节(Get Log Page: SMART / Health Information Log)——NVMe 设备的健康信息读取。
- Micron Technical Note TN-FD-34. “Understanding SSD Performance and Endurance.” 讨论了 SSD GC、OP 和延迟的关系。
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
数据库内核实验索引
汇总本站数据库内核与存储引擎实验文章,重点覆盖从零实现 LSM-Tree 及其工程权衡。
存储工程索引
汇总本站存储工程系列文章,覆盖 HDD、SSD、NVMe、持久内存、索引结构、压缩、分布式存储与对象存储。
【存储工程】云块存储架构
深入剖析云块存储——分布式块存储架构原理、AWS EBS与阿里云ESSD架构分析、云盘性能规格解读、性能测试方法与选型成本优化
【存储工程】云对象存储内部架构
深入剖析云对象存储——S3的11个9持久性实现、元数据-索引-存储三层架构、跨AZ复制策略、存储类别实现差异与成本模型分析