首页 游戏 软件 排行 专题

网卡悄悄丢包,ping 却全绿:软中断队列堵死的 3 层

来源: 本站整理 日期:2026/11/3 15:16:34

上周处理一台跑网关的机器,业务方反馈「上午十点开始偶发超时,重启一下就好」。我第一反应还是老套路:ping 一下网关,通的;mtr 一下路由,也通;ping 公网 IP,还是通。既然链路没问题,那就往主机内部查。结果一查 `ip -s link`,`RX dropped` 那个计数器已经涨到两千多万,而且还在涨网卡在收包,但内核没全部收下。

如果你也在排查「带宽一跑满就开始丢包、但 ping 看着还挺正常」这类问题,可以先看看这几家的网络方案再决定要不要换机器:Back Waves(IEPL/IP Transit 专线与全球网络)、VMRack(游戏金融级零抖动全球网络)。专线和优化线路在物理层面能减少拥塞丢包,但像今天这种主机内部队列排队,得靠系统参数和业务调优。

这篇就把网卡收包这件事拆成三层来讲清楚:包到达网卡之后的硬件缓冲、内核的收包队列(`netdev_max_backlog`)、以及软中断把包交给协议栈的速度。任何一层堵住,表现都是「丢包」,但排查入口完全不同。

第一层:网卡硬件缓冲满了,驱动直接丢

先看最外层。网卡收到一个以太网帧后,会先放进自己的硬件接收缓冲(RX ring buffer),然后由驱动批量交给内核。这个环形缓冲区的大小写在 `/sys/class/net/eth0/queues/rx-0/rps_cpus` 之外,主要看 `ethtool -g eth0` 里的 RX ring 参数:

ethtool -g eth0

典型输出里 Prealloc 那一项如果只有 256,说明每个中断最多只能预分配 256 个包,超了就得动态分配,延迟和丢包概率都会上去。VirtIO 网卡的 virtio_net_rxq_*、KVM 下的队列数量,都跟这一层直接相关。这一层的典型特征是 `ip -s link` 里 `dropped` 和 `errors` 一起涨,而且 `/proc/net/softnet_stat` 里对应的 CPU 列 `dropped` 也在涨。

硬件缓冲这层的排查命令是 ethtool -S eth0,会打出 rx_missed_errors、rx_no_buffer_count 这类驱动级计数器。如果这些数在涨,那就是包来得比驱动取走的快,最有效的解法是把中断亲和性打散,别让所有流量都压在 CPU0 上:

cat /proc/interrupts | grep eth0

把网卡 IRQ 分布到多个队列、多个 CPU 核上,硬件缓冲积压的速度会立刻降下来。云主机上没法加网卡时,这一层能做的有限,往往要靠上一层的队列参数兜住。

Linux内核网络栈数据包队列积压示意图

第二层:内核收包队列 backlog 溢出

包被驱动交给内核后,会先进一个「每 CPU 的收包队列」,排队等待软中断处理。这个队列的长度由 `netdev_max_backlog` 控制,默认常见值是 1000;老内核是 300。队列满了,新来的包就被内核直接丢掉,这个丢弃计数落在 `/proc/net/softnet_stat` 里,而不是网卡的 `dropped` 里。

很多人查丢包只盯着 `ip -s link`,看到 `RX dropped` 涨就想去调 `netdev_max_backlog`。但如果硬件缓冲那层没堵、`RX dropped` 也没涨,而 `softnet_stat` 的第三列(dropped)在涨,那就是 backlog 溢出了。这时候把 `netdev_max_backlog` 调大确实有用:

sysctl -w net.core.netdev_max_backlog=4096

但我上次就是在这儿栽了跟头把 1000 改成 4096,跑满带宽丢包照样发生。原因是这一层只解决了「排队等待」的问题,如果真正的瓶颈在下一层的软中断处理速度上,队列开得再大也只是把丢包推迟、并且让延迟更难看(包排了几千个才被处理,RTT 看起来会凭空多出几十毫秒)。

还有一个细节:`netdev_max_backlog` 只对「NAPI 还没来得及收」的包有意义,对多队列网卡,每个队列的默认 backlog 是 `net.core.netdev_budget` 之外单独的一套(`netdev_max_backlog` 是全局软上限,真实生效值是 `RPS` 分配到每个 CPU 之后的那份)。所以在高包小包的场景下,把 `net.core.rps_sock_flow_entries` 和 RPS 掩码一起调,往往比单独调 backlog 更管用。

工程师在终端查看服务器网络吞吐监控

第三层:软中断处理不过来,socket 缓冲区也满

再往里就是协议栈和 socket 了。包通过软中断进入内核协议栈,最终投递给应用程序的 socket 接收缓冲区。如果应用程序读得慢(比如自己的服务是 PHP-FPM、或者数据库在刷脏页),socket 缓冲会满,Linux 只能丢掉后续数据,并且把压力回传给 TCP表现为重传、拥塞窗口收缩。这层在应用侧的典型现象是:连接建立没问题,小请求正常,大响应体经常被截断,或者客户端报「Connection reset by peer」。

怎么确认是这一层?盯两个数:

ss -lnt | head -20

看 Recv-Q 是不是持续堆积但 Send-Q 为空,这说明应用没及时读;再配合 nstat -az | grep -i drop,重点看 TcpExtListenOverflows 和 TcpExtListenDrops,这两个数一涨,就是 accept 队列(backlog 参数)满了,属于应用侧压根没调用 accept。

软中断本身的 CPU 开销可以用 /proc/interrupts 里软中断行(LOC 那一列)看到。如果某个核的软中断时间长期接近 100%,说明这个核是瓶颈。这时候要么把 RPS 掩码打散,要么干脆用 ethtool -C eth0 rx-usecs 调整中断合并,把中断频率降下来、每次处理的包变多,CPU 占用会平滑不少。

网卡环形缓冲区积压溢出告警示意图

多台服务器通过交换机互联的等距示意图

三层之间怎么串起来看

我现在的排查顺序基本固定了,三条命令按顺序跑一遍,基本能定位到层:

ip -s link | grep -A1 eth0 看硬件层,cat /proc/net/softnet_stat 看内核队列层,ss -lnt 看 socket 层。三个计数器哪个在涨,就先动对应的那一层参数,不要一上来就 sysctl 一把梭哈。

顺便提醒一句,内核的参数改完只是临时的,重启就丢。要长期生效得写进 /etc/sysctl.d/ 下的文件;但在容器里这招不一定管用netns 隔离下有些参数是宿主全局的,你改完自己生效了,别人家的容器却跟着受影响。这点在多租户机器上尤其要小心。

最后回到开头那台机器:根因是网关进程把流量全打在 CPU0 上,软中断挤在单核,backlog 溢出。我把 RPS 掩码打散到 8 个核,再把 netdev_max_backlog 从 1000 提到 4096,`RX dropped` 归零,业务超时也没再出现。

所以下次再看到「ping 全绿但业务超时」,先别急着换线路,先去 /proc/net/softnet_stat 看看,那一列数字比 ping 诚实得多。

玩家留言 跟帖评论
查看更多评论
相关文章
猜你喜欢
同类下载