土法炼钢兴趣小组的算法知识备份

【存储工程】存储全链路延迟分析

文章导航

分类入口
storage
标签入口
#latency-analysis#tail-latency#p99#latency-heatmap#biolatency#full-stack-tracing#io-path

目录

一次 read() 调用花了 200 毫秒,但 iostat 显示磁盘平均延迟只有 0.3 毫秒——延迟到底丢在了哪一层?这个问题在存储系统调优里反复出现,但很少有人能给出准确答案。原因很简单:从应用发起 I/O 到数据真正从介质返回,中间至少经过七层软件栈,每一层都可能引入排队、锁竞争、上下文切换和中断延迟。只看平均值更是灾难——平均延迟 0.5 毫秒的系统,P99 可能已经飙到 50 毫秒。

本文从存储 I/O 路径的逐层分解开始,分析各层延迟的分布特征和放大机制,然后聚焦尾延迟(Tail Latency)、延迟热图(Latency Heatmap)两个核心分析方法,最后用 biolatencyext4distfileslowerext4slower 四个 BCC/bpftrace 工具做全链路延迟追踪实战。所有命令在 Linux 5.15+(Ubuntu 22.04)上验证,内核源码引用基于 Linux 6.1。


一、全链路延迟的工程意义

1.1 延迟与吞吐的区别

吞吐量(Throughput)回答的是”每秒能搬多少数据”,延迟(Latency)回答的是”一次操作要等多久”。高吞吐不等于低延迟——一条高速公路可以每小时通过一万辆车,但如果入口收费站排队半小时,单车延迟依然很高。

在存储场景里,吞吐优化和延迟优化经常冲突。批量合并 I/O 能提高吞吐,但会让先到达的请求等待后到达的请求一起下发,增加延迟。I/O 调度器为了提高吞吐做请求排序,但排序本身引入延迟。理解这个矛盾是做存储调优的前提。

1.2 为什么要做全链路分析

只看块设备层延迟(iostatawait)不够,原因有三:

  1. 应用感知的延迟包含了所有软件层的开销。 文件系统的日志提交、Page Cache 的回写抖动、VFS 的锁竞争都不会反映在块设备层的统计里。
  2. 延迟可能在中间层被放大。 一个 4 KB 的 read() 到达文件系统后可能触发元数据读取,变成多个块设备 I/O,每个 I/O 的延迟累加后远超单次块设备延迟。
  3. 排队延迟只有在请求入口才能观测。 块设备层的 await 包含了设备端排队时间,但不包含在块设备层入口的排队时间——在高负载下,后者可能远大于前者。

1.3 延迟分析的核心目标

延迟分析不是为了得出一个数字,而是为了回答三个问题:


二、存储 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 应用层

应用层的延迟包括:

应用层延迟的特点是:与 I/O 栈无关,只能通过应用层的追踪工具(strace、应用内埋点)观测。

2.3 系统调用层

read() / write() 系统调用本身的开销包括:

io_uring 通过共享环形缓冲区(Submission Queue / Completion Queue)和内核轮询(IORING_SETUP_SQPOLL)减少系统调用开销,在高频小 I/O 场景下可以把系统调用层延迟从微秒级降到接近零。

2.4 VFS 层

VFS(Virtual File System,虚拟文件系统)是内核中所有文件系统的统一接口层。它的延迟来源包括:

2.5 文件系统层

以 ext4 为例,文件系统层的延迟来源:

2.6 页缓存层

页缓存(Page Cache)是读写延迟的关键分水岭:

2.7 块设备层

块设备层的延迟组成:

2.8 设备驱动与硬件层


三、各层延迟分布特征

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 -Tperf trace 单次系统调用
VFS 层 funclatency vfs_read(bpftrace) 函数级
文件系统层 ext4distext4slowerfunclatency 操作类型级
页缓存层 cachestatfunclatency filemap_fault 命中率、缺页延迟
块设备层 biolatencybiosnoopblktrace 单个 bio/request
设备驱动层 nvme-cli smart-logiostat 设备级聚合
硬件层 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。

常用的百分位及其含义:

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 尾延迟分析方法

分析尾延迟的步骤:

  1. 确认问题存在。 用直方图工具(biolatency)或百分位统计确认 P99/P999 远高于 P50。
  2. 定位问题层次。 在不同层次分别采集延迟分布:应用层(strace -T)、文件系统层(ext4dist)、块设备层(biolatency)。对比各层的 P99,找到延迟跳变最大的层。
  3. 追踪慢操作。 用阈值工具(fileslowerext4slowerbiosnoop)捕获超过阈值的单次操作,关联到进程、文件和 I/O 类型。
  4. 关联外部事件。 把延迟尖峰和系统事件对齐:是不是 GC?是不是回写?是不是内存压力?dmesgvmstatsar 的时间线对齐很重要。

五、延迟热图

5.1 什么是延迟热图

延迟热图(Latency Heatmap)是 Brendan Gregg 推广的一种延迟可视化方法。它把延迟分布随时间的变化画成一张二维图:

延迟热图示意(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 延迟热图的优势

相比直方图和百分位时间序列,延迟热图有三个优势:

  1. 同时看到分布和趋势。 直方图丢失了时间信息,百分位时间序列丢失了分布形状。延迟热图两者兼顾。
  2. 直观发现双峰分布。 如果延迟分布有两个峰(比如缓存命中和缓存未命中),热图上会出现两条水平亮带,一眼就能看出来。用均值或 P99 根本看不到双峰。
  3. 定位延迟异常的时间窗口。 延迟尖峰在热图上表现为竖直方向的颜色突变,可以精确定位到哪个时间段发生了异常。

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.svg

5.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        |                                        |

解读要点:

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 的选择

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 5
operation = '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=45pgscan_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 限流?│   │ 网络延迟?   │
                          └──────────────┘   └──────────────┘

关键原则:

  1. 自底向上排查。 先看块设备层——如果设备本身延迟正常,问题一定在软件栈。
  2. 看分布而非均值。 每一层都看直方图,不看平均值。平均值会掩盖尾延迟问题。
  3. 用阈值工具追踪个案。 直方图告诉你”有多少请求慢了”,阈值工具(fileslowerext4slower)告诉你”哪个请求、读哪个文件、是什么操作”。
  4. 关联时间线。 把延迟尖峰和系统事件对齐——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_ratiodirty_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_uringIORING_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 度

十、参考文献

书籍

  1. Gregg, Brendan. Systems Performance: Enterprise and the Cloud, 2nd Edition. Addison-Wesley, 2020. 第 9 章(Disks)和第 16 章(Case Studies)详细讨论了存储延迟分析方法。
  2. Gregg, Brendan. BPF Performance Tools. Addison-Wesley, 2019. 第 9 章(Disk I/O)包含 biolatencybiosnoopext4distext4slower 等工具的详细说明和案例。

论文与演讲

  1. Dean, Jeff and Barroso, Luiz Andre. “The Tail at Scale.” Communications of the ACM, Vol. 56, No. 2, 2013. 系统分析了大规模分布式系统中尾延迟的成因和缓解策略。
  2. Gregg, Brendan. “Latency Heat Maps.” 2010. 延迟热图方法论的原始描述,包括可视化方法和解读指南。
  3. Axboe, Jens. “Efficient I/O with io_uring.” Linux Plumbers Conference, 2019. io_uring 的设计与延迟优势。

内核源码

  1. Linux 6.1, block/blk-mq.c, blk_mq_submit_bio()——块设备层 bio 提交入口。
  2. Linux 6.1, fs/ext4/file.c, ext4_file_read_iter()——ext4 读操作入口。
  3. Linux 6.1, fs/jbd2/journal.c, jbd2_journal_commit_transaction()——jbd2 日志事务提交流程。
  4. Linux 6.1, mm/page-writeback.c, balance_dirty_pages()——脏页回写节流逻辑。
  5. Linux 6.1, mm/vmscan.c, __alloc_pages_direct_reclaim()——直接内存回收路径。

工具文档

  1. BCC (BPF Compiler Collection) 项目文档——biolatencyext4distext4slowerfileslower 等工具的使用说明和源码。
  2. bpftrace 参考指南——bpftrace 语法和内核探针的使用方法。
  3. iostat(1) 手册页——块设备性能统计工具。
  4. vmstat(8) 手册页——虚拟内存统计工具,可观测内存回收行为。

NVMe 与 SSD

  1. NVM Express Base Specification, Revision 2.0. 第 5.15 节(Get Log Page: SMART / Health Information Log)——NVMe 设备的健康信息读取。
  2. Micron Technical Note TN-FD-34. “Understanding SSD Performance and Endurance.” 讨论了 SSD GC、OP 和延迟的关系。

上一篇: 读取性能优化 下一篇: 云块存储架构

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-04-22 · db / storage

数据库内核实验索引

汇总本站数据库内核与存储引擎实验文章,重点覆盖从零实现 LSM-Tree 及其工程权衡。

2026-04-22 · storage

存储工程索引

汇总本站存储工程系列文章,覆盖 HDD、SSD、NVMe、持久内存、索引结构、压缩、分布式存储与对象存储。

2025-10-18 · storage

【存储工程】云块存储架构

深入剖析云块存储——分布式块存储架构原理、AWS EBS与阿里云ESSD架构分析、云盘性能规格解读、性能测试方法与选型成本优化

2025-10-19 · storage

【存储工程】云对象存储内部架构

深入剖析云对象存储——S3的11个9持久性实现、元数据-索引-存储三层架构、跨AZ复制策略、存储类别实现差异与成本模型分析


By .