网络协议栈优化:TCP/IP的痛点、RDMA/RoCEv2原理、用户态协议栈

做市商系统里,网络延迟就是真金白银。我见过太多团队,算法写得再漂亮,结果被网络协议栈拖了后腿。今天咱们就聊聊这块硬骨头怎么啃。

一、TCP/IP的痛点:为什么它不适合做市商?

TCP/IP设计之初,考虑的是可靠性和公平性,不是低延迟。说白了,它压根没为高频交易优化过。

核心痛点清单:

  • 内核态开销:每次收发数据都要系统调用,上下文切换成本极高。我测过,一次syscall大概要几百纳秒,积少成多就吓人了。
  • 数据拷贝:从网卡到内核缓冲区,再到用户态缓冲区,至少两次拷贝。你想想看,行情数据每秒几十万笔,光拷贝就吃掉大量CPU。
  • 中断风暴:传统网卡每来一个包就触发一次中断。高吞吐下,CPU光处理中断就忙不过来了。
  • 拥塞控制:TCP的慢启动、重传机制,在低延迟场景下简直是灾难。行情丢了就丢了,谁等你重传?

我在一个项目里遇到过,同样的硬件,从TCP换成用户态协议栈,延迟直接降了一个数量级。嗯,这就是差距。

二、RDMA/RoCEv2原理:绕过内核的捷径

RDMA(远程直接内存访问)的思路很暴力——让网卡直接读写远端内存,CPU全程不参与。RoCEv2则是把RDMA跑在以太网上,成本比InfiniBand低得多。

它的工作流程是这样的:

  1. 应用层注册一块内存区域,告诉网卡“这是我的地盘”
  2. 发送数据时,网卡直接从用户态内存取数据,打包成网络包发出去
  3. 接收端网卡直接把数据写入用户态内存,零拷贝

个人经验:RoCEv2对网络质量要求极高。我曾经在交换机上没开PFC(优先级流控),结果丢包率飙升,RDMA性能还不如TCP。记住,RoCEv2需要无损网络环境。

下面这张图展示了传统TCP/IP和RDMA的路径对比:

传统TCP/IP vs RDMA 数据路径对比 传统TCP/IP路径 应用层(用户态) 系统调用(上下文切换) 内核协议栈(TCP/IP) 数据拷贝(内核→用户) 网卡(NIC) RDMA路径 应用层(用户态) 零拷贝 内存注册(MR) 无系统调用 RDMA引擎(硬件) 硬件处理 智能网卡(RNIC) 延迟降低:60%~80% CPU占用:降低90%+

三、用户态协议栈:OpenOnload与DPDK

如果不想换网卡,用户态协议栈是性价比最高的方案。核心思路就一句话:把协议栈搬到用户态,绕过内核

3.1 OpenOnload

Solarflare家的方案,配合他们的网卡用。我最早接触是在2016年,当时帮一家期货公司做优化。装上Onload后,延迟从50微秒直接降到5微秒。

工作原理:

  • 通过LD_PRELOAD劫持socket API调用
  • 网卡直接把数据推送到用户态环形缓冲区
  • 应用层轮询读取,零中断
# 使用OpenOnload运行你的程序
export ONLOAD_ACCEPT=1
export ONLOAD_POLL=1
onload ./your_trading_app

避坑指南:我曾经遇到一个坑——Onload和某些加密库不兼容。如果你的程序用了OpenSSL,记得测试一下。另外,Onload对多线程支持一般,建议每个线程绑定独立网卡队列。

3.2 DPDK(数据平面开发套件)

DPDK更底层,也更灵活。它直接接管网卡,把收包、发包全部交给用户态控制。

核心组件:

  • UIO/VFIO:把网卡映射到用户态
  • 大页内存:减少TLB miss
  • 轮询模式驱动:告别中断,CPU死循环检查新包
  • 无锁环形队列:核间通信零等待
// DPDK初始化示例(简化版)
int main(int argc, char *argv[]) {
    // 1. 初始化EAL
    int ret = rte_eal_init(argc, argv);
    
    // 2. 分配内存池
    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());
    
    // 3. 初始化网卡
    struct rte_eth_conf port_conf = {0};
    rte_eth_dev_configure(0, 1, 1, &port_conf);
    
    // 4. 启动收包循环
    while (1) {
        struct rte_mbuf *bufs[BURST_SIZE];
        uint16_t nb_rx = rte_eth_rx_burst(0, 0, bufs, BURST_SIZE);
        // 处理收到的包...
    }
}

我的建议:DPDK学习曲线陡,但值得投入。如果你团队有C++高手,直接上DPDK。如果求稳,先试试Onload。我个人的经验是,DPDK配合CPU隔离(isolcpus)和绑核,延迟能做到1微秒以内。

四、多播与行情分发

做市商系统里,行情数据基本都是多播。为什么?因为交易所一发就是几百个订阅者,用单播的话,交换机CPU直接炸了。

多播的核心优势:

  • 带宽效率:一份数据,所有订阅者共享
  • 低延迟:交换机硬件复制,比软件转发快得多
  • 扩展性:加订阅者不影响发送端

但多播也有坑:

我曾经踩过的坑:

  • IGMP snooping没开:交换机把多播当广播发,全网卡顿。那次事故导致整个交易大厅延迟飙升,被老板骂惨了。
  • 丢包无重传:UDP多播丢了就丢了。解决方案是用应用层FEC(前向纠错)或者双通道冗余。
  • 组播地址冲突:不同行情源用了相同的组播IP,数据混在一起。后来我强制要求每个行情源独立组播组+端口。

行情分发架构,我推荐这种分层设计:

交易所行情源 多播流 行情网关(接收+缓存+转发) 内部多播/共享内存 做市策略A 做市策略B 做市策略C 风控/监控节点

这套架构的好处是:行情网关做一次接收和缓存,内部用共享内存或DPDK多播分发给各个策略进程。每个策略独立运行,互不干扰。

总结一下网络优化的优先级:

  1. 先上用户态协议栈(Onload/DPDK),成本最低,效果立竿见影
  2. 如果还不够,上RDMA/RoCEv2,但要做好网络调优
  3. 多播是必须的,但一定要配好IGMP和组播管理
  4. 最后,别忘了CPU隔离和绑核,否则协议栈优化效果打折扣

嗯,网络这块就聊到这儿。记住,没有银弹,每个方案都有适用场景。关键是要理解原理,然后根据你的业务特点做取舍。