4. 操作系统延迟优化:CPU亲和性、中断绑定、实时内核、内存大页
做市引擎跑在操作系统上,操作系统本身却是个「拖后腿」的家伙。你想想看,你的交易线程正忙着处理行情,操作系统突然说「嘿,你该歇歇了,让别的线程跑一会儿」——这就是上下文切换。一次切换几百纳秒,在纳秒级竞争的做市领域,这简直是在烧钱。
我个人习惯,拿到一台新机器,第一件事不是装软件,而是先把操作系统「扒一层皮」。把那些不必要的服务全关掉,把CPU和内存的控制权牢牢抓在自己手里。今天聊的这四个方向,就是我这些年反复折腾过的经验总结。
4.1 CPU亲和性:把线程焊死在核心上
CPU亲和性,说白了就是告诉操作系统:「这个线程只能在这几个核心上跑,别给我乱挪。」
为什么要这么做?因为每次线程被调度到另一个核心,L1/L2缓存就全废了。你想想看,好不容易把热数据加载到缓存里,一换核心,全得重来。对于做市引擎这种对延迟极度敏感的应用,这简直是灾难。
我在项目中遇到过最夸张的一次,一个订单处理线程在8个核心间来回跳,延迟抖动从5微秒直接飙到50微秒。后来绑定了核心,抖动立刻降到个位数。
Linux下设置亲和性,我一般用两种方式:
方式一:命令行工具 taskset
# 启动时绑定到CPU 0和1
taskset -c 0,1 ./market_maker
# 对已运行的进程绑定
taskset -pc 0,1 <PID>
方式二:代码里用 sched_setaffinity
#include <sched.h>
#include <pthread.h>
void pin_thread_to_core(pthread_t thread, int core_id) {
cpu_set_t cpuset;
CPU_ZERO(&cpuset);
CPU_SET(core_id, &cpuset);
int ret = pthread_setaffinity_np(thread, sizeof(cpu_set_t), &cpuset);
if (ret != 0) {
// 嗯,这里要注意,权限不够会失败
perror("pthread_setaffinity_np");
}
}
4.2 中断绑定:别让网络中断打扰交易线程
网卡收到一个数据包,会触发一个硬件中断。这个中断默认可能在任何一个核心上处理。你想想看,你的交易线程正在CPU 2上跑着,结果网卡中断来了,操作系统把它安排到CPU 2上——你的交易线程就被打断了。
我曾经在一个实盘环境里排查延迟毛刺,查了三天,最后发现是网卡中断和交易线程抢同一个核心。每次中断处理要花几微秒,但关键是打断了交易线程的流水线,恢复执行时缓存都凉了。
解决方案很简单:把中断绑定到专门的核心上,和交易线程彻底隔离。
查看当前中断分布:
cat /proc/interrupts | grep eth0
修改中断亲和性:
# 假设网卡中断号是 78,绑定到 CPU 3
echo 8 > /proc/irq/78/smp_affinity
# 注意:这里的值是位掩码
# CPU 0 = 0x01
# CPU 1 = 0x02
# CPU 2 = 0x04
# CPU 3 = 0x08
4.3 实时内核:让交易线程插队
普通Linux内核是「公平调度」的,每个线程轮流用CPU。但做市引擎需要的是「特权调度」——行情来了,交易线程必须立刻执行,其他线程靠边站。
实时内核(PREEMPT_RT)就是干这个的。它允许你给线程设置实时优先级,让高优先级的线程可以抢占低优先级的线程。
安装实时内核:
# Ubuntu/Debian
apt-get install linux-image-rt-amd64
# CentOS/RHEL
yum install kernel-rt
设置实时优先级:
# 使用 chrt 命令
chrt -f 99 ./market_maker
# 或者在代码里设置
#include <sched.h>
struct sched_param param;
param.sched_priority = 99;
int ret = pthread_setschedparam(pthread_self(), SCHED_FIFO, ¶m);
实时内核的代价是什么?吞吐量会下降。因为实时调度为了低延迟,牺牲了一部分CPU利用率。但对于做市引擎,延迟是第一位的,吞吐量够用就行。
4.4 内存大页:减少TLB缺失
这个知识点,说实话,很多人容易忽略。但它的收益非常直接。
操作系统管理内存是以「页」为单位的。默认4KB一页。你的做市引擎可能占了几十GB内存,想想看,4KB一页,得有多少页?TLB(页表缓存)根本装不下。每次访问新页面,都要去查页表,这就是TLB缺失。
大页(Huge Pages)把页大小从4KB提升到2MB甚至1GB。页数少了,TLB命中率自然就上去了。
配置大页:
# 查看当前大页配置
cat /proc/meminfo | grep Huge
# 设置大页数量(假设每页2MB,需要1024页)
echo 1024 > /proc/sys/vm/nr_hugepages
# 或者用 sysctl
sysctl -w vm.nr_hugepages=1024
在代码里使用大页:
#include <sys/mman.h>
// 分配大页内存
void *addr = mmap(NULL, size,
PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_HUGETLB,
-1, 0);
if (addr == MAP_FAILED) {
perror("mmap huge pages failed");
}
4.5 把这些技术组合起来
单独用其中一项,效果有限。真正的优化是把它们组合成一个整体方案。
下面是我在一个项目里用过的核心分配方案:
| CPU核心 | 用途 | 说明 |
|---|---|---|
| 0 | 系统/中断 | 网卡中断、时钟中断、系统进程 |
| 1 | 行情接收 | 实时优先级80,绑定大页 |
| 2 | 订单处理 | 实时优先级95,绑定大页 |
| 3 | 风控/日志 | 普通优先级,不参与实时竞争 |
这个方案的核心思路是:隔离。把实时任务和系统任务彻底分开,互不干扰。
嗯,这些技术说起来简单,但真正落地的时候,每个环节都可能踩坑。我建议你从CPU亲和性开始,这个改动最小,收益最明显。然后逐步加上中断绑定和实时内核。最后再优化内存大页——因为大页需要应用层配合改代码,工作量稍大一些。
做市引擎的延迟优化,操作系统这关必须过。过了这关,你的引擎才算真正「跑在硬件上」,而不是「跑在操作系统里」。