应用线程收到一个网络包之前,数据通常已经走过网卡 DMA、RX queue、中断、NAPI、协议栈、socket 队列和一次线程唤醒。任何一段的 CPU 迁移、队列积压、软中断预算、内存分配或锁竞争,都可能让端到端延迟比“网卡中断很快”大得多。实时网络优化因此不是把应用优先级调高,而是让整条路径的队列、CPU 和时间戳都可见。
把收包路径拆开看
网卡将报文 DMA 到 RX ring;随后 IRQ 提醒 CPU,Linux 常在 NAPI 轮询中批量收包以避免中断风暴;协议栈解析以太网/IP/UDP/TCP,将数据放入 socket 接收队列;最后阻塞在 recvmsg/epoll 的应用线程才被唤醒。高吞吐时,NAPI、softirq 和 ksoftirqd 的行为尤其重要,它们可能把很多包聚在一次调度中处理。
优化前先记录“数据年龄”
只测 recv() 返回后的时间没有意义,因为包可能已在队列里等很久。应尽可能记录源端发送时间、网卡/内核时间戳和应用消费时刻,至少得到:
1 | network_age = application_consume_time - source_send_time |
若硬件允许,可研究 SO_TIMESTAMPING 与网卡硬件时间戳;若协议本身有序号和发送时刻,也应让业务消息携带它们。没有原始时间戳,排队问题常被误判成“网络偶尔慢”。
CPU 与队列要一起规划
让一个关键 UDP 流的 RX queue、IRQ、NAPI 处理和应用线程尽量靠近同一个 CPU/NUMA 节点,能减少迁移和缓存抖动;但不能把所有网络工作塞到实时控制核。高吞吐背景流通常应放到 housekeeping CPU,必要时用 RSS/RPS/RFS 等机制调整分流,并持续看 /proc/interrupts、socket 丢包和 softnet 统计。
1 | cat /proc/interrupts |
这些命令只给出排障起点,字段需要结合内核版本和流量理解。一次修改 RSS、IRQ 或 NAPI 参数后,要在真实包长、并发和背景流下复测,而不是只对 ping 做优化。
过期包比丢包更危险时怎么办
控制系统常宁可丢弃过期状态,也不愿使用 300 ms 前的命令。应用协议应带序列号和时间戳,接收端定义最大可接受年龄;超过阈值的包直接丢弃或触发降级,而不是悄悄在 socket 队列里排队消费。网络实时性的核心是“信息在 deadline 内有用”,不是“每个字节最终都可靠送达”。
参考:Scaling in the Linux Networking Stack · socket timestamping