摘要:“netstat -s 显示有 drop,tcpdump 却一个包都没抓到”是排障现场最常见的悖论之一。本文以 Linux 收包路径为分析主线,把丢包拆成“抓包点之前”与“抓包点之后”两个空间,逐层定位网卡环形缓冲、NAPI 轮询预算、软中断 backlog、协议栈入口与套接字接收队列五类丢包点,并给出基于 /proc/net、dropwatch、perf 与 eBPF 的可操作定位路径。核心结论是:tcpdump 只能看见“已经进入协议栈”的包,凡是在 netif_receive_skb 之前被丢弃的帧,抓包工具天然不可见。
目录
一、引言:一个被反复误读的“矛盾”
在线上排障中,几乎每个做网络的人都会遇到这样的场景:业务侧反馈丢包,运维登录机器执行 netstat -s,输出里 packet receive errors 或 receive buffer errors 在持续增长;于是立刻在对应网卡上开 tcpdump -i eth0,结果抓了几分钟,一个异常包都没看到,甚至抓到的包数量与业务侧统计的发送量完全对得上。于是问题就变成了:既然内核说丢了包,为什么抓包工具抓不到?包到底是在哪一层被丢掉的?
这个“矛盾”之所以反复出现,根源在于多数人对 netstat -s 的计数器语义和 tcpdump 的抓包位置存在双重误解。前者把来自不同子系统、不同统计口径的计数器汇总成一个“drop”印象;后者则只在协议栈的特定钩子点上复制数据,凡是没走到这个钩子的帧,它根本无从知晓。本文的主线就是:以“抓包点”为分界线,把丢包拆成“抓包点之前”和“抓包点之后”两个空间,逐层定位。
需要先说明本文的分析边界。本文讨论的是 Linux 内核(以 5.x/6.x 主线为参照)在接收方向上的丢包,聚焦以太网与 IP 层,不展开 TCP 重传、拥塞控制等传输层语义;涉及具体计数器名称时以 /proc/net/snmp、/proc/net/netstat、/proc/net/softnet_stat 与 ethtool -S 的实际输出为准。本文评述:把“drop”当作单一事实是一种危险的简化,工程上必须把它还原为“哪个计数器、在哪个代码路径、由什么条件触发”的三元组,否则任何调优都是盲目的。
二、背景与问题定义:drop 计数到底在数什么
2.1 netstat -s 的计数器来源与口径
netstat -s 本质上是读取 /proc/net/snmp 与 /proc/net/netstat 并按协议族格式化输出。它里面的“drop”并不是一个统一计数器,而是至少来自三个不同层次的统计:IP 层的 InDiscards、TCP/UDP 层的 InErrors 与 RcvbufErrors,以及套接字层的 drops。它们的触发条件、代码位置和是否经过抓包点,完全不同。
更隐蔽的是,netstat -s 并不会把网卡驱动层的丢包(例如 ring buffer 溢出、DMA 失败)计入其中,这些数字只出现在 ethtool -S 的驱动私有计数里,以及 /proc/net/dev 的 rx_dropped 中。因此,当有人说“netstat -s 有 drop”时,第一件事不是抓包,而是确认这个 drop 究竟来自哪个文件、哪一行。
# 网卡层丢包(驱动/ring buffer)
cat /proc/net/dev | awk '{print $1, $4, $5}'
ethtool -S eth0 | grep -iE 'drop|discard|miss|err'
# 软中断层丢包(backlog / 预算耗尽)
cat /proc/net/softnet_stat
# 协议栈层丢包(IP/TCP/UDP)
grep -E 'InDiscards|InErrors|RcvbufErrors' /proc/net/snmp /proc/net/netstat
2.2 tcpdump 的抓包位置决定了它的“盲区”
tcpdump 依赖 libpcap,在 Linux 上通过 AF_PACKET 套接字接收数据。其数据来源是内核在 netif_receive_skb 路径上分发给 ptype_all 链表的副本(传统路径),或通过 dev_queue_xmit_nit 在发送方向复制。这意味着:任何在进入协议栈主路径之前就被丢弃的帧,tcpdump 都看不到。这不是 tcpdump 的 bug,而是它的设计位置决定的。
这里有一个常被忽略的细节:在启用 RPS/RFS 或使用 XDP 的现代内核上,包的分发路径已经发生变化,AF_PACKET 的 tap 点可能位于 RPS 之后、也可能位于 XDP 之后,取决于内核版本与配置。本文评述:把 tcpdump 当作“地面真值”是一种方法论错误,它只是一个“协议栈视角的观测点”,而不是“物理链路的观测点”。真正的地面真值应当来自网卡硬件计数器与交换机端口统计。
三、原理:Linux 收包路径与五类丢包点
3.1 收包路径全景
一个以太网帧从到达网卡到被应用读取,大致经过以下阶段:网卡 DMA 写入 RX ring → 触发硬中断 → 驱动在中断处理中关闭中断并调度 NAPI → 软中断中 napi_poll 调用驱动 poll 函数取包 → napi_gro_receive → netif_receive_skb(此处分发到 ptype_all,tcpdump 可见)→ 协议栈处理 → 放入套接字接收队列 → 应用 recv() 读取。
在这条链路上,至少有五个位置会因资源不足而丢包,且它们是否被 tcpdump 看见,取决于是否位于 netif_receive_skb 之前。下表给出这五类丢包点的对照。
| 丢包点 | 触发条件 | 观测计数器 | tcpdump 可见 |
|---|---|---|---|
| RX ring 溢出 | DMA 写入时描述符耗尽 | ethtool -S rx_dropped / rx_missed | 否 |
| NAPI 预算耗尽 | 单次 poll 处理包数超 netdev_budget | softnet_stat 第 2 列 | 部分可见 |
| backlog 队列满 | enqueue_to_backlog 失败 | softnet_stat 第 1 列 | 否 |
| 协议栈入口丢弃 | 校验和错、无路由、防火墙 | snmp InDiscards/InErrors | 是 |
| 套接字队列满 | sk_rmem_alloc 超 rcvbuf | snmp RcvbufErrors | 是 |
3.2 RX ring 与 DMA:最容易被 tcpdump 漏掉的一层
网卡收到帧后,通过 DMA 把数据写入驱动预先分配的 RX 描述符环形缓冲区。如果 CPU 来不及消费,环形缓冲区被填满,后续到达的帧就没有描述符可用,网卡只能直接丢弃。这一层丢包发生在驱动与硬件之间,数据从未进入内核网络栈,因此 tcpdump 完全不可见。它的典型特征是:ethtool -S 中 rx_dropped、rx_missed_errors、rx_no_buffer_count 等计数增长,而 /proc/net/dev 的 rx_dropped 也可能同步增长。
判断 ring 是否过小的一个经验方法是观察 ethtool -g eth0 的当前值与最大值,并结合 ethtool -S 的丢弃计数变化率。若在流量高峰时丢弃计数持续增长,而 CPU 软中断并未饱和,则优先怀疑 ring 过小或中断合并参数不当。本文评述:ring buffer 调优不是越大越好,过大的 ring 会增加单次丢包时的突发延迟,且占用更多 DMA 内存;合理的做法是结合 rx-usecs 中断合并参数一起调整,而不是单纯拉满。
3.3 NAPI 预算与 softnet_stat:最常被误读的计数器
/proc/net/softnet_stat 是定位软中断层丢包的核心文件。每一行对应一个 CPU,十六进制输出。其中第 1 列是 processed(已处理包数),第 2 列是 dropped(因 backlog 队列满而丢弃的包数),第 3 列是 time_squeeze(因预算耗尽而提前结束 poll 的次数)。很多资料把第 2 列直接称为“NAPI 丢包”,这是不准确的:它统计的是 enqueue_to_backlog 失败,即包在进入 per-CPU backlog 队列时被丢弃,而不是 NAPI poll 内部丢弃。
真正与 NAPI 预算相关的是 time_squeeze。当一次 net_rx_action 处理的总包数超过 netdev_budget(默认 300)或总时间超过 netdev_budget_usecs(默认 2000 微秒)时,软中断会主动让出 CPU,剩余包留待下次调度。这本身不是丢包,但如果 backlog 队列在此期间被填满,就会触发第 2 列的丢弃。本文评述:把 time_squeeze 增长直接等同于丢包是一种常见误判,它更多反映 CPU 争用与调度延迟,需要结合第 2 列与业务侧重传率一起看。
# 列: processed dropped time_squeeze ...
# 以下为模拟数据,仅用于说明列含义
0001a2b3 0000000c 0000002f ...
# 第2列 0x0c = 12 次 backlog 入队失败
# 第3列 0x2f = 47 次预算耗尽提前退出
3.4 协议栈入口与套接字队列:tcpdump 能看见但仍会丢
当包成功进入 netif_receive_skb 之后,tcpdump 就能看到它了。但这并不意味着它最终会被应用收到。IP 层会因校验和错误、TTL 耗尽、无路由、防火墙规则等原因丢弃,这些计入 InDiscards 或 InErrors;UDP 层在套接字接收队列满时会丢弃并计入 RcvbufErrors;TCP 层虽然会通过窗口控制避免接收队列溢出,但在零窗口探测失败或内存压力下仍可能丢包。
这一类丢包的特点是:tcpdump 能看到包进来,但应用层统计不到。因此,如果 tcpdump 抓到了包而应用侧没有收到,问题就在协议栈内部或套接字队列,而不是网卡层。本文评述:排障时应先建立“tcpdump 是否可见”这一二分,再在各自空间内细分,这比一上来就调 ring buffer 或改内核参数要高效得多。
四、关键数据与案例:从计数器反推丢包位置
4.1 一套可复用的定位流程
下面给出一条经过工程验证的定位路径,按“先分层、再定位、后验证”的顺序推进。每一步都给出具体命令与判据,读者可直接在目标机器上执行。
# 网卡层
ethtool -S eth0 | grep -iE 'drop|miss|err|no_buffer'
# 软中断层
awk '{print $1, $2, $3}' /proc/net/softnet_stat
# 协议栈层
netstat -s | grep -iE 'drop|error|buffer'
# 套接字层
ss -lntu # 观察 Recv-Q 是否持续非零
# 安装后运行,观察 kfree_skb 调用点
dropwatch -l kas
# 输出示例(模拟):
# 0xffffffff8a1b2c3d netif_receive_skb_internal+0x1d
# 0xffffffff8a1b4e5f enqueue_to_backlog+0x8f
dropwatch 的原理是跟踪 kfree_skb 及其变体,并打印调用栈。它能直接告诉你包是在哪个内核函数里被释放的,是定位“抓不到”问题的最有效工具之一。本文评述:dropwatch 的输出需要结合内核符号表解读,且在高流量下自身开销不可忽略,建议在复现窗口内短时开启,而不是长期挂载。
# 使用 bpftrace 跟踪 kfree_skb 的调用者
bpftrace -e 'kprobe:kfree_skb { @[kstack] = count(); }'
# 跟踪 backlog 入队失败
bpftrace -e 'kretprobe:enqueue_to_backlog /retval != 0/ { @ = count(); }'
4.2 一个典型的“抓不到”案例复盘
某在线服务在流量高峰期出现 UDP 丢包告警,netstat -s 显示 RcvbufErrors 持续增长,但 tcpdump 抓包数量与发送端统计一致,未发现异常。按前述流程排查:ethtool -S 无丢弃,softnet_stat 第 2 列无增长,说明包已成功进入协议栈。进一步用 ss -lun 观察发现目标 UDP 套接字的 Recv-Q 长期处于高位,最终定位为应用读取速度不足、SO_RCVBUF 偏小。
这个案例的关键启示是:tcpdump 抓得到包,不代表应用收得到包。RcvbufErrors 属于“抓包点之后”的丢包,tcpdump 当然能看到包进来,但它在套接字队列处被丢弃了。本文评述:很多团队把“抓包正常”当作“链路正常”的证据,这在 UDP 场景下尤其危险,因为 UDP 没有重传与窗口控制,接收队列溢出是静默的。
4.3 数据来源与预处理说明
本文引用的内核默认参数(netdev_budget=300、netdev_budget_usecs=2000、netdev_max_backlog=1000)来自 Linux 内核源码 net/core/dev.c 与 include/linux/netdevice.h 的默认定义,不同发行版可能通过 sysctl 覆盖,实际值应以 sysctl net.core.netdev_budget 等命令为准。文中标注为“模拟数据”的表格与输出仅用于说明格式与列含义,不代表任何真实生产环境统计。
五、对比与局限:tcpdump、dropwatch、eBPF 的能力边界
5.1 三类工具的能力对照
| 工具 | 观测位置 | 能否看到 ring 丢包 | 开销 |
|---|---|---|---|
| tcpdump | netif_receive_skb 之后 | 否 | 中高 |
| dropwatch | kfree_skb 调用点 | 部分(依赖驱动是否调用) | 中 |
| eBPF/bpftrace | 任意内核函数 | 可(跟踪驱动) | 低(需谨慎) |
| ethtool -S | 驱动统计 | 是 | 极低 |
从表中可以看出,没有任何单一工具能覆盖全部丢包点。ethtool 能看到硬件层但看不到协议栈细节;tcpdump 能看到协议栈入口但看不到硬件层;dropwatch 与 eBPF 介于两者之间,但需要内核符号与编程能力。本文评述:合理的做法是建立“分层观测矩阵”,先用 ethtool 与 softnet_stat 做粗筛,再用 dropwatch/eBPF 做精确定位,最后用 tcpdump 验证协议栈行为,而不是指望一个工具解决所有问题。
5.2 局限与常见误区
第一个误区是把 softnet_stat 第 2 列的增长直接归因为“网卡丢包”。实际上它统计的是 backlog 入队失败,通常与 RPS 配置、单 CPU 软中断过载有关,而非网卡硬件。第二个误区是认为“加大 ring buffer 就能解决所有丢包”,但在 CPU 软中断饱和的场景下,加大 ring 只会把丢包点从网卡层推迟到 backlog 层,问题并未消失。第三个误区是忽视 GRO/LRO 对 tcpdump 的影响:启用 GRO 后,tcpdump 看到的是聚合后的大包,与线上实际帧不一致,容易误判包大小与数量。
此外,虚拟化与容器环境引入了额外变量。veth 对、网桥、OVS 数据路径都可能在 netif_receive_skb 之前或之后引入新的丢包点,且这些点的计数器分散在 /proc/net/dev 的虚拟接口条目中。本文评述:在容器化环境中,排障必须把“宿主机物理网卡—网桥/veth—容器内网卡”三段分别观测,任何一段的 drop 都可能导致应用侧丢包,而 tcpdump 在容器内只能看到最后一段。
六、结论与展望:把“抓不到”变成“可解释”
回到最初的问题:netstat -s 有 drop 计数而 tcpdump 抓不到,内核在哪个环节把包丢了?答案不是单一的,而是取决于 drop 计数来自哪个文件。若来自 ethtool -S 或 /proc/net/dev 的 rx_dropped,丢包点在 RX ring 或 DMA,tcpdump 天然不可见;若来自 softnet_stat 第 2 列,丢包点在 backlog 入队,同样在抓包点之前;若来自 snmp 的 RcvbufErrors,丢包点在套接字队列,tcpdump 能看到包但应用收不到。
因此,正确的排障顺序应当是:先定位计数器来源,再判断是否位于抓包点之前,最后选择对应工具验证。这条主线可以把大量“玄学丢包”转化为可解释、可复现、可验证的工程问题。展望未来,随着 XDP、AF_XDP 与 eBPF 在生产环境的普及,收包路径的可观测性正在从“事后抓包”转向“路径内埋点”,内核社区也在持续改进 drop_monitor 与 tracepoint 的覆盖范围。本文评述:工具会演进,但“以抓包点为分界、以计数器为线索”的分析框架不会过时,掌握这条主线比记住某个命令更重要。
拓展阅读方面,建议结合内核文档与社区教程动手验证:Linux 内核网络文档、net/core/dev.c 源码、Brendan Gregg 的 eBPF 资料。这些资源能帮助读者把本文的路径图与真实代码对应起来。
主要参考文献
- Linux Kernel Documentation. Networking. kernel.org, 2024.
- Torvalds L. et al. net/core/dev.c. Linux kernel source, 2024.
- Gregg B. BPF Performance Tools. Addison-Wesley, 2019.
- Høiland-Jørgensen T. et al. The eXpress Data Path. ACM CoNEXT, 2018.
- Red Hat. Monitoring and Tuning the Linux Networking Stack. 2023.
- Cloudflare Blog. How to Drop 10 Million Packets per Second. 2017.
- Linux Foundation. softnet_stat 与 NAPI 调优实践. 2023.
- IETF RFC 9293. Transmission Control Protocol (TCP). 2022.

