新特性!基于 eBPF 的 Linux 内核全栈丢包检测
在云原生环境中,网络丢包是最让人头疼的问题之一(应用日志报连接超时,TCP 重传率飙升),但丢包发生在内核协议栈的哪个环节?是防火墙规则丢弃、路由查找失败、还是接收缓冲区溢出?传统的 dropwatch 或 perf record -e skb:kfree_skb 只能给出丢包地点的符号地址,缺乏数据包上下文,难以快速判断丢包是否影响了业务流量。
HUATUO 项目的 dropwatch 工具从内核 Tracepoint tracepoint/skb/kfree_skb 出发,在 eBPF 探针中一次性采集丢包事件的完整上下文:IP 五元组、进程信息、网络设备、MAC 地址,以及触发丢包的内核调用栈。更重要的是,它支持 tcpdump 风格的内核侧过滤——过滤逻辑在加载时被编译为 eBPF 字节码,只有匹配的数据包才会上报到用户空间,避免海量无关丢包事件淹没真正的问题线索。
本文将从实际排查场景出发,介绍 dropwatch 的工作方式、过滤能力、输出结构,以及如何与 huatuo-bamai 集成实现长期丢包观测。
从一个线上问题说起
某 Kubernetes 集群中,订单服务的 P99 延迟周期性从 50ms 飙升至 2s。应用日志只有零星的 connection reset by peer,而节点上的 netstat -s 显示 TCPLostRetransmit 在持续增长。
传统的排查路径是:先 tcpdump 抓包确认对端是否收到 SYN,再 iptables -L 检查规则,再 ip route get 验证路由,最后 perf record -e skb:kfree_skb 看内核丢包点——整个流程至少需要 30 分钟,而且 perf 输出的符号地址还需要手动对照内核符号表。
用 dropwatch,一条命令就能定位:
sudo dropwatch --bpf-path bpf/dropwatch.o \
--filter "tcp and port 8080" \
--device eth0 \
--duration 60 \
--output json
输出中每条事件都携带了完整的丢包上下文:哪个进程的包被丢、源目 IP 和端口、丢包发生时的内核调用栈。结合 jq 过滤,几秒内就能找到根因。
内核侧过滤:把无关丢包挡在内核态
生产环境中 kfree_skb 的触发频率极高——邻居表清理、套接字关闭、驱动 DMA 完成等都会触发,但它们并不是业务层面的丢包。如果在用户空间逐条过滤,不仅 CPU 开销高,还会丢失真正关键的丢包信号。
dropwatch 的方案是将过滤逻辑下沉到内核态执行。它内置了一个纯 Go 实现的 pcap 编译器 internal/pcapfilter,在 eBPF 程序加载时将 tcpdump 风格的过滤表达式编译为 eBPF 字节码,直接嵌入探针逻辑。只有匹配的数据包才会通过 perf ring buffer 上报到用户空间。
支持的过滤原语
internal/pcapfilter 实现了 tcpdump 标准语法的一个实用子集:
协议与方向
ip ip6 tcp udp icmp icmp6 arp
ip proto tcp ip6 proto udp
src host 10.0.0.1 dst host 10.0.0.1
src port 443 dst port 8080
src net 192.168.1.0/24
布尔组合
tcp and port 443
tcp or udp
not arp
ip and src net 192.168.1.0/24 and tcp dst port 3306
不支持的表达式包括字节偏移(tcp[tcpflags])、数字协议号(ip proto 6)、端口范围(portrange)等,完整的不支持列表可参考使用文档。
过滤实战
# 只监控目标端口的 TCP 丢包
--filter "tcp and port 443"
# 排除元数据服务 IP 的噪声
--filter "tcp and not host 169.254.169.254"
# 精确锁定某个子网到数据库端口的丢包
--filter "src net 192.168.1.0/24 and tcp dst port 3306"
--filter 与设备过滤(--device / --device-excluded)相互正交,同时指定时两者均生效(AND 语义)。设备白名单会丢弃没有 net_device 的 SKB,黑名单则放行——这在容器 veth 设备场景下尤其有用。
事件输出:结构化的丢包上下文
每条丢包事件以 NDJSON 格式输出,核心字段如下:
{
"observed_timestamp": "2026-06-20T08:30:12.123456789Z",
"comm": "nginx",
"pid": 4521,
"container_id": "a1b2c3d4",
"netdev_name": "eth0",
"packet_eth_proto": "0x0800",
"packet_len": 74,
"layers": {
"label": "IPv4/TCP",
"ether": { "src": "aa:bb:cc:dd:ee:ff", "dst": "11:22:33:44:55:66", "type": "IPv4" },
"ipv4": { "src": "10.0.1.5", "dst": "10.0.2.10", "ttl": 64, "protocol": "TCP" },
"tcp": { "sport": 54321, "dport": 8080, "flags": "SYN", "seq": 0, "ack": 0, "window": 65535, "sk_state": "SYN_SENT" }
},
"stack": "kfree_skb\ntcp_v4_rcv\ntcp_rcv_established\n..."
}
几个值得关注的字段:
layers:分层协议解析结果,每层为一个嵌套对象,缺失的层自动省略。不再依赖单独的协议枚举,下游消费者直接用字段存在性判断协议组合(如ev.Layers.TCP != nil)。stack:触发丢包的完整内核调用栈,换行分隔。这是区分丢包原因的关键——同样是 TCP 丢包,tcp_v4_rcv和ip_output对应的排查方向完全不同。container_id:由 huatuo-bamai 根据内存 cgroup CSS 地址或网络命名空间 cookie 解析填充,直接关联到 Kubernetes Pod。netdev_linkstatus:网络设备链路标志数组,可用来判断设备是否处于 carrier-down 或 dormant 状态。
layers 各层的完整字段列表:
| 层 | 字段 |
|---|---|
ether |
src、dst、type、len(仅 802.3 帧) |
ipv4 |
version、ihl、tos、len、id、flags、frag_offset、ttl、protocol、checksum、src、dst |
ipv6 |
version、traffic_class、flow_label、len、next_header、hop_limit、src、dst |
tcp |
sport、dport、seq、ack、data_offset、flags、window、checksum、urgent、sk_state |
udp |
sport、dport、len、checksum |
icmp |
type、code、checksum、id、seq |
arp |
addr_type、protocol、hw_address_size、prot_address_size、operation、sender_mac、sender_ip、target_mac、target_ip |
用 jq 做用户态过滤
当内核侧过滤不够精细时,可以在用户态用 jq 对 JSON 输出做二次筛选:
# 只看 RST 包
sudo dropwatch --bpf-path bpf/dropwatch.o --output json 2>/dev/null \
| jq 'select(.layers.tcp.flags == "RST")'
# 排除调用栈包含 ip_finish_output 的事件(通常是正常路由输出)
sudo dropwatch --output json --duration 10 --bpf-path bpf/dropwatch.o \
| jq -c 'select(.stack | test("ip_finish_output") | not)'
# 只看元数据,不打印调用栈(适合快速统计丢包分布)
sudo dropwatch --output json --duration 10 --bpf-path bpf/dropwatch.o \
| jq -c 'del(.stack)'
jq -c 将每条事件压缩为单行 JSON,便于保存为 NDJSON 文件或继续管道处理。
噪声过滤:屏蔽非数据面丢包
并非所有 kfree_skb 事件都是真正的数据面丢包。以下三类事件在默认配置下会被 huatuo-bamai 过滤:
| 模式 | 调用栈特征 | 为什么不是丢包 |
|---|---|---|
TCP CLOSE_WAIT + skb_rbtree_purge |
栈帧 skb_rbtree_purge/ |
正常的套接字关闭:内核释放 CLOSE_WAIT 状态套接字中飞行中的 SKB |
| ARP/邻居表到期 | 栈帧 neigh_invalidate/ |
邻居表项到期清理,不影响活跃数据流 |
| bnxt 网卡 TX 完成 | bnxt_tx_int/ |
Broadcom bnxt 驱动在 DMA 发送完成后释放 SKB,是正常行为 |
在 huatuo-bamai 配置中,噪声规则通过 EventTracing.IssuesList 管理:
[EventTracing]
IssuesList = [["neigh_invalidate", "neigh_invalidate"], ["bnxt_tx_int", "bnxt_tx_int"]]
[EventTracing.Dropwatch]
Filter = "tcp"
MaxEventsPerSecond = 100
如果需要观察邻居表相关的丢包(例如调试 ARP 风暴),可以从 IssuesList 中移除对应规则。
与 huatuo-bamai 集成:长期丢包观测
单次运行 dropwatch 适合临时排查,但生产环境更需要持续的丢包观测。huatuo-bamai 以子进程方式启动 dropwatch,通过 --output-storage 将事件经 Unix socket 发送到内置处理流程,最终存储至 Elasticsearch。
dropwatch \
--bpf-path <CoreBpfDir>/dropwatch.o \
--output-storage /var/run/huatuo/events.sock \
--filter "tcp"
存储后可以实现:
- 时间线关联:在 Grafana 中将丢包事件叠加到应用延迟曲线上,对齐丢包时间点与延迟 spike
- 多维聚合:按
container_id、netdev_name、layers.label聚合丢包分布,快速锁定问题 Pod 或设备 - 历史回溯:保留丢包事件的完整上下文,支持事后根因分析,无需复现问题
全部命令行参数
| 参数 | 默认值 | 说明 |
|---|---|---|
--bpf-path <path> |
必填 | eBPF 对象文件路径 |
--filter <expr> |
(无) | tcpdump 风格过滤表达式 |
--device <names> |
(无) | 设备白名单,逗号分隔(如 eth0,eth1) |
--device-excluded <names> |
(无) | 设备黑名单;与 --device 互斥 |
--duration <n> |
0 | 运行 N 秒后退出(0 = 持续运行) |
--output <json|text> |
text |
输出格式;设置 --output-storage 时被忽略 |
--output-storage <path> |
(无) | 通过 Unix socket 将事件发送给 huatuo-bamai |
--task-id <id> |
(无) | 关联本次会话的任务 ID |
--max-events-per-second <n> |
0 | 全局上报限速,0 = 不限速 |
小结
网络丢包的排查难点在于:丢包发生在内核协议栈深处,传统工具缺乏数据包上下文,难以判断丢包是否影响了业务流量。dropwatch 通过 eBPF 探针在 kfree_skb Tracepoint 上采集完整的丢包上下文,并将过滤逻辑编译为 eBPF 字节码在内核态执行,在不影响宿主机性能的前提下,将关键丢包事件精准地呈现给运维人员。
配合 huatuo-bamai 的持续集成能力,dropwatch 可以从"临时排查工具"升级为"长期丢包观测基础设施",将内核丢包与应用异常在时间线上精确关联,把网络丢包的定位从"猜测 + 验证"转变为"观测 + 归因"。