一、为什么交易系统需要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微秒。
为什么会这样?因为绑核之后:
- L1/L2缓存始终是热的——行情数据、订单状态都在缓存里
- 分支预测器学会了你的模式——CPU知道你的代码执行路径
- TLB缓存了你的页表——虚拟地址到物理地址的转换不再需要查内存
小技巧:我建议你在绑核之后,用taskset -cp <pid>确认一下线程确实跑在了你指定的核心上。别问我为什么强调这个——有一次我绑错了核心号,查了三天才找到原因。
最后,我用一张图来总结本章的核心逻辑:
这张图把整个逻辑串起来了。说白了就是:没有亲和性 → 线程乱跑 → 缓存失效 → 延迟抖动 → 滑点亏损。而CPU亲和性绑定,就是切断这个链条最直接的手段。
下一章,我会带你看看具体的绑核工具和操作方法。到时候咱们用taskset和sched_setaffinity来实战一把。
公众号:蓝海资料掘金营,微信deep3321