硬件选型与操作系统调优:CPU绑核与NUMA感知
做高频交易,说白了就是跟时间赛跑。我入行那会儿,第一件事就是被师傅拉着去机房看服务器选型。他说了一句话,我到现在都记得:「你代码写得再快,硬件跟不上,全是白搭。」
这一章,咱们就聊聊硬件选型和操作系统调优。嗯,这里面的坑不少,我一个个给你拆开讲。
CPU绑核:别让进程到处乱跑
你想想看,一个数据包从网卡进来,到你的交易策略处理完,中间要经过多少层?如果CPU核心换来换去,缓存命中率直接崩掉。延迟?那更是没法看。
绑核的核心思想:把关键进程或线程,固定到特定的CPU核心上。别让操作系统调度器瞎折腾。
我个人习惯用 taskset 或者 sched_setaffinity 来做。举个例子:
# 把进程PID 1234 绑定到 CPU 0 和 CPU 1
taskset -cp 0,1 1234
# 启动时直接绑定
taskset -c 0,1 ./trading_engine
我在项目中遇到过一个问题:明明绑了核,延迟还是不稳定。查了半天,发现是内核的软中断(softirq)跑到了同一个核心上。嗯,这里要注意——绑核不只是绑用户态进程,还得把中断也绑走。
NUMA感知:别让内存成为瓶颈
NUMA(非统一内存访问)架构,说白了就是每个CPU核心访问自己的本地内存最快,访问远端内存就慢。你想想看,如果交易线程在CPU 0上跑,却去访问CPU 1的内存,那延迟能低吗?
NUMA感知的核心原则:
- 线程绑在哪个CPU核心,内存就分配在哪个NUMA节点
- 避免跨节点内存访问
- 网卡中断也要绑定到同一个NUMA节点
我建议用 numactl 来管理:
# 查看 NUMA 拓扑
numactl --hardware
# 在 NUMA 节点 0 上运行程序,并分配内存
numactl --cpunodebind=0 --membind=0 ./trading_engine
为什么会这样?因为现代服务器动辄几十个核心,跨NUMA节点访问内存的延迟,比本地访问高出30%-50%。做高频交易,这个差距就是生死线。
lstopo 命令可以直观看到CPU、内存、PCIe设备的拓扑关系。我每次部署新机器,第一件事就是跑这个命令。
网卡选型:Solarflare vs Mellanox
网卡这块,高频交易圈子里基本就两家:Solarflare(现在叫Xilinx/AMD)和Mellanox(现在叫NVIDIA)。
| 特性 | Solarflare | Mellanox |
|---|---|---|
| 延迟 | 极低(硬件时间戳精准) | 极低(RoCE v2 加持) |
| 驱动生态 | onload(用户态协议栈) | MLNX_OFED(性能调优丰富) |
| 硬件卸载 | TCP/UDP 卸载、流过滤 | RDMA、GPUDirect |
| 典型场景 | 纯高频交易、低延迟TCP | 高性能计算、RDMA交易 |
我个人更偏爱Solarflare的onload方案。为什么?因为它能让你用标准的socket API,却享受到接近内核旁路的性能。说白了,就是不用改代码,延迟就能降一大截。
但如果你要做RDMA(远程直接内存访问),那Mellanox是唯一选择。我在一个项目中用过Mellanox的ConnectX-5,配合DPDK,延迟能压到1微秒以下。
驱动优化:别用默认配置
很多人装完网卡驱动就不管了。嗯,这是大忌。驱动参数不调,性能至少差30%。
Solarflare onload 优化示例:
# 设置中断合并(减少中断次数)
ethtool -C eth0 rx-usecs 0 rx-frames 1
# 调整环形缓冲区大小
ethtool -G eth0 rx 4096 tx 4096
# 开启硬件时间戳
ethtool -T eth0
Mellanox 优化示例:
# 设置最大中断速率
echo 0 > /proc/irq/$(cat /sys/class/net/eth0/device/irq)/smp_affinity
# 开启硬件卸载
ethtool -K eth0 gro on gso on tso on
内核旁路:DPDK与XDK原理
传统网络栈,数据包从网卡到应用,要经过内核协议栈。中断、拷贝、上下文切换……每一步都在消耗时间。内核旁路,说白了就是绕过内核,让应用直接跟网卡打交道。
DPDK(数据平面开发套件):
- 用户态轮询模式(PMD),代替内核中断
- 大页内存(HugePages),减少TLB miss
- 无锁环形缓冲区(Ring Buffer),高效传递数据
XDK(极致开发套件):
- Solarflare 自家的用户态网络栈
- 比DPDK更易用,但绑定硬件
- 支持TCP/UDP,无需重写协议栈
我举个例子,DPDK的收包流程:
// 初始化DPDK环境
rte_eal_init(argc, argv);
// 分配内存池
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);
// 启动收包循环
while (1) {
struct rte_mbuf *bufs[BURST_SIZE];
uint16_t nb_rx = rte_eth_rx_burst(port_id, 0, bufs, BURST_SIZE);
// 处理数据包
for (int i = 0; i < nb_rx; i++) {
process_packet(bufs[i]);
rte_pktmbuf_free(bufs[i]);
}
}
你看,没有中断,没有系统调用,就是用户态轮询。延迟?能压到1微秒以内。
知识体系总览
下面这张图,是我梳理的本章核心逻辑。你可以把它当作一个检查清单:
这张图把三大模块串起来了。你从CPU绑核开始,再到网卡选型,最后用内核旁路收尾。每一步都在为「确定性」和「低延迟」服务。
好了,这一章的内容就到这儿。记住,硬件选型和系统调优是高频交易的基础。基础不牢,地动山摇。下一章咱们聊聊数据管道的核心——内存管理与缓存优化。
公众号:蓝海资料掘金营,微信deep3321