1. 存储性能基础:IO栈全景图、IO路径与延迟构成、性能指标

做存储性能优化,第一件事不是调参数,而是搞清楚数据到底走了哪条路。我见过太多人一上来就改内核参数,结果问题根本不在那里。说白了,你得先看懂IO栈,才能知道延迟从哪来。

1.1 IO栈全景图:数据从应用到磁盘的完整旅程

你写一个 write() 系统调用,数据不是直接飞到磁盘上的。它要穿过用户态、内核态、文件系统、块层、驱动,最后才落到硬件。我习惯把这趟旅程分成四层:

  • 应用层:你的业务代码,调用 read/write,或者用 libaio、SPDK 这类用户态框架。
  • 内核VFS层:虚拟文件系统,负责把系统调用翻译成通用文件操作。这里会做权限检查、路径解析。
  • 文件系统层:ext4、xfs、btrfs 这些。负责把文件逻辑地址映射成块逻辑地址,还要处理元数据、日志、缓存。
  • 块层与驱动:通用块层做 IO 调度、合并、排队。驱动层最终把命令发给硬件(NVMe、SATA、iSCSI 等)。

嗯,这里要注意:每一层都可能成为瓶颈。我曾经在一个项目中,发现应用层 IO 深度只有 1,结果磁盘利用率不到 10%,延迟却很高。问题不在磁盘,而在上层没把 IO 发起来。

核心认知:IO栈的每一层都会增加延迟。优化不是跳过这些层,而是让每一层都工作在最佳状态。

下面这张图,我把它叫做「IO栈全景图」。你盯着它看五分钟,就能理解数据是怎么一步步走下去的。

IO栈全景图:数据从应用到磁盘的完整旅程 应用层 用户态:read() / write() / libaio / SPDK / io_uring VFS层(虚拟文件系统) 路径解析 / 权限检查 / 通用文件操作接口 文件系统层 ext4 / xfs / btrfs / zfs 逻辑地址映射 / 元数据管理 / 日志 / 缓存 通用块层 IO调度 / 合并 / 排队 / 请求队列管理 驱动与硬件层 NVMe驱动 / SATA驱动 / iSCSI / 磁盘 / SSD 每一层都可能成为瓶颈。优化不是跳过这些层,而是让每一层都工作在最佳状态。

1.2 IO路径与延迟构成:时间都花在哪了?

一个 IO 请求从发起到完成,延迟可以拆成几段。我个人习惯用 「前端延迟」和 「后端延迟」来划分。

延迟阶段 典型来源 占比(典型场景)
应用处理延迟 序列化、业务逻辑、内存拷贝 5% - 15%
系统调用延迟 上下文切换、VFS处理 5% - 10%
文件系统延迟 元数据操作、日志提交、锁竞争 10% - 30%
IO调度延迟 请求排队、合并等待 5% - 20%
硬件延迟 寻道、旋转、NAND编程、网络 30% - 70%

你想想看,如果硬件延迟占了 70%,你优化应用层代码,效果能有多大?反过来,如果硬件延迟只有 30%,但文件系统锁竞争占了 40%,那你换再快的盘也没用。

我的经验:遇到性能问题,先用 iostat -x 1 看 await 和 svctm(或 aqu-sz)。如果 await 远大于 svctm,说明延迟主要在排队,不在硬件。这时候调调度器或加大队列深度,比换盘管用。

1.3 性能指标:IOPS、带宽、延迟的物理含义与测量方法

这三个指标,是存储性能的「三驾马车」。但很多人只背概念,不理解它们之间的制约关系。

IOPS(每秒IO操作数)

说白了就是「每秒能处理多少个请求」。它跟 IO 大小强相关。4K 随机读,NVMe SSD 能做到百万级 IOPS。但换成 1MB 顺序写,IOPS 可能掉到几千。为什么?因为硬件处理大 IO 的时间更长。

测量方法:用 fio 是最常见的。我一般这样测随机读 IOPS:

fio --name=randread --ioengine=libaio --iodepth=64 \
    --rw=randread --bs=4k --direct=1 --numjobs=1 \
    --size=10G --runtime=60 --time_based

注意 --direct=1,跳过页缓存,测的是真实硬件性能。如果不加这个,你测出来的可能是内存速度。

带宽(吞吐量)

带宽 = IOPS × IO大小。这个公式很简单,但很多人忽略了一个事实:带宽有上限。PCIe 3.0 x4 的理论带宽约 4GB/s,NVMe 盘再快也突破不了这个物理限制。

我曾经帮一个团队排查问题,他们用 8 块 SSD 做 RAID 0,但总带宽只有 2GB/s。一查,PCIe 插槽是 x2 的,带宽被卡死了。换到 x16 插槽后,带宽直接翻倍。

测量带宽,把 --rw 改成 read 或 write,--bs 设大一点(1M):

fio --name=seqread --ioengine=libaio --iodepth=16 \
    --rw=read --bs=1m --direct=1 --numjobs=1 \
    --size=10G --runtime=60 --time_based

延迟(时延)

延迟是「一个请求从发出到完成的时间」。它通常用 P99(99% 的请求延迟低于这个值)来衡量,而不是平均值。平均值会掩盖抖动。

举个例子:某系统平均延迟 2ms,但 P99 是 50ms。这意味着 1% 的请求要等 50ms。对于数据库这种场景,这 1% 的慢请求可能就导致超时。

避坑指南:我曾经只盯着平均延迟看,结果线上频繁超时。后来加上 P99 监控,才发现问题。记住:平均延迟是骗人的,P99 才是真相。

测量延迟,用 fio 的 --latency-percentiles 参数:

fio --name=latencytest --ioengine=libaio --iodepth=1 \
    --rw=randread --bs=4k --direct=1 --numjobs=1 \
    --size=1G --runtime=30 --time_based \
    --latency-percentiles=1

这里 --iodepth=1 是关键。队列深度为 1 时,测的是纯硬件延迟,没有排队干扰。

1.4 三个指标的制约关系

你不可能同时让 IOPS、带宽、延迟都最优。这就像「不可能三角」:

  • 追求低延迟 → 队列深度要小 → IOPS 上不去
  • 追求高 IOPS → 队列深度要大 → 延迟会升高
  • 追求高带宽 → IO 要大 → IOPS 自然下降

我建议你在做性能测试前,先明确目标:是 OLTP 场景(低延迟、高 IOPS),还是 OLAP 场景(高带宽)?目标不同,优化方向完全不同。

一句话总结:IOPS 看并发能力,带宽看吞吐能力,延迟看响应速度。三个指标一起看,才能判断系统真正的瓶颈在哪。

好了,这一章的内容就是这些。记住 IO 栈的每一层,理解延迟从哪来,学会用 fio 测三个指标。后面我们会深入每一层,讲具体的优化手段和故障排查方法。