第二章:硬件与网络基础——CPU架构(NUMA)、内存层级、网卡、交换机与时钟同步
做低延迟系统,说白了就是跟硬件打交道。你代码写得再漂亮,不懂硬件怎么跑,那都是纸上谈兵。我个人习惯是,拿到一个新项目,先看硬件拓扑图,再看代码逻辑。这一章,我们就聊聊那些藏在机箱里的“硬核”知识。
2.1 CPU架构:NUMA 与 缓存亲和性
先说说 NUMA。非一致性内存访问,名字听着绕口,其实道理很简单。以前一个 CPU 管所有内存,大家抢着用,谁抢到算谁的。现在服务器都是多路 CPU,每个 CPU 有自己的“本地内存”。
为什么要关心这个?
因为跨 CPU 访问内存,延迟会高出一大截。我在项目中遇到过,同样的数据操作,放在本地内存跑只要 100 纳秒,跨 NUMA 节点就飙到 300 纳秒。你想想看,高频交易里,这 200 纳秒的差距,可能就是几百万的盈亏。
核心原则: 绑定线程到指定 CPU 核心,并确保它使用的内存在同一 NUMA 节点上。
具体怎么做?我一般用 numactl 工具来查看和设置。比如:
# 查看 NUMA 拓扑
numactl --hardware
# 绑定进程到 node 0 的 CPU 核心,并分配 node 0 的内存
numactl --cpunodebind=0 --membind=0 ./my_low_latency_app
嗯,这里要注意,光绑定 CPU 还不够。你还要防止操作系统把线程迁移到别的核心上。我习惯用 taskset 配合 pthread_setaffinity_np 来锁死核心。
避坑指南: 我曾经在一个项目里,明明绑定了核心,但性能还是忽高忽低。查了半天,发现是内核的 irqbalance 服务在作怪,它把网卡中断随机分配到不同核心上了。关掉它,世界清净了。
2.2 内存层级:L1/L2/L3 缓存
内存层级,是低延迟系统的“命门”。你写的每一行代码,最终都要变成 CPU 指令,去内存里取数据。而 CPU 和内存之间,隔着好几层缓存。
我画了一张图,帮你理解这个层级结构:
这张图你看懂了吗?L1 最快,但只有几十 KB。L3 慢一些,但容量大,而且是所有核心共享的。我经常跟团队说,你的热点数据,必须想办法塞进 L1 缓存。怎么塞?数据结构要紧凑,不要用指针链,尽量用数组。比如:
// 不好的做法:链表,缓存不友好
struct Order {
Order* next;
int64_t id;
double price;
};
// 好的做法:数组,连续内存
struct Order {
int64_t id;
double price;
};
Order orders[1024]; // 连续存储,遍历时预取到缓存
为什么会这样?因为 CPU 有预取器,它会猜测你接下来要访问哪个内存地址。如果是数组,它猜得准,数据提前就加载到 L1 了。如果是链表,它猜不准,每次都要去主内存拿,延迟就上去了。
警告: 别小看缓存行(Cache Line)的概念。一个缓存行通常是 64 字节。如果你有两个线程修改同一个缓存行里的不同变量,就会发生“伪共享”(False Sharing),性能直接腰斩。我见过一个交易系统,就因为两个计数器挨在一起,吞吐量死活上不去。解决办法很简单:用 alignas(64) 把它们隔开。
2.3 网卡:Solarflare 与 Mellanox
网卡是行情数据的入口。普通网卡延迟在 10 微秒级别,而低延迟网卡可以做到 1 微秒以内。差距在哪?
我个人习惯用 Solarflare 和 Mellanox 这两家。Solarflare 的 OpenOnload 技术,说白了就是绕过内核协议栈,直接在用户态处理网络数据。Mellanox 的 ConnectX 系列,配合 RDMA,可以直接从网卡把数据写到应用内存,CPU 几乎不参与。
| 特性 | Solarflare (X2522) | Mellanox (ConnectX-6) |
|---|---|---|
| 用户态网络栈 | OpenOnload (TCP/UDP) | RDMA (InfiniBand/RoCE) |
| 典型延迟 | ~1.5 μs | ~0.8 μs (RDMA) |
| CPU 卸载 | 部分卸载 | 完全卸载 (数据直达) |
| 适用场景 | TCP/UDP 行情 | 极低延迟、多播行情 |
嗯,这里要注意,用 Solarflare 的 OpenOnload,你得把网卡驱动和库装好,然后设置环境变量:
# 启用 OpenOnload
export ONLOAD=1
# 指定使用的 CPU 核心
export EF_TX_CPU_AFFINITY=2
# 运行你的应用
./my_trading_app
我曾经踩过一个坑:OpenOnload 默认会占用一个核心做中断处理。如果你没预留这个核心,你的业务线程就会被抢走 CPU 时间。后来我学乖了,永远给网卡中断留一个专用核心。
2.4 交换机与时钟同步
交换机,很多人觉得就是个“分线器”。但在低延迟系统里,交换机的选型直接决定了你的数据能多快到达。我一般推荐用 Arista 或 Cisco Nexus 系列,端口到端口的延迟要控制在 500 纳秒以内。
时钟同步,这个容易被忽视。你想想看,如果两台机器的时钟差了几毫秒,你拿什么来对比行情时间戳?拿什么来做回放对齐?
PTP vs NTP:
- NTP:精度在毫秒级,适合普通应用。但网络抖动大的时候,误差可能到几十毫秒。
- PTP (IEEE 1588):精度在微秒甚至纳秒级。需要交换机支持 PTP 硬件时间戳。
我建议,做行情存储和回放,必须上 PTP。具体配置,以 Linux 为例:
# 安装 PTP 工具
apt-get install linuxptp
# 配置 ptp4l (主时钟模式)
# /etc/linuxptp/ptp4l.conf
[global]
interface eth0
clock_class 255
network_transport L2
delay_mechanism E2E
# 启动 PTP 服务
ptp4l -f /etc/linuxptp/ptp4l.conf -i eth0
避坑指南: 我曾经在一个机房部署 PTP,结果发现时钟偏差一直在 10 微秒左右波动。查了半天,发现是交换机的 PTP 硬件时间戳功能没开启。记住,软件 PTP 精度有限,必须用硬件时间戳。交换机上要配 ptp enable 之类的命令。
好了,硬件和网络基础就聊这么多。这些知识,你写代码的时候可能用不上,但排查问题的时候,它们就是你的“照妖镜”。
公众号:蓝海数据掘金营,微信deep3321