上一篇 下一篇 分享链接 返回 返回顶部

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

发布人:Minchunlin 发布时间:2026-01-13 10:22 阅读量:468


我最怕的不是“慢”,而是那种只在订单高峰出现、平峰一切正常的“抖一下”。接口偶发 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 / 网关:强制输出分段耗时(可直接复制)

log_format a5_timing '$remote_addr - $request '
                    'rt=$request_time '
                    'uct=$upstream_connect_time '
                    'uht=$upstream_header_time '
                    'urt=$upstream_response_time '
                    'st=$status ua="$http_user_agent"';

access_log /var/log/nginx/access_a5_timing.log a5_timing;

解释(只讲关键点):

  • rt:端到端总耗时
  • uct/uht/urt:上游连接/首包/响应耗时

如果 rt 大但 urt 不大,问题多在应用/DB;如果 urt 大且伴随重传/RTT 抖动,问题偏网络回包。

2.2 应用侧:给 DB 写入单独打点(伪代码)

无论你是 PHP / Java / Go,都要做到两件事:

1)DB 调用耗时单独打点(不要只打总耗时)

2)记录订单号/事务ID/线程ID/连接ID(用于跨日志对齐)

示例(伪代码):

t0 = now()
begin_tx()

t1 = now()
insert order ...
t2 = now()

commit()
t3 = now()

log.info("order_create",
  total_ms=t3-t0,
  sql_ms=t2-t1,
  commit_ms=t3-t2,
  conn_id=db_conn_id,
  trace_id=trace_id)

关键:很多抖动其实卡在 commit_ms(fsync/journal/WAL),不是卡在那条 insert。

3)先画“证据地图”:磁盘慢、网络慢分别长什么样

3.1 一个很重要的坑:别再信 svctm

在较新的 sysstat / RHEL9 体系里,iostatsvctm 已被明确认为不可靠并移除;它在现代存储(尤其 NVMe、多队列)场景下很容易误导你。你应该优先看 awaitavgqu-sz、以及分位数直方图类工具。

3.2 快速判别:用“分叉树”把方向收敛到磁盘 or 网络

磁盘分支更像这样:

  • iostat -xawait 抬升、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)

# 0) 基本信息
date; uname -a; lscpu; free -h; lsblk -o NAME,TYPE,SIZE,MODEL,MOUNTPOINT

# 1) IO 侧
iostat -x 1 600 > /tmp/a5_iostat.log
pidstat -dru -h 1 600 > /tmp/a5_pidstat.log

# 2) NVMe 健康与温控
nvme list
nvme smart-log -H /dev/nvme0n1 > /tmp/a5_nvme_smart.log

# 3) 网络侧
ss -tiH > /tmp/a5_ss_ti_1.log
nstat -az 1 600 > /tmp/a5_nstat.log
sar -n DEV 1 600 > /tmp/a5_sar_dev.log

# 4) 网卡驱动统计(drop/no_buffer)
ethtool -S eth0 > /tmp/a5_eth0_stats_1.log

5)磁盘方向:把 NVMe 的“慢”定位到块层/文件系统/设备本体

5.1 第一次就要跑对:iostat 你只看这三列

iostat -x 1

重点字段:

  • await:请求在队列里等待 + 实际被设备处理的总时间(毫秒)
  • avgqu-sz:队列长度(能直接看出“堆不堆”)
  • %util参考,但别迷信(NVMe 多队列下“100%”不等价于饱和)

读法口诀:

  • await 高、avgqu-sz 也高:更像队列堆积(负载/参数/写放大/刷盘策略)
  • await 高、avgqu-sz 很小:更像设备本体慢(温控降频、固件、介质问题)——这类在 Brendan Gregg 的案例里很典型

5.2 用 eBPF 拉出“尾延迟直方图”:你要看的是分布

订单抖动往往是“多数请求很快,少数请求极慢”,平均值会骗你。biolatency 这种直方图工具就是为这个场景设计的:它把 I/O 延迟分布直接画出来。

示例:

# 需要 bcc-tools
biolatency -mT 1 60

如果你看到直方图出现明显长尾(比如 50ms、100ms、300ms 桶突然多),基本可以确认“慢”在 I/O 路径里。

5.3 NVMe 自检:先排除“温控降频/健康异常”

nvme-cli 的 SMART Log 是最直接的证据入口:温度、critical warning、介质错误、非正常断电次数都在里面。

示例:

nvme smart-log -H /dev/nvme0n1

你要重点盯:

  • critical_warning 非 0(直接红灯)
  • 温度是否长期高位(很多盘在高温会触发降频)
  • media_errorsnum_err_log_entries 是否增长(需要进一步看 error log)

5.4 用 fio 复刻“订单写入形态”:不要只跑 IOPS

fio 的价值在于:你能把“订单写入”抽象成一个可复刻的 I/O 工作负载。fio 的官方文档对 jobfile、iodepth、direct、percentile 输出都有完整说明。

核心点:

  • 很多订单系统并不是“大顺序写”,而是小块随机写 + 频繁 fsync/commit
  • 如果你不模拟 fsync,压测结果会“虚高”,上线还是抖

一个更贴近订单写入的 fio jobfile(示例):

# /tmp/a5_order_write.fio
[global]
ioengine=libaio
direct=1
time_based=1
runtime=120
group_reporting=1
randrepeat=0
norandommap=1
filename=/data/fio_testfile
size=8G

# 模拟:小块随机写 + 频繁提交(fsync)
[order_fsync]
rw=randwrite
bs=4k
iodepth=16
numjobs=4
fsync=1

跑法:

fio /tmp/a5_order_write.fio --output-format=json+ --output=/tmp/a5_fio.json

你要看的不是“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 的手册中有说明。

示例:

ss -tiH '( dport = :443 )' | head -n 50

你要盯:

  • 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 内核文档把这三类来源讲得很清楚。

抓驱动统计:

ethtool -S eth0 | egrep -i 'drop|dropped|miss|no_buffer|timeout|error'

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 长尾 ↑、iostat await
  • 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_commitsync_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                
           
目录结构
全文