同一专线、同一秒、延迟不一样——从一次排查看网卡中断与核心隔离的本质
本文从一个真实的加密货币交易系统延迟异常出发,逐层拆解网卡中断机制、多队列架构、PPS 瓶颈、Buffer Bloat、网卡隔离与 CPU 核组规划,帮助读者建立从物理网卡到 CPU 处理的完整认知。
一、问题现象 #
在一台 AWS c8g.metal-48xl(Graviton4,192 核,裸金属)上,同一条专线接入了多个交易所的行情。某天观察到以下现象:
| 时间 | BN 延迟 | GATE 延迟 | 间隔 |
|---|---|---|---|
| 17:33:36 | 248ms | 20ms | 同秒,相隔 7ms |
| 17:35:46 | 73-82ms(连续 8 包) | 20ms | 同秒,相隔 170ms |
| 17:41:17 | 86ms | 20ms | 同秒,相隔 142ms |
关键矛盾:如果是专线本身的问题(光纤故障、中继设备拥塞),同一时刻所有流量都应该受影响。但 GATE 始终稳定在 20ms,只有 BN 在波动。
结论:问题出在 TY 端到 BN 的独有路径上。 但这次排查引出了一系列关于网卡架构和 CPU 资源分配的深层问题,值得系统梳理。
二、中断到底是什么:一次中断,两个角色 #
很多人把"网卡中断"和"CPU 中断"当作两种不同的中断,但实际上它们描述的是同一次中断的发起方和执行方。
打个比方:有人按了你家门铃(网卡发中断),你听到铃声后起身去开门、收快递、拆包裹(CPU 处理中断)。门铃和你的行动不是两件独立的事,而是同一件事的两端。
网卡中断(发起方):网卡通过 DMA 将数据写入 Ring Buffer 后,通过 PCIe 总线向 CPU 的中断控制器发送一个消息写入(MSI-X,Message Signaled Interrupt)——本质是一次 PCIe 内存写事务,而非传统 INTx 那样的专用电气信号线——含义是"Queue 5 有包到了,请处理"。这个动作本身几乎没有开销,网卡发完就结束了。
CPU 中断处理(执行方):CPU 收到信号后的一系列动作——暂停当前任务、保存上下文、执行硬中断处理函数、调度软中断(softirq)、NAPI 轮询收包、协议栈处理、放入 socket buffer。所有计算开销全在 CPU 侧。
一句话总结:网卡只负责喊一声,CPU 负责跑全程。 人们说"网卡中断"时侧重"谁触发的",说"CPU 中断"时侧重"谁在干活"。
三、一张网卡的内部结构:多队列、Ring Buffer 与 IRQ #
3.1 多队列与 RSS #
现代网卡并不是一个"单通道"设备。一张网卡内部有多个独立的收发队列(RX/TX Queue),每个队列有自己独立的 Ring Buffer 和专属的 IRQ 编号(IRQ,全称 Interrupt Request,中断请求)。
多队列解决了一个根本问题:如果只有一个队列,所有包只能由一个 CPU 核顺序处理,吞吐量受限于单核性能。多队列让不同的包分散到不同队列,由不同 CPU 核并行处理,从而线性提升收包吞吐量。
但"把包分到哪个队列"需要一套确定性的分流机制——这就是 RSS(Receive Side Scaling,接收端扩展)。
RSS 的工作机制 #
RSS 是网卡硬件内部的包分流引擎,在包进入 Ring Buffer 之前就完成分流决策,不消耗任何 CPU 资源。整个过程分三步:
第一步:提取哈希输入。 网卡从包头中提取四元组(或二元组)作为哈希输入:
- TCP/UDP 包:
{src_ip, dst_ip, src_port, dst_port}(四元组) - 非 TCP/UDP 的 IP 包:
{src_ip, dst_ip}(二元组) - 非 IP 包:通常落入默认队列(Queue 0)
第二步:计算 Toeplitz 哈希。 网卡使用 Toeplitz 哈希算法对提取的字段计算 32-bit hash 值。Toeplitz 哈希的特点是:计算快(纯位移和异或运算,适合硬件实现)、分布均匀、且哈希密钥可配置。密钥(hash key)是一个 40 字节的随机数,可通过 ethtool -X 查看和修改:
# 查看当前 RSS 配置(hash key + indirection table)
ethtool -x enP11p4s0
# 修改 hash key(改变包到队列的映射关系)
ethtool -X enP11p4s0 hkey <hex_string>
第三步:查 Indirection Table 得到队列号。 计算出的 hash 值并不直接对队列数取模,而是用 hash 低位(通常低 7 位)作为索引,查找 Indirection Table(RETA,Redirection Table)。RETA 是一张由网卡硬件维护的映射表,每个条目存放一个目标队列号。
hash = Toeplitz(src_ip, dst_ip, src_port, dst_port)
queue = RETA[hash & 0x7F] // 取低 7 位,索引 128 条目的 RETA
RETA 的好处是灵活:默认按轮转(round-robin)填充,效果接近 hash % N,但也可以做非均匀分配(比如让热门队列分到更多 RETA 条目),且支持在线修改而无需更换 hash key:
# 将所有流量集中到队列 0 和 1(修改 indirection table)
ethtool -X enP11p4s0 equal 2
# 自定义权重:队列 0 占 3/4,队列 1 占 1/4
ethtool -X enP11p4s0 weight 3 1
整个 RSS 流程在网卡硬件中完成,示意如下:
一张网卡内部结构
┌─────────────────────────────────────────────────┐
│ RSS 引擎(硬件) │
│ │
│ ① 提取四元组 {src_ip, dst_ip, src_port, dst_port}│
│ ② Toeplitz 哈希 → 32-bit hash │
│ ③ RETA[hash & 0x7F] → 目标队列号 │
└─────────────────┬───────────────────────────────┘
│
┌─────────────┼─────────────┬──────────┐
▼ ▼ ▼ ▼
Queue 0 Queue 1 Queue 2 ... Queue 31
Ring Buffer Ring Buffer Ring Buffer Ring Buffer
IRQ 132 IRQ 133 IRQ 134 IRQ 163
RSS 的关键特性 #
同一条 TCP 连接(四元组固定)的所有包,hash 值相同,始终落入同一个队列。这保证了单连接内的包顺序,同时让不同连接的处理可以并行——这是 RSS 设计的核心约束:在保序的前提下最大化并行度。
3.2 IRQ 与 CPU 的绑定关系 #
每个 Queue 的专属 IRQ number 决定了"谁来喊",而 IRQ 到 CPU core 的绑定关系决定了"谁来干活"。
IRQ 被分配到哪个 core,那个 core 就负责处理这个 Queue 的 Ring Buffer 里的包——包括硬中断、softirq、NAPI 轮询和协议栈处理,全部在这个 core 上完成。
绑定方式有两种:
- irqbalance 自动分配:系统守护进程动态调整 IRQ 到 core 的映射,目标是均衡负载。但动态迁移会导致 cache 失效和延迟突刺,对低延迟系统有害。
- 手动绑定:通过
smp_affinity_list将 IRQ 固定到指定 core,确保不漂移。
# 将 IRQ 132(Queue 0)固定由 CPU 5 处理
echo 5 > /proc/irq/132/smp_affinity_list
# 之后网卡 Queue 0 每次发 IRQ 132,都只有 CPU 5 响应
3.3 32 个 Queue 不一定需要 32 个独占的 core #
一张网卡有 32 个 Queue 就有 32 个 IRQ,每个都需要绑定到某个 core。但这不意味着需要 32 个独占的 core。
实际操作中,行情连接可能只命中其中几个 Queue(因为 RSS 按四元组 hash,连接数有限),大部分 Queue 可能几乎没有流量。合理的做法是:
- 先看哪些 Queue 实际有流量:
cat /proc/interrupts | grep enP11p4s0,找出中断计数高的 Queue。 - 热门 Queue 一对一绑定独立 core。
- 冷门 Queue 可以多个共享同一个 core,反正没什么流量不会互相影响。
或者更直接:减少队列数。BN 行情可能就几条到十几条连接,4 个 Queue 足够覆盖,只需要绑 4 个 core,管理简单,资源不浪费。
# 把队列数从 32 降到 4
ethtool -L enP11p4s0 combined 4
3.4 共享资源的边界:NIC On-Chip Buffer #
每个 Queue 有独立的 Ring Buffer(位于主存)和 IRQ,但所有 Queue 共享网卡芯片内部的物理硬件资源。理解这些共享资源,才能理解为什么多 Queue 不等于完全隔离。
NIC 内部的 On-Chip Buffer #
在数据包从网线到达到 DMA 写入主存之间,还有一个常被忽略的环节:NIC 芯片内部的 SRAM。
网线信号到达
│
▼
PHY 层:电信号/光信号 → 数字比特流
│
▼
MAC 层:帧校验(CRC)
│
▼
★ NIC On-Chip SRAM ★ ← 包暂存在这里,所有 Queue 共享
│
▼
RSS 引擎:算四元组哈希,选目标 Queue
│
▼
DMA 引擎:从 On-Chip SRAM → 通过 PCIe → 写入主存中该 Queue 的 Ring Buffer
包通过 MAC 校验后,不是直接 DMA 到主存,而是先暂存在网卡芯片上的 SRAM 中。原因是 DMA 不一定能立即执行——DMA 引擎可能正在搬上一个包、PCIe 总线可能正忙、目标 Ring Buffer 的描述符可能还没准备好。所以包先在 NIC 内部 SRAM 排队等待。
这块 On-Chip SRAM 的关键特性:
- 容量有限:通常几百 KB 到几 MB(远小于主存中的 Ring Buffer)
- 所有 Queue 共享:不是每个 Queue 有自己的 SRAM 分区,而是所有 Queue 的包都在同一块 SRAM 中排队
- 满了直接丢包:当 SRAM 满时,新到达的包被静默丢弃(tail drop),这是比 Ring Buffer 溢出更底层的丢包点,
ethtool -S中的rx_fifo_errors或厂商特定计数器可以反映这类丢包
共享资源全景 #
┌─────────────────────────────────────────────────────┐
│ NIC 芯片内部(所有 Queue 共享) │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌────────────┐ │
│ │ On-Chip SRAM │ │ RSS 引擎 │ │ DMA 控制器 │ │
│ │ (包暂存区) │ │ (哈希+分流) │ │ │ │
│ └──────────────┘ └──────────────┘ └────────────┘ │
│ │
│ ┌──────────────┐ ┌───────────────────────────────┐│
│ │ PCIe 接口 │ │ 固件调度逻辑 / MAC / PHY ││
│ └──────────────┘ └───────────────────────────────┘│
└─────────────────────────────────────────────────────┘
│
══════════════╪══════════════ PCIe 总线(共享带宽)
│
┌────────────────────┼────────────────────┐
▼ ▼ ▼
主存 Ring Buffer 0 主存 Ring Buffer 1 主存 Ring Buffer 2 ← 这里才是 per-Queue 独立的
正常流量下这些共享资源不会成为瓶颈。但极端突发时(比如某个交易所瞬间推送大量行情),On-Chip SRAM 拥塞、DMA 引擎排队、PCIe 带宽饱和,都会连锁影响同一张网卡上所有 Queue 的收包延迟。这也是第七节"为什么要分网卡"的硬件层面根因。
延伸阅读:本文假设读者对网卡收包路径有基本了解。如需回顾从物理层到用户态的完整七阶段流程,参见《从网线到策略引擎:Linux 网络收包全路径深度解析》。
四、硬中断与 softirq:谁轻谁重 #
理解了中断的基本概念后,需要进一步区分硬中断和 softirq,因为这直接决定了"CPU 时间到底花在哪"。
4.1 硬中断:只喊不干 #
硬中断处理函数必须极快(微秒级),因为在硬中断上下文中所有本地中断被屏蔽。所以 Linux 网络子系统的设计哲学是:硬中断做最少的事,把真正的工作推迟到 softirq。
硬中断只做两件事:
- 关闭该队列的中断(防止同一队列反复触发中断风暴)
- 调度 NAPI 轮询(注册一个待处理的
NET_RX_SOFTIRQ)
然后就结束了,几百纳秒的事。
4.2 softirq:真正干活的地方 #
CPU 退出硬中断后,检查到有待处理的 NET_RX_SOFTIRQ,开始执行 softirq。网络收包的所有重活都在这里:
- NAPI poll——从 Ring Buffer 批量取包(每次最多 64 个)
- 填充 skb 元数据——协议类型、校验和、时间戳
- GRO 合并——将同一 TCP 流的小包合并为大包(可关闭)
- IP 层处理——头校验、Netfilter 规则、路由查找
- TCP/UDP 层处理——序列号校验、窗口更新、socket 查找
- 放入 socket buffer——数据入队,唤醒用户进程
每个包几微秒,几十个包下来就是几百微秒。这才是 CPU 时间的大头。
4.3 softirq 是抢占式的,不是时间片调度 #
一个容易混淆的概念:softirq 的排队和用户态进程的时间片调度是两回事。
时间片是进程调度器(CFS)的概念,用于用户态进程之间的轮转。而硬中断的优先级比任何用户进程都高——中断来了直接打断当前任务,不需要等时间片。softirq 则在硬中断返回路径上执行(irq_exit() → __do_softirq()),此时仍优先于用户态代码。
不过需要注意:__do_softirq() 有保护机制——如果 softirq 处理时间超过 2ms 或循环超过 10 次(MAX_SOFTIRQ_RESTART),内核会停止在中断上下文中继续处理,转而唤醒 ksoftirqd 内核线程。ksoftirqd 是 SCHED_NORMAL 优先级的普通内核线程,需要通过 CFS 调度器调度运行。在微突发场景下,softirq 工作量很容易超过阈值,后续包的处理会落入 ksoftirqd,延迟特性会发生变化——这是低延迟系统需要关注的。
所以当多个 Queue 的 IRQ 绑在同一个 core 上时,不是"时间片被挤压",而是 NAPI 实例在同一个 softirq 内串行轮询——同一个 CPU 上只有一个 NET_RX_SOFTIRQ,触发后由 net_rx_action() 依次轮询 poll_list 中所有已调度的 NAPI 实例。BN 队列的 NAPI poll 执行期间,GATE 队列的 NAPI poll 必须等待,因为它们在同一次 net_rx_action() 调用中被顺序处理。
4.4 softirq 排队会造成多大延迟 #
单个包的 softirq 处理只要几微秒,正常情况下不会有感知。
但微突发时不一样。比如 BN 突然 10ms 内来了几千个包,NAPI 会连续轮询:取 64 个包处理完,队列里还有,再取 64 个,再处理……这一轮 softirq 可能持续几百微秒甚至几毫秒。在这段时间内,同一个 core 上其他 Queue 的 softirq 确实要等。
不过,几毫秒的 softirq 排队解释不了本文开头图中 80-248ms 的延迟。那个量级的延迟更可能源于 TY 到 BN 的中间链路设备(交换机、路由器)本身引入的 Buffer Bloat——包在某个交换机端口的缓冲队列里排了上百毫秒。
所以回到最初的判断:本案例的根因在网络路径上,不在本机 softirq 抢占。 但本机的网卡隔离和核组规划是预防性优化,防止未来本机侧也出现类似的延迟传导。
五、带宽没满也会卡:PPS 才是小包场景的真瓶颈 #
5.1 带宽与 PPS 的换算 #
网卡的带宽(如 50Gbps)标的是比特传输速率(Gbps = Gigabit per second,每秒十亿比特)。但每个包不管多小,在线路上都有固定开销:
最小以太网帧 64 字节 + 前导码 8 字节 + 帧间隔 12 字节 = 84 字节 = 672 bit
以不同带宽为例(假设 100 字节的行情包,线路开销后 960 bit/包):
| 带宽 | 理论最大 PPS |
|---|---|
| 50 Mbps | ~52,000 |
| 1 Gbps | ~1,040,000 |
| 50 Gbps | ~52,000,000 |
加密货币 BBO 行情消息通常只有几十到几百字节。在低带宽专线(如 50Mbps)上,带宽还没满,PPS 就可能先到上限。
5.2 网卡 PPS vs CPU PPS #
网卡硬件的 PPS 上限(DMA + 描述符处理)通常在百万级以上,很难打满。
真正的瓶颈在 CPU 每秒能从 Ring Buffer 取走并处理完的包数。每个包都需要经历完整的 softirq 流程:NAPI poll → 协议栈解析 → socket buffer 入队。这套流程的 CPU 开销是固定的。
普通云服务器(有虚拟化开销)通常在几万到十几万 PPS;裸金属服务器(如 c8g.metal-48xl)可以到几百万 PPS。
5.3 微突发:平均不高,瞬时要命 #
加密货币行情的 PPS 有强烈的突发特征。平时可能只有几千 PPS,但行情剧烈波动时(插针、大单成交),交易所对大量币对同时推送更新,10ms 内堆积上万包完全现实。
这就是微突发(microburst)——平均流量远低于上限,但瞬时到达速率可能超过 CPU 处理速率。
六、Buffer 能救命吗:缓冲 ≠ 消化 #
6.1 三层 Buffer,各司其职 #
收包路径上有三层不同的 Buffer,位于不同阶段,行为各异:
网卡 DMA 写入 → Ring Buffer → CPU softirq 取走处理 → Socket Buffer → 用户 recv() 拷贝 → 用户 Buffer
① ② ③
① 网卡 Ring Buffer:位于主内存中,由网卡驱动初始化。网卡通过 DMA 把包写进来,CPU 通过 NAPI 轮询取走。满了的话网卡硬件直接丢包,软件完全不知道(rx_missed_errors 计数增加,但没有任何通知机制)。Ring Buffer 的大小可以调整:
# 查看当前值和硬件支持的最大值
ethtool -g enP11p4s0
# 设置 Ring Buffer 大小(每个队列的描述符数量)
ethtool -G enP11p4s0 rx 4096
同样的取舍:设大了能扛更长的突发不丢包,但包在队列里排队时间更长,延迟更高。
② Socket Buffer(sk_receive_queue):位于内核空间,协议栈处理完后把数据放进来,等用户进程 recv() 取走。满了之后的行为取决于协议:TCP 会通告窗口为 0,让对端停止发送(不丢包但阻塞对端);UDP 直接丢包,无任何通知。用 ss -tnpm 查看使用量。Socket Buffer 的大小可以通过系统参数调整:
# 单个 socket 接收缓冲区的全局上限和默认值
sysctl -w net.core.rmem_max=26214400
sysctl -w net.core.rmem_default=26214400
# TCP 专属的自动调优范围:最小值、默认值、最大值
sysctl -w net.ipv4.tcp_rmem="4096 87380 26214400"
也可以在代码里对单个 socket 设置:setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &buf_size, sizeof(buf_size))。但注意:Buffer 设大了能扛更长的突发,代价是 Buffer Bloat 更严重——数据在内核里排队更久才触发流控,延迟更高。对交易系统来说,宁可早点流控甚至丢包,也不要让过期行情在 buffer 里排几百毫秒才送到应用层,所以不是越大越好。
③ 用户进程 Buffer:位于用户空间,就是 recv(fd, buf, len) 里的那个 buf。数据从内核 Socket Buffer 拷贝到这里后,由应用逻辑消费。如果应用处理不过来(比如 JSON 解析太慢),数据在用户 Buffer 里堆积,表现为应用层延迟增长。
6.2 每一层都能缓冲,但都不能消化 #
三层 Buffer 共享同一个本质:只能缓冲一时的突发,消化速度跟不上就出问题。 但出问题的表现各不相同:
| 层级 | 满了之后 | 感知度 |
|---|---|---|
| Ring Buffer | 网卡硬件静默丢包 | 最低——只能通过 ethtool -S 计数器发现 |
| Socket Buffer | TCP 流控阻塞 / UDP 丢包 | 中等——TCP 表现为吞吐下降,UDP 表现为数据丢失 |
| 用户 Buffer | 应用层延迟堆积 | 最高——直接体现在业务指标上 |
6.3 Buffer Bloat:缓冲区越大,延迟越高 #
Buffer Bloat(缓冲区膨胀) 是网络领域一个经典且反直觉的问题:缓冲区越大,延迟反而越高。
为什么缓冲区大了反而有害 #
直觉上,缓冲区越大越安全——能吸收更多突发,不丢包。但问题在于 TCP 的拥塞控制机制依赖丢包作为"网络拥塞了"的反馈信号。整个恶性循环是这样展开的:
1. 发送端全速发送数据
2. 中间设备(交换机/路由器)的大缓冲区吸收了所有突发流量,不丢包
3. TCP 没收到丢包信号 → 认为网络没有拥塞 → 继续全速发送甚至加速(慢启动/拥塞避免)
4. 缓冲区里的包越排越多,每个包的排队时间越来越长
5. 延迟从毫秒级飙到几百毫秒甚至秒级
6. 但吞吐量看起来完全正常,丢包率为零——所有传统监控指标都是绿的
如果缓冲区小一点,步骤 2 就会丢包,TCP 在步骤 3 就会降速,延迟反而保持在低位。大缓冲区用高延迟换取了零丢包,而这个交换对低延迟系统来说是致命的。
Buffer Bloat 发生在哪里 #
Buffer Bloat 可以发生在数据路径上的任何一层:
| 位置 | 缓冲区 | 典型场景 |
|---|---|---|
| 中间链路设备 | 交换机/路由器的端口出口队列 | 多条流汇聚到同一个出口端口,队列深度配置过大 |
| 本机网卡 | Ring Buffer | ethtool -G rx 8192 设太大,NAPI 来不及消费 |
| 本机内核 | Socket Buffer | rmem_max 设太大,应用层来不及 recv() |
| ISP / 云网关 | 运营商设备内部队列 | 专线接入点的流量整形设备缓冲过深 |
本案例中 BN 延迟 80-248ms 而 GATE 正常,最可能的位置是 TY 到 BN 的中间链路设备——某台交换机或流量整形设备的出口队列配置了过深的缓冲,BN 突发流量被缓冲而非丢弃,导致排队延迟。
为什么 Buffer Bloat 比丢包更危险 #
对 HFT 系统来说,丢包其实不可怕——TCP 会重传,应用层能检测序列号跳变,丢包率是标准监控指标。但 Buffer Bloat 的表现是:
- 不丢包——所有数据最终都会送到,监控上丢包率为零
- 吞吐量正常——带宽利用率可能还很高,看起来一切健康
- 延迟悄悄升高——除非你主动测量每包延迟,否则根本发现不了
- 过期数据被当作最新数据处理——行情消息在缓冲区排了 200ms 才到达应用层,策略引擎基于 200ms 前的"旧行情"做决策,但它以为这是最新的
HFT 系统的应对策略 #
Buffer Bloat 的根本解法是让缓冲区保持小,宁可早丢包也不要攒包:
- Ring Buffer 不要设太大:够扛正常突发即可,过大只会增加排队延迟(见 6.1 节)
- Socket Buffer 不要设太大:对行情连接,宁可流控也不要让过期行情排队几百毫秒(见 6.1 节)
- 关闭中断合并:
rx-usecs 0 rx-frames 1,每包立即处理,不在网卡侧攒包(见 9.5.4 节) - 应用层丢弃过期数据:即使数据到了应用层,如果时间戳表明它已经过期,直接丢弃不处理
- 对中间链路设备:要求网络供应商在专线接入设备上配置较小的队列深度,或启用 AQM(Active Queue Management,如 CoDel / fq_codel)算法,在队列开始膨胀时主动丢包
回到本案例 #
排查中观察到 BN 的延迟从正常飙到 80-248ms,而 GATE 始终稳定在 20ms。这不是丢包(TCP 连接正常),而是典型的 Buffer Bloat——包在 TY 到 BN 路径上的某台设备的缓冲队列里排了上百毫秒。对低延迟交易系统来说,这比丢包更隐蔽也更致命:丢包至少能被检测到,而 Buffer Bloat 看起来一切正常,只是"慢了一点"。
延伸阅读:关于 Socket Buffer 与 TCP 窗口调优的更多细节,参见《网络 Buffer 与 UDP 丢包排查》;Ring Buffer 与网卡队列的内核视角,参见《网卡多队列与中断优化》。
七、为什么要分网卡:从逻辑隔离到物理隔离 #
7.1 同一张网卡:逻辑隔离的局限 #
如果 BN 和 GATE 的行情走同一张网卡,RSS 会根据四元组 hash 把它们分到不同的 Queue。这是逻辑隔离——不同的 Queue、不同的 Ring Buffer、不同的 IRQ 编号,看似各走各路。
但实际上:
- 共享网卡内部硬件资源:RSS 引擎、DMA 控制器、PCIe 接口、固件调度逻辑。正常流量下无感,极端突发时可能互相影响。
- softirq 层面互相竞争:不同 Queue 的 IRQ 如果被分配到相同的 CPU core,它们的 softirq 处理会串行排队。BN 微突发时大量 softirq 占据 CPU,同一核上 GATE 的 softirq 被延迟。
7.2 分网卡:物理隔离的本质 #
不同网卡的队列完全独立。比如 enP11p4s0 有 Queue 0-31(IRQ 132-163),enP11p8s0 也有自己的 Queue 0-31,但对应完全不同的 IRQ 编号、完全不同的 Ring Buffer 内存区域、完全不同的 DMA 通道。它们之间没有任何共享。
分网卡的本质是把逻辑隔离升级为物理隔离。逻辑隔离(同卡不同 Queue)在正常流量下够用,但扛不住微突发时的连锁影响;物理隔离(不同卡)在任何情况下都互不干扰。
将不同业务分到不同网卡后,隔离是从硬件到软件全链路的:
独立的物理网卡硬件
→ 独立的 Ring Buffer
→ 独立的 DMA 通道
→ 独立的 IRQ 编号
→ 独立的 CPU 核组(通过 IRQ 亲和性绑定)
→ 独立的 softirq 处理
→ 独立的协议栈处理
7.3 IRQ 集合绑定到独立的 CPU 核组 #
分网卡方案中最关键的一步:每张网卡的所有队列的 IRQ,固定分配给一组专用的 CPU 核。
enP11p4s0 (BN 行情) → Queue 0-31 的 IRQ → 绑定 CPU 0-31
enP11p8s0 (GATE 行情) → Queue 0-31 的 IRQ → 绑定 CPU 32-63
enP11p5s0 (TD 成交回报) → Queue 0-31 的 IRQ → 绑定 CPU 64-95
BN 突发时,硬中断、softirq、协议栈处理全部在 CPU 0-31 上跑。CPU 32-63 和 CPU 64-95 完全不知道发生了什么,GATE 和 TD 的延迟纹丝不动。
如果不分网卡,所有业务的 IRQ 混在同一套 Queue 里,很难做到这种干净的 CPU 核组隔离。
7.4 代价:总中断数可能增加 #
将流量从 1 张网卡分散到多张网卡后,总中断数可能反而增加。
原因在于 NAPI 的轮询机制。流量集中在一张网卡时,每个队列的包到达密集,NAPI 进入轮询后一次能捞出大量包,中断被长时间抑制。分散后每个队列的到达密度下降,NAPI 更频繁地遇到"队列空了"退出轮询、重开中断,下一个包又触发新中断。
但对交易系统来说,这个代价完全值得。我们关心的不是"总共产生了多少次中断",而是"某个业务的延迟会不会因为另一个业务的突发而被拖累"。硬件级流量隔离带来的确定性,远比节省几次中断更有价值。
八、收包核 vs 计算核:两种哲学的取舍 #
分完网卡和 IRQ 核组之后,还有一个关键决策:IRQ 处理和应用进程应该在同一个 core 上,还是分开?
8.1 同核方案:延迟极低,但抖动大 #
如果策略引擎绑在 CPU 5,该连接对应的 IRQ 也绑在 CPU 5。每次来包:
- CPU 5 正在跑策略计算
- 中断来了,CPU 5 被迫暂停策略计算
- 执行硬中断 + softirq + 协议栈(几微秒到几十微秒)
- 数据直接在 CPU 5 的 L1 cache 中,
recv()时延迟极低 - 回到策略计算
好处是数据路径最短——从 softirq 处理到用户态 recv() 全在同一个 core 的 L1 cache 中,零跨核开销。代价是策略计算被频繁打断,执行时间变得不确定。微突发时 softirq 密集触发,策略引擎可能被连续打断好几百微秒都跑不了。
8.2 分核方案:抖动小,延迟略高 #
将 IRQ 处理和应用进程分到不同的 core:
CPU 0-31 → 专门处理 IRQ + softirq + 协议栈(收包核)
CPU 32-63 → 专门跑应用进程(计算核)
收包核把数据处理完放进 socket buffer,计算核通过 recv() 取走数据安心计算,不会被中断打断。
代价是数据要跨核传递。数据在收包核的 L1/L2 cache 中,计算核读取需要走 L3/SLC(Graviton4 的 System Level Cache,同 socket 内约 25ns)甚至跨 socket 缓存一致性访问(c8g.metal-48xl 是双 socket,跨 socket 约 139ns)。比同核方案多了几十到上百纳秒延迟。(延迟数据来源:Chips and Cheese - Arm’s Neoverse V2, in AWS’s Graviton 4,使用 pointer chasing 实测)
8.3 怎么选 #
| 维度 | 同核 | 分核 |
|---|---|---|
| 延迟 | 极低(L1 cache 命中) | 略高(跨 SLC ~25ns / 跨 socket ~139ns) |
| 抖动 | 大(中断随时打断计算) | 小(计算核不受中断干扰) |
| 适用场景 | 极致延迟优先、流量稳定 | 确定性优先、流量有突发 |
大多数交易系统选分核,因为确定性比极致延迟更重要。一个稳定的 10μs 比一个平均 5μs 但偶尔飙到 500μs 的系统更可靠。
如果选分核方案,IRQ core 和应用进程 core 至少应该在**同一个 socket(即同一个 NUMA 节点)**内。c8g.metal-48xl 双 socket 架构下,同 socket 内的 SLC 访问约 25ns,跨 socket 则飙到约 139ns,差距巨大。
延伸阅读:关于 isolcpus、housekeeping 核规划、中断核与计算核分离的系统性方案,参见《低延迟系统的 CPU 核心规划:从 isolcpus 到 Busy Poll 的内核级实战》。
九、实战配置:原理与调优 #
9.1 分网卡策略(以 6 ENI 为例) #
原理:同一张网卡的所有队列共享 RSS 引擎、DMA 控制器、PCIe 接口和固件调度逻辑(见 3.4 节)。正常流量下这些共享资源不是瓶颈,但微突发时一个业务的流量风暴会挤占共享资源,连带影响同卡上的其他业务。将不同业务分到不同物理网卡,把共享资源变为独占资源,消除硬件层面的交叉影响。
分配原则:中断量最大的业务独占一张卡;延迟最敏感的业务独占一张卡;发送为主的业务(如信号转发)独立出去,避免 TX 中断干扰 RX 收包;管理流量(SSH、监控)走单独的卡,防止运维操作影响交易路径。
enP11p4s0 → BN 行情(中断量最大,独占一张卡)
enP11p8s0 → OKX 行情
enP11p5s0 → GATE 行情 + 成交回报(延迟最敏感)
enP11p6s0 → 信号转发(UDP,发送为主)
enP11p7s0 → 管理流量(SSH、监控)
enP11p9s0 → 备用
9.2 精简队列数 #
原理:RSS 按四元组哈希分流,同一条连接的包始终落入同一个队列。如果 BN 行情只有 8 条 WebSocket 连接,那最多只有 8 个队列有流量(实际可能更少,因为不同连接的哈希值可能碰撞到同一个队列)。剩下的 24 个队列完全空闲,却各占一个 IRQ 编号,需要绑定 core、占用描述符内存。
减少队列数的好处:(1)IRQ 数量减少,绑核配置更简单;(2)每个队列分到的描述符更多(Ring Buffer 总大小不变时),单队列抗突发能力更强;(3)RETA 条目集中到更少的队列,哈希分布更均匀。
# 把队列数从 32 降到 4
ethtool -L enP11p4s0 combined 4
9.3 停用 irqbalance,手动绑定 IRQ #
原理:irqbalance 是 Linux 的中断负载均衡守护进程,它周期性地(默认每 10 秒)检查各核的中断负载,将 IRQ 从繁忙的核迁移到空闲的核。对通用服务器这是合理的,但对低延迟系统有三个致命问题:
- Cache 冷启动:IRQ 从 CPU A 迁移到 CPU B 后,CPU B 的 L1/L2 cache 里没有 NAPI 结构体、skb 内存池、socket 哈希表等热数据,前几次中断处理需要从 L3 甚至内存重新加载,延迟突增几百纳秒到几微秒。
- 不可预测性:迁移时机取决于 irqbalance 的采样周期和算法决策,你无法预知 IRQ 什么时候会漂移。在行情微突发的关键时刻发生迁移,会叠加出极端延迟。
- 违反 NUMA 亲和性:irqbalance 可能把 IRQ 迁移到跨 NUMA 节点的核上,引入跨 socket 访问延迟(~139ns)。
手动绑定通过 smp_affinity_list 将 IRQ 固定到指定核,确保处理路径永远"热"在同一个核的 cache 中。
# 停止 irqbalance
systemctl stop irqbalance
systemctl disable irqbalance
# 将 enP11p4s0 的 Queue 0-3 绑定到 CPU 0-3
for i in $(seq 0 3); do
echo $i > /proc/irq/$((132 + i))/smp_affinity_list
done
# 将 enP11p8s0 的队列绑定到 CPU 4-7
# (IRQ 编号需根据实际 /proc/interrupts 查询)
9.4 收包核与计算核分离 #
原理:硬中断是抢占式的——无论计算核当前在做什么(策略计算、风控逻辑),中断来了必须立即响应。一次硬中断 + softirq 处理耗时几微秒到几十微秒,微突发时可能连续打断几百微秒。将 IRQ 处理和应用进程绑到不同核,计算核上永远不会有中断打断,执行时间完全可预测。
代价是跨核数据传递的额外延迟(同 socket 内约 25ns,见 8.2 节),但对大多数交易系统来说,确定性比极致延迟更重要。
# 应用进程绑到计算核(避免被 IRQ 打断)
taskset -c 64-95 ./binance-md
taskset -c 96-127 ./okx-md
# 确保计算核不处理任何 IRQ
# 通过上面的 smp_affinity_list 已经实现:
# IRQ 只在低编号 core,应用只在高编号 core
9.5 关闭增加延迟的内核特性 #
收包路径上有多个默认开启的内核特性,它们为通用场景优化吞吐量,但每一个都在延迟上做了不利于 HFT 的取舍。
9.5.1 关闭 GRO(Generic Receive Offload) #
原理:GRO 在 softirq 阶段(NAPI poll 之后、协议栈之前)拦截属于同一条 TCP 流的多个小包,将它们合并成一个大的"super-skb"再送入协议栈。这样协议栈只处理一个包而不是多个,减少了 per-packet 的头解析、Netfilter 遍历、socket 查找等开销,对大流量下载/流媒体等场景能显著提升吞吐量。
为什么 HFT 要关闭:GRO 的合并需要"等"——收到第一个包后不立即上送,而是暂存起来看后续有没有同流的包可以合并。这个等待窗口虽然很短(通常在同一次 NAPI poll 内),但对行情数据来说,每条消息都是独立的、需要立即处理的事件。GRO 把第一条行情消息扣住等第二条来合并,白白增加了第一条的处理延迟。
ethtool -K enP11p4s0 gro off
9.5.2 关闭 conntrack(连接跟踪) #
原理:nf_conntrack 是 Netfilter 的连接跟踪模块,它为每个经过的连接维护一个状态机(NEW → ESTABLISHED → RELATED → …),存储在一个全局哈希表中。每个包在进入协议栈时都要执行:哈希计算 → 查表 → 匹配连接 → 更新状态 → 引用计数操作。这是有状态防火墙(如 -m state --state ESTABLISHED -j ACCEPT)和 NAT 的基础。
为什么 HFT 要关闭:每个包的 conntrack 查找和更新大约增加 100-500ns 的开销(取决于哈希表大小和 cache 命中率)。交易系统不需要有状态防火墙和 NAT——行情连接是简单的长连接,安全策略通过 VPC Security Group 在网络层实现,不需要主机级的状态跟踪。
rmmod nf_conntrack
9.5.3 清空 iptables 规则 #
原理:Linux 内核的 Netfilter 框架在收包路径上注册了多个 hook 点(PREROUTING、INPUT 等)。即使 iptables 规则为空(只有默认 ACCEPT 策略),每个包仍然要遍历 hook 链表、执行函数调用、检查规则。每个 hook 点的遍历开销约 50-100ns。
为什么 HFT 要关闭:卸载 nf_conntrack 并清空 iptables 后,Netfilter 的 hook 注册被移除,内核在收包路径上直接跳过整个 Netfilter 子系统,相当于砍掉了协议栈中一段不必要的处理路径。
iptables -F
9.5.4 关闭中断合并(Interrupt Coalescing) #
原理:网卡默认开启中断合并(Interrupt Coalescing),即收到包后不立即触发中断,而是等待一段时间(rx-usecs)或累积一定数量的包(rx-frames)后才触发一次中断。自适应模式(adaptive-rx on)会根据当前流量动态调整这两个阈值——流量大时多攒、流量小时少攒。
这样做的目的是减少中断次数、提升吞吐量。但代价是每个包都要额外等待——第一个到达的包可能等了几十微秒才触发中断开始处理。
为什么 HFT 要关闭:adaptive-rx off rx-usecs 0 rx-frames 1 的含义是:关闭自适应、不等待任何时间、每收到 1 个包就立即触发中断。每条行情到达后零延迟进入处理流程。代价是中断频率更高,但在行情 PPS(通常几千到几万)的量级下,CPU 完全承受得住。
ethtool -C enP11p4s0 adaptive-rx off rx-usecs 0 rx-frames 1
9.6 本文未展开的关键调优手段 #
以下调优手段与本文主题密切相关,但涉及更深的内核机制,在系列的其他文章中有详细展开。此处给出原理概述和调优方向。
9.6.1 Busy Poll:跳过中断,用户态直接收包 #
原理:常规收包路径是"中断驱动"的——网卡触发中断 → softirq 处理 → 数据放入 socket buffer → 唤醒睡眠的用户进程。Busy Poll 改变了这个模型:用户线程调用 recv() / epoll_wait() 时,不睡眠等待唤醒,而是在内核态直接调用网卡驱动的 NAPI poll 函数,主动从 Ring Buffer 取包、跑协议栈、放入 socket buffer,然后立即返回数据。
整条路径从"中断 → softirq → 唤醒 → 系统调用 → 拷贝"缩短为"系统调用 → 直接 poll → 拷贝",省掉了中断触发、softirq 调度和进程唤醒三个环节,延迟可从 5-10μs 降到 1-2μs。
代价是用户线程在没有数据时会在内核态空转(spin),持续消耗 CPU。对行情处理线程来说这通常是值得的——反正这些核也不会跑别的任务。
# 全局启用:epoll_wait / recv 内核态轮询 50μs
sysctl -w net.core.busy_poll=50
sysctl -w net.core.busy_read=50
# 或单个 socket 设置
setsockopt(fd, SOL_SOCKET, SO_BUSY_POLL, &val, sizeof(val));
深入阅读:Busy Poll 的内核实现细节(
sk_busy_loop()函数路径、与 NAPI 的交互、与用户态 busy loop 的本质区别),参见《低延迟系统的 CPU 核心规划:从 isolcpus 到 Busy Poll 的内核级实战》 第五节。
9.6.2 CPU 隔离:isolcpus / nohz_full / rcu_nocbs #
原理:即使将 IRQ 和应用进程分到了不同核,计算核上仍然会被以下内核活动打断:
- 调度器负载均衡:内核每隔几毫秒检查各核负载,可能把其他核上的任务迁移过来。
isolcpus将指定核从调度域中移除,阻止任何任务被自动迁移到这些核上。 - 定时器中断(tick):默认每个核每秒产生 250-1000 次时钟中断(HZ=250/1000),用于时间片轮转和定时器到期检查。
nohz_full在核上只有一个 runnable 任务时关闭 tick,消除周期性打断。 - RCU 回调:Read-Copy-Update 机制的回调函数默认在所有核上执行,可能在任意时刻打断用户线程。
rcu_nocbs将指定核的 RCU 回调卸载到专门的内核线程上,由 housekeeping 核处理。
三者配合使用,将计算核变成"几乎只跑用户代码"的干净环境。
# 内核启动参数(/etc/default/grub)
isolcpus=managed_irq,domain,6-95
nohz_full=6-95
rcu_nocbs=6-95
深入阅读:isolcpus 的
managed_irq和domain标志的区别、Housekeeping 核的规划、192 核系统的完整分配方案,参见《低延迟系统的 CPU 核心规划》 第一、二、六节。
9.6.3 C-state / P-state 锁定:消除 CPU 唤醒延迟 #
原理:CPU 空闲时会进入低功耗状态(C-state)以省电。C-state 越深,功耗越低,但唤醒延迟越大:
| C-state | 典型唤醒延迟 | 状态 |
|---|---|---|
| C0 | 0 | 正在执行指令 |
| C1 | ~1μs | 时钟门控,核心暂停 |
| C6 | 10-100μs | 核心断电,需要重新上电和恢复状态 |
当一个处于 C6 状态的核突然收到中断,它需要 10-100μs 才能醒过来开始处理。这个唤醒延迟会叠加到包处理延迟上,且完全不可预测(取决于核在中断到达时碰巧处于哪个 C-state)。
processor.max_cstate=0 禁止 CPU 进入任何睡眠状态,idle=poll 让空闲核在 idle 循环中 spin 而不是调用 mwait 进入低功耗。代价是功耗增加,但在裸金属实例上电费不是瓶颈。
# 内核启动参数
processor.max_cstate=0 idle=poll
深入阅读:C-state 与完整的内核启动参数方案,参见《低延迟系统的 CPU 核心规划》 第 6.4 节。
9.6.4 硬件时间戳:纳秒级延迟测量 #
原理:排查延迟问题时,时间戳的精度决定了你能看到多细的粒度。Linux 提供三个层级的时间戳:
| 层级 | 打戳位置 | 精度 | 开销 |
|---|---|---|---|
| 应用层时间戳 | gettimeofday() / clock_gettime() | 微秒级 | 最低 |
| 内核软件时间戳 | softirq 处理包时内核打戳 | 微秒级 | 低 |
| 网卡硬件时间戳 | 网卡 MAC 层收到包的瞬间由硬件打戳 | 纳秒级 | 零(硬件完成) |
硬件时间戳在包进入网卡的瞬间就被记录,不受中断延迟、softirq 排队、协议栈处理的影响。通过对比硬件时间戳和应用层时间戳,可以精确拆分出"网卡到应用"的每一段延迟。
// 开启硬件时间戳(需要网卡支持)
int flags = SOF_TIMESTAMPING_RX_HARDWARE | SOF_TIMESTAMPING_RAW_HARDWARE;
setsockopt(fd, SOL_SOCKET, SO_TIMESTAMPING, &flags, sizeof(flags));
// 通过 recvmsg() 的 cmsg 读取时间戳
struct msghdr msg = { ... };
recvmsg(fd, &msg, 0);
// 遍历 msg.msg_control 中的 SCM_TIMESTAMPING 消息
# 检查网卡是否支持硬件时间戳
ethtool -T enP11p4s0
延伸阅读:关于收包路径各阶段的完整时序和延迟分布,参见《从网线到策略引擎:Linux 网络收包全路径深度解析》。
9.6.5 内核旁路:终极方案 #
原理:以上所有调优都是在 Linux 内核协议栈的框架内优化。内核旁路(Kernel Bypass)则跳出这个框架——用户态程序直接接管网卡,跳过整个内核收包路径:
常规路径:NIC → DMA → Ring Buffer → 硬中断 → softirq → IP → TCP → socket → recv()
约 5-25μs
旁路路径:NIC → DMA → 用户态 Ring Buffer → 应用直接读取
约 1-3μs
省掉了中断、softirq、整个协议栈和内核-用户态拷贝。延迟降低一个数量级,且完全消除了内核侧的不确定性。
主流方案:DPDK(完全接管网卡的用户态驱动框架)、AF_XDP(Linux 5.4+,利用 XDP 钩子将包零拷贝送到用户态)、Solarflare OpenOnload / ef_vi(硬件级用户态协议栈)。AWS ENA 网卡对旁路的支持有限,如果使用自建服务器 + Solarflare/Mellanox 网卡,旁路是 HFT 的标准选择。
深入阅读:内核旁路的架构对比和实战部署,参见《从网线到策略引擎》 第 10.6 节。
9.7 确认配置的关键命令 #
# 网卡驱动信息
ethtool -i enP11p4s0
# Ring Buffer 大小
ethtool -g enP11p4s0
# 队列数量
ethtool -l enP11p4s0
# 各队列丢包统计
ethtool -S enP11p4s0 | grep -i drop
# IRQ 亲和性
cat /proc/irq/132/smp_affinity_list
# 硬件时间戳支持
ethtool -T enP11p4s0
# 实例类型(EC2 IMDSv2)
TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" \
-H "X-aws-ec2-metadata-token-ttl-seconds: 60")
curl -s -H "X-aws-ec2-metadata-token: $TOKEN" \
http://169.254.169.254/latest/meta-data/instance-type
十、总结 #
回到最初的问题:同一条专线、同一秒内,为什么 BN 延迟 80-248ms 而 GATE 只有 20ms?
排除了专线本身的问题后,根因在 TY 端到 BN 的独有路径上——中间链路设备的 Buffer Bloat 导致了上百毫秒的排队延迟。本机 softirq 排队在微突发时最多造成几毫秒的延迟,解释不了这个量级的异常。
但这次排查揭示了一个更深层的架构问题:当多个业务共享同一张网卡和同一组 CPU 核时,微突发带来的 Buffer Bloat 和 softirq 竞争会在业务之间传导延迟。 即使本次根因在网络路径上,本机侧的隔离优化也是必要的预防措施。
解决方案的核心思想是三层隔离:
- 网卡隔离(逻辑隔离 → 物理隔离)——不同业务走不同物理网卡,Ring Buffer、DMA、IRQ 全部独立,硬件层面互不干扰。
- IRQ 核组隔离——每张网卡的 IRQ 绑定到专属 CPU 核组,softirq 处理互不抢占。精简队列数以匹配实际连接数,避免资源浪费。
- 收包核与计算核分离——IRQ 和 softirq 在收包核上处理,应用进程在计算核上运行,中断不打断策略计算,换取执行时间的确定性。
更新记录 #
| 日期 | 变更 |
|---|---|
| 2026-04-10 | 扩展 3.4 节"共享资源的边界",从一句话展开为完整的 NIC On-Chip Buffer 讲解:包括包从 PHY/MAC 到 DMA 之间在网卡片上 SRAM 暂存的完整流程图、SRAM 容量与共享特性、共享资源全景图,并阐明这是第七节"分网卡做物理隔离"的硬件层面根因 |
在 192 核、6 张 ENA 网卡的 c8g.metal-48xl 上,硬件资源是充裕的。瓶颈从来不是绝对性能,而是资源分配的合理性。