在存储系统与数据库性能调优中,写放大系数(WAF, Write Amplification Factor) 是决定 SSD 寿命和系统写入吞吐量的核心指标。
许多工程师在测试 WAF 时,经常会遇到数据对不上的情况:为什么 RocksDB 统计的 WAF 是 4,而 SSD 盘监控到的物理写入量却翻了十几倍?
要准确测量物理写放大,必须区分两个层级的 WAF,并建立完整的观测链路:
- 软件/应用层 WAF(Software WAF):由数据库 LSM-Tree 整理(Compaction)或文件系统日志(Journal)引起。
- 硬件/物理层 WAF(Hardware/SSD WAF):由 SSD 内部闪存转换层(FTL)的垃圾回收(GC)引起。
$$\text{系统总 WAF} = \text{软件层 WAF} \times \text{文件系统 WAF} \times \text{SSD 物理 WAF}$$
本文将介绍如何使用 Fio 和 db_bench,配合底层硬件观测工具,精准测出底层的物理写放大系数。
一、 核心前置条件:SSD 必须进入“稳态(Steady State)”
在新盘(Fresh Out of Box)或空盘上做任何 WAF 测试,结果都是完全错误的。
因为空盘有充足的空白闪存块,SSD 内部不需要进行垃圾回收(GC),此时物理 WAF 接近于 1。只有当 SSD 被写满至少一次以上,触发 FTL 的 GC 后,物理 WAF 才会显现真实值。
怎么做 SSD 预调理(Pre-conditioning)?
在运行正式测试前,必须对盘进行持续的随机写入,使其性能和 GC 行为达到稳定状态。
- 对于纯 Fio 测试:使用 4KB 随机写入全盘,持续写入至少 2 倍盘容量。
- 对于 RocksDB 测试:预先 load 超过 SSD 容量 70% 以上的数据,并让 Compaction 充分运行。
二、 使用 Fio 准确测量 SSD 物理 WAF
Fio 是块设备层面的测试工具。用 Fio 测试物理 WAF 的核心逻辑是:记录 Fio 写入的逻辑数据量(Host Writes)与 SSD 内部闪存实际写入的物理数据量(NAND Writes)的比值。
1. 采集底层的工具准备
标准的 NVMe SMART 信息中,Data Units Written 仅表示 Host 写入量。要获取 NAND(Flash)实际写入量,需要使用 SSD 厂商的专有工具。
- Intel / Solidigm SSD:
寻找# 安装 solidigmtool 或 isdct sst show -sensorNandWrites或NAND Bytes Written指标。 - Samsung 企业级 SSD:
使用smartctl查看厂商自定义属性(通常是 Attribute 241/242 或特定的 NVMe Log Page):
寻找smartctl -a /dev/nvme0n1Physical Writes或NAND Sector Writes。
2. 标准测试流程
为了避免文件系统缓存和双写日志(Journal)的干扰,Fio 测试必须绕过文件系统,直接测试裸块设备,并开启 direct=1。
Step 1: 记录初始值
在测试开始前,记录当前的 Host 写入总量和 NAND 写入总量:
- $Host_{start}$ =
Data Units Written$\times$ 512 KB - $NAND_{start}$ = 厂商工具读出的
NAND Bytes Written
Step 2: 运行 Fio 稳态随机写测试
fio --name=waf_test \
--filename=/dev/nvme0n1 \
--ioengine=libaio \
--direct=1 \
--rw=randwrite \
--bs=4k \
--numjobs=4 \
--iodepth=32 \
--size=100G \
--runtime=7200 \
--time_based \
--group_reporting
注:测试时间必须足够长(建议 2 小时以上),以确保 FTL 的 GC 充分并发。
Step 3: 记录结束值
测试完成后,再次记录:
- $Host_{end}$
- $NAND_{end}$
Step 4: 计算物理 WAF
$$\text{SSD Physical WAF} = \frac{NAND_{end} - NAND_{start}}{Host_{end} - Host_{start}}$$
如果计算出的 WAF 明显大于 1(例如 2.5 或 3.0),说明该负载下 SSD 正在经历剧烈的 GC。
三、 使用 RocksDB db_bench 测量多层级写放大
RocksDB 作为一个 LSM-Tree 架构的存储引擎,其软件写放大非常显著。在使用 db_bench 时,我们需要理清:用户写了多少 $\rightarrow$ RocksDB 写入了多少(SST+WAL) $\rightarrow$ 操作系统/文件系统写了多少 $\rightarrow$ SSD 闪存写了多少。
1. 启动 db_bench 并收集软件层 WAF
db_bench 运行结束后,会在终端或 LOG 文件中输出详尽的 Compaction Stats。
运行一个典型的随机写测试:
./db_bench \
--benchmarks="fillrandom,stats" \
--db=/mnt/nvme/rocksdb_test \
--num=100000000 \
--value_size=400 \
--statistics=true \
--stats_interval_seconds=10 \
--histogram=true
在输出结果的 Header 或 Compaction Stats 中,重点看这一行:
Sum(MB) write: 12345.60 (user), 54321.00 (raw), 98765.40 (actual)
------------------------------------------------------------------
Write Amp: 8.0 (actual / user)
- User Writes: 客户端写入的 Key-Value 原始大小。
- Actual Writes: RocksDB 实际写入到文件系统的 SST 文件和 WAL 日志的总量。
- RocksDB WAF =
Actual Writes / User Writes。
2. 精准测出 RocksDB 导致的物理/硬件 WAF
仅看 RocksDB 自身的 Write Amp 是不够的,因为底层的 ext4/XFS 文件系统以及 SSD 还会二次放大。要拿到物理写放大,需要把 db_bench、/proc/diskstats 和 SSD SMART 联动起来。
闭环测试框架设计:
在 db_bench 运行期间,开启一个后台守护脚本,每 5 秒采集一次系统 I/O 和 NVMe 状态。
#!/bin/bash
# 监测目标盘,例如 nvme0n1
DISK="nvme0n1"
get_disk_sectors() {
# 读出 /sys/block/nvme0n1/stat 中的第 7 个字段(写入的扇区数)
cat /sys/block/$DISK/stat | awk '{print $7}'
}
get_nand_writes() {
# 根据你的 SSD 厂商调整,此处以 Solidigm/Intel 为例
sst show -sensor | grep "NAND Bytes Written" | awk '{print $4}'
}
# 记录起点
Host_Sectors_Start=$(get_disk_sectors)
Nand_Bytes_Start=$(get_nand_writes)
echo "Benchmark 开始,监控中..."
# 等待 db_bench 进程结束
wait $DB_BENCH_PID
# 记录终点
Host_Sectors_End=$(get_disk_sectors)
Nand_Bytes_End=$(get_nand_writes)
# 计算
Host_Written_Bytes=$(( (Host_Sectors_End - Host_Sectors_Start) * 512 ))
Nand_Written_Bytes=$(( Nand_Bytes_End - Nand_Bytes_Start ))
echo "Host 写入量 (OS 角度): $(( Host_Written_Bytes / 1024 / 1024 )) MB"
echo "NAND 写入量 (SSD 角度): $(( Nand_Written_Bytes / 1024 / 1024 )) MB"
通过这个闭环,你可以推导出两个极其关键的物理指标:
文件系统 + FTL 的综合物理放大率(System-to-Physical WAF):
$$WAF_{Sys2Phys} = \frac{\text{Nand Written Bytes}}{\text{Host Written Bytes (OS 角度)}}$$
如果这个值远大于 1.5,说明文件系统的文件碎片化严重,或者 SSD 内部发生了严重的对齐冲突。端到端真正的物理写放大(End-to-End Physical WAF):
$$WAF_{E2E} = \frac{\text{Nand Written Bytes}}{\text{db_bench 统计的 User Writes (Bytes)}}$$
这个值代表了每写入 1 字节用户数据,闪存颗粒要承受多少字节的物理损耗。它是 RocksDB 参数调优(如选择 Size-tiered 还是 Levelized Compaction)的最真实反馈。
四、 避免评测偏差的避坑指南
1. 文件系统 Journal(日志)的隐形放大
在 ext4 文件系统上,默认开启了 data=ordered,这意味着元数据会被强制写入 Journal。在小数据随机写入时,Journal 的写入量可能比数据本身还要大。
- 解决方案:测试 RocksDB 等自身有 WAL 日志的引擎时,挂载 ext4 时使用
-o noatime,nodiratime,data=writeback,或直接使用 XFS 文件系统。
2. 忽略了 Page Cache 的刷盘行为
如果 RocksDB 未开启 use_direct_io_for_flush_and_compaction,写入 SST 的流量会先进入操作系统的 Page Cache。由后台 pdflush 线程异步刷盘时,会进行 I/O 合并。
这会导致 OS 统计到的 Host 写入量显著低于 RocksDB 报告的实际写入量,呈现“写缩小”的假象,但其实只是数据还滞留在内存中。
- 解决方案:测试结束前,必须运行
sync强制刷盘,并多等待几分钟,直到iostat流量归零,再读取终点数据。
3. 未关闭 SSD 的 Trim/Discard 功能
如果挂载参数带有 discard,当 RocksDB 频繁删除旧 SST 文件时,会向 SSD 发送 Trim 指令。SSD 接收到 Trim 后会加速 GC 的进行,虽然这会释放空间,但会短时间内推高 NAND Writes。
- 评估建议:建议在测试期间保持与生产环境一致的 Discard 配置(建议使用后台
fstrim定时任务,而不是挂载时开启discard),以获得最贴近真实的生产 WAF 表现。