高频交易系统中行情模块绑核设计
  • 原文链接: 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 的对应关系。
1
2
sudo systemctl stop irqbalance
sudo systemctl disable irqbalance

2.3 视情况关闭 SMT / Hyper-Threading

  • 如果机器专门用于极低延迟场景,可以考虑关闭 SMT;
  • 这样做的代价是吞吐下降,但能换来更干净的物理核心资源;
  • 是否关闭,要结合机器用途和实际压测结果决定。

3: C++ 中如何实现线程绑核

  • Linux 下常见做法是使用 pthread_setaffinity_np
  • 如果项目里用的是 std::threadstd::jthread,可以封装一层通用 helper,避免到处散落平台细节;
  • 除了设置亲和性,最好还要增加一次实际验证,确认线程真的运行在预期 CPU 上。

3.1 一个简单的绑核辅助函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <pthread.h>
#include <sched.h>
#include <unistd.h>

#include <iostream>
#include <stdexcept>
#include <thread>

void bind_thread_to_cpu(std::thread& th, int cpu_id) {
    cpu_set_t cpuset;
    CPU_ZERO(&cpuset);
    CPU_SET(cpu_id, &cpuset);

    int rc = pthread_setaffinity_np(
        th.native_handle(),
        sizeof(cpu_set_t),
        &cpuset
    );

    if (rc != 0) {
        throw std::runtime_error("pthread_setaffinity_np failed");
    }
}

void worker() {
    std::cout << "running on cpu: " << sched_getcpu() << std::endl;
}

int main() {
    std::thread th(worker);
    bind_thread_to_cpu(th, 2);
    th.join();
    return 0;
}

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 的干扰;
  • 对需要极致稳定性的核心,这一点也很关键。
1
GRUB_CMDLINE_LINUX="isolcpus=0-8 nohz_full=0-8 rcu_nocbs=0-8"
  • 上面的参数只是示例;
  • 实际使用时应结合机器拓扑、服务部署方式以及运维策略统一规划。

6: 如何验证绑核是否真的生效

  • 绑核不是“代码能运行就算完成”;
  • 必须从代码层、系统层和性能层分别验证。

6.1 代码层验证

  • 在线程工作函数中调用 sched_getcpu()
  • 确认线程运行时所在核心是否符合预期。
1
std::cout << "current cpu = " << sched_getcpu() << std::endl;

6.2 系统层验证

  • 使用 taskset -cp <pid> 查看某个线程或进程的 CPU 亲和性;
  • 也可以结合 ps -eLo pid,tid,psr,comm 观察线程实际落在哪个 CPU 上。
1
2
taskset -cp 12345
ps -eLo pid,tid,psr,comm | grep your_process

6.3 性能层验证

  • 最终还是要看上下文切换、cache miss、延迟抖动这些指标有没有下降;
  • 常见工具是 perf stat
  • 如果绑核后上下文切换次数和 cache miss 没有明显改善,就说明设计仍需调整。
1
perf stat -e context-switches,cache-misses,cache-references ./your_app

7: NUMA 是绑核设计里最容易被忽略的问题

  • 在双路或多路服务器上,CPU、内存、PCIe 设备通常分布在不同 NUMA 节点;
  • 如果线程绑在 node0,但网卡和内存分配落在 node1,那么跨 NUMA 访问会直接拉高延迟;
  • 这会抵消很多绑核带来的收益。

7.1 NUMA 设计原则

  • 行情接收线程、网卡队列、中间缓冲区最好放在同一个 NUMA node;
  • 排序线程与后续串行线程也尽量保持局部性;
  • 低延迟系统做绑核时,不能只看 CPU 编号,还要看拓扑关系。

7.2 常用排查工具

1
2
numactl --hardware
lscpu
  • 如果需要更直观的硬件拓扑,也可以借助 lstopo 查看。

8: 一个更稳妥的实践结论

  • 行情模块绑核的目标,不是为了“看起来专业”,而是为了降低链路中的非确定性;

  • 真正有效的方案通常同时包含以下几部分:

  • 关键线程绑定物理核;

  • 网卡队列与接收线程对齐;

  • 停止 irqbalance 并控制中断分布;

  • 对关键核心做 isolcpus / nohz_full / rcu_nocbs 隔离;

  • 避免跨 NUMA 访问;

  • sched_getcputasksetperf stat 做闭环验证。

  • 只有把这些动作连起来,绑核设计才真正能为高频交易系统的行情模块带来稳定收益。