第八章:延迟优化技术
做市商系统里,延迟就是金钱。这句话我反复跟团队讲过。你比别人慢一微秒,可能就抢不到单子。今天聊聊我这些年用过的延迟优化手段——内核旁路、低延迟网络协议、还有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 知识体系总览
下面这张图是我整理的延迟优化技术体系。你可以看到,从硬件到软件,每个层面都有优化空间。
这张图展示了延迟优化的四个层次。从硬件选型开始,到内核旁路、网络协议、最后到CPU优化。每一层都能压榨出几微秒的延迟。做市商系统里,这几微秒可能就是盈利和亏损的分界线。
核心要点:
- DPDK适合行情接收和订单发送,延迟可降到5μs以下
- RDMA适合撮合引擎内部通信,延迟可降到1μs
- UDP Multicast是行情广播的标准方案,注意乱序问题
- CPU亲和性和NUMA优化是基础,不花钱就能提升性能
好了,这一章就到这里。延迟优化是个持续的过程,没有银弹。我的建议是:先用工具测量延迟分布,找到瓶颈再针对性优化。别一上来就搞DPDK,可能你的瓶颈在别的地方。