Appearance
通用网卡通信时延优化方案
本文面向对通信时延和抖动敏感的网卡业务场景,例如高频交易、实时音频、工业控制、云游戏、运动控制和低延迟数据采集等。时延优化的目标是缩短数据从应用到网卡、从网卡到应用的路径,并减少调度、中断、排队和协议栈处理带来的等待时间。
需要注意:低时延优化经常与高吞吐优化存在取舍。例如增大 GRO 聚合数、增大缓冲区、提高中断聚合程度可能提升吞吐,但会增加等待时间。因此实际调优时应先明确业务目标:低时延优先、吞吐优先,还是二者平衡。
优化思路
| 方向 | 目标 | 常见手段 |
|---|---|---|
| 缩短路径 | 减少协议栈处理开销 | AF_PACKET 旁路、驱动轮询、减少中间队列 |
| 减少调度抖动 | 降低线程迁移和抢占 | 线程绑核、提高优先级 |
| 减少等待 | 降低中断和聚合等待 | 减小中断聚合、降低 GRO 聚合数 |
| 降低排队 | 避免大缓冲堆积 | 控制 socket 缓冲区、ring 深度和应用处理速率 |
| 稳定 CPU 资源 | 保障关键线程运行 | 将业务线程放到相对空闲 CPU 核 |
发送方向优化
使用 AF_PACKET 旁路协议栈
如果业务报文可以直接以二层以太网帧形式发送,可通过 AF_PACKET 接口绕过传统协议栈路径,将业务报文直接发送到设备二层处理单元。
适用场景:
- 自定义二层协议;
- 对 UDP/TCP 协议栈能力依赖较少;
- 对发送路径确定性要求高;
- 业务自身可处理报文格式、校验和、重传或可靠性策略。
注意:旁路协议栈会降低通用协议能力,需要业务自行承担更多报文处理逻辑。
发送线程绑核
将发送线程绑定到相对空闲的 CPU 核,减少线程在不同核心之间迁移带来的 cache 失效和调度抖动。
bash
# 4010032 为 thread id,3 为 cpu id
affinity 4010032 3建议:
- 将发送线程、相关业务计算线程尽量放在固定核心或相邻核心。
- 避免与高负载网络协议栈线程、中断线程、后台任务抢同一个核心。
- 调优前后记录平均时延和 P99/P999 抖动。
提高发送线程优先级
提高发送线程优先级,让 CPU 更优先处理报文发送逻辑。
bash
# 15 为优先级,4010032 为 thread id
sprio 15 4010032建议:
- 优先级不宜盲目调到最高,避免影响系统关键线程。
- 与绑核配合使用效果更稳定。
- 调整后需要观察系统整体响应和其他实时任务是否受影响。
接收方向优化
接收线程与应用线程绑核
将接收线程绑定到固定 CPU 核,并将消费该数据的应用线程绑定到同一核心或相邻核心,减少跨核迁移和数据 cache 迁移成本。
bash
# 401002c 为接收线程 id,3 为 cpu id
affinity 401002c 3如果网卡驱动、中断线程、协议栈线程可识别,建议将关键接收路径上的线程放到固定核心,并避开其他高负载任务。
提高接收线程优先级
提高接收线程优先级可以减少报文到达后等待调度的时间。
bash
# 15 为优先级,401002c 为接收线程 id
sprio 15 401002c该方法适合报文频率较高但每包处理量较小的业务。若应用处理逻辑较重,应同时优化应用线程优先级和处理路径。
使用 AF_PACKET 接收
如果业务可直接处理二层报文,可通过 AF_PACKET 将报文直接送到应用,减少协议栈解析、分发和排队过程。
适用场景:
- 自定义二层协议;
- 低延迟行情、采集、控制报文;
- 应用侧可直接解析以太网帧;
- 不依赖 TCP/UDP socket 语义。
使用驱动轮询
如果驱动支持轮询模式,可由应用主动调用轮询服务函数从驱动抓取报文,规避中断等待和中断调度开销。
常用函数:
netdev_poll_enablenetdev_poll_svc
示例:
c
int main(int argc, char **argv)
{
static char *if_name = NULL;
struct netdev *pNetDev = NULL;
int ret;
if_name = argv[1];
pNetDev = netdev_find_by_ifname(if_name);
ret = netdev_poll_enable(pNetDev, poll_rcv, NULL);
if (ret) {
return ret;
}
while (1) {
netdev_poll_svc(pNetDev);
}
return 0;
}轮询模式可降低接收等待时间,但通常会提高 CPU 占用。适合低延迟优先且可独占部分 CPU 资源的业务。
降低中断聚合等待
如果驱动支持中断聚合,可降低聚合个数或缩短聚合时间,减少报文在网卡或驱动中等待“凑批”的时间。
注意:降低中断聚合会显著提高中断频率和 CPU 占用,可能影响系统吞吐和其他任务。建议结合 CPU 余量和业务时延目标逐步调整。
与吞吐量优化的取舍
低时延优化和吞吐量优化常见冲突如下:
| 配置 | 对吞吐量 | 对时延 |
|---|---|---|
| 增大 GRO 聚合数 | 通常提升 | 增加等待和抖动 |
| 增大 socket 缓冲区 | 减少断流 | 增加排队延迟 |
| 增大 RX/TX 描述符 | 吸收突发流量 | 可能增加排队深度 |
| 提高中断聚合 | 降低 CPU 中断压力 | 增加报文等待时间 |
| 轮询接收 | 可降低等待 | CPU 占用升高 |
| 线程绑核/提优先级 | 稳定关键路径 | 可能挤占其他任务 |
推荐调优流程
- 建立基线:记录平均时延、P99/P999 时延、CPU 占用、吞吐量和丢包。
- 固定关键线程:对发送线程、接收线程和应用处理线程进行绑核。
- 提高关键线程优先级:优先保障收发路径线程调度。
- 减少协议栈路径:评估是否使用 AF_PACKET 或驱动轮询。
- 降低等待类配置:减小中断聚合、降低 GRO 聚合数、控制缓冲区和 ring 深度。
- 复测吞吐影响:低延迟配置完成后,确认吞吐仍满足业务最低要求。
- 长期稳定性验证:进行长时间压测,观察时延抖动、丢包、CPU 占用和系统稳定性。
验证指标
低时延场景不应只看平均值,建议同时关注:
| 指标 | 说明 |
|---|---|
| 平均时延 | 基础参考值,但不能代表极端抖动 |
| P99 / P999 | 衡量尾延迟,对实时业务更关键 |
| 最大时延 | 判断是否存在异常调度或中断阻塞 |
| 抖动 | 反映时延稳定性 |
| CPU 占用 | 判断是否因轮询或高频中断导致资源耗尽 |
| 丢包/重传 | 判断低延迟配置是否破坏链路稳定性 |
经验结论
低时延优化的关键不是单纯打开某个开关,而是让报文路径更短、关键线程更稳定、等待队列更少。对于极低延迟场景,可优先考虑绑核、线程优先级、AF_PACKET、轮询接收和降低中断聚合;对于吞吐和低延迟都重要的场景,应保留适度聚合和缓冲,避免 CPU 被中断打满。