硬件选型与操作系统调优: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。
numactl --hardware。然后根据拓扑,把交易引擎的核心线程和它的工作内存放在同一个节点上。这样能稳定省下几十纳秒。
下面这张图,展示了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的核心流程:
- 大页内存:DPDK启动时会预留大页内存,用于存放数据包。避免TLB miss。
- UIO/VFIO驱动:把网卡绑定到用户态驱动,内核不再管理这个网卡。
- 轮询模式:用户态程序不断轮询网卡接收队列,有数据就立刻处理,没有中断开销。
- 无锁队列: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,还有OpenOnload(Solarflare的)和VMA(Mellanox的)这类用户态协议栈。它们比DPDK更上层一些,提供了完整的TCP/UDP协议栈,你不需要自己解析协议。但灵活性不如DPDK,延迟也略高一点点。
总结一下
硬件选型和操作系统调优,没有银弹。你得根据你的做市商策略、交易品种、延迟要求来权衡。但有几个原则是通用的:
- CPU一定要绑核+隔离,别让系统瞎调度。
- NUMA架构下,线程和内存要待在同一个节点。
- 网卡选Solarflare或Mellanox,配合内核旁路技术。
- DPDK适合极致低延迟,用户态协议栈适合快速开发。
嗯,这块内容就这些。下一章咱们聊聊内存池设计和缓存优化,那又是另一番天地了。