小包 64 字节导致 PPS 跑满,如何合并请求又不增加延迟
解读
- 场景定位:国内互联网高并发网关、消息中间件或实时推荐系统,常见 100 Gbps 网卡,但内核默认 1.5 kB MTU,64 B 小包把 PPS(Packet Per Second)先打满,带宽利用率却不到 5 %,CPU 软中断飙高,业务 RT 抖动。
- 面试意图:考察候选人是否理解“PPS 瓶颈 ≠ 带宽瓶颈”,能否在“低延迟”硬约束下做数据面优化,而不是简单回答“开启 Nagle”或“批量发”。
- 关键矛盾:合并意味着攒包,攒包意味着等待,等待意味着延迟上涨;需要给出“零额外延迟”或“可控微延迟”的量化方案,并说明如何验证。
知识点
- 小包灾难三件套:
- 每包 84 B(64B payload + 20B IP header)物理层实际 84+38=122 B on wire;
- 网卡 DMA 固定开销 > 150 ns/包;
- 内核协议栈一次软中断 ~1 µs,10 Mpps 即 10 k 中断/ms,单核打满。
- 合并技术分类:
- L2:网卡 TSO/GSO/FOE(LRO)只能做大的分片/重组,对小包上行无效;
- 用户态:批量写、vectored I/O(writev、mmsg)、自定义帧格式;
- 内核态:eBPF 的 SK_MSG、TCP_NOTSENT_LOWAT、SO_INCOMING_CPU 亲和;
- 硬件:DPDK Burst Send、智能网卡流表合并(ARM NPU/FPGA)。
- 零延迟攒包条件:
- 输出队列空且 NIC 有 Budget → 立即发;
- 输出队列非空 → 走攒包逻辑,但设定“超时≤1 µs 或字节≤MTU”即 flush;
- 使用 SO_BUSY_POLL + ETHTOOL coalesce rx-usecs 1,保证 flush 及时完成。
- 衡量指标:
- 业务 RT P99 增量 ≤ 0.1 ms;
- CPU si 下降 ≥ 30 %;
- 网卡 PPS 下降 ≥ 40 %,带宽利用率提升 ≥ 5×。
答案
“我们采用‘预测型微批’加‘用户态 vectored I/O’两步法,把 PPS 降下去,同时把 RT 的增量压到 100 µs 以内。
第一步,用户态发线程维护一个 per-NIC-txq 的 lock-free 环形数组,数组项是 64 B 业务消息。当以下任一条件触发立即 flush:
- 数组填满 24 项(24×64≈1.5 kB,等于一个 MTU);
- 从首条入队到当前时间差 ≥ 1 µs(用 RDTSC 读 CPU 周期,换算后约 3 000 cycles@3 GHz,远小于一次上下文切换)。
flush 时一次性调用 sendmmsg 下发 24 条消息,内核走 GSO 合并为单包,网卡 DMA 一次,PPS 理论下降 24×。
第二步,收端配置 NIC 开启静态 LRO(ethtool -K eth0 lro on),并调大 coalesce rx-frames 32、rx-usecs 1,保证 24 合 1 的包在硬件 LRO 缓存里立即重组,不引入额外中断延迟。
第三步,灰度验证:用 Moq 自研压测框架打 1 000 w 在线长连接,每连接 100 qps 小包,对比基线。结果 PPS 从 9.8 Mpps 降到 0.41 Mpps,CPU si 从 38 % 降到 9 %,业务 RT P99 只增加 0.08 ms,满足 SLA。
如果未来业务模型变为单连接 1 w qps,可把 flush 阈值改成‘字节 ≥ 4 kB 或时间 ≥ 500 ns’,仍能保持 RT 增量 < 0.1 ms。”
拓展思考
- 若业务为 UDP 且对顺序不敏感,可自定义“包头带长度数组”的批量协议,一次 sendmsg 携带 32 条消息,接收端用 recvmmsg 批量收,PPS 下降 32×,且无需内核 LRO。
- 在 Service Mesh 场景,Sidecar 到业务进程走 UDS,可开启 SCM_RIGHTS + VSOCK 把多消息一次性注入,避免内核协议栈,PPS 瓶颈转移到进程间通信,延迟可压到 20 µs 以内。
- 如果未来网卡升级到 400 Gbps 且单口 PPS 上限 500 Mpps,上述微批逻辑可能成为 CPU 瓶颈,可改用 DPDK 用户态 PMD,把“预测型微批”逻辑下沉到网卡 FPGA,实现真正的零拷贝、零延迟合并。