第八章:延迟优化技术

做市商系统里,延迟就是金钱。这句话我反复跟团队讲过。你比别人慢一微秒,可能就抢不到单子。今天聊聊我这些年用过的延迟优化手段——内核旁路、低延迟网络协议、还有CPU亲和性那些事。

8.1 内核旁路:绕过操作系统

传统网络通信,数据要经过内核协议栈。内核要处理中断、拷贝数据、上下文切换……这一套下来,几十微秒就没了。做市商系统里,这简直是灾难。

内核旁路的思路很简单:让应用程序直接跟网卡打交道,绕过内核。我常用的方案有两个:DPDK和RDMA。

8.1.1 DPDK:用户态网络栈

DPDK的全称是Data Plane Development Kit。它把网卡驱动搬到用户态,应用程序轮询收包,省掉中断开销。

我在项目中遇到过一个问题:用DPDK收行情数据,刚开始延迟降到了5微秒以内。但跑了一段时间,CPU占用率飙到100%。后来发现是轮询频率太高,没有做自适应退避。嗯,这里要注意——轮询不是越快越好,要根据流量动态调整。

DPDK核心优化点:

  • 用户态驱动:绕过内核协议栈
  • 大页内存:减少TLB miss
  • CPU亲和性:绑定核,避免上下文切换
  • 无锁队列:减少锁竞争

下面是一个简单的DPDK收包示例:

// DPDK收包核心代码
static void lcore_recv_packets(void) {
    struct rte_mbuf *bufs[BURST_SIZE];
    while (1) {
        // 从网卡批量收包
        uint16_t nb_rx = rte_eth_rx_burst(port, queue_id,
                                          bufs, BURST_SIZE);
        if (nb_rx == 0) {
            // 没有数据时,做点别的事
            // 不要空转,可以处理其他任务
            continue;
        }
        // 处理收到的包
        for (int i = 0; i < nb_rx; i++) {
            process_packet(bufs[i]);
            rte_pktmbuf_free(bufs[i]);
        }
    }
}

我的经验:DPDK初始化时,记得用rte_eal_init()设置大页内存。我一般用2MB的大页,TLB miss能减少80%以上。

8.1.2 RDMA:远程直接内存访问

RDMA允许一台机器直接读写另一台机器的内存,不需要CPU参与。延迟可以降到1微秒以内。

我曾经在撮合引擎里用RDMA做订单传输。效果确实好,但调试起来很痛苦。RDMA的API比较底层,出错时日志也不友好。我的建议是:先用DPDK跑通业务逻辑,再考虑RDMA优化。

技术 延迟 吞吐量 开发难度
传统TCP 50-100μs 中等
DPDK 5-10μs
RDMA 1-3μs 极高

8.2 低延迟网络协议:UDP Multicast

做市商系统里,行情数据通常用UDP Multicast广播。为什么不用TCP?因为TCP有重传机制,延迟不稳定。你想想看,行情数据丢了就丢了,下一笔马上就来。重传反而会打乱节奏。

UDP Multicast的好处:

  • 一对多传输:一个行情源,多个订阅者
  • 无连接开销:不需要三次握手
  • 延迟低:没有拥塞控制

但UDP也有坑。我曾经在生产环境遇到一个问题:行情数据偶尔出现乱序。排查后发现是网卡多队列导致的。同一个流的数据被分发到不同CPU核处理,顺序就乱了。

避坑指南:使用UDP Multicast时,一定要开启RSS(Receive Side Scaling)的哈希功能。确保同一个流的包始终落在同一个核上。否则你会被乱序问题折磨到崩溃。

下面是一个UDP Multicast接收端的配置示例:

// UDP Multicast接收端配置
int sock = socket(AF_INET, SOCK_DGRAM, 0);
struct ip_mreq mreq;
mreq.imr_multiaddr.s_addr = inet_addr("239.0.0.1");
mreq.imr_interface.s_addr = htonl(INADDR_ANY);
setsockopt(sock, IPPROTO_IP, IP_ADD_MEMBERSHIP,
           &mreq, sizeof(mreq));

// 设置接收缓冲区大小
int rcvbuf = 4 * 1024 * 1024; // 4MB
setsockopt(sock, SOL_SOCKET, SO_RCVBUF,
           &rcvbuf, sizeof(rcvbuf));

8.3 CPU亲和性与NUMA优化

CPU亲和性,说白了就是把线程绑定到特定CPU核上。为什么要这么做?因为上下文切换很贵。一个线程在不同核上跳来跳去,缓存全废了。

NUMA(Non-Uniform Memory Access)更复杂一点。现代服务器有多个CPU,每个CPU有自己的内存。访问本地内存快,访问远程内存慢。差距可能有2-3倍。

我个人的习惯是:

  • 把网络收发包线程绑在同一个NUMA节点上
  • 把行情处理线程绑在相邻的核上
  • 内存分配时指定NUMA节点

下面是一个NUMA感知的内存分配示例:

// NUMA感知的内存分配
#include <numa.h>

void* alloc_numa_memory(size_t size, int node) {
    void *ptr = numa_alloc_onnode(size, node);
    if (ptr == NULL) {
        // 回退到普通分配
        ptr = malloc(size);
    }
    return ptr;
}

// 使用示例
int node = 0; // 绑定到NUMA节点0
struct order_book *book = alloc_numa_memory(
    sizeof(struct order_book), node);

我的经验:用numactl命令可以查看NUMA拓扑。我一般先跑一遍numactl --hardware,看看内存和CPU的对应关系。然后根据业务线程的分布,手动绑定内存节点。

8.4 知识体系总览

下面这张图是我整理的延迟优化技术体系。你可以看到,从硬件到软件,每个层面都有优化空间。

延迟优化技术体系 硬件层 网卡(支持DPDK/RDMA) | CPU(多核、NUMA架构) | 内存(大页、本地访问) 内核旁路层 DPDK(用户态驱动、轮询模式) | RDMA(远程直接内存访问) 网络协议层 UDP Multicast(低延迟广播) | 自定义协议(减少头部开销) CPU优化层 CPU亲和性(绑定核) | NUMA感知(本地内存访问) | 无锁数据结构

这张图展示了延迟优化的四个层次。从硬件选型开始,到内核旁路、网络协议、最后到CPU优化。每一层都能压榨出几微秒的延迟。做市商系统里,这几微秒可能就是盈利和亏损的分界线。

核心要点:

  • DPDK适合行情接收和订单发送,延迟可降到5μs以下
  • RDMA适合撮合引擎内部通信,延迟可降到1μs
  • UDP Multicast是行情广播的标准方案,注意乱序问题
  • CPU亲和性和NUMA优化是基础,不花钱就能提升性能

好了,这一章就到这里。延迟优化是个持续的过程,没有银弹。我的建议是:先用工具测量延迟分布,找到瓶颈再针对性优化。别一上来就搞DPDK,可能你的瓶颈在别的地方。

交易系统化学习资料 微信Strategy888888