硬件选型与操作系统调优:CPU绑核与隔离、NUMA架构优化、网卡与DPDK、内核旁路技术

做市商系统,说白了就是跟时间赛跑。你想想看,别人下单比你快1微秒,你的单子就排在他后面,成交概率直线下降。所以硬件选型和操作系统调优,是整个低延迟架构的基石。这块要是没打好,后面写再多代码也白搭。

我个人习惯,先搞定CPU和内存的布局,再考虑网卡怎么把数据最快地送进来。咱们一个一个说。

CPU绑核与隔离:别让操作系统瞎调度

默认情况下,操作系统会把你的进程在各个CPU核心之间来回切换。每次切换,缓存就废了,TLB也废了。对于做市商这种微秒级敏感的系统,这简直是灾难。

绑核(CPU Pinning),就是把你的关键线程钉死在某个或某几个核心上。我建议用taskset或者sched_setaffinity系统调用来做。比如:

// 将当前线程绑定到CPU 2和3
cpu_set_t cpuset;
CPU_ZERO(&cpuset);
CPU_SET(2, &cpuset);
CPU_SET(3, &cpuset);
pthread_setaffinity_np(pthread_self(), sizeof(cpu_set_t), &cpuset);

光绑核还不够。你想想看,如果系统里其他进程或者中断处理程序也跑到你绑定的核心上,那还是会被打断。所以需要CPU隔离

在Linux内核启动参数里加上isolcpus=2,3,这样除了你指定的进程,其他用户态进程就不会跑到这两个核心上。但注意,内核线程和中断还是可能进来。所以还要配合irqbalance的配置,把网卡中断也挪到别的核心去。

避坑指南:我曾经在一个项目里只做了绑核,没做隔离。结果发现延迟抖动特别大,查了半天,原来是系统自带的监控进程每隔几秒就跳过来采样一次。从那以后,我都是绑核+隔离一起上。

NUMA架构优化:别让你的数据跨节点访问

现在的服务器都是NUMA架构。说白了,就是CPU和内存被分成了多个“节点”。每个CPU访问自己节点上的内存最快,访问别的节点就慢。这个延迟差异,大概在几十纳秒到一百多纳秒之间。对于做市商来说,这可不是小数目。

怎么优化?核心原则就一条:让线程和它使用的内存在同一个NUMA节点上

具体做法:

  • numactl命令启动你的进程,指定--cpunodebind=0 --membind=0,强制进程在节点0上运行,内存也分配在节点0上。
  • 在代码里,用mbind或者libnuma库来精细控制内存分配策略。
  • 分配大块内存时,用mmap配合MAP_HUGETLB,用大页减少TLB miss。
我的经验:我习惯在系统启动时就把NUMA拓扑打印出来,用numactl --hardware。然后根据拓扑,把交易引擎的核心线程和它的工作内存放在同一个节点上。这样能稳定省下几十纳秒。

下面这张图,展示了NUMA架构下,数据访问路径的差异:

NUMA Node 0 CPU 0 Memory 0 本地访问(快) NUMA Node 1 CPU 1 Memory 1 跨节点访问(慢) CPU 0 访问 Memory 0:本地,延迟低 CPU 0 访问 Memory 1:跨节点,延迟高 优化原则:线程和内存绑定在同一个NUMA节点

网卡选型:Solarflare vs Mellanox

做市商系统里,网卡是数据进出的第一道关口。普通网卡延迟在几十微秒级别,而专用网卡可以做到1-2微秒甚至更低。我主要用过两家:Solarflare和Mellanox。

特性 Solarflare Mellanox
延迟(典型值) 1-2微秒 1-3微秒
内核旁路技术 OpenOnload(用户态协议栈) RDMA / VMA(用户态协议栈)
DPDK支持 原生支持 原生支持
硬件时间戳 支持,精度高 支持
我的推荐场景 纯低延迟交易,追求极致 需要高吞吐+低延迟的混合场景

我个人习惯,如果做市商策略对延迟极度敏感,比如做高频做市,我会选Solarflare配合OpenOnload。如果既要低延迟又要处理大量数据,比如同时做行情和交易,Mellanox的RDMA方案更均衡。

DPDK与内核旁路技术

传统网络收发包,数据要经过内核协议栈。内核协议栈里有什么?中断处理、内存拷贝、上下文切换、锁竞争……每一步都是延迟杀手。内核旁路,说白了就是绕过内核,让用户态程序直接跟网卡硬件打交道。

DPDK(Data Plane Development Kit) 是目前最主流的内核旁路方案。它提供了一套用户态的库和驱动,让你可以直接从网卡收发包,不经过内核。

DPDK的核心流程:

  1. 大页内存:DPDK启动时会预留大页内存,用于存放数据包。避免TLB miss。
  2. UIO/VFIO驱动:把网卡绑定到用户态驱动,内核不再管理这个网卡。
  3. 轮询模式:用户态程序不断轮询网卡接收队列,有数据就立刻处理,没有中断开销。
  4. 无锁队列:DPDK内部用无锁环形队列(ring buffer)在核间传递数据包。

一个简单的DPDK收包示例:

// 初始化DPDK环境
int ret = rte_eal_init(argc, argv);

// 配置网卡端口
struct rte_eth_conf port_conf = {0};
rte_eth_dev_configure(port_id, 1, 1, &port_conf);

// 分配内存池
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());

// 启动网卡
rte_eth_dev_start(port_id);

// 轮询收包
while (1) {
    struct rte_mbuf *bufs[BURST_SIZE];
    uint16_t nb_rx = rte_eth_rx_burst(port_id, 0, bufs, BURST_SIZE);
    if (nb_rx > 0) {
        // 直接处理数据包,不经过内核
        process_packets(bufs, nb_rx);
        rte_pktmbuf_free_bulk(bufs, nb_rx);
    }
}
关键点:DPDK让收包延迟从几十微秒降到了1-2微秒。但代价是,你得自己管理内存、自己处理协议栈。说白了,就是把内核的活揽到自己身上。

除了DPDK,还有OpenOnload(Solarflare的)和VMA(Mellanox的)这类用户态协议栈。它们比DPDK更上层一些,提供了完整的TCP/UDP协议栈,你不需要自己解析协议。但灵活性不如DPDK,延迟也略高一点点。

我的建议:如果你做的是UDP组播行情接收,DPDK是首选。如果你需要TCP连接做交易,OpenOnload或VMA更省事。我曾在同一个系统里混用:行情走DPDK,交易走OpenOnload。效果不错。

总结一下

硬件选型和操作系统调优,没有银弹。你得根据你的做市商策略、交易品种、延迟要求来权衡。但有几个原则是通用的:

  • CPU一定要绑核+隔离,别让系统瞎调度。
  • NUMA架构下,线程和内存要待在同一个节点。
  • 网卡选Solarflare或Mellanox,配合内核旁路技术。
  • DPDK适合极致低延迟,用户态协议栈适合快速开发。

嗯,这块内容就这些。下一章咱们聊聊内存池设计和缓存优化,那又是另一番天地了。


专注资料整理