一、为什么交易系统需要CPU亲和性

做量化交易的朋友,应该都遇到过这种情况:策略跑得好好的,突然某一次下单延迟飙到了几百微秒。你查了一圈,网络没问题,代码也没bug,最后发现——是操作系统把你的交易线程挪到了另一个CPU核心上。

嗯,这就是我们今天要聊的核心问题。CPU亲和性,说白了就是让线程或进程「钉死」在某个核心上,别乱跑。我做了这么多年低延迟系统,可以负责任地告诉你:这是交易系统优化的第一道门槛

延迟敏感型应用的痛点

交易系统对延迟有多敏感?我给你个数字:在纳秒级竞争的市场里,1微秒的抖动就可能导致滑点,10微秒的延迟可能让你错过一笔盈利交易

我见过最夸张的一次,某家私募的CTA策略,就因为线程被调度到了另一个NUMA节点,延迟从5微秒跳到了80微秒。你想想看,80微秒在期货高频里是什么概念?足够价格跳两三个tick了。

交易系统的延迟痛点,主要集中在三个地方:

  • 不可预测的调度延迟——操作系统随时可能把你的线程挂起,换别的进程上来跑
  • 缓存失效——线程换到新核心后,L1/L2缓存全是冷的,得重新加载数据
  • NUMA访问惩罚——跨内存节点的访问,延迟能差2-3倍

核心观点:交易系统的延迟,不是平均延迟,而是尾部延迟(P99.9)决定了你的生死。CPU亲和性解决的就是这个尾部延迟问题。

上下文切换的代价

很多人觉得上下文切换不就是保存几个寄存器嘛,能有多大事?

我刚开始做交易系统时也这么想。直到有一次,我用perf stat跑了一下,发现上下文切换的代价远不止寄存器保存那么简单。

来看一组真实数据:

操作 耗时(纳秒) 换算成CPU周期(3GHz)
函数调用 ~5 ns ~15 cycles
系统调用 ~100 ns ~300 cycles
上下文切换(同进程) ~1,000 ns ~3,000 cycles
上下文切换(跨进程) ~3,000 ns ~9,000 cycles
TLB刷新+缓存预热 ~10,000 ns ~30,000 cycles

看到了吗?真正的代价不是那几千个周期的寄存器保存,而是后续的缓存和TLB重建。我习惯把上下文切换比作「搬家」——搬东西本身不累,累的是到了新地方啥都得重新布置。

避坑指南:我曾经遇到过一个案例,某团队把交易线程和日志线程绑在同一个核心上。结果日志一刷盘,交易线程就被迫让出CPU。嗯,这种「伪亲和性」比不绑还糟糕。记住:一个核心只干一件事

缓存命中率与性能的关系

为什么缓存命中率对交易系统这么重要?我给你算笔账:

  • L1缓存命中:~1 ns
  • L2缓存命中:~4 ns
  • L3缓存命中:~12 ns
  • 主存访问:~60-100 ns

一个交易订单的处理流程,可能要访问几百次内存。如果缓存命中率从99%降到90%,总延迟可能翻倍。你想想看,在微秒级别的竞争中,这差距有多大?

我个人习惯用perf stat -e cache-misses来监控缓存表现。有一次帮一家量化团队做优化,发现他们的订单处理线程缓存缺失率高达15%。绑核之后,直接降到了2%以下,延迟从平均12微秒降到了4微秒。

为什么会这样?因为绑核之后:

  1. L1/L2缓存始终是热的——行情数据、订单状态都在缓存里
  2. 分支预测器学会了你的模式——CPU知道你的代码执行路径
  3. TLB缓存了你的页表——虚拟地址到物理地址的转换不再需要查内存

小技巧:我建议你在绑核之后,用taskset -cp <pid>确认一下线程确实跑在了你指定的核心上。别问我为什么强调这个——有一次我绑错了核心号,查了三天才找到原因。

最后,我用一张图来总结本章的核心逻辑:

CPU亲和性对交易系统的影响链路 无CPU亲和性 线程被随意调度 缓存频繁失效 NUMA跨节点访问 导致 高尾部延迟 P99.9 恶化 最终 滑点 交易亏损 解决方案:CPU亲和性绑定 将线程固定在指定核心上运行 缓存命中率↑ 上下文切换↓ 延迟稳定

这张图把整个逻辑串起来了。说白了就是:没有亲和性 → 线程乱跑 → 缓存失效 → 延迟抖动 → 滑点亏损。而CPU亲和性绑定,就是切断这个链条最直接的手段。

下一章,我会带你看看具体的绑核工具和操作方法。到时候咱们用tasksetsched_setaffinity来实战一把。


公众号:蓝海资料掘金营,微信deep3321