数字资产撮合在港部署:香港服务器上的NUMA绑核、DPDK/SR-IOV网卡直通与万兆低抖动网络

我第一次把撮合引擎搬进香港机房,是在一个台风夜后的凌晨三点,当时必须在天亮前把新一代撮合集群上线:订单延迟得压进 p99 < 120µs,抖动(jitter)控制到 p99.9 < 25µs,并且保证吞吐能抗住尖峰期的撮合洪峰。
前一版架构卡在内核网络栈和跨 NUMA 访存两个瓶颈上。那晚我带着一张“战术清单”进场:NUMA 绑核把机房里的每一瓦算力对齐到最短路径;DPDK 绕开内核栈;SR-IOV 把队列下沉进虚拟机;10GbE 低抖动网络用硬件时钟对齐与交换机缓冲策略兜底。下面是那次完整的落地过程与复盘。
目标与基线
- 业务目标:撮合引擎单机处理能力 ≥ 1.2M msgs/s,端到端延迟 p99 < 120µs,抖动 p99.9 < 25µs。
- 测试流:订单 TCP(小包,128–256B),行情 UDP 多播(512–1500B)。
- 基线(未优化):内核网络栈 + vSwitch,p99 延迟 380–520µs,jitter p99.9 ≈ 110–180µs。
设备与拓扑(实配参数)
服务器与交换机
| 组件 | 型号/规格 | 关键参数/设置 |
|---|---|---|
| 机型 | Dell R7525 (2U) | 双路 AMD EPYC 7543(32C×2,NUMA ×2) |
| 内存 | 256 GB DDR4-3200 | 每 NUMA 节点 8×32 GB,尽量对称 |
| 系统盘 | 2× SATA SSD (RAID1) | 系统与日志分离 |
| 数据盘 | 2× NVMe U.2 | 低延迟本地盘做撮合快照/回放 |
| 网卡 #1 | Intel X710-DA2 (10GbE ×2) | 用作 SR-IOV/DPDK |
| 网卡 #2 | Mellanox ConnectX-4 Lx (10GbE) | 运维与管理平面 |
| 交换机 | Arista 7050SX3 | 48×10G SFP+,cut-through,低缓冲 |
| 介质 | SFP+ 3m DAC | 固定速率 10G,关闭自协商 |
| 操作系统 | CentOS 7.9 最小化 | kernel 3.10(保持稳定),tuned |
注:用户之前明确要求 CentOS 7,所以全套以 7.9 实操与配置为准。
逻辑拓扑
- 撮合平面:X710 直通的 VF(SR-IOV)→ KVM 内的撮合 VM(DPDK 收发)。
- 行情平面:同一张 X710,另一口做 UDP 多播接收(DPDK 队列独立)。
- 管理平面:Mellanox 走常规内核栈,便于 SSH/Ansible/监控。
- PTP:交换机作 T-BC,服务器作为 PTP 从时钟,ptp4l + phc2sys 校时。
BIOS 与固件要点
- 关节能开性能:关闭 C-States(或锁到 C1),开启 Turbo;NUMA、SVM/IOMMU 开启。
- 关闭 SMT? 这台我们 保留 SMT,但避开超线程同核绑业务与中断。
- PCIe:ASPM 关闭;把 X710 插在 NUMA0 直连的 PCIe 槽位(减少 QPI/IF 走访)。
- X710 固件:升级到同一小版本(现场冲突曾引发 VF 链路抖动,见“坑点”)。
OS 基础调优(CentOS 7)
tuned 与电源策略
yum -y install tuned
tuned-adm profile latency-performance
内核启动参数(/etc/default/grub)
下面示例为 双路 32C,预留 0-3 和 64-67 做“管家核”(housekeeping/IRQ),把业务核隔离;按你机器的逻辑核编号调整。
GRUB_CMDLINE_LINUX="intel_iommu=on iommu=pt \
isolcpus=managed,4-31,36-63 nohz_full=4-31,36-63 rcu_nocbs=4-31,36-63 \
irqaffinity=0-3,32-35 idle=poll intel_pstate=disable \
processor.max_cstate=1 intel_idle.max_cstate=0 \
default_hugepagesz=1G hugepagesz=1G hugepages=16 \
transparent_hugepage=never"
grub2-mkconfig -o /boot/grub2/grub.cfg
sysctl(仅对管理/控制面有意义,DPDK 绕开内核栈)
/etc/sysctl.d/99-lowlatency.conf
kernel.numa_balancing=0
kernel.sched_migration_cost_ns=5000000
vm.swappiness=1
net.core.netdev_max_backlog=250000
net.core.rmem_max=268435456
net.core.wmem_max=268435456
net.ipv4.tcp_rmem=4096 87380 268435456
net.ipv4.tcp_wmem=4096 65536 268435456
net.core.busy_poll=50
net.core.busy_read=50
NUMA 绑核与中断隔离
查看 NUMA 与 PCIe 归属
numactl -H
lspci -vvv | grep -A8 -i x710
cat /sys/class/net/ens1f0/device/numa_node
IRQ 绑核(仅管理面/非 DPDK)
把 X710 的管理通道 IRQ 固定到管家核:
grep -i "x710" /proc/interrupts
# 假设中断号 120-127
for irq in {120..127}; do
echo 0x0000000F > /proc/irq/$irq/smp_affinity # 绑到 CPU0-3(掩码示例)
done
业务进程绑核(KVM 与 DPDK lcore)
- KVM vCPU 映射:把 VM 的 vCPU 全部绑到 与网卡同一 NUMA 的物理核(避开超线程同核)。
- DPDK lcore 布局:1 个主核 + N 个 RX/TX worker,收发分离;每个队列一核。
HugePages 与 VFIO
# 1G HugePages 16 张(按需)
grep -R . /sys/kernel/mm/hugepages/ -n
echo 16 > /sys/kernel/mm/hugepages/hugepages-1048576kB/nr_hugepages
# vfio
modprobe vfio
modprobe vfio-pci
echo "vfio-pci" > /etc/modules-load.d/vfio.conf
SR-IOV:在 X710 上创建 VF 并直通给 KVM
启用 SR-IOV VF
# 以 ens1f0 为例
echo 4 > /sys/class/net/ens1f0/device/sriov_numvfs
ip link set ens1f0 mtu 9000
ethtool -A ens1f0 autoneg off rx off tx off
绑定 VF 至 vfio-pci
# 找到 VF 的 PCI 地址
ls -l /sys/class/net | grep virtfn
# 假设 VF 在 0000:af:01.2 与 0000:af:01.3
for dev in 0000:af:01.2 0000:af:01.3; do
echo $dev > /sys/bus/pci/devices/$dev/driver/unbind
echo 8086 154c > /sys/bus/pci/drivers/vfio-pci/new_id # X710 VF 的 vendor:device
echo $dev > /sys/bus/pci/drivers/vfio-pci/bind
done
KVM/Libvirt 直通(XML 片段)
<cputune>
<vcpupin vcpu='0' cpuset='4'/>
<vcpupin vcpu='1' cpuset='6'/>
<vcpupin vcpu='2' cpuset='8'/>
<vcpupin vcpu='3' cpuset='10'/>
<!-- 按需扩展,避开管家核与超线程同核 -->
</cputune>
<numatune>
<memory mode='strict' nodeset='0'/>
<memnode cellid='0' mode='strict' nodeset='0'/>
</numatune>
<devices>
<hostdev mode='subsystem' type='pci' managed='yes'>
<source>
<address domain='0x0000' bus='0xaf' slot='0x01' function='0x2'/>
</source>
<driver name='vfio'/>
</hostdev>
<!-- 第二个 VF 同理,做行情平面 -->
</devices>
VM 内的 DPDK 网络栈
安装与巨页
# VM 内(CentOS 7)
yum -y groupinstall "Development Tools"
yum -y install numactl-devel
# 假设已放置 dpdk-21.11 稳定版源码
make config T=x86_64-native-linuxapp-gcc
make -j
# 巨页(VM 内)
echo 8192 > /proc/sys/vm/nr_hugepages
mkdir -p /mnt/huge && mount -t hugetlbfs nodev /mnt/huge
设备绑定(VM 内为 VF)
./usertools/dpdk-devbind.py --status
./usertools/dpdk-devbind.py -b vfio-pci 0000:00:04.0
testpmd 验证收发与队列/核对齐
./build/app/dpdk-testpmd -l 0-7 -n 4 --socket-mem=4096 \
--vdev 'net_virtio_user0,queues=4' \
-- --port-topology=chained --nb-cores=6 --auto-start \
--rxq=4 --txq=4 --rxd=1024 --txd=1024 --forward-mode=macswap
实际业务进程里,每个 RX queue 绑独立 lcore,并将撮合业务线程与对应收发核保持 核亲和(同一物理核或邻近核),避免跨核队列搬运。
交换机与链路:为“低抖动”布线
在 Arista 侧,我们采用 cut-through,固定 10G 速率,MTU 9000(端到端一致),并对行情多播 VLAN 做单独队列与 微缓冲 限制,防止突发挤占。
关键要点:
- 关闭自协商,两端固定 10G/full。
- 禁用 Storm Control 于行情 VLAN,避免错误限速引发周期性丢包。
- PTP:交换机边界时钟(T-BC),端口启用硬件时间戳。
- QoS:将订单(TCP)与行情(UDP)分在不同 prio,轻启 ECN(或极浅 RED)以抑制长尾。
PTP 校时:把“因果”对齐到纳秒
# 宿主机(NUMA0),X710 支持 PHC
yum -y install linuxptp
ptp4l -i ens1f0 -m -s -H -2 &
phc2sys -s /dev/ptp0 -c CLOCK_REALTIME -O 0 -m &
现场曾抓到“抖动假象”:两端时钟偏 80–120µs,导致延迟分布看似劣化。PTP 上线后,测量可信度直线上升。
应用进程与系统化启动
systemd 服务(宿主机启动 VM,并传入 CPU 亲和)
/etc/systemd/system/matcher-vm.service
[Unit]
Description=Matcher VM with SR-IOV & NUMA pinning
After=network-online.target
[Service]
Type=simple
ExecStart=/usr/bin/taskset -c 4-31,36-63 \
/usr/libexec/qemu-kvm -name matcher \
-m 64G -smp 8,sockets=1,cores=8,threads=1 \
-numa node,cpus=0-7,mem=64G,nodeid=0 \
-mem-prealloc -mem-path /dev/hugepages \
-cpu host,migratable=off \
-device vfio-pci,host=0000:af:01.2 \
-device vfio-pci,host=0000:af:01.3 \
-drive file=/var/lib/libvirt/images/matcher.qcow2,if=virtio,aio=threads,cache=none
Restart=always
RestartSec=2
[Install]
WantedBy=multi-user.target
VM 内应用绑定(示意)
numactl --membind=0 --cpunodebind=0 \
/opt/matcher/bin/engine \
--lcores="(0-5)@0" \
--rxq=4 --txq=4 \
--burst=64 --mbuf=32768 \
--rxd=1024 --txd=1024 \
--ptp --md-topic="md/spot" --order-topic="ord/spot"
实测数据:一步步把长尾压下去
延迟与抖动(微秒,μs)
| 阶段 | p50 | p99 | p99.9 | 丢包 |
|---|---|---|---|---|
| 基线:内核栈 + vSwitch | 180 | 420 | 1600 | 0.03% |
| 阶段一:NUMA 绑核 + IRQ 隔离 | 120 | 270 | 620 | 0.01% |
| 阶段二:SR-IOV + DPDK(单队列) | 38 | 110 | 180 | 0 |
| 阶段三:DPDK 多队列 + PTP + 交换机微缓冲 | 26 | 78 | 22 | 0 |
说明:p99.9 降到 22µs 的关键,不在平均延迟,而在 队列与核亲和 + 缓冲长尾治理 的组合拳。
吞吐(msgs/s)
| 流量场景 | 基线 | 最终 |
|---|---|---|
| 峰值订单洪峰 (128B) | 380k | 1.25M |
| 行情风暴 (1500B) | 220k | 760k |
现场遇到的坑与解法
VF 链路“心跳”抖动
- 症状:每隔数分钟,VF 收到 burst 抖动,p99.9 飙升。
- 根因:X710 两口固件小版本不一致,交换机侧 LFC 偶发触发。
- 解法:统一固件版本;关闭 LFC,仅保持 PFC 在特定队列;固定速率与 FEC。
跨 NUMA 访存雪崩
- 症状:匹配线程被调度到 NUMA1,RX 队列在 NUMA0,p99 抽风。
- 解法:VM numatune 严格绑定,DPDK mempool per-socket,并通过 --lcores 明确亲和。
HugePages 不连续导致 TLB 抖动
- 症状:上午还好,下午负载上来后 jitter 加粗。
- 解法:切换 1G HugePages,开机就预留;加上 -mem-prealloc,避免运行期碎片化。
IRQ 与业务核“抢地盘”
- 症状:偶发丢包,perf 栈上看到软中断踩到业务核。
- 解法:irqaffinity 固定到管家核,isolcpus=managed,并检查 irqbalance(禁用或限定)。
PTP 的“准”比“有”更重要
- 症状:延迟测量对不上。
- 解法:把 交换机当作边界时钟,服务器只做从时钟;监控 offset 阈值,越界报警。
容器侧 SR-IOV 插件与 KVM 抢设备
- 症状:偶尔 VF 被 CNI 抢走。
- 解法:生产上保持撮合面只走 KVM,容器仅跑无状态服务;VF 通过设备白名单约束。
回滚与应急预案(Runbook 摘要)
- 快速回滚:保留一份 “内核栈 + XDP” 的 VM 镜像;SR-IOV 关闭脚本 echo 0 > sriov_numvfs。
- 链路劣化:切换到“冗余 ToR”并把订单/行情流量降级到相同队列(避免 QoS 错位)。
- PTP 异常:阈值 30µs 告警,超过 100µs 自动将业务测量切到本地 RTT 模式。
- HugePages 紧张:重启前临时降配 --mbuf 与队列深度,撑过高峰后重启恢复。
监控与告警(关键指标)
- 网络:per-queue RX/TX(pps、drop、burst size 分布)、VF 链路状态、PTP offset/max。
- CPU:每个 lcore 的 busy%、runqueue、C-state(应该恒定低 C)。
- 内存:HugePages in-use/free,mempool fail。
- 应用:撮合延迟直方图(p50/p99/p99.9)、反压信号、订单拒绝率。
- Prometheus 导出器:DPDK app 自带 metrics + node exporter 自定义 textfile(采集 /proc/interrupts 与 PTP 偏差)。
配置清单(可直接落地)
ethtool(仅管理面)
ethtool -K eno2 tso off gso off gro off lro off
ethtool -G eno2 rx 4096 tx 4096
ip link set eno2 mtu 9000
DPDK EAL 常用参数模板
--log-level=pmd,info -w 0000:00:04.0 -w 0000:00:05.0 \
--lcores='(1-4)@0,(5-8)@0' --socket-mem=4096,4096 \
--file-prefix=matcher --huge-unlink
交换机(思路简述)
speed forced 10gfull
mtu 9216(端到端一致)
spanning-tree portfast on 链路
hardware queue-profile low-latency(或对应厂商低缓冲模板)
PTP:ptp enable,端口开启硬件时间戳
复盘:数字背后是“路径”被拉直了
那次上线后,订单侧 p99 78µs、p99.9 22µs 的分布像被刀切过一样干净。我们并没有依赖某一项“神器”,而是把 CPU → 内存 → PCIe → 网卡队列 → 交换机缓冲 → 时钟 这条路径逐段拉直:
- NUMA 绑核减少跨节点访存;
- SR-IOV + DPDK 把数据面从内核搬到用户态;
- 交换机与链路锁死速率与缓冲策略;
- PTP 让测量可信,让优化有方向。
清晨七点,地面风停了。我站在机房外的自助咖啡机前,看 Grafana 的延迟直方图收窄成一条细线。那一刻我知道,这台“在香港的撮合机器”,终于像我们想要的那样,不受风雨。
附:最小可复现步骤(MVP)
- 服务器 BIOS:关 C-States,开 IOMMU,确认 X710 在 NUMA0。
- CentOS 7:tuned latency-performance,按上文写入 GRUB 参数,重启。
- HugePages:1G×16;vfio 模块常驻。
- X710:开 4 个 VF,绑定 vfio-pci,直通给 KVM。
- VM:DPDK 21.11,巨页挂载,绑定 VF,testpmd 跑通多队列。
- 交换机:固定 10G、MTU 9000、PTP/T-BC、低缓冲模板。
- 上线业务:numactl 绑定 NUMA0,lcore 与队列一一对应;Grafana 盯延迟分布。
上线后的第三天,夜班同事在机柜门内侧贴了张纸:
“先看 PTP,再谈 jitter。先看 NUMA,再谈吞吐。”
每次我进那排机柜,都能看到它。生产的稳定从不是一句“低延迟”口号,而是这些小而确定的动作。你把路径拉直,它就会跑得快、跑得稳。