2. 逐笔行情解析线程的CPU亲和性绑定与抖动观测

为什么解析线程会被“赶出”目标核

逐笔行情解析线程的延迟抖动,很多时候不是解析逻辑慢,而是线程在核间来回迁移:每次被调度器迁走,L1/L2 缓存与 TLB 局部性被破坏,唤醒后还要重新抢 CPU。要定位这类抖动,需要把“绑定”和“观测”分开做:先用 taskset 或 sched_setaffinity 把解析线程钉在指定核,再用 perf sched / /proc/<tid>/status 的 voluntary_ctxt_switches 与 nonvoluntary_ctxt_switches 对比绑定前后的上下文切换次数,同时用分位数统计对比 P99 解析延迟。

绑定前:线程在核间漂移 CPU 2 空闲 CPU 3 空闲 CPU 4 解析线程 CPU 5 空闲 调度迁移 观测指标(示例值) 解析延迟 P99:约 480 µs 上下文切换:约 1200 次/秒 迁移次数:约 300 次/秒 延迟采样(示意) P99 绑定后:线程固定在 CPU 4 CPU 2 空闲 CPU 3 空闲 CPU 4 解析线程(独占) CPU 5 空闲 taskset -pc 4 <解析线程TID> chrt -f 80 <解析线程TID> # 可选实时优先级 观测指标(示例值) 解析延迟 P99:约 210 µs 上下文切换:约 150 次/秒 迁移次数:0 次/秒 延迟采样(示意) P99 绑定

绑定与观测的具体做法

假设解析线程 TID 为 18422,目标核为 CPU 4。先记录绑定前的基线,再执行绑定,最后用同一套采样脚本复测,保证对比条件一致。

# 1) 绑定前基线:上下文切换计数
grep ctxt_switches /proc/18422/status
# voluntary_ctxt_switches:     1187
# nonvoluntary_ctxt_switches:   63

# 2) 绑定到 CPU 4(掩码 0x10)
taskset -pc 4 18422
# pid 18422's current affinity list: 0-7
# pid 18422's new affinity list: 4

# 3) 绑定后复测
grep ctxt_switches /proc/18422/status
# voluntary_ctxt_switches:      142
# nonvoluntary_ctxt_switches:     8

# 4) 观察调度迁移与延迟分布
perf sched latency -p 18422 -- sleep 10
perf stat -p 18422 -e context-switches,cpu-migrations -- sleep 10

延迟分位数用同一份采样脚本在绑定前后各跑 60 秒,记录每笔消息从“收到完整帧”到“解析完成”的时间差,再取 P50/P99/P999。示例数据如下(教学示例,非实测):

指标绑定前绑定后变化
解析延迟 P5085 µs78 µs-8%
解析延迟 P99480 µs210 µs-56%
解析延迟 P9991.9 ms0.6 ms-68%
voluntary_ctxt_switches1187142-88%
nonvoluntary_ctxt_switches638-87%
cpu-migrations约 300 次/秒0消除
绑定后 P50 改善有限而 P99 大幅下降,说明瓶颈主要来自“偶发迁移/抢占”而非解析逻辑本身。若绑定后 P99 仍高,应继续排查同核上的中断、其他线程争抢以及 NUMA 跨节点访存。

抖动来源的判读

  • nonvoluntary_ctxt_switches 偏高:线程被更高优先级任务抢占,常见于同核运行了中断处理、定时器或其他实时线程。可结合 perf sched 查看抢占者。
  • voluntary_ctxt_switches 偏高:线程主动让出 CPU,通常是等待锁、条件变量或 IO。需要检查解析线程是否在热路径上做了阻塞调用。
  • cpu-migrations 不为 0:亲和性未生效或被其他代码重置,检查是否有 sched_setaffinity 覆盖、容器 cpuset 限制或线程池重新创建。
  • 绑定后延迟反而变差:目标核被中断或其他忙线程占用,应换核或把中断亲和性一并调整,避免“钉在忙核上”。
绑定到单核会牺牲并行度,只适合解析线程本身是串行热路径的场景。若解析线程内部有多路并行处理,应绑定到一组核(如 taskset -pc 4-5),并保证同组核不被其他进程争用。