4、网络协议栈优化:内核旁路技术

网络延迟,是高频交易系统的命门。

我见过太多团队,策略模型跑得飞起,结果一到实盘就被网络拖垮。说白了,传统Linux网络协议栈是为通用场景设计的,不是为微秒级交易准备的。今天我们就来聊聊,怎么把网络这块「短板」补上。

4.1 内核旁路技术:DPDK 与 RDMA

先问一个问题:数据从网卡到应用程序,中间经历了什么?

传统路径是:网卡 → 内核协议栈 → 系统调用 → 用户态应用。每一步都有开销——中断处理、内存拷贝、上下文切换。我曾在项目中实测过,光是内核态到用户态的切换,一次就要消耗 1-3 微秒。高频交易里,这简直是天文数字。

内核旁路的思路很简单:绕过内核,让应用直接操作网卡。

DPDK(数据平面开发套件)

DPDK 的核心是「轮询模式」。传统网卡收包靠中断,DPDK 改用应用程序主动轮询。听起来粗暴,但效果惊人。

我个人习惯在 DPDK 中这样配置收包环境:

/* DPDK 初始化示例 */
int rte_eal_init(argc, argv);  // 初始化 EAL
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());

// 配置网卡
struct rte_eth_conf port_conf = {
    .rxmode = { .max_rx_pkt_len = ETHER_MAX_LEN }
};
rte_eth_dev_configure(port_id, 1, 1, &port_conf);

嗯,这里要注意:DPDK 需要独占网卡。你没法让操作系统和 DPDK 共用同一个物理端口。我在项目中吃过这个亏——测试环境配了两块网卡,一块给系统,一块给 DPDK,才搞定。

RDMA(远程直接内存访问)

RDMA 更狠。它允许一台机器的应用直接读写另一台机器的内存,完全绕过 CPU 和操作系统。

为什么适合高频交易?因为行情数据从交易所到你的策略进程,延迟可以降到 1 微秒以内。我曾经在机房实测过,用 InfiniBand 做 RDMA,两端延迟稳定在 0.8 微秒左右。

核心对比:DPDK vs RDMA

特性DPDKRDMA
延迟1-3 微秒0.5-1 微秒
CPU 占用较高(轮询)极低(硬件卸载)
适用场景高频行情接收跨机房数据传输
硬件要求普通网卡 + DPDK 驱动专用网卡(Mellanox/Chelsio)

4.2 TCP/IP vs UDP:高频场景下的生死抉择

这个问题我几乎每次培训都会被问到。我的回答很直接:高频交易里,UDP 是默认选项,TCP 是不得已的选择

为什么会这样?

TCP 有三大「原罪」:

  • 拥塞控制:网络一抖,TCP 自动降速。你想想看,行情爆发时大家都在抢,TCP 却在那「礼貌退让」——这能忍?
  • 重传机制:丢一个包,TCP 要等超时重传。高频场景下,等 200 毫秒重传?黄花菜都凉了。
  • 头部开销:TCP 头部 20 字节,加上 IP 头部 20 字节,总共 40 字节。UDP 只有 8 字节头部。

UDP 就简单多了——发出去就不管了。丢包?那是应用层的事。

我在项目中遇到过这样的情况:某家交易所同时提供 TCP 和 UDP 行情源。我们实测下来,UDP 路径的延迟中位数比 TCP 低 40%。但代价是什么?UDP 会丢包,尤其是行情爆发时,丢包率可能到 0.1%。

我的建议:行情接收用 UDP + 应用层纠错(比如前向纠错 FEC)。订单发送用 TCP,因为订单不能丢。说白了,读数据可以丢,写数据不能丢。

4.3 零拷贝技术:少搬一次数据,快一微秒

传统数据路径:网卡 → 内核缓冲区 → 用户态缓冲区。数据被拷贝了两次。

零拷贝的目标:数据从网卡直接到应用内存,一次拷贝都不做

实现方式主要有两种:

  • mmap:把内核缓冲区映射到用户空间。应用直接读写映射后的内存,省去一次拷贝。
  • sendfile:用于文件传输场景,数据从磁盘到网卡,不经过用户态。

我曾在项目中用 mmap 优化行情数据读取:

// 使用 mmap 实现零拷贝读取
int fd = open("market_data.dat", O_RDONLY);
struct stat sb;
fstat(fd, &sb);

char *data = mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
// 现在 data 指针直接指向内核缓冲区
// 读取行情数据时,不需要 read() 系统调用
process_market_data(data, sb.st_size);
munmap(data, sb.st_size);

嗯,这里有个坑:mmap 后的内存是共享的,如果你修改了数据,内核也会看到。所以高频交易里一般用 MAP_PRIVATE,写时复制,避免污染原始数据。

曾经踩过的坑:零拷贝不是万能的。如果数据需要频繁修改(比如订单簿的增删改),mmap 反而会因为缺页中断增加延迟。我建议只对「只读」的行情数据用零拷贝,订单处理还是走传统路径更稳妥。

4.4 套接字优化:把 TCP 调到极致

如果你实在要用 TCP(比如连接交易所的订单网关),那至少把它优化到极致。

我个人习惯的套接字优化清单:

  1. 禁用 Nagle 算法TCP_NODELAY。Nagle 会把小包攒成大包再发,延迟受不了。
  2. 调整缓冲区大小SO_SNDBUFSO_RCVBUF。高频交易里,缓冲区太大反而增加延迟,我一般设到 64KB 左右。
  3. 使用非阻塞 I/O:配合 epoll 或 io_uring。阻塞 I/O 在高频场景下就是灾难。
  4. 禁用 TCP 延迟确认TCP_QUICKACK。让 ACK 立即发送,不等待。

代码示例:

int sock = socket(AF_INET, SOCK_STREAM, 0);
int flag = 1;
setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, &flag, sizeof(flag));
setsockopt(sock, IPPROTO_TCP, TCP_QUICKACK, &flag, sizeof(flag));

int rcvbuf = 65536;
setsockopt(sock, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof(rcvbuf));

// 设置为非阻塞
fcntl(sock, F_SETFL, O_NONBLOCK);

你想想看,这些优化加起来,每个包能省下 5-10 微秒。在高频交易里,这就是一个价位的差距。

知识体系总览

下面这张图,是我对本章核心逻辑的总结:

网络协议栈优化知识体系 网络协议栈优化 内核旁路技术 DPDK RDMA TCP/IP vs UDP TCP(订单) UDP(行情) 零拷贝技术 mmap sendfile 套接字优化 TCP_NODELAY 非阻塞I/O TCP_QUICKACK

这张图把四个优化方向串起来了。内核旁路解决「谁处理数据」的问题,TCP/UDP 解决「用什么协议」的问题,零拷贝解决「数据怎么搬」的问题,套接字优化解决「怎么用好 TCP」的问题。四者缺一不可。

最后说一句:别想着一步到位。我建议先从套接字优化入手,成本最低,效果立竿见影。等延迟瓶颈到了微秒级,再考虑 DPDK 或 RDMA。一口吃不成胖子,优化也是。


公众号:蓝海资料掘金营,微信deep3321