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

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

发布人:Minchunlin 发布时间:2025-09-05 09:53 阅读量:791


凌晨 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

如果你也在香港的机房里和微秒过招,希望这份手记让你少走几步夜路。

目录结构
全文