011-高频交易系统中行情模块绑核设计
文章目录
高频交易系统中行情模块绑核设计
- 原文链接:
https://mp.weixin.qq.com/s/AocomOFTTr2V0PPL2rKiRA - 本文按照当前
content/post/C++目录文章风格整理,核心关注点是低延迟行情链路中的 CPU 绑核设计。
1: 为什么行情模块要绑核
- 在高频交易系统中,行情接收、排序、因子计算、策略串行处理都属于延迟极其敏感的链路;
- 如果线程完全交给操作系统调度,线程可能在不同 CPU 核之间迁移;
- 一旦发生迁移,就可能引入上下文切换、缓存失效、TLB 抖动等额外开销;
- 对于普通业务系统,这类开销通常可以接受,但在微秒级优化场景里,抖动本身就是问题;
- 因此,行情模块做绑核,本质上是在用更强的确定性换更低的延迟波动。
2: 绑核前需要先处理哪些前提
- 绑核并不是简单地把线程固定到某个 CPU 上;
- 如果机器本身还有很多后台任务、IRQ 中断、SMT 竞争,单纯调用一次绑核接口往往效果有限;
- 一般要先处理以下几个前提。
2.1 优先使用物理核
- 对低延迟线程,优先绑定到物理核心而不是超线程;
- 因为同一个物理核心上的两个超线程仍然共享执行单元、缓存和带宽;
- 如果关键线程落在 SMT sibling 上,抖动很容易被放大。
2.2 处理 irqbalance
irqbalance会自动平衡硬件中断;- 在通用服务器场景这很有用,但在低延迟交易系统中,它可能把网络中断重新分配到不希望被打扰的核心上;
- 所以实践里常见做法是停掉
irqbalance,然后手动规划网卡队列和 CPU 的对应关系。
| |
2.3 视情况关闭 SMT / Hyper-Threading
- 如果机器专门用于极低延迟场景,可以考虑关闭 SMT;
- 这样做的代价是吞吐下降,但能换来更干净的物理核心资源;
- 是否关闭,要结合机器用途和实际压测结果决定。
3: C++ 中如何实现线程绑核
- Linux 下常见做法是使用
pthread_setaffinity_np; - 如果项目里用的是
std::thread或std::jthread,可以封装一层通用 helper,避免到处散落平台细节; - 除了设置亲和性,最好还要增加一次实际验证,确认线程真的运行在预期 CPU 上。
3.1 一个简单的绑核辅助函数
| |
3.2 更实用的封装建议
- 对
cpu_id做合法性检查,避免绑定到不存在的核心; - 给绑定操作加日志,方便排查部署环境差异;
- 在线程启动后增加一次
sched_getcpu()验证; - 如果是线程池或多阶段流水线,建议把绑核规则和业务线程角色分离配置。
4: 行情模块的绑核分层设计
- 行情模块不是一个线程,而是一条流水线;
- 真正有效的做法,不是“所有线程都绑核”,而是按照链路角色进行分层绑定;
- 常见的划分方式如下。
4.1 行情接收线程
- 行情接收线程负责从网卡队列读取数据,是链路最前端;
- 这类线程应当优先独占物理核,并尽量和 NIC queue 一一对应;
- 这样可以减少中断分发和线程迁移带来的额外抖动。
4.2 排序线程
- 如果行情源较多,接收后通常还需要排序或重组;
- 排序线程应与接收线程分开绑定到独立核心;
- 最好放在拓扑上相近的核心,降低跨核通信代价。
4.3 串行核心线程
- 因子、模型、策略串行主链路通常更强调确定性;
- 这一类线程往往要独占一个核心,尽可能不与其他任务共享;
- 它是整条交易决策链的热点线程之一。
4.4 监控与日志线程
- 监控、埋点、日志落盘线程优先级相对较低;
- 它们可以被安排到非关键核心,甚至低价值的超线程上;
- 原则是不要抢占行情主链路的缓存与调度资源。
4.5 一个典型的核心分配示意
| 线程类型 | 建议 CPU | 设计原则 |
|---|---|---|
| 行情接收线程 | CPU 0-3 | 与网卡队列一一对应,独占物理核 |
| 排序线程 | CPU 4-7 | 与接收线程相邻,减少跨核通信 |
| 串行核心线程 | CPU 8 | 独占核心,承载策略主链路 |
| 监控 / 日志线程 | CPU 15 | 放在非关键核或低优先级资源上 |
5: 仅仅线程绑核还不够
- 如果操作系统仍然把其它任务调度到这些核心上,那么“独占核心”只是一种错觉;
- 真正想把关键 CPU 留给行情链路,还需要做系统级隔离。
5.1 isolcpus
isolcpus用于把指定 CPU 从常规调度域里隔离出去;- 这样系统中的普通任务不会轻易落到这些 CPU 上。
5.2 nohz_full
nohz_full用于减少指定 CPU 上的调度 tick;- 对低延迟线程来说,减少无关系统打扰是有价值的。
5.3 rcu_nocbs
rcu_nocbs可以让指定 CPU 避开 RCU callback 的干扰;- 对需要极致稳定性的核心,这一点也很关键。
| |
- 上面的参数只是示例;
- 实际使用时应结合机器拓扑、服务部署方式以及运维策略统一规划。
6: 如何验证绑核是否真的生效
- 绑核不是“代码能运行就算完成”;
- 必须从代码层、系统层和性能层分别验证。
6.1 代码层验证
- 在线程工作函数中调用
sched_getcpu(); - 确认线程运行时所在核心是否符合预期。
| |
6.2 系统层验证
- 使用
taskset -cp <pid>查看某个线程或进程的 CPU 亲和性; - 也可以结合
ps -eLo pid,tid,psr,comm观察线程实际落在哪个 CPU 上。
| |
6.3 性能层验证
- 最终还是要看上下文切换、cache miss、延迟抖动这些指标有没有下降;
- 常见工具是
perf stat; - 如果绑核后上下文切换次数和 cache miss 没有明显改善,就说明设计仍需调整。
| |
7: NUMA 是绑核设计里最容易被忽略的问题
- 在双路或多路服务器上,CPU、内存、PCIe 设备通常分布在不同 NUMA 节点;
- 如果线程绑在 node0,但网卡和内存分配落在 node1,那么跨 NUMA 访问会直接拉高延迟;
- 这会抵消很多绑核带来的收益。
7.1 NUMA 设计原则
- 行情接收线程、网卡队列、中间缓冲区最好放在同一个 NUMA node;
- 排序线程与后续串行线程也尽量保持局部性;
- 低延迟系统做绑核时,不能只看 CPU 编号,还要看拓扑关系。
7.2 常用排查工具
| |
- 如果需要更直观的硬件拓扑,也可以借助
lstopo查看。
8: 一个更稳妥的实践结论
行情模块绑核的目标,不是为了“看起来专业”,而是为了降低链路中的非确定性;
真正有效的方案通常同时包含以下几部分:
关键线程绑定物理核;
网卡队列与接收线程对齐;
停止
irqbalance并控制中断分布;对关键核心做
isolcpus/nohz_full/rcu_nocbs隔离;避免跨 NUMA 访问;
用
sched_getcpu、taskset、perf stat做闭环验证。只有把这些动作连起来,绑核设计才真正能为高频交易系统的行情模块带来稳定收益。
文章作者 lucas
上次更新 2026-04-03