如何在香港服务器的 CentOS 7 上启用 DPDK 加速网卡,优化高频金融交易的低延迟通信

凌晨 2 点,我坐在香港 MEGA-i 机房的机柜前,手边是一台临时接上的串口小盒子。离 HKEX 的撮合引擎几条光纤而已,但我们的订单回执总是比隔壁柜的“老对手”慢 3~5 微秒。那一夜我决定把整条路径“从网卡到用户态”重走一遍,换掉内核网络栈,直接上 DPDK。下面就是那一夜(以及之后连续几周)我在 CentOS 7 上把 DPDK 落地、踩坑、复盘、打磨到稳态的完整过程。
场景与目标
目标:把订单链路(UDP/TCP FIX/OUCH)延迟压到亚 10 微秒级别,并显著降低抖动(P99 与 P50 的差距)。
路径:采用 DPDK 旁路内核网络栈,绑定交易用网卡,用户态轮询 + NUMA/CPU 绑核 + HugePages + BIOS/内核调优。
环境:香港机房(低延迟直连交易所),CentOS 7.9,单机双端口 10/25GbE 低延迟网卡,直连 L2/跨接交换机。
硬件与系统清单(真实可复用)
| 项目 | 选型/版本 | 备注 |
|---|---|---|
| 机型 | Dell R6525 / Supermicro 1029 系列 | 单路/双路都可,关键看内存与 PCIe 拓扑 |
| CPU | AMD EPYC 7xx2 或 Intel Xeon Gold 62xx | 频率>核心数;一致性高;开启恒定频率 |
| 内存 | 128GB DDR4 ECC | 1GiB HugePages 预留 4~8GiB |
| 网卡(延迟优先) | Intel X710/XL710(i40e),82599(ixgbe),或 Mellanox ConnectX-5/6 (mlx5) | Mellanox 往往更稳;DPDK 驱动成熟 |
| 系统盘 | NVMe SSD | 日志与 core dump 友好 |
| 系统 | CentOS 7.9(3.10 内核) | 与用户态 DPDK LTS 版本兼容 |
| DPDK | 19.11 LTS 或 21.11 LTS(建议配 devtoolset-11 构建) | 21.11 需较新编译器 |
| 外设 | 串口 console、IPMI | 做网卡绑定前务必准备,避免把 SSH 网卡绑走 |
小提示:CentOS 7 自带 GCC 4.8.5 偏老,编译 DPDK 21.11+ 往往要 devtoolset-9/11 或 Clang。我最后用了 devtoolset-11。
网络拓扑与队列规划
- 端口划分:Port 0 收市场数据,Port 1 发订单(或双活绑定按队列划分)。
- 队列策略:每个关键流量单独队列(RXQ/TXQ=1~2),一队列一核,减少 cache 抖动。
- NUMA 亲和:网卡插在与 CPU 绑定的同一 NUMA 节点,内存与线程都在该节点分配。
Step 0:基线测试(没有 DPDK 之前)
用传统内核栈(UDP)在相同链路上测一次 RTT 与抖动,后面对比用。
# 仅示例:可用自研 UDP ping、或交换机回环下做 T4T(tick-to-trade)基线
ethtool -C eth1 adaptive-rx off rx-usecs 0 # 关中断合并
sysctl -w net.core.busy_read=50
sysctl -w net.core.busy_poll=50
# ... 采集 P50 / P95 / P99
Step 1:BIOS & 内核通用调优(真正的“地基”)
BIOS:
- 关全部 C-State(或锁到 C1),开 Performance(固定频率),关超线程(HT 对极致延迟常带来干扰)。
- 关 ASPM,PCIe 固定 Gen3/4。
- NUMA 展示开启(Non-Uniform Memory Access),保证亲和可控。
- 如果有 PPS/PHC 模块,开启 PTP 支持。
GRUB 内核参数(CentOS 7):编辑 /etc/default/grub 的 GRUB_CMDLINE_LINUX,追加/确认:
intel_iommu=on iommu=pt \
isolcpus=1-3,9-11 rcu_nocbs=1-3,9-11 nohz_full=1-3,9-11 \
transparent_hugepage=never mitigations=off \
nmi_watchdog=0 audit=0
说明:
- intel_iommu=on iommu=pt 是 VFIO 的前置条件;
- isolcpus / rcu_nocbs / nohz_full 给轮询线程“独占核”;
- mitigations=off 关闭安全补丁相关的频繁栈,换稳定时延;
生产上需综合安全策略评估。
生成 GRUB:
# Legacy BIOS:
grub2-mkconfig -o /boot/grub2/grub.cfg
# UEFI:
grub2-mkconfig -o /boot/efi/EFI/centos/grub.cfg
reboot
开机后:
tuned-adm profile latency-performance
cpupower frequency-info
cpupower frequency-set -g performance
Step 2:HugePages(1GiB 优先)
# 若 CPU/内核支持 1GiB hugepage,GRUB 中追加:
# default_hugepagesz=1G hugepagesz=1G hugepages=8
# 重启后确认:
cat /proc/meminfo | egrep 'HugePages_Total|Hugepagesize'
# 如果只用 2MiB:
sysctl -w vm.nr_hugepages=4096
经验:1GiB hugepage 带来更少 TLB miss 与更稳定的 P99,但要预留足够内存,否则会 OOM。生产上我一般预留 4~8GiB。
Step 3:工具链与 DPDK 编译
3.1 安装 devtoolset(推荐)
yum install -y centos-release-scl
yum install -y devtoolset-11 devtoolset-11-gcc devtoolset-11-gcc-c++ numactl-devel \
meson ninja-build libpcap-devel
scl enable devtoolset-11 bash
gcc --version # 确认已切换
3.2 拉取并构建 DPDK(以 21.11 为例)
cd /opt
git clone https://dpdk.org/git/dpdk-stable
cd dpdk-stable
git checkout v21.11
meson build --prefix=/usr/local/dpdk -Dexamples=all
ninja -C build
ninja -C build install
echo "/usr/local/dpdk/lib" > /etc/ld.so.conf.d/dpdk.conf
ldconfig
若坚持 19.11 LTS,可用 make 体系构建;21.11 起官方推荐 Meson/Ninja。
Step 4:VFIO 绑定网卡(把“交易口”交给用户态)
加载 VFIO:
modprobe vfio
modprobe vfio-pci
找到网卡与驱动:
/usr/local/dpdk/share/dpdk/usertools/dpdk-devbind.py --status
# 记录 0000:xx:yy.z 与当前内核驱动(如 i40e / ixgbe / mlx5_core)
解绑并绑定:
# 假设交易口是 0000:5e:00.0 和 0000:5e:00.1
/usr/local/dpdk/share/dpdk/usertools/dpdk-devbind.py -u 0000:5e:00.0 0000:5e:00.1
/usr/local/dpdk/share/dpdk/usertools/dpdk-devbind.py -b vfio-pci 0000:5e:00.0 0000:5e:00.1
确认:
/usr/local/dpdk/share/dpdk/usertools/dpdk-devbind.py --status
生存法则:不要把 SSH 所在的网口绑定走!做之前请先接 IPMI/串口。我第一晚就把远程口绑走过一次,只能跑柜门口救火。
Step 5:TestPMD 快速自检(跑通 DPDK 的最小闭环)
testpmd -l 1-3 -n 4 --proc-type=auto --log-level=pmd,info \
--file-prefix=dpdk0 -- \
--total-num-mbufs=8192 --nb-cores=2 --rxq=1 --txq=1 \
--rxd=1024 --txd=1024 --forward-mode=io --auto-start \
--disable-link-check --rxq-affinity=0:2
- -l 1-3:把轮询线程绑到隔离的 CPU 1~3;
- --rxq-affinity=0:2:RX 队列绑到核心 2;
- --forward-mode=io:零改动直进直出,验证收发稳定。
在交换机侧做一个端口回环/对打,观察 Throughput 与 Rx/Tx Errors。TestPMD 稳了再上业务。
Step 6:业务最小可用(MVP)代码(C,接收+时间戳+回发)
下面用一个极简 DPDK 程序说明 队列初始化、轮询、时间戳与回发。
仅作骨架示例,生产请加:丢包保护、批量收发、校验/重传逻辑、风控钩子、日志打点等。
// file: hft_dpdk_mvp.c
#include <rte_eal.h>
#include <rte_ethdev.h>
#include <rte_cycles.h>
#include <rte_mbuf.h>
#define RX_RING_SIZE 1024
#define TX_RING_SIZE 1024
#define NUM_MBUFS (8192-1)
#define MBUF_CACHE_SIZE 250
#define BURST_SIZE 32
static const uint16_t PORT_ID = 0;
static int port_init(uint16_t port, struct rte_mempool *mbuf_pool) {
struct rte_eth_conf port_conf = {0};
port_conf.rxmode.mq_mode = ETH_MQ_RX_NONE;
port_conf.rxmode.offloads = 0;
port_conf.txmode.mq_mode = ETH_MQ_TX_NONE;
if (!rte_eth_dev_is_valid_port(port)) return -1;
int ret = rte_eth_dev_configure(port, 1, 1, &port_conf);
if (ret < 0) return ret;
ret = rte_eth_rx_queue_setup(port, 0, RX_RING_SIZE,
rte_eth_dev_socket_id(port), NULL, mbuf_pool);
if (ret < 0) return ret;
ret = rte_eth_tx_queue_setup(port, 0, TX_RING_SIZE,
rte_eth_dev_socket_id(port), NULL);
if (ret < 0) return ret;
ret = rte_eth_dev_start(port);
if (ret < 0) return ret;
rte_eth_promiscuous_enable(port);
return 0;
}
int main(int argc, char **argv) {
int ret = rte_eal_init(argc, argv);
if (ret < 0) rte_exit(EXIT_FAILURE, "EAL init failed\n");
unsigned lcore_id = rte_lcore_id();
printf("running on lcore %u\n", lcore_id);
struct rte_mempool *mbuf_pool = rte_pktmbuf_pool_create("MBUF_POOL",
NUM_MBUFS, MBUF_CACHE_SIZE, 0, RTE_MBUF_DEFAULT_BUF_SIZE,
rte_socket_id());
if (mbuf_pool == NULL) rte_exit(EXIT_FAILURE, "mbuf pool create failed\n");
if (port_init(PORT_ID, mbuf_pool) != 0)
rte_exit(EXIT_FAILURE, "port init failed\n");
const uint64_t tsc_hz = rte_get_tsc_hz();
struct rte_mbuf *pkts[BURST_SIZE];
while (1) {
const uint16_t nb_rx = rte_eth_rx_burst(PORT_ID, 0, pkts, BURST_SIZE);
if (nb_rx == 0) continue;
const uint64_t t_rx = rte_rdtsc_precise();
for (uint16_t i=0; i<nb_rx; i++) {
// 简单回发:把收到的包原样发回(或构造订单包)
pkts[i]->ol_flags = 0;
}
uint16_t nb_tx = rte_eth_tx_burst(PORT_ID, 0, pkts, nb_rx);
// 统计与时间戳(示例:把 TSC 转 ns)
const uint64_t t_tx = rte_rdtsc_precise();
const double rx_to_tx_ns = (double)(t_tx - t_rx) * 1e9 / (double)tsc_hz;
// 简陋打印(生产请换 lock-free 计数)
// printf("rx=%u, tx=%u, rx->tx=%.2f ns\n", nb_rx, nb_tx, rx_to_tx_ns);
// 释放未发出的 mbuf
for (uint16_t i = nb_tx; i < nb_rx; i++) {
rte_pktmbuf_free(pkts[i]);
}
}
return 0;
}
构建与运行:
export PKG_CONFIG_PATH=/usr/local/dpdk/lib64/pkgconfig:$PKG_CONFIG_PATH
gcc hft_dpdk_mvp.c -o hft_dpdk_mvp $(pkg-config --cflags --libs libdpdk)
# 绑核运行(用隔离的核心,比如 2 号)
taskset -c 2 ./hft_dpdk_mvp -l 2 -n 4 --file-prefix=dpdk1 --proc-type=auto
生产建议:
- 单核专用:一条 RX 队列 + 一条 TX 队列 + 专用 LCore;
- 无锁路径:把内存池、环、统计分片到该核,避免共享;
- 把 FIX/OUCH 编解码放在同核,减少跨核同步。
Step 7:PTP 与时间同步(撮合时间线对齐)
交易撮合常要求 硬件时间戳 与 PTP 同步。
网卡(Intel/Mellanox)一般有 PHC(PTP Hardware Clock)。
CentOS 7 开 ptp4l / phc2sys:
yum install -y linuxptp
# 假设 PHC 在 /dev/ptp0、系统时钟同步 PHC
ptp4l -i enp94s0f0 -m -s -2 &
phc2sys -s /dev/ptp0 -c CLOCK_REALTIME -O 0 -m &
DPDK 侧:在 PMD 支持下可用 rte_eth_timesync_* API 拿硬件时间戳,做端到端测量。
Step 8:系统化上线(Systemd + 开机自启动)
绑定脚本 /usr/local/bin/dpdk_bind.sh:
#!/bin/bash
modprobe vfio
modprobe vfio-pci
/usr/local/dpdk/share/dpdk/usertools/dpdk-devbind.py -b vfio-pci 0000:5e:00.0 0000:5e:00.1
Systemd 服务:
# /etc/systemd/system/hft-dpdk.service
[Unit]
Description=HFT DPDK Engine
After=network.target
[Service]
Environment="LD_LIBRARY_PATH=/usr/local/dpdk/lib:/usr/local/dpdk/lib64"
ExecStartPre=/usr/local/bin/dpdk_bind.sh
ExecStart=/usr/local/bin/hft_dpdk_mvp -l 2 -n 4 --file-prefix=dpdk1 --proc-type=auto
Restart=on-failure
CPUAffinity=2
NoNewPrivileges=true
LimitMEMLOCK=infinity
[Install]
WantedBy=multi-user.target
systemctl daemon-reload
systemctl enable --now hft-dpdk
Step 9:监控与可观测性
- dpdk-proc-info:查看端口、队列、mempool 占用。
- 自研指标:qps、丢包率、环水位、P50/P99/P999 延迟(环内 TSC 打点)。
- 外部:Node Exporter + Textfile Exporter 汇报到 Prometheus,Grafana 看板细化到 每核。
实测数据(一次典型迭代)
交换机侧同端口对打,线缆 3m DAC,PPS 以 64B 包计(含以太头,不含 IFG)。
| 方案 | P50 (µs) | P99 (µs) | 抖动(P99-P50) | 丢包@5Mpps |
|---|---|---|---|---|
| 内核 UDP(busy_poll=50) | 13.2 | 22.8 | 9.6 | 偶发(<0.001%) |
| DPDK TestPMD(io) | 4.6 | 7.3 | 2.7 | 0 |
| DPDK 业务 MVP | 3.9 | 6.1 | 2.2 | 0 |
结论:DPDK 把“抖动”显著收紧;一核一队列的亲和布局基本锁死了延迟尾部。
常见坑位与现场解法
VFIO 绑定报 Operation not permitted
- 忘了 intel_iommu=on iommu=pt 或 BIOS IOMMU 关着。改 GRUB/BIOS,重启。
- 某些内核需 modprobe vfio-pci 先于 dpdk-devbind.py。
绑定后链路灯灭 / 无法收包
- 交换机侧端口配置不匹配(速度/自动协商/流控)。
- DPDK 默认不上 VLAN/LLDP,需要在 PMD 或业务代码里显式处理。
- Mellanox 要装 OFED/固件较新;Intel 82599 对直连 DAC 线更挑。
HugePages 无法分配 1GiB
- 需要在 GRUB 用 default_hugepagesz=1G hugepagesz=1G hugepages=N 预留,热分配通常失败。
- 退而求其次用 2MiB,但尽量多给,避免 mempool 不足。
队列跨 NUMA,P99 抖动大
- lspci -vv + numactl -H 找到网卡与 CPU 的 NUMA 亲和;
- 业务线程、mempool 都在同节点。
nohz_full/isolcpus 后系统“卡顿”
- 不要把所有核都隔离;留足管理核给内核/中断/系统服务。
- irqbalance 可关闭,但确保管理口的 IRQ 有核可用。
编译不过 / 链接错
- CentOS 7 的 GCC 太老;上 devtoolset-11 或 Clang。
- PKG_CONFIG_PATH 与 LD_LIBRARY_PATH 别忘了。
时间不准,撮合时间线错位
- 开 ptp4l + phc2sys,尽量硬件时间戳;
- 宿主与 NIC PHC 偏差定时校正,重启后确认存活。
性能进一步打磨清单(把每 0.1µs 都抠出来)
- 包路径裁剪:禁止一切不必要的 offload(除非确实有收益),代码路径内存对齐,避免分支。
- 批量收发:按场景权衡 BURST_SIZE(小批次更低延迟,大批次更稳吞吐)。
- Cache 布局:per-core ring/mempool,false sharing 零容忍。
- PMD 参数:调 Rx/Tx desc(512/1024),关中断、纯轮询;
- 锁死频率:BIOS + cpupower 保证恒定频率与温度边界。
- 内核最小化:裁掉多余服务(irqbalance、auditd…),NUMA 屏蔽跨节点内存分配。
- 流水线:解码、风控、打包下单尽量同核同缓存;跨核用 SPSC lock-free 队列。
回归与对比(业务日内)
将 DPDK 版本切到 隔离交易口(市场数据与下单分开),保留一条“逃生”管理口走内核栈。
稳定运行一周后对比 成交回执落地时间 与 撤单响应时间 的滑窗统计,发现:
- 成交回执 P99 从 ~21µs 降到 ~8µs;
- 撤单响应 P99 从 ~18µs 降到 ~7µs;
- 日内尖峰(开盘、收盘)尾部抖动不再“炸”。
清单式复盘(可当 Checklist)
- BIOS:C-State=off、HT=off、Perf=on、ASPM=off、PTP=on
- GRUB:intel_iommu=on iommu=pt isolcpus nohz_full rcu_nocbs mitigations=off
- HugePages:1GiB 优先(4~8GiB)
- 工具链:devtoolset-11 + DPDK 21.11 LTS
- VFIO:只绑交易口,预备 OOB
- NUMA:网卡/线程/内存同节点,一队列一核
- 观测:P50/P99/P999、mempool、drop、环水位
- PTP:ptp4l + phc2sys,硬件时间戳
- 上线:systemd、自恢复、限权、memlock=inf
- 回归:基线对照 + 峰值时段专项验证
尾声:柜门前的 3 微秒
第二周周五的收盘竞价,我还守在柜门口,盯着 Grafana 的延迟面板。P99 一直压在 6 微秒上下,波动像尺子量过。隔壁柜的同事路过笑着说:“你们这回是真快。”我把串口小盒子收进包里,给团队发了一句“收工,周末吃火锅”。
这些微秒,来自一堆“无聊”的小事:BIOS 的一个开关、HugePages 的一页、队列对齐的一次缓存命中、以及在凌晨两点不把管理口绑走的克制。DPDK 不是银弹,但在香港这种离撮合一墙之隔的地方,它就是让你比别人早一个 CPU 周期看到未来的工具。
附:一键回滚(出问题时的“安全带”)
# 解绑,恢复到内核驱动(以 i40e 为例)
/usr/local/dpdk/share/dpdk/usertools/dpdk-devbind.py -u 0000:5e:00.0 0000:5e:00.1
/usr/local/dpdk/share/dpdk/usertools/dpdk-devbind.py -b i40e 0000:5e:00.0 0000:5e:00.1
systemctl stop hft-dpdk
# 恢复 irqbalance / 调优参数
systemctl start irqbalance
如果你也在香港的机房里和微秒过招,希望这份手记让你少走几步夜路。