“丢包”是网络故障排查中最常见也最棘手的问题。一个包可能在内核的几十个位置被丢弃——网卡
ring buffer 满、校验和错误、Netfilter 规则拒绝、socket
缓冲区溢出、TCP 窗口外、XDP 程序丢弃、qdisc 队列满……传统的
ifconfig 和 /proc/net/dev
只能看到笼统的 rx_dropped /
tx_dropped
计数器,无法回答”在哪个内核函数、因为什么原因丢的”这个关键问题。
Linux 内核从 2.6 时代就引入了 drop_monitor
子系统,而 5.17+ 进一步增强了 kfree_skb
tracepoint——现在每次丢包都携带精确的
drop_reason
枚举值。本文从内核源码出发,完整拆解丢包追踪的工具链和实战方法。
一、kfree_skb:丢包追踪的基石
1.1 kfree_skb vs consume_skb
内核中释放 sk_buff 有两个路径:
kfree_skb_reason(skb, reason):表示包被异常丢弃(如校验和错误、规则拒绝),触发kfree_skbtracepointconsume_skb(skb):表示包被正常消费(如应用已读取),触发consume_skbtracepoint
两者的区别是语义上的——kfree_skb
意味着”这个包没有到达它的预期目的地”,是丢包追踪的核心。
1.2 kfree_skb tracepoint 结构
/* include/trace/events/skb.h:24 */
TRACE_EVENT(kfree_skb,
TP_PROTO(struct sk_buff *skb, void *location,
enum skb_drop_reason reason),
TP_ARGS(skb, location, reason),
TP_STRUCT__entry(
__field(void *, skbaddr)
__field(void *, location) /* 丢包的内核函数地址 */
__field(unsigned short, protocol) /* 协议号(ETH_P_IP 等) */
__field(enum skb_drop_reason, reason) /* 丢弃原因枚举 */
),
...
);三个关键字段:
location:调用kfree_skb_reason()的内核函数地址,通过ksym()或/proc/kallsyms解析为函数名protocol:被丢弃包的以太网协议号reason:enum skb_drop_reason枚举值,精确标识丢弃原因
1.3 consume_skb tracepoint
/* include/trace/events/skb.h:54 */
TRACE_EVENT(consume_skb,
TP_PROTO(struct sk_buff *skb, void *location),
TP_ARGS(skb, location),
TP_STRUCT__entry(
__field(void *, skbaddr)
__field(void *, location)
),
...
);consume_skb 没有 reason
字段——正常消费不需要理由。在追踪中,可以通过同时监听两个
tracepoint 来区分正常消费和异常丢弃。
二、drop_reason:80+ 种丢包原因全解
2.1 核心丢包原因枚举
enum skb_drop_reason 定义在
include/net/dropreason-core.h,通过
DEFINE_DROP_REASON
宏展开。以下按网络栈层次分类:
L2/驱动层丢包:
| reason | 含义 | 常见触发场景 |
|---|---|---|
FULL_RING |
网卡 ring buffer 满 | 网卡接收速率超过 CPU 处理速率 |
CPU_BACKLOG |
per-CPU backlog 队列满 | RPS/RFS 分发后目标 CPU 处理不过来 |
DEV_HDR |
设备头部错误 | VLAN 头、PPP 头异常 |
DEV_READY |
设备未就绪 | 接口 down 或正在初始化 |
OTHERHOST |
包不属于本机 | 混杂模式下收到他人的包 |
L3(IP)层丢包:
| reason | 含义 | 常见触发场景 |
|---|---|---|
IP_CSUM |
IP 校验和错误 | 硬件卸载故障或链路噪声 |
IP_INHDR |
IP 头部异常 | 头部长度、版本号错误 |
IP_RPFILTER |
反向路径过滤失败 | 非对称路由且 rp_filter=1 |
IP_NOPROTO |
不支持的 IP 协议 | 未加载对应协议模块 |
IP_INADDRERRORS |
目标地址错误 | 非本机地址且未开启转发 |
IP_INNOROUTES |
无路由 | 路由表中无匹配项 |
IP_OUTNOROUTES |
出口无路由 | 发送时找不到出口路由 |
PKT_TOO_BIG |
包太大(超过 MTU) | MTU 不匹配且 DF 位置位 |
PKT_TOO_SMALL |
包太小 | 小于最小合法长度 |
L4(TCP)层丢包:
| reason | 含义 | 常见触发场景 |
|---|---|---|
TCP_CSUM |
TCP 校验和错误 | 链路损坏或中间设备修改 |
TCP_FLAGS |
TCP 标志位异常 | 无效的标志组合 |
TCP_ZEROWINDOW |
零窗口 | 接收端窗口为 0 |
TCP_OLD_DATA |
旧数据 | 已确认的数据重复到达 |
TCP_OVERWINDOW |
超出接收窗口 | 序列号超出接收窗口范围 |
TCP_OFOMERGE |
OFO 队列合并丢弃 | 乱序队列中的重叠段 |
TCP_RFC7323_PAWS |
PAWS 时间戳检查失败 | 时间戳回退 |
TCP_OLD_SEQUENCE |
旧序列号 | 重传的旧包 |
TCP_INVALID_SEQUENCE |
无效序列号 | 不在预期范围内 |
TCP_RESET |
RST 包 | 连接被重置 |
TCP_INVALID_SYN |
无效 SYN | SYN 到非 LISTEN socket |
TCP_CLOSE |
连接已关闭 | 包到达已关闭的 socket |
TCP_FASTOPEN |
TFO 失败 | Fast Open cookie 不匹配 |
TCP_OLD_ACK |
旧 ACK | ACK 号低于 snd_una |
TCP_TOO_OLD_ACK |
过旧 ACK | ACK 严重滞后 |
TCP_ACK_UNSENT_DATA |
ACK 未发送数据 | ACK 号超过 snd_nxt |
TCP_OFO_QUEUE_PRUNE |
OFO 队列修剪 | 内存压力下清理乱序队列 |
TCP_OFO_DROP |
OFO 队列丢弃 | 乱序队列满 |
TCP_MINTTL |
TTL 太小 | TTL 低于 min_ttl(BGP 安全) |
L4(UDP)层丢包:
| reason | 含义 | 常见触发场景 |
|---|---|---|
UDP_CSUM |
UDP 校验和错误 | 数据损坏 |
NO_SOCKET |
找不到目标 socket | 无进程监听该端口 |
Socket/缓冲区丢包:
| reason | 含义 | 常见触发场景 |
|---|---|---|
SOCKET_RCVBUFF |
socket 接收缓冲区满 | 应用读取太慢,SO_RCVBUF 太小 |
PROTO_MEM |
协议内存限制 | net.ipv4.udp_mem 等达到上限 |
SOCKET_BACKLOG |
backlog 队列满 | net.core.somaxconn 或 listen()
backlog 不足 |
SOCKET_FILTER |
socket filter 丢弃 | BPF socket filter 返回 0 |
防火墙/策略丢包:
| reason | 含义 | 常见触发场景 |
|---|---|---|
NETFILTER_DROP |
Netfilter 丢弃 | iptables/nftables DROP 规则 |
XDP |
XDP 程序丢弃 | XDP 返回 XDP_DROP |
TC_INGRESS |
TC ingress 丢弃 | TC BPF 返回 TC_ACT_SHOT |
TC_EGRESS |
TC egress 丢弃 | TC egress filter 丢弃 |
BPF_CGROUP_EGRESS |
cgroup BPF 丢弃 | cgroup egress 策略拒绝 |
XFRM_POLICY |
IPsec 策略失败 | XFRM 策略检查不通过 |
队列/调度丢包:
| reason | 含义 | 常见触发场景 |
|---|---|---|
QDISC_DROP |
qdisc 丢弃 | 队列满(pfifo_fast/fq_codel/HTB) |
QUEUE_PURGE |
队列清除 | qdisc 被删除或重置 |
2.2 子系统扩展机制
Linux 6.x 引入了 enum skb_drop_reason_subsys
允许各子系统注册自己的丢包原因:
/* include/net/dropreason.h */
enum skb_drop_reason_subsys {
SKB_DROP_REASON_SUBSYS_CORE, /* 核心丢包原因 */
SKB_DROP_REASON_SUBSYS_MAC80211_UNUSABLE, /* Wi-Fi 不可用帧 */
SKB_DROP_REASON_SUBSYS_MAC80211_MONITOR, /* Wi-Fi 监控帧 */
SKB_DROP_REASON_SUBSYS_OPENVSWITCH, /* Open vSwitch */
SKB_DROP_REASON_SUBSYS_NUM,
};
void drop_reasons_register_subsys(enum skb_drop_reason_subsys subsys,
const struct drop_reason_list *list);
void drop_reasons_unregister_subsys(enum skb_drop_reason_subsys subsys);这意味着 Open vSwitch、mac80211
等子系统可以定义自己的丢包原因,通过同一个
kfree_skb tracepoint 报告。
三、drop_monitor 子系统
3.1 工作原理
drop_monitor 是内核的 netlink
子系统,专门用于监控丢包事件。它注册了
kfree_skb tracepoint 的回调,将丢包事件通过
netlink socket 发送到用户态。
相比直接使用 bpftrace 追踪 kfree_skb
tracepoint,drop_monitor 的优势在于:
- 聚合:在内核态聚合相同位置的丢包事件,减少 netlink 消息量
- 硬件丢包:可以通过
devlink trap接收网卡硬件报告的丢包 - 标准接口:通过 netlink 提供标准化的丢包监控 API
3.2 使用 dropwatch 工具
dropwatch 是 drop_monitor
的用户态前端:
# 安装
apt install dropwatch # Debian/Ubuntu
yum install dropwatch # RHEL/CentOS
# 启动交互模式
dropwatch -l kas
# 在交互模式中
> start
# 输出示例:
# Initalizing kallsyms db
# 1 drops at tcp_v4_rcv+0x3c (sobject: netfilter)
# 5 drops at __udp4_lib_rcv+0x2a1 (sobject: NO_SOCKET)
# 2 drops at nf_hook_slow+0x4b (sobject: netfilter)
> stop
> exitdropwatch
输出每个丢包位置的函数名和聚合计数,快速定位丢包热点。
3.3 devlink trap:硬件丢包监控
现代智能网卡可以通过 devlink trap
机制报告硬件级丢包:
# 列出网卡支持的 trap 类型
devlink trap list pci/0000:03:00.0
# 启用特定 trap
devlink trap set pci/0000:03:00.0 trap source_mac_is_multicast action trap
# 查看 trap 统计
devlink trap show pci/0000:03:00.0四、实战:多工具丢包定位
4.1 bpftrace:按原因聚合丢包
最常用的丢包追踪脚本——按 drop_reason 和
location 聚合:
bpftrace -e '
tracepoint:skb:kfree_skb {
@by_reason[args->reason] = count();
@by_location[ksym(args->location)] = count();
}
interval:s:10 {
printf("\n--- Drop reasons ---\n");
print(@by_reason);
printf("\n--- Drop locations ---\n");
print(@by_location);
clear(@by_reason);
clear(@by_location);
}
'按网络设备过滤:
bpftrace -e '
#include <linux/skbuff.h>
tracepoint:skb:kfree_skb {
$skb = (struct sk_buff *)args->skbaddr;
if ($skb->dev != 0) {
$dev = $skb->dev;
@by_dev[str($dev->name)] = count();
}
@by_reason[args->reason] = count();
}
'4.2 bpftrace:追踪特定原因的丢包调用栈
当发现某个 drop_reason
异常增长时,追踪完整内核调用栈定位根因:
# 追踪 SOCKET_RCVBUFF 丢包的调用栈
bpftrace -e '
tracepoint:skb:kfree_skb
/args->reason == 14/ /* SKB_DROP_REASON_SOCKET_RCVBUFF */
{
printf("SOCKET_RCVBUFF drop at %s protocol=0x%x\n",
ksym(args->location), args->protocol);
print(kstack);
}
'追踪 Netfilter 丢包并显示五元组:
bpftrace -e '
#include <linux/skbuff.h>
#include <linux/ip.h>
tracepoint:skb:kfree_skb
/args->reason == 6/ /* SKB_DROP_REASON_NETFILTER_DROP */
{
$skb = (struct sk_buff *)args->skbaddr;
$iph = (struct iphdr *)($skb->head + $skb->network_header);
printf("NF_DROP: %s -> %s proto=%d\n",
ntop(AF_INET, &$iph->saddr),
ntop(AF_INET, &$iph->daddr),
$iph->protocol);
}
'4.3 perf:记录丢包事件
# 记录 60 秒的丢包事件及调用栈
perf record -a -g --call-graph dwarf \
-e 'skb:kfree_skb' -- sleep 60
# 查看丢包栈
perf script | head -200
# 按丢包函数统计
perf report --sort comm,dso,symbol
# 按 drop_reason 统计
perf script -F event,trace | grep 'reason:' | \
awk -F'reason: ' '{print $2}' | sort | uniq -c | sort -rn4.4 ftrace:最轻量追踪
在无法安装 bpftrace 的环境中(如最小化容器):
# 启用 kfree_skb tracepoint
echo 1 > /sys/kernel/debug/tracing/events/skb/kfree_skb/enable
# 实时查看
cat /sys/kernel/debug/tracing/trace_pipe
# 输出示例:
# <idle>-0 [003] ..s1 12345.678: kfree_skb: skbaddr=0xffff... protocol=2048
# location=tcp_v4_rcv+0x1a3 reason: TCP_INVALID_SEQUENCE
# 过滤特定原因(通过 filter 文件)
echo 'reason == 2' > /sys/kernel/debug/tracing/events/skb/kfree_skb/filter
# 关闭
echo 0 > /sys/kernel/debug/tracing/events/skb/kfree_skb/enable4.5 nstat 和 /proc/net/snmp:统计计数器
在深入追踪之前,先通过统计计数器确认丢包方向:
# TCP 相关统计
nstat -az | grep -i 'drop\|error\|fail\|overflow\|prune\|retrans'
# 关键计数器解读
# TcpExtListenDrops - SYN 队列满丢弃
# TcpExtListenOverflows - Accept 队列满溢出
# TcpExtTCPBacklogDrop - socket backlog 满丢弃
# TcpExtTCPMinTTLDrop - TTL 太小丢弃
# TcpExtTCPRcvQDrop - 接收队列满丢弃
# TcpExtTCPOFODrop - OFO 队列丢弃
# TcpExtTCPRetransFail - 重传失败
# TcpExtPFMemallocDrop - 内存压力丢弃
# UDP 丢包
cat /proc/net/snmp | grep Udp
# Udp: InDatagrams NoPorts InErrors OutDatagrams RcvbufErrors SndbufErrors
# InErrors = 校验和错误等
# RcvbufErrors = 接收缓冲区满丢弃
# NoPorts = 无监听端口
# 网卡级丢包
cat /proc/net/dev | awk 'NR>2{print $1, "rx_drop="$5, "tx_drop="$13}'
# ethtool 统计
ethtool -S eth0 | grep -i 'drop\|error\|discard\|miss\|overflow'五、常见丢包场景诊断手册
5.1 网卡 ring buffer 满(FULL_RING)
症状:ethtool -S 显示
rx_missed_errors 或
rx_no_buffer_count 增长。
诊断:
bpftrace -e '
tracepoint:skb:kfree_skb /args->reason == 67/ { /* FULL_RING */
@by_cpu[cpu] = count();
}
interval:s:5 { print(@by_cpu); clear(@by_cpu); }
'解决:
# 增大 ring buffer
ethtool -G eth0 rx 4096
# 检查当前 ring buffer 大小
ethtool -g eth05.2 socket 接收缓冲区满(SOCKET_RCVBUFF)
症状:nstat 显示
TcpExtTCPRcvQDrop 或
UdpRcvbufErrors 增长。
诊断:
bpftrace -e '
tracepoint:skb:kfree_skb /args->reason == 14/ { /* SOCKET_RCVBUFF */
printf("RCVBUF full at %s\n", ksym(args->location));
@stacks = count();
}
'解决:
# 增大默认接收缓冲区
sysctl -w net.core.rmem_default=262144
sysctl -w net.core.rmem_max=16777216
# 或应用层设置 SO_RCVBUF5.3 Netfilter/iptables 丢弃(NETFILTER_DROP)
症状:iptables -L -v -n
显示 DROP 规则匹配计数增长。
诊断:
bpftrace -e '
tracepoint:skb:kfree_skb /args->reason == 6/ { /* NETFILTER_DROP */
printf("NF drop at %s\n", ksym(args->location));
print(kstack(5));
}
'通过调用栈区分是 INPUT、FORWARD 还是 OUTPUT 链的规则。
5.4 TCP 序列号相关丢弃
症状:nstat 显示
TcpExtTCPPAWSEstab 或
TcpExtTCPSACKDiscard 增长。
诊断:
bpftrace -e '
tracepoint:skb:kfree_skb
/args->reason >= 33 && args->reason <= 44/ /* TCP_OLD_DATA..TCP_OFO_DROP */
{
@tcp_drop_reason[args->reason] = count();
}
interval:s:10 { print(@tcp_drop_reason); clear(@tcp_drop_reason); }
'5.5 qdisc 队列满(QDISC_DROP)
症状:tc -s qdisc show dev eth0
显示 dropped 计数增长。
诊断:
bpftrace -e '
tracepoint:skb:kfree_skb /args->reason == 57/ { /* QDISC_DROP */
printf("qdisc drop at %s\n", ksym(args->location));
@by_cpu[cpu] = count();
}
'解决:
# 增大 txqueuelen
ip link set dev eth0 txqueuelen 10000
# 或更换 qdisc
tc qdisc replace dev eth0 root fq_codel limit 10240六、丢包追踪流程总结
6.1 系统化排查流程
第一步:nstat + /proc/net/snmp 快速定界
↓ 确认丢包方向(RX/TX)和协议(TCP/UDP)
第二步:ethtool -S 检查网卡级丢包
↓ 排除硬件/驱动层问题
第三步:bpftrace kfree_skb 按 reason 聚合
↓ 确定具体丢包原因
第四步:bpftrace 追踪该 reason 的调用栈
↓ 定位到具体内核函数
第五步:根据函数和原因调整配置
→ ring buffer / rcvbuf / iptables / qdisc / sysctl
6.2 drop_reason 快速查询
在生产环境中,可以使用以下脚本快速获取丢包原因的可读名称:
# 一行命令:统计 10 秒内的丢包原因
bpftrace -e '
tracepoint:skb:kfree_skb {
@[args->reason] = count();
}
interval:s:10 { exit(); }
' 2>/dev/null结合 dropreason-core.h
中的枚举定义,将数字映射为名称。或者使用 perf 自动解析:
perf stat -a -e 'skb:kfree_skb' -- sleep 106.3 性能开销
| 方法 | 开销 | 适合场景 |
|---|---|---|
nstat / /proc/net/snmp |
零 | 始终开启,快速定界 |
ethtool -S |
零 | 始终可查,网卡级 |
kfree_skb tracepoint + bpftrace |
~100-200 ns/丢包 | 实时诊断 |
perf record -e skb:kfree_skb |
~100 ns/丢包 | 录制分析 |
ftrace event |
~50-100 ns/丢包 | 最小化环境 |
dropwatch |
~100 ns/丢包 | 快速交互式诊断 |
由于丢包通常是低频事件(相对于正常包处理),追踪开销在绝大多数场景下可以忽略。
参考文献
- Linux 内核源码
include/trace/events/skb.h:kfree_skb和consume_skbtracepoint 定义 - Linux 内核源码
include/net/dropreason-core.h:enum skb_drop_reason完整定义(80+ 种原因) - Linux 内核源码
include/net/dropreason.h:enum skb_drop_reason_subsys子系统扩展机制 - Linux 内核文档
Documentation/networking/kfree_skb_reason.rst:drop reason 文档 - Linux 内核源码
net/core/drop_monitor.c:drop_monitornetlink 子系统实现
上一篇:内核网络追踪工具箱:bpftrace/perf/ftrace 实战
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【Linux 网络子系统深度拆解】内核网络追踪工具箱:bpftrace/perf/ftrace 实战
从内核 tracepoint 定义出发,系统讲解 bpftrace、perf、ftrace 三大工具在网络诊断中的实战用法:TCP 重传根因分析、softirq 延迟定位、收发包路径延迟剖析、conntrack 表满监控、per-function 火焰图,以及各工具的适用场景与性能开销对比。
【Linux 网络子系统深度拆解】eBPF 网络钩子全景:TC/XDP/socket/cgroup
从内核源码全面拆解 eBPF 在网络子系统中的所有挂载点:TC BPF direct-action 模式与 bpf_mprog 多程序链、XDP 驱动级钩子回顾、socket ops 回调与 TCP 生命周期事件、cgroup BPF 策略控制、sk_msg/sk_skb 的 sockmap 重定向引擎、struct_ops 实现自定义拥塞控制,以及 bpftrace 可观测实战。
【Linux 网络子系统深度拆解】XDP 内核实现:在驱动层重编程网络栈
从内核源码拆解 XDP 的完整实现:xdp_buff 数据结构、驱动级钩子、五种动作路径、AF_XDP 零拷贝通道、devmap/cpumap/xskmap 重定向机制、多缓冲区支持,以及 bpftrace 可观测实战。
【Linux 网络子系统深度拆解】网络子系统内存管理:sk_buff 分配、page pool 与 NUMA
从内核源码拆解网络子系统的内存管理全貌:sk_buff 分配路径与 slab 缓存、page_pool 页面回收机制、NUMA 感知分配策略、socket 内存记账与反压,以及 bpftrace 可观测实战。