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

如何在香港服务器中对 Samsung PM1743 NVMe 与 EXT4 进行配置,提升金融交易系统的存储延迟表现?

发布人:Minchunlin 发布时间:2025-09-05 11:04 阅读量:766


凌晨两点,香港深水埗的机房像一直在喘气,交易盘前回放压测告诉我:订单落地日志(append-only,小块随机写 + fsync)在尖峰时段出现 p99.9 ≈ 2.2ms 的抖动,影响上游风控的确认链路。硬件是 Samsung PM1743(企业级 NVMe,PCIe 5.0 x4,U.2 形态,容量 3.84TB,带 PLP 断电保护),系统是 CentOS 7(公司基线要求),文件系统要用 EXT4(审计与工具链历史包袱)。

目标:不改业务写法的前提下,把 4K 同步写(fsync=1) 的尾延迟(p99~p99.9)尽量压低、压稳;同时保障断电一致性与审计需求。

1. 现场环境与基线

1.1 服务器与拓扑

  • 机型:Supermicro 1U,双路 Xeon Gold 6330(2×28C),BIOS 打开了 PCIe Gen5。
  • 内存:256GB(2×128GB,NUMA=2)。
  • 存储槽位:前置 10× U.2 NVMe,后面板 2× U.2。
  • 网卡:Intel E810 100GbE(与 NVMe 不在同一根 NUMA)。
  • NVMe:Samsung PM1743 3.84TB(U.2,PCIe 5.0 x4),固件记为 GDCxxxx(现场以 nvme list 查到)。
  • OS:CentOS 7.9,初始内核 3.10(老),SELinux Enforcing。

盘用途:

  • nvme0n1:交易日志(append-only,小块写 + fsync)。
  • nvme1n1:撮合快照/热数据(读多写少)。
  • 其他:系统盘为 SATA SSD,避免系统日志争用 NVMe。

1.2 基线测试(“什么都不调”的真实延迟)

我用 fio 做了两个代表性场景——随机读(观察设备栈)与 fsync 写(贴近业务):

# 场景A:4K 随机读,iodepth=1,直连,观察设备栈延迟下限
fio --name=randread --filename=/mnt/pm1743/testA \
    --rw=randread --bs=4k --iodepth=1 --numjobs=1 \
    --direct=1 --ioengine=libaio --size=8G --time_based \
    --runtime=60 --group_reporting --lat_percentiles=1

# 场景B:4K 顺序写 + 每次fsync,模拟订单日志落地
fio --name=fsync --filename=/mnt/pm1743/testB \
    --rw=write --bs=4k --iodepth=1 --numjobs=1 \
    --direct=1 --fsync=1 --ioengine=libaio --size=1G \
    --time_based --runtime=60 --group_reporting --lat_percentiles=1

基线(CentOS 7 默认内核 + 默认 EXT4)实测:

  • 场景A p99 ≈ 180 μs,p99.9 ≈ 260 μs
  • 场景B p99 ≈ 1.6 ms,p99.9 ≈ 2.2 ms(抖动明显)

2. 策略设计:把“尾巴”按住

我把优化拆成四条主线:

  • 内核与 NVMe 栈:CentOS 7 自带 3.10 内核的 blk-mq、nvme 驱动太老。上 ELRepo 的 kernel-ml(5.15+),拿到成熟的 io_uring、NVMe polling/队列、EXT4 改进。
  • NUMA 与中断亲和:设备与 CPU 拆到同一 NUMA,固定中断到本地核,关掉会把中断“弄丢”的策略。
  • 块层/队列:对 NVMe 用 scheduler=none、收窄 nr_requests、降低 read_ahead_kb、禁随机熵计入。
  • EXT4 格式化+挂载:noatime、commit=1、journal_async_commit、lazytime、discard=async,并保留日志与屏障(利用 PM1743 的 PLP 做更稳的取舍)。

3. 动手:一步步落地

3.1 升级内核(CentOS 7 保持不变,内核走新)

# 安装 ELRepo 并上 kernel-ml(示例为 5.15/6.x 系列)
rpm --import https://www.elrepo.org/RPM-GPG-KEY-elrepo.org
yum install -y https://www.elrepo.org/elrepo-release-7.el7.elrepo.noarch.rpm
yum --enablerepo=elrepo-kernel install -y kernel-ml kernel-ml-devel

# 设置新内核为默认启动
egrep ^menuentry /etc/grub2.cfg | cut -d "'" -f2
grub2-set-default 0
reboot

验证:

uname -r    # 看到 5.15.x/6.x 即可

坑 1:一开始我想偷懒用 3.10 + backport 的 nvme 驱动,结果 io_uring 行为不稳定,直接放弃。
坑 2:升级内核后,老版 ixgbe/ice 驱动与新内核某次迭代冲突,先确保网卡驱动匹配(我们现场是 E810,用内核自带新驱动OK)。

3.2 确认 NUMA 与插槽

lspci | grep -i nvme
cat /sys/block/nvme0n1/device/numa_node    # 输出例如 0
numactl --hardware                          # 看下 CPU/内存分布

把交易进程绑到 nvme0n1 所在 NUMA:

# 例:设备在 node 0,则绑 node 0
numactl --cpunodebind=0 --membind=0 ./trading_app ...

3.3 中断亲和与 irqbalance

先看中断号:

grep -i nvme /proc/interrupts

给每个 NVMe 队列中断绑到本地 NUMA 的几个物理核(例如 CPU 4,6,8,10):

# 假设中断号为 154,155,156,157;CPU mask 写十六进制位图
echo 0x0000000000000050 > /proc/irq/154/smp_affinity
echo 0x0000000000000050 > /proc/irq/155/smp_affinity
echo 0x0000000000000050 > /proc/irq/156/smp_affinity
echo 0x0000000000000050 > /proc/irq/157/smp_affinity

提示:临时先停 irqbalance,确认亲和生效后把它配置成 oneshot 引导,或者对这些 IRQ 做 IRQBALANCE_BANNED_CPUS,避免它改回去。

3.4 CPU 电源与内核启动参数(尾延迟很在意)

BIOS 里把 C-State 深度限制到 C1,关掉深睡;

Linux 启动参数增加:

intel_pstate=disable processor.max_cstate=1 intel_idle.max_cstate=1

开机后:

yum install -y kernel-tools
cpupower frequency-set -g performance
tuned-adm profile latency-performance

坑 3:有次我把 idle=poll 开太狠,CPU 空转发热导致机箱后排温度升到 39℃,PM1743 进了轻微 thermal throttling,尾延迟反而上去了。低延迟不等于极限拉满,要兼顾散热与持续性。

3.5 NVMe 设备特性与固件

yum install -y nvme-cli

nvme list
nvme id-ctrl /dev/nvme0 | egrep "mn|fr|ps"
# 检查型号 mn / 固件 fr;有条件的话按变更流程升级到官方建议固件

中断合并(Interrupt Coalescing):PM1743 默认会做少量合并以减 CPU;低延迟场景我倾向 减小甚至关闭。

# 查看/设置特性 0x8(Interrupt Coalescing)
nvme get-feature -f 0x8 /dev/nvme0
nvme set-feature -f 0x8 -v 0 /dev/nvme0   # 关闭合并,视CPU占用决定

坑 4:某批固件升级后 0x8 默认值改变,p99 变差。回滚后恢复。变更前先离线压测,别在盘前窗口动生产。

3.6 分区对齐与 EXT4 格式化

对齐:从 1MiB 开始,保持 4K 边界。

parted /dev/nvme0n1 --script mklabel gpt
parted -a optimal /dev/nvme0n1 --script mkpart tlog ext4 1MiB 100%

mkfs.ext4:保留日志(配合 PLP),减少初始化时间的惰性行为(我们要稳定)。

mkfs.ext4 \
  -E lazy_itable_init=0,lazy_journal_init=0 \
  -O metadata_csum,64bit \
  -b 4096 -I 256 -L tlog /dev/nvme0n1p1

-E lazy_* = 0 是为了避免首次写入期间的“后台初始化”偶发拉高尾延迟;初始化阶段多等几分钟,后面更稳。

3.7 挂载参数(延迟优先)

/etc/fstab:

UUID=<你的UUID>  /mnt/tlog  ext4  noatime,nodiratime,commit=1,journal_async_commit,lazytime,discard=async  0 2
  • noatime,nodiratime:不写入访问时间。
  • commit=1:1 秒提交窗口,fsync 仍然是强制刷,但批量提交元数据更及时,减少后台抖动。
  • journal_async_commit:异步提交日志,降低每次提交的同步等待;轻微提高崩溃窗口(结合 PLP 与业务容忍度)。
  • lazytime:延迟更新时间戳写入,减轻元数据写压力。
  • discard=async:异步 TRIM,不阻塞 IO;或关掉 discard,用定时 fstrim -v /mnt/tlog 也行。

挂载之后先跑一轮 fsfreeze/fsync 观察是否有异常波峰。

3.8 块层队列参数(/sys/block/*)

# 调度器
echo none > /sys/block/nvme0n1/queue/scheduler
# 收窄并发队列,避免堆积造成尾部排队(按业务写入速率微调)
echo 32  > /sys/block/nvme0n1/queue/nr_requests
# 读预读对日志无益,设小
echo 8   > /sys/block/nvme0n1/queue/read_ahead_kb
# 随机源统计关掉,避免额外开销
echo 0   > /sys/block/nvme0n1/queue/add_random
# 合并策略尽量少
echo 2   > /sys/block/nvme0n1/queue/nomerges

经验:nr_requests 太大,p99.9 容易被队列尾“长尾”拖拽;太小会牺牲吞吐。我的交易日志是小块写+fsync 单流,32~64 比较均衡。

3.9 EXT4 层的 io_uring 与应用侧

如果你的应用能切到 io_uring + SQPOLL/HIPRI(内核 5.10+):

# randread 示例以 io_uring + HIPRI
fio --name=randread --filename=/mnt/tlog/testA \
    --rw=randread --bs=4k --iodepth=1 --numjobs=1 \
    --direct=1 --ioengine=io_uring --hipri=1 --sqthread_poll=1 \
    --size=8G --time_based --runtime=60 --group_reporting --lat_percentiles=1

我们的业务进程是老代码,落地日志走 open(O_DSYNC) + write + fsync,没法立刻切;但内核升级后的块层本身已经对尾延迟很有帮助。

4. 结果对比与数据

4.1 FIO 场景结果(同一台机器、同一时段、恒温)

场景 配置 p50 p99 p99.9 备注
A 4K randread iodepth=1 基线(3.10 内核 + 默认 EXT4) 105 μs 180 μs 260 μs 抖动随中断漂移
A 4K randread iodepth=1 优化后(5.15 内核 + NUMA/IRQ + 队列 + EXT4 挂载) 78 μs 120 μs 170 μs 尾巴收敛明显
B 4K write + fsync=1 基线 0.8 ms 1.6 ms 2.2 ms 尾部抖动肉眼可见
B 4K write + fsync=1 优化后 0.55 ms 0.95 ms 1.35 ms 峰值极端点(thermal/合并)消失

注:不同 PM1743 容量/固件、主板 PCIe 拓扑会有差异,上表仅为我现场的真实样本。你应在自己的机房做对比复测。

4.2 生产指标(撮合盘前回放 30 分钟)

  • 交易日志链路 p99 从 ~1.8ms → ~1.1ms
  • p99.9 从 ~2.4ms → ~1.5ms
  • 单核 CPU 因关闭合并略升(+6~8%),整体余量充足。
  • 温度控制:风扇曲线上调 5%,NVMe 温度维持 32~34℃,无降频。

5. 风险与取舍说明(非常重要)

journal_async_commit / commit=1:这两项降低同步等待,但在极端掉电的最后一瞬,会把极少量最近的元数据更新留在易失态(数据一致性 OK,时间戳与部分元数据可能回退 ≤1s)。在有 PM1743 PLP 的前提下我接受这个取舍;如监管对“元数据秒级一致”也有要求,就把 journal_async_commit 去掉,或提高 commit。

中断合并关闭:降低尾延迟,增加 CPU;如果 CPU 紧张,适当设一个很小的合并窗口(比如几十微秒),看指标。

温度与功耗:限制 C-State、性能频率、关闭合并都会带来热量。务必盯 NVMe 与背板温度,温控没做好,热降频会把你前面所有优化打回原形。

固件升级:交易行业要严格走变更流程。我的建议是 双机滚动 + 离线压测 + 观测 24h 再全量。

6. 常见坑位与现场解法

坑位 A:PCIe 拓扑“看起来”是 x4 Gen5,实际上挂在了 CPU1→PCH→开关器→背板

症状:空载延迟还行,一旦并发 IO 或网卡同向流量上来,尾延迟抬头。

解法:把 PM1743 换到 CPU0 直连的 root port 槽位(看 lspci -tv),p99.9 直接降了 ~12%。

坑位 B:EXT4 用了 nobarrier 的旧习惯

症状:压测看起来更快,但审计风险高;且新内核 barrier 语义/默认策略变化,配置不一致。

解法:我们保留屏障,靠 journal_async_commit + commit=1 去拿延迟,既稳又可解释。

坑位 C:用 discard(同步 TRIM)导致周期性尖刺

症状:一分钟一次或若干 MB 一次的小尖刺。

解法:换成 discard=async,或关闭 discard 定时跑 fstrim。

坑位 D:nr_requests 调太小

症状:吞吐掉太多,日志赶不上网口。

解法:日志是单流?3264;多流并发?64128 试梯度,配合队列亲和。

7. 复现场景:把配置固化成脚本(可直接用)

7.1 一次性执行脚本(root)

#!/usr/bin/env bash
set -euo pipefail

DEV=/dev/nvme0n1
PART=${DEV}p1
MNT=/mnt/tlog

# 1. 基础包
yum install -y nvme-cli kernel-tools tuned

# 2. CPU & tuned
cpupower frequency-set -g performance || true
tuned-adm profile latency-performance

# 3. NVMe feature:关闭中断合并(按需)
nvme set-feature -f 0x8 -v 0 /dev/nvme0 || echo "skip set-feature"

# 4. 分区 & 格式化
if ! lsblk -no FSTYPE $PART | grep -q ext4; then
  parted $DEV --script mklabel gpt
  parted -a optimal $DEV --script mkpart tlog ext4 1MiB 100%
  mkfs.ext4 -E lazy_itable_init=0,lazy_journal_init=0 -O metadata_csum,64bit -b 4096 -I 256 -L tlog $PART
fi

# 5. 挂载点
mkdir -p $MNT
grep -q "$MNT" /etc/fstab || {
  UUID=$(blkid -s UUID -o value $PART)
  echo "UUID=$UUID  $MNT  ext4  noatime,nodiratime,commit=1,journal_async_commit,lazytime,discard=async  0 2" >> /etc/fstab
}
mount -a

# 6. 队列参数
echo none > /sys/block/$(basename $DEV)/queue/scheduler
echo 32   > /sys/block/$(basename $DEV)/queue/nr_requests
echo 8    > /sys/block/$(basename $DEV)/queue/read_ahead_kb
echo 0    > /sys/block/$(basename $DEV)/queue/add_random
echo 2    > /sys/block/$(basename $DEV)/queue/nomerges

echo "DONE. Verify NUMA & IRQ affinity manually."

7.2 验证项清单(上线前打勾)

  •  uname -r 为 5.15+/6.x
  •  nvme list 固件记录在案
  •  lspci -tv 确认直连 root port
  •  numactl --hardware + /sys/block/.../numa_node 一致
  •  /proc/interrupts 中断亲和固定
  •  /sys/block/.../queue/scheduler 为 none
  •  mount 看到预期的 ext4 选项
  •  fio 场景 A/B p99 与 p99.9 达标
  •  温度 24h 监控无降频

8. FAQ:几个常被问到的取舍

Q:为什么不用 XFS?
A:我们试过。XFS 在高并发吞吐很漂亮,但我们的主诉是 小块同步写尾延迟,EXT4 在保留日志一致性同时更容易把 fsync 尾巴压下去(特别是 journal_async_commit + 新内核)。当然,具体要看你的应用写法与对元数据的一致性要求。

Q:要不要关掉 EXT4 日志(-O ^has_journal)?
A:强烈不建议。交易审计要痕迹与一致性,我宁愿从业务写法(批量、异步)和设备侧(NVMe 队列、亲和、合并)拿延迟,也不去牺牲日志完整性。

Q:barrier=0 / nobarrier 能再快一点吗?
A:某些固件/内核组合确实会再好一点,但风险解释成本高、版本差异大。我倾向保守:保留屏障,用其他手段拿到 80% 的收益,把稳定性放第一位。

9. 收尾:机房外的清晨

做完最后一轮回放,Grafana 上那条令人烦躁的 p99.9 峰,像被刀口削平。把 irqbalance 的配置 commit 到 Ansible,固件版本与 fstab 也都登记进 CMDB。走出机房,空气里是香港清晨特有的潮气。手机弹出一条交易同事的消息:“回放数据 OK,盘前不熬人了。”

优化并不神秘:选对战场(内核/队列/亲和/EXT4)、量化每一步、控制热与风险。PM1743 给了我们很好的硬件底座,EXT4 在对的姿势下也能很“能打”。希望这份现场笔记能帮你在自己的机房里,稳稳地把那条“尾巴”按住。

附:快捷对比表(可做你们的变更文档附件)

类别 基线 优化后 说明
内核 3.10(CentOS7 默认) 5.15+/6.x(ELRepo kernel-ml) 新 NVMe/blk-mq/io_uring
电源 默认 performance + 限制 C-State 降尾延迟抖动
NVMe 默认合并 关闭或极小合并窗口 以 CPU 换 Tail
队列 scheduler=cfq/deadline nonenr_requests=32;read_ahead_kb=8 低并发小块写更稳
EXT4 默认 noatime,nodiratime,commit=1,journal_async_commit,lazytime,discard=async 兼顾一致性与延迟
NUMA/IRQ 随机 亲和到本地核 避免跨 NUMA 抖动
温度 默认 风扇曲线 +5% 防热降频
p99(fsync) ~1.6ms ~0.95ms 场景样本值
p99.9(fsync) ~2.2ms ~1.35ms 场景样本值
目录结构
全文