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

凌晨两点,香港深水埗的机房像一直在喘气,交易盘前回放压测告诉我:订单落地日志(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 | none;nr_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 | 场景样本值 |