香港服务器NVMe SSD:订单高峰写入抖动,如何把“慢”定位到磁盘还是网络?

我最怕的不是“慢”,而是那种只在订单高峰出现、平峰一切正常的“抖一下”。接口偶发 10–20 秒、队列堆起来、业务同学一句“你们服务器又卡了”,而你打开监控:CPU 不高、内存够用、带宽也没打满。
这类问题想要一次性解决,靠拍脑袋改参数没用,必须把“慢”拆成可被证据证明的路径:到底卡在磁盘写入链路,还是卡在网络回包链路(以及两者的耦合点)。
1)现场基线
1.1 业务与抖动现象
- 业务:跨境电商下单/支付回调/库存扣减/订单落库
- 现象:订单高峰(活动开始/整点)P99 延迟从 300ms 飙到 8–20s,偶发超时;恢复后又正常
- 共性:问题难复现、只在高并发写入时出现
1.2 一套“可复刻”的参考配置(示例)
- 机型:香港物理机 / 独享云均可
- CPU:AMD EPYC 7xx3 / Intel Xeon Gold 同级
- 内存:64–128GB
- 磁盘:1–2 块 NVMe SSD(U.2 / M.2),XFS/ext4
- 网络:100M BGP(带 CN2 直连段)
- OS:Ubuntu 22.04 LTS / RockyLinux 9
- 中间件:Nginx + MySQL 8.0(或 PostgreSQL)+ Redis
2)把“慢”拆成 4 段时间:你要的不是平均值,是证据链
订单高峰抖动,最常见的误区是:只盯一个监控图(CPU、带宽、IOPS),然后开始“玄学调参”。正确做法是先把一次请求拆成 4 段时间,并把它写进日志。
2.1 Nginx / 网关:强制输出分段耗时(可直接复制)
解释(只讲关键点):
rt:端到端总耗时uct/uht/urt:上游连接/首包/响应耗时
如果 rt 大但 urt 不大,问题多在应用/DB;如果 urt 大且伴随重传/RTT 抖动,问题偏网络回包。
2.2 应用侧:给 DB 写入单独打点(伪代码)
无论你是 PHP / Java / Go,都要做到两件事:
1)DB 调用耗时单独打点(不要只打总耗时)
2)记录订单号/事务ID/线程ID/连接ID(用于跨日志对齐)
示例(伪代码):
关键:很多抖动其实卡在
commit_ms(fsync/journal/WAL),不是卡在那条 insert。
3)先画“证据地图”:磁盘慢、网络慢分别长什么样
3.1 一个很重要的坑:别再信 svctm
在较新的 sysstat / RHEL9 体系里,iostat 的 svctm 已被明确认为不可靠并移除;它在现代存储(尤其 NVMe、多队列)场景下很容易误导你。你应该优先看 await、avgqu-sz、以及分位数直方图类工具。
3.2 快速判别:用“分叉树”把方向收敛到磁盘 or 网络
磁盘分支更像这样:
iostat -x:await抬升、avgqu-sz堆积(队列变长),写入为主- eBPF
biolatency:尾延迟出现长尾(几十 ms 到几百 ms) - 应用日志:
commit_ms显著增加(或 DB 慢日志集中在 COMMIT)
网络分支更像这样:
ss -ti:retrans、rttvar 变大、send-q 堆积nstat:重传相关计数增长ethtool -S:rx/tx 队列 drop、no_buffer 增加(或 softirq 打满)- 应用日志:上游响应慢但 DB 写耗时不一定高
4)一次性采全证据:高峰窗口不要手动敲命令
高峰期你最缺的不是工具,是“对齐时间轴”。下面这套采集建议你固定成脚本:每次出现抖动直接跑 5–10 分钟,把证据装袋带走。
4.1 采集命令清单(建议 1s 采样,持续 600s)
5)磁盘方向:把 NVMe 的“慢”定位到块层/文件系统/设备本体
5.1 第一次就要跑对:iostat 你只看这三列
重点字段:
await:请求在队列里等待 + 实际被设备处理的总时间(毫秒)avgqu-sz:队列长度(能直接看出“堆不堆”)%util:参考,但别迷信(NVMe 多队列下“100%”不等价于饱和)
读法口诀:
await高、avgqu-sz也高:更像队列堆积(负载/参数/写放大/刷盘策略)await高、avgqu-sz很小:更像设备本体慢(温控降频、固件、介质问题)——这类在 Brendan Gregg 的案例里很典型
5.2 用 eBPF 拉出“尾延迟直方图”:你要看的是分布
订单抖动往往是“多数请求很快,少数请求极慢”,平均值会骗你。biolatency 这种直方图工具就是为这个场景设计的:它把 I/O 延迟分布直接画出来。
示例:
如果你看到直方图出现明显长尾(比如 50ms、100ms、300ms 桶突然多),基本可以确认“慢”在 I/O 路径里。
5.3 NVMe 自检:先排除“温控降频/健康异常”
nvme-cli 的 SMART Log 是最直接的证据入口:温度、critical warning、介质错误、非正常断电次数都在里面。
示例:
你要重点盯:
critical_warning非 0(直接红灯)- 温度是否长期高位(很多盘在高温会触发降频)
media_errors、num_err_log_entries是否增长(需要进一步看 error log)
5.4 用 fio 复刻“订单写入形态”:不要只跑 IOPS
fio 的价值在于:你能把“订单写入”抽象成一个可复刻的 I/O 工作负载。fio 的官方文档对 jobfile、iodepth、direct、percentile 输出都有完整说明。
核心点:
- 很多订单系统并不是“大顺序写”,而是小块随机写 + 频繁 fsync/commit
- 如果你不模拟 fsync,压测结果会“虚高”,上线还是抖
一个更贴近订单写入的 fio jobfile(示例):
跑法:
你要看的不是“IOPS 很高”,而是:
clat(完成延迟)分位数:P95/P99 有没有长尾- 抖动是否随时间出现“阶段性恶化”(后台 GC、温控、队列策略)
关于 iodepth 的语义:它表示“同时在飞的 I/O 数”;但对同步引擎,iodepth>1 并不总能提高并发,需要配合 numjobs 扩展并发。
5.5 文件系统与写放大:最常见的“你没想到”
订单高峰写抖动,经常不是 NVMe 盘“坏”,而是写放大把尾延迟拉爆:
- journal / WAL / binlog 叠加 fsync
- 容器 overlayfs 引入额外 copy-up
- dirty page 一次性回写造成“批量抖动”(周期性尖刺)
这时候你的证据应该能对上:commit_ms 抬升 + biolatency 长尾 + iostat await/queue 同步尖刺。
5.6 数据库刷盘策略:用“耐久等级”换“高峰稳定性”
以 MySQL/InnoDB 为例,写入高峰抖动最常见的根因之一,就是每事务提交都触发严格刷盘(尤其还叠加 binlog)。MySQL 官方文档对 innodb_flush_method(含 O_DIRECT_NO_FSYNC)等行为有明确描述。
同时,sync_binlog 的取值与性能/安全权衡也有清晰说明。
“三级耐久策略”(示例):
| 场景 | innodb_flush_log_at_trx_commit | sync_binlog | 风险 | 适用 |
|---|---|---|---|---|
| 金融级最稳 | 1 | 1 | 性能最低 | 极端重一致 |
| 常规生产折中 | 1 | 100 / 500 | 可能丢少量 binlog | 大多数业务 |
| 高峰优先(可丢秒级) | 2 | 100 / 500 | 宕机可能丢 1s 内事务 | 秒杀/活动高峰 |
注意:这类改动必须写“可接受的数据丢失窗口”,并配套主从/重放策略;否则只是把风险转移给业务。
6)网络方向:把“慢”定位到链路、内核网络栈,还是软中断
6.1 先用 ss 看“这条连接是否在重传/抖动”
ss -i 能输出 TCP 的 rtt、rttvar、重传相关信息;其字段含义在 ss 的手册中有说明。
示例:
你要盯:
rtt:与rttvar:是否在高峰明显增大retrans:是否出现累积send-q/recv-q是否堆积(应用处理不过来或回包不畅)
6.2 看“是不是主机自己处理不过来”:softirq、队列与 drop
当网络层慢不是链路问题,而是主机处理不过来,最常见的证据是:
- softirq 偏高(尤其 NET_RX/NET_TX)
- 网卡驱动统计出现 rx_no_buffer / dropped
- backlog 堆积(netdev backlog)
网卡统计来源分为标准统计、协议统计、驱动统计(ethtool),Linux 内核文档把这三类来源讲得很清楚。
抓驱动统计:
6.3 RPS/RFS/队列绑核:高并发时这是“尾延迟杀手”
当单队列/单核成为瓶颈时,RPS/RFS 能把包处理分散到更多 CPU,提高缓存命中、降低延迟。内核文档对 RPS 的机制有详细解释。
Red Hat 的性能调优文档也明确说明了 RPS/RFS 的用途与开启方式。
判断标准:
- CPU 总体不高,但某个核的 softirq 打满(top/htop/perf 能看到)
- ethtool 显示只有 1–2 个 RX queue 在忙
- 打开/调整 RSS/RPS/RFS 后,P99 延迟明显回落
这里一定要强调:这是“需要证据支撑”的改动,不是默认就该开。
7)最容易误判的耦合点:磁盘慢会伪装成网络慢,反过来也成立
7.1 “磁盘慢 → 网络看起来慢”的经典链路
1)MySQL commit 抖动(fsync/WAL/journal)
2)应用线程阻塞,连接池排队
3)Nginx 上游响应变慢,urt 变大
4)客户端觉得“网络慢”
拆穿它的方法:
- 同一时间窗里,应用日志
commit_ms↑、biolatency 长尾 ↑、iostatawait↑ - 但
ss/nstat并没有明显重传与 rtt 抖动
7.2 “网络慢 → 磁盘看起来慢”的反向链路
1)回包慢/重传
2)请求积压,DB 连接长时间占用
3)后台刷盘与 checkpoint 在高压下更容易尖刺
4)iostat 看起来也“忙”
拆穿它的方法:
-
先证实 TCP 层(retrans/rttvar/send-q)异常,再看 I/O 尖刺是否是被动结果
8)一套可直接落地的优化清单(按收益/风险分层)
8.1 磁盘侧(优先做“无损/可回滚”的)
A. 设备健康与温控
- NVMe SMART:温度/告警/错误日志
- 机箱风道与散热片:很多“只在高峰抖”的盘,根因就是温控降频(证据:温度与 await/biolatency 尖刺同窗)
B. 证据驱动的写放大治理
- 将 binlog / redo / 数据文件分盘(如果条件允许)
- 容器写路径尽量落到独立数据盘,避免 overlayfs 的额外开销
C. 块层观察而不是“盲调”
-
你可以在文末加一句:blk-mq 的多队列设计就是为了让 NVMe 发挥并行能力,但调优要以队列与延迟证据为准。
8.2 数据库侧(把“耐久等级”写进方案)
- MySQL:明确
innodb_flush_log_at_trx_commit与sync_binlog的组合含义与风险边界 - 如果你是 PostgreSQL:官方文档明确指出
synchronous_commit关闭能带来大量性能收益,但风险与fsync=off不同;非关键事务可降级。
8.3 网络侧(先把“软中断/队列 drop”证据抓到再动)
- 先证实:drop/no_buffer/softirq 是否存在
- 再讨论:RSS/RPS/RFS、IRQ affinity、队列数
9)验收:你要用指标证明“抖动消失”,而不是“感觉快了”
9.1 验收指标(建议写成表格放文末)
| 维度 | 指标 | 目标 |
|---|---|---|
| 应用 | 下单接口 P99 | 从 8–20s → < 800ms(示例) |
| DB | commit_ms P99 | 明显下降且无尖刺 |
| 磁盘 | biolatency 长尾桶 | 100ms+ 桶基本消失 |
| 磁盘 | iostat await/avgqu-sz | 高峰不再阶跃抬升 |
| 网络 | retrans/rttvar | 高峰期不异常增长 |
| 网卡 | ethtool drop/no_buffer | 不持续增长 |
9.2 变更记录模板(建议你要求团队强制填写)
| 变更项 | 变更前 | 变更后 | 风险 | 回滚方式 | 验收窗口 |
|---|---|---|---|---|---|
| MySQL flush 策略 | … | … | … | … | 活动高峰 10 分钟 |
| RPS/RFS | … | … | … | … | 同上 |
| NVMe 散热/固件 | … | … | … | … | 同上 |
10)附录
10.1 抖动时间轴表(核心)
| 时间点 | 请求P99 | rt | urt | commit_ms | iostat await | biolatency P99 | retrans | 备注 |
|---|---|---|---|---|---|---|---|---|
| 20:00:00 | ||||||||
| 20:00:10 | ||||||||
| … |