小包 64 字节导致 PPS 跑满,如何合并请求又不增加延迟

解读

  1. 场景定位:国内互联网高并发网关、消息中间件或实时推荐系统,常见 100 Gbps 网卡,但内核默认 1.5 kB MTU,64 B 小包把 PPS(Packet Per Second)先打满,带宽利用率却不到 5 %,CPU 软中断飙高,业务 RT 抖动。
  2. 面试意图:考察候选人是否理解“PPS 瓶颈 ≠ 带宽瓶颈”,能否在“低延迟”硬约束下做数据面优化,而不是简单回答“开启 Nagle”或“批量发”。
  3. 关键矛盾:合并意味着攒包,攒包意味着等待,等待意味着延迟上涨;需要给出“零额外延迟”或“可控微延迟”的量化方案,并说明如何验证。

知识点

  1. 小包灾难三件套:
    • 每包 84 B(64B payload + 20B IP header)物理层实际 84+38=122 B on wire;
    • 网卡 DMA 固定开销 > 150 ns/包;
    • 内核协议栈一次软中断 ~1 µs,10 Mpps 即 10 k 中断/ms,单核打满。
  2. 合并技术分类:
    • 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)。
  3. 零延迟攒包条件:
    • 输出队列空且 NIC 有 Budget → 立即发;
    • 输出队列非空 → 走攒包逻辑,但设定“超时≤1 µs 或字节≤MTU”即 flush;
    • 使用 SO_BUSY_POLL + ETHTOOL coalesce rx-usecs 1,保证 flush 及时完成。
  4. 衡量指标:
    • 业务 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:

  1. 数组填满 24 项(24×64≈1.5 kB,等于一个 MTU);
  2. 从首条入队到当前时间差 ≥ 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。”

拓展思考

  1. 若业务为 UDP 且对顺序不敏感,可自定义“包头带长度数组”的批量协议,一次 sendmsg 携带 32 条消息,接收端用 recvmmsg 批量收,PPS 下降 32×,且无需内核 LRO。
  2. 在 Service Mesh 场景,Sidecar 到业务进程走 UDS,可开启 SCM_RIGHTS + VSOCK 把多消息一次性注入,避免内核协议栈,PPS 瓶颈转移到进程间通信,延迟可压到 20 µs 以内。
  3. 如果未来网卡升级到 400 Gbps 且单口 PPS 上限 500 Mpps,上述微批逻辑可能成为 CPU 瓶颈,可改用 DPDK 用户态 PMD,把“预测型微批”逻辑下沉到网卡 FPGA,实现真正的零拷贝、零延迟合并。