第9章 网络分析
从 socket 到网卡
章节导读
网络是分布式系统的血管,也是最难排查的器官:数据包跨越应用、socket 缓冲、TCP 协议栈、IP 层、qdisc 队列、驱动、网卡、交换机、对端——每一跳都可能引入延迟和丢包,而传统的 tcpdump 只能在单点抓包,netstat/ss 只能看瞬时状态,"这个请求的 200ms 到底花在哪一段"的问题长期无解。BPF 改变了这一切:它能在内核网络栈的每一层埋点,把连接的完整生命周期、每个报文的路径、每次重传的原因变成可观测的数据流。
先走一遍 TCP 数据的完整路径。发送方向:应用 write() → socket 发送缓冲区(sk_sndbuf,满则阻塞或 EAGAIN)→ TCP 层分段(MSS 切割、拥塞窗口 cwnd 允许的量进入发送队列)→ IP 层路由 → qdisc 队列(流量整形,满则丢包——netem/tbf 等)→ 驱动发送环 → 网卡 DMA → 线缆。接收方向:网卡收包 DMA → 硬中断 → NAPI 软中断(NET_RX)→ IP 层 → TCP 层(校验、排序、ACK)→ socket 接收缓冲区(sk_rcvbuf,满则丢包或窗口收缩)→ 应用 read()。每个环节都有对应的 BPF 观测点:socket 层(tcp_sendmsg/tcp_recvmsg kprobe、sock tracepoint)、TCP 状态机(tcp tracepoint 家族:tcp_probe、tcp_retransmit_skb、tcp_send_reset 等)、连接生命周期(inet_sock_set_state tracepoint,tcplife 的基础)、qdisc 与驱动(skb tracepoint、XDP/tc 钩子)。
本章的核心工具围绕 TCP 生命周期展开。tcplife 是旗舰:追踪每个 TCP 连接从建立到关闭的完整记录——进程、PID、源/目的地址端口、发送字节、接收字节、存活时长。它回答的问题传统工具无法回答:"这个短连接到底传了多少数据、活了多久?"(ss/netstat 只能看当前存在的连接,短连接在采样间隙生灭无痕)。tcpconnect 追踪主动连接(connect),tcpaccept 追踪被动接受(accept),两者组合是连接审计的基础——谁在连谁、连接速率是否异常(连接风暴、扫描行为一目了然)。tcpretrans 追踪重传事件:重传是网络质量的直接信号——正常数据中心网络重传率应低于 0.01%,持续重传说明丢包(链路质量、交换机缓冲溢出、对端接收窗口满、RTO 超时),tcpretrans 输出重传的时刻、连接、序列号和状态(RTO 超时重传 vs 快速重传),配合 tcplife 的字节统计可以计算每条连接的重传率。
网络延迟分析是本章的方法论核心。一次请求-响应的延迟分解:应用处理时间 + 发送路径(socket 缓冲排队 + TCP 发送 + qdisc)+ 网络传输(线缆 + 交换机)+ 对端处理 + 接收路径(软中断 + socket 缓冲排队 + 应用读取)。BPF 工具覆盖每一段:tcplife 的时间戳给出连接级视角;sockq(BCC 实验工具)和 bpftrace 脚本测量 socket 缓冲的排队时间(数据在 sk_receive_queue 里等了多久才被应用读走——应用读取不及时是"网络慢"假象的常见真因);tcp 的 RTT(ss -i 的 rtt 字段、BPF 追踪 tcp_rcv_established 中的 RTT 采样)给出网络往返基线;重传分析解释长尾(一次 RTO 重传至少加 200ms——初始 RTO 是 1 秒,最小 200ms,这就是"偶发 200ms 毛刺"的经典网络解释)。
XDP 和 tc 代表 BPF 的网络编程面(不只是观测)。XDP(eXpress Data Path)在驱动层(甚至网卡硬件 offload)处理包,早于内核网络栈分配 skb——每包处理成本从微秒级降到百纳秒级,Facebook 的 Katran 用 XDP 做四层负载均衡单机转发 4000 万 pps,Cloudflare 用它做 DDoS 清洗。tc(traffic control)层的 BPF 在 skb 阶段处理,能力更全(可改写、重定向、封装)。虽然本书聚焦可观测性,但理解 XDP/tc 的存在很重要:它们既是观测点(xdp 类 tracepoint、tc 的 bpf 程序可以计数丢包)也是高性能网络工具的实现基础(Cilium 用 BPF 替换 iptables/kube-proxy,性能提升一个量级)。
应用层协议追踪是 BPF 的"越层"能力:uprobe 挂 SSL_read/SSL_write 看 HTTPS 明文(第 5 章已讲);kprobe 挂 udp_sendmsg 追踪 DNS 查询(BCC 的 dns 相关工具、bpftrace 脚本可以输出每个 DNS 请求的域名和响应延迟);HTTP/1.x 明文可以直接从 socket 层数据追踪。这类"协议感知"的观测让 BPF 工具逼近了 APM 的能力(无侵入的服务级指标),pixie、DeepFlow 等产品正是基于此构建全栈可观测性。
网络丢包定位是本章的压轴方法论。丢包可能发生在十几个位置,BPF 提供了系统化的排查链:应用层(socket 缓冲满——SO_SNDBUF/SO_RCVBUF 统计、tcp_drop tracepoint)→ TCP 层(重传、乱序、窗口探测——tcp tracepoint 家族)→ qdisc(tc -s qdisc 的 dropped 计数、BPF 追踪 qdisc 丢弃)→ 驱动/网卡(ethtool -S 的 tx_dropped/rx_dropped、ring buffer 溢出——BPF 可追踪驱动计数)→ 软中断(NET_RX 时间过长导致 backlog 溢出——softirqs + netstat -s 的 squeezed 计数)。netstat -s(或 nstat)的 TCP 统计大全是丢包排查的地图:listen overflows(accept 队列溢出)、listen drops(SYN 队列溢出)、TCPRcvQDrop、TCPBacklogDrop 等每一项都对应栈内一个具体的丢弃点,BPF 工具把这些计数变成带上下文的事件流(哪个连接、哪个进程、什么时刻)。
核心概念
- tcplife
- tcpretrans
- TCP 追踪
- socket 分析
- XDP
- 网络栈
- 丢包定位
要点精析
【tcplife 深入:连接生命周期的完整记录】tcplife 追踪 TCP 连接从建立到关闭的全程,输出:PID、COMM、LADDR/LPORT、RADDR/RPORT、TX_KB、RX_KB、MS(存活时长)。实现原理:挂载 inet_sock_set_state tracepoint(或老内核的 tcp 状态函数 kprobe),在连接进入 ESTABLISHED 时开始记录,CLOSE 时输出汇总——字节数从 sock 结构的累计计数器读取。杀手级用法:(1) 短连接审计——微服务间每秒数千短连接,每个连接传了多少数据?大量"建立即关闭、只传几百字节"的连接说明连接复用失败(HTTP Keep-Alive 未生效、连接池配置错误),连接建立的三次握手 + 慢启动成本被白白支付;(2) 连接泄漏检测——MS 列出现存活数小时的"僵尸连接"(应用未正确关闭),累积耗尽 fd;(3) 流量归因——TX_KB/RX_KB 按进程聚合就是进程级流量账本(替代 nethogs);(4) 安全审计——异常外联(未知进程连接外部地址)实时可见。选项:-t 时间戳、-T 追踪全部(含监听 socket)、-4/-6 限定协议族、按 LPORT/RPORT 过滤。开销:每连接一条事件,连接速率每秒数千时输出可控(这正是事件型工具的舒适区——对比逐包追踪的百万级频率)。
【tcpretrans 与重传分析:网络质量的听诊器】tcpretrans 追踪 tcp_retransmit_skb(重传发生点),输出时间、PID、连接四元组、状态(RTO 超时 or 快速重传)、序列号。重传的两种机制要区分:快速重传(收到 3 个重复 ACK 触发,说明部分包到达、部分丢失,恢复快约 1 个 RTT);RTO 超时重传(ACK 完全无响应,等待超时——初始 RTO 1 秒,Linux 最小 RTO 200ms,一次 RTO 事件给延迟加 200ms+,且拥塞窗口归零重新慢启动)。分析要点:(1) 重传率——重传段数/总发送段数(nstat 的 TCPRetransSegs 与 TCPOutSegs 比值),数据中心内应 <0.01%,>1% 说明网络有实质问题;(2) 重传模式——集中在特定对端(链路问题)、特定时刻(交换机缓冲溢出、微突发 microburst)、还是随机分布(网卡/驱动问题);(3) 与延迟关联——应用 P99 毛刺时刻对照 tcpretrans 输出,RTO 重传与 200ms 级毛刺的对应关系是经典证据链。tcpretrans -l 还追踪尾部丢包探测(TLP)。深入工具:bpftrace 挂 tcp_retransmit_skb 按目的地址聚合(定位问题链路)、按 kstack 聚合(区分重传触发路径);tcpconns(BCC)追踪连接事件流。云环境注意:虚拟化网络(vSwitch、安全组、NAT 网关)的丢包在 guest 内表现为重传,但根因在虚拟网络层——需要云平台侧的流日志配合。
【socket 缓冲分析:应用层"网络慢"的常见真因】socket 有发送缓冲(sk_sndbuf)和接收缓冲(sk_rcvbuf),缓冲行为是"网络延迟"的重要来源但常被忽略。接收侧:数据到达内核后放入 sk_receive_queue,应用 read() 才取走——应用读取慢(单线程事件循环被阻塞、GC 停顿、CPU 饥饿)导致队列堆积,后果链:队列满 → TCP 通告窗口收缩(rwnd 减小)→ 对端发送受限 → 吞吐下降 → 对端看起来"网络慢",实际是本端应用慢!发送侧:write() 只是拷入 sk_write_queue,实际发送受拥塞窗口(cwnd)和 RTT 控制——发送缓冲堆积说明网络拥塞或对端接收慢。分析工具:ss -tm 显示每个 socket 的缓冲使用(Recv-Q/Send-Q 列是瞬时值,ss -i 的详细信息含 cwnd、rtt、rto);BPF 方案追踪 tcp_recvmsg 测量"数据在接收队列的停留时间"(入队时间戳存 map,recvmsg 读取时计算差值——这个"socket 排队延迟"是应用读取及时性的直接度量);sockq 类工具输出缓冲占用分布。诊断口诀:Recv-Q 持续大 = 应用读得慢(查应用);Send-Q 持续大 = 网络或对端慢(查重传、RTT、对端 Recv-Q);两者都小但延迟高 = 真正的网络传输延迟(查 RTT、路由)。
【TCP 连接建立与接受队列:listen overflow 问题】连接建立涉及两个队列:SYN 队列(半连接,收到 SYN 未收到最终 ACK,tcp_max_syn_backlog 控制)和 accept 队列(全连接,已完成握手等待应用 accept(),min(somaxconn, 应用 backlog) 控制)。队列溢出的后果:SYN 队列满 → 丢弃 SYN(客户端重试,表现为连接超时)或 syncookies 启用(安全降级但可用);accept 队列满 → 默认丢弃最终 ACK(客户端以为连接成功开始发数据,服务端无响应——"连接成功但请求超时"的诡异现象),tcp_abort_on_overflow=1 时发 RST(客户端立即报错,反而更好排查)。追踪工具:nstat 的 TCPReqQFullDrop/TCPAbortOnOverflow 计数、tcplife 看不到被丢弃的连接(它们从未 ESTABLISHED)、BPF 方案挂 tcp_v4_connect/inet_csk_complete_hashdance 追踪队列状态。高并发服务的标准检查清单:somaxconn(默认 128 太小,调到 65535)、应用的 listen backlog 参数(nginx worker_connections、Java 的 ServerSocket backlog)、tcp_max_syn_backlog、以及 accept 速度(应用 accept() 是否及时——事件循环阻塞会拖垮 accept 队列)。SYN flood 防御(syncookies)与 accept 队列调优是运维面试常客,BPF 追踪让"队列到底满没满"从猜测变成数据。
【DNS 与应用层协议追踪】DNS 是分布式系统最脆弱的依赖之一(解析延迟、缓存失效、ndots 配置陷阱),BPF 提供零侵入的 DNS 观测:追踪 udp_sendmsg/udp_recvmsg(DNS 主要走 UDP 53)或 socket 层,解析报文提取查询域名和响应。bpftrace 实现思路:kprobe:udp_sendmsg 过滤目的端口 53,读取 skb 数据解析 DNS 头(BPF 内解析协议需要 bpf_probe_read 分段读取,复杂但可行);更简单的方案是 BCC 的工具或 uprobe 挂 getaddrinfo(glibc 解析入口,能看到应用查询的域名和耗时——包括 /etc/resolv.conf 的 search 域展开导致的多次查询!)。K8s 场景的经典问题:Pod 的 ndots:5 配置使短域名先尝试 5 个 search 域后缀(每次都是完整 DNS 往返),一个解析变成 6 次查询——uprobe 追踪 getaddrinfo 的频率和耗时直接暴露这个问题。HTTP 追踪:明文 HTTP/1.x 可以从 tcp_sendmsg 的数据内容解析(BPF 字符串匹配);HTTPS 用 SSL uprobe(第 5 章);HTTP/2 的二进制分帧解析在 BPF 内困难,通常交给用户态(perf buffer 传原始数据,用户态解析——pixie/DeepFlow 的架构)。协议追踪的价值:无侵入的服务级指标(请求率、延迟、错误率)——APM 的核心数据,但不需要改代码、不需要 agent、跨语言通用。
【XDP 与 tc:BPF 的网络数据面】XDP 程序挂在网卡驱动的收包最早期(skb 分配之前),三种运行模式:native(驱动内,性能最高)、offload(网卡硬件执行,如 Netronome/部分 Mellanox)、generic(协议栈兜底,性能差仅调试用)。XDP 动作:XDP_DROP(丢弃,DDoS 防御的核心——每包成本约 200ns,单核可丢数百万 pps)、XDP_PASS(交给内核栈)、XDP_TX(原路弹回)、XDP_REDIRECT(重定向到别的网卡/CPU/socket——Katran 负载均衡的基础)。tc(traffic control)BPF 在 skb 层运行(qdisc 的 clsact),能力更全:改写包头(NAT、封装)、跨命名空间重定向(容器网络的核心——Cilium 用 tc BPF 实现 Pod 间转发,绕过 iptables 的 O(n) 规则遍历,性能提升 10 倍+)、采样观测。可观测性视角的 XDP/tc:它们既是工具也是观测点——xdp:xdp_exception tracepoint 追踪 XDP 程序错误;tc -s qdisc show 的 dropped 计数;BPF 程序本身可以内嵌计数逻辑(每个规则命中的包数)。学习路径:先理解观测型用法(本章主线),再按需学数据面编程(Cilium 文档和 xdp-tutorial 仓库是最好的教材)。对性能工程师的要求:知道 XDP/tc BPF 的存在和位置——当 tcpdump 抓到的包与网卡计数对不上时,中间的 XDP/tc BPF 程序(容器环境几乎必然存在)就是嫌疑人。
【网络延迟分解:RTT、处理时间与排队】端到端延迟的完整账本:客户端应用处理 + 客户端 socket 发送排队 + 客户端协议栈/驱动/qdisc + 网络传输(光速延迟:同机房 <0.1ms,跨城 10-50ms,跨洋 100ms+)+ 服务端网卡/软中断 + 服务端 socket 接收排队 + 服务端应用处理 + 原路返回。BPF 测量各段的方法:(1) 网络 RTT——ss -i 的 rtt 字段(TCP 的 SRTT 采样)、BPF 追踪 tcp_rcv_established 读取 tp->srtt_us;(2) 服务端处理时间——uprobe 挂应用请求处理函数(第 5 章)或 USDT;(3) socket 排队——接收队列停留时间追踪(本要点前述);(4) 软中断处理延迟——包到达网卡到协议栈处理的时间(skb 的时间戳 vs 处理时刻);(5) qdisc 排队——tc 统计。经典案例的分解示范:"客户端看到 API 延迟 P99=300ms"→ RTT 0.5ms(网络无辜)→ 服务端处理 P99=20ms(应用基本无辜)→ socket 接收排队 P99=250ms(元凶!)→ 应用事件循环被某个同步操作阻塞(GC、锁、慢查询)导致读取不及时。没有分段测量,这个案例会被误诊为"网络问题"折腾数周——网络延迟分析的第一原则:先分段,再定罪。
【丢包定位系统化:从 netstat -s 到 BPF 事件】丢包排查的地图是 nstat/netstat -s 的 TCP/UDP 统计,关键项及含义:TCPRetransSegs(重传段——已发出但丢失);ListenOverflows/ListenDrops(accept/SYN 队列溢出——连接层丢弃);TCPBacklogDrop(socket backlog 满);TCPRcvQDrop(接收队列满,需开启 RCVBUF 限制追踪);UDPInErrors/UDPRcvbufErrors(UDP 接收缓冲溢出——DNS、视频流常见);TCPOFODrop(乱序队列满)。BPF 把这些计数变成事件流:tcp:tcp_drop tracepoint(5.x 内核,所有 TCP 丢包点的统一事件,携带 skb 和状态——按 kstack 聚合即知丢弃位置);udp 类似。系统化排查流程:(1) nstat -az | grep -i drop 全量扫描丢弃计数,找到增长项;(2) 对应到栈内位置(每个计数项有明确的内核丢弃点);(3) BPF 追踪该点获取上下文(哪个连接、哪个进程、什么速率);(4) 按位置修复:socket 缓冲满 → 调 rmem/wmem 或查应用读取速度;队列溢出 → 调 backlog/somaxconn;UDP 溢出 → 调 net.core.rmem_max(DNS 服务器标准操作);重传 → 查物理链路/交换机(ethtool -S 网卡计数、交换机端口统计);软中断挤压 → 多队列/RPS(第 6 章)。网卡层丢包(rx_dropped、ring overflow)用 ethtool -S 和 BPF 驱动 tracepoint,这层的丢包 tcpdump 完全看不到(tcpdump 在丢弃点之后抓包)——"tcpdump 没有丢包但应用报超时"的悖论通常就是网卡/qdisc 层丢弃。
【容器与云网络的可观测性挑战】容器网络路径(以 bridge CNI 为例):Pod 应用 → veth pair → 网桥/路由 → iptables/ipvs(Service NAT)→ 宿主机协议栈 → 物理网卡。每层都是观测点和丢包点。挑战:(1) 命名空间隔离——容器内 tcpdump 只看到 veth 一侧,宿主机抓包需要进入正确的 netns(nsenter)或用 BPF 的全局视角(BPF 在宿主机内核运行,天然看到所有容器的流量——按 cgroup id 或 netns id 过滤即可分租户统计,这是 BPF 对容器可观测性的根本优势);(2) iptables 规则爆炸——大集群的 kube-proxy iptables 规则数万条,每包 O(n) 匹配,BPF 方案(Cilium)用哈希表 O(1);追踪 iptables 决策路径极其困难(规则链遍历无事件),BPF 替换后决策可编程可观测;(3) conntrack 表——连接跟踪表满(nf_conntrack: table full, dropping packet)是容器网络丢包的高发原因,BPF 可追踪 conntrack 事件和表使用率;(4) overlay 封装(VXLAN/IPIP)——抓包看到封装报文,BPF 在封装前后都有观测点(tc 层)。K8s 排查工具链:宿主机 tcplife/tcpretrans 按 Pod IP 过滤、cilium 的 Hubble(BPF 原生的流可见性,每条被策略允许/拒绝的流都有记录)、以及各 CNI 的 BPF 观测能力。核心认知:容器网络问题 90% 的排查发生在宿主机层,BPF 是唯一能同时看到"所有容器 + 宿主机 + 物理网络边界"的技术。
【网络性能调优的 BPF 验证方法】网络调优参数众多(rmem/wmem 缓冲、拥塞算法、Nagle、延迟 ACK、somaxconn、tw_reuse...),BPF 的角色是"调优前后的定量验证"。方法论:(1) 建立基线——tcplife 的连接时长/字节分布、tcpretrans 的重传率、RTT 分布(BPF 直方图)、socket 排队延迟分布,四个基线指标覆盖连接质量全貌;(2) 单变量调整——每次只改一个参数(如拥塞算法 cubic → bbr);(3) 对照测量——同负载下重测四个指标,差异即效果;(4) 长尾关注——平均值改善但 P99 恶化是常见陷阱(BBR 提吞吐但某些场景增加延迟方差),直方图对比优于均值对比。经典调优案例的验证要点:TCP 缓冲自动调优(rmem_max/wmem_max 上限决定自动调优的天花板,大带宽×延迟积链路必须调大——验证方法是追踪 sk_sndbuf 的实际使用是否触顶);Nagle 与延迟 ACK 的死锁(40ms 延迟毛刺的经典组合——write-write-read 模式下 Nagle 等 ACK、延迟 ACK 等数据,互相等待 40ms;TCP_NODELAY 解决,验证方法是毛刺直方图中 40ms 峰的消失);TIME_WAIT 堆积(短连接高并发场景 tw_reuse 的效果用 tcplife 的状态时长分布验证)。拥塞控制分析:BPF 可以追踪拥塞窗口演化(tcp_probe 老接口或 tcp tracepoint),慢启动、拥塞避免、恢复阶段的行为可视化——理解"为什么吞吐上不去"(cwnd 受限还是 rwnd 受限还是应用受限)的终极手段。
代码示例
/* 示例1:tcplife——连接生命周期审计 */
sudo /usr/share/bcc/tools/tcplife -t -T
/* TIME PID COMM LADDR LPORT RADDR RPORT TX_KB RX_KB MS
10:23:01 3021 curl 10.0.0.5 44312 93.184.216.34 443 0.52 1.20 235.1
10:23:01 2233 mysqld 10.0.0.5 3306 10.0.0.9 52344 12.30 0.85 45.2
*/
/* 短连接风暴检测:每秒新连接数 */
sudo bpftrace -e 'kprobe:tcp_v4_connect { @conn_rate = count(); } interval:s:1 { print(@conn_rate); clear(@conn_rate); }'tcplife 每行是一个完整连接的"死亡报告":谁(PID/COMM)、从哪到哪(四元组)、传了多少(TX_KB/RX_KB)、活了多久(MS)。上面的第一行揭示一个典型问题:curl 连接只传了 0.52KB 就花了 235ms——建连 + TLS 握手 + 少量数据 + 关闭,如果是应用内高频发生的模式(而非人工 curl),说明缺少连接复用,每次请求都支付完整的握手成本(TCP 1-RTT + TLS 1-2 RTT)。第二行是健康的数据库连接:12KB 下行数据、45ms 存活。短连接风暴的量化:bpftrace 统计 tcp_v4_connect 速率,每秒数千次连接尝试即风暴信号——后果是 TIME_WAIT 堆积(主动关闭方保留 60 秒)、端口耗尽(ephemeral port 范围 28K 个,每秒 3000 短连接 × 60 秒 = 18 万 > 28K,必然耗尽!)、以及 accept 队列压力。解决方案:客户端连接池(HTTP Keep-Alive、数据库连接池)、服务端调 somaxconn、必要时 tw_reuse。tcplife 的 TX/RX 统计还揭示"空连接"(TX_KB 和 RX_KB 都接近 0)——健康检查或探测流量,数量巨大时也是开销(LB 健康检查每秒一次 × 100 个后端 = 每分钟 6000 个连接的建立销毁)。/* 示例2:tcpretrans 与重传率分析 */
sudo /usr/share/bcc/tools/tcpretrans -c
/* TIME PID IP LADDR:LPORT T> RADDR:RPORT STATE SEQ
10:23:01 2233 4 10.0.0.5:3306 R> 10.0.0.9:52344 ESTABLISHED 8823401
10:23:05 2233 4 10.0.0.5:3306 R> 10.0.0.9:52344 ESTABLISHED 8823555 <- 同连接连续重传
*/
/* 重传率计算: */
nstat -az | grep -E "TCPRetransSegs|TCPOutSegs"
/* 按目的地址聚合重传(定位问题链路): */
sudo bpftrace -e 'kprobe:tcp_retransmit_skb { $sk = (struct sock *)arg0; @[ntop($sk->__sk_common.skc_daddr)] = count(); }'tcpretrans -c 的 -c 显示序列号(连续重传同一连接的不同 seq 说明持续丢包而非单次事件)。上面的输出显示到 10.0.0.9 的连接在 4 秒内两次重传——单连接重复重传是"路径质量问题"的强信号(对比:所有连接均匀偶发重传更可能是随机噪声)。重传率的定量:nstat 的 TCPRetransSegs/TCPOutSegs 比值,>0.1% 值得关注,>1% 严重。bpftrace 按目的地址聚合是定位问题链路的关键一步:重传集中在某个对端 → 到该对端的路径有问题(特定交换机、特定可用区链路);重传集中在某个网段 → 机架级网络问题;重传均匀分布 → 本机网卡/驱动或全局拥塞。RTO 重传(tcpretrans 输出中状态标记)与快速重传的区分很重要:RTO 说明 ACK 完全丢失(严重),快速重传说明部分包到达(轻度)。深入排查的下一层:ethtool -S 看网卡级丢包计数(tx_errors、ring 溢出);对端同样检查;中间交换机端口统计(需要网络团队);云环境查安全组/流日志。记住重传的延迟代价:快速重传 +1 RTT(毫秒级,可容忍),RTO +200ms 起步(P99 杀手)——所以"P99 偶发 200ms"的网络排查第一步永远是 tcpretrans。/* 示例3:DNS 查询追踪——getaddrinfo 视角 */
sudo bpftrace -e '
uprobe:/lib/x86_64-linux-gnu/libc.so.6:getaddrinfo {
@start[tid] = nsecs;
@name[tid] = str(arg0);
}
uretprobe:/lib/x86_64-linux-gnu/libc.so.6:getaddrinfo /@start[tid]/ {
$ms = (nsecs - @start[tid]) / 1000000;
printf("%s: %s took %d ms (ret %d)\n", comm, @name[tid], $ms, retval);
@dns_ms = hist($ms);
delete(@start[tid]); delete(@name[tid]);
}'
/* 输出:
app: api.internal took 5023 ms (ret 0) <- DNS 解析 5 秒!
app: api.internal.cluster.local took 2 ms (ret -2) <- search 域尝试失败
*/getaddrinfo 是 glibc 的域名解析入口(几乎所有语言的 DNS 最终走这里),uprobe 配对追踪它的耗时和查询名。上面的输出暴露了 Kubernetes 的经典 DNS 陷阱:应用查询短名 api.internal,resolv.conf 的 search 域列表(api.internal.cluster.local、api.internal.svc.cluster.local...)逐个尝试,每个失败查询都是一次完整的 DNS 往返(含超时重试),ndots:5 配置下短名要先试完全部 search 域才查绝对名——一次解析变成 5-6 次查询,最坏情况秒级延迟。诊断特征:同一逻辑域名的多个变体查询、失败查询(ret -2 = EAI_NONAME)占多数、总耗时远大于单次查询。解决方案:FQDN 写法(域名末尾加点:api.internal.)、降低 ndots、NodeLocal DNSCache(集群内 DNS 缓存)、或应用层 DNS 缓存。这个脚本的价值在于"应用视角":它看到的是应用发起的解析(含 search 展开后的全部查询),而 tcpdump 抓 53 端口只看到 UDP 报文——两者对照(应用 1 次 getaddrinfo = 抓包 6 次查询)就是 search 域问题的铁证。生产化:按查询名聚合耗时直方图(@dns_ms[hostname] = hist(ms))做持续的 DNS 质量监控。/* 示例4:TCP 接收队列排队延迟——应用读取及时性 */
sudo bpftrace -e '
tracepoint:sock:inet_sock_set_state { } /* 占位说明:完整实现需 kprobe */
kprobe:tcp_recvmsg { @recv_start[tid] = nsecs; }
kretprobe:tcp_recvmsg /@recv_start[tid]/ {
@recvmsg_us = hist(nsecs - @recv_start[tid]);
delete(@recv_start[tid]);
}
kprobe:tcp_data_queue { @queued = count(); }'
/* 配合 ss 观察队列状态: */
ss -tn state established '( sport = :3306 )' | head
/* Recv-Q 持续 > 0 的 socket 即应用读取不及时 */tcp_recvmsg 是应用 read() socket 的内核入口,它的耗时分布反映"应用取数据的模式":健康的非阻塞事件循环中 recvmsg 快速返回(数据已在队列,微秒级);阻塞模式下 recvmsg 耗时 = 等待数据到达的时间(毫秒级正常);病态模式是"数据早到了但应用很久才调用 recvmsg"——这需要更精细的追踪:在 tcp_data_queue(数据入接收队列)时记录 skb 时间戳,recvmsg 读取时计算"队列停留时间"(BCC 的 sockq 思路,完整实现需要解析 skb 队列,较复杂)。简化诊断用 ss:Recv-Q 列是接收队列当前字节数,持续非零且增长 = 应用读取跟不上到达速率。后果链要理解:接收队列满 → 窗口通告收缩 → 对端发送变慢 → 对端 Send-Q 堆积 → 对端应用 write 阻塞——本端应用慢会"传染"为对端的"网络慢"。排查方向:应用线程阻塞(offcputime 看是否在 epoll_wait 之外的地方睡眠)、GC 停顿(JVM 的 USDT GC 追踪对照)、事件循环里的同步操作(Node.js 的同步 API、Python 的阻塞调用)、CPU 饥饿(runqlat)。这个案例链展示了网络分析与应用分析的交界:socket 队列是两者的分水岭,队列以上归应用,以下归网络。/* 示例5:accept 队列溢出追踪 */
sudo bpftrace -e '
tracepoint:tcp:tcp_drop_synack { @synack_drop = count(); }
kprobe:inet_csk_complete_hashdance { @accept_enqueue = count(); }
interval:s:5 { print(@synack_drop); print(@accept_enqueue); clear(@synack_drop); clear(@accept_enqueue); }'
/* 对照系统计数: */
nstat -az | grep -E "ListenOverflows|ListenDrops|TCPReqQFull"
ss -ltn /* LISTEN socket 的 Recv-Q/MaxRecv-Q = 当前accept队列/上限 */accept 队列问题的三层观测:(1) nstat 的 ListenOverflows(accept 队列溢出次数)和 ListenDrops(连接层丢弃总数)是累计计数,增长即有问题;(2) ss -ltn 的 LISTEN 行显示实时队列状态——Recv-Q 是当前 accept 队列中的连接数,MaxRecv-Q 是队列上限(min(somaxconn, listen backlog)),Recv-Q 逼近 MaxRecv-Q 即临界;(3) BPF 追踪入队事件(inet_csk_complete_hashdance 是握手完成入 accept 队列的点)与丢弃事件的速率对比。溢出时的默认行为(tcp_abort_on_overflow=0)极具迷惑性:服务端丢弃客户端的最终 ACK,连接在服务端不存在但客户端认为已建立——客户端发请求后石沉大海直到超时,表现为"连接成功但请求超时",且服务端日志毫无记录(连接从未到达应用层!)。这就是为什么这类问题排查困难:两端的常规日志都看不到。BPF 的价值:在握手完成点追踪,能看到"连接建立了但从未被 accept"的事件流。修复清单:应用 backlog 参数(Java ServerSocket(port, backlog)、nginx listen backlog=)、net.core.somaxconn(默认 4096,老内核 128)、应用 accept 速度(事件循环健康度)。高并发服务上线前的压测必须包含这项检查:压测时 watch nstat 的 ListenOverflows,非零即调参。/* 示例6:全栈丢包扫描——tcp_drop 按内核栈聚合 */
sudo bpftrace -e '
tracepoint:sk:sk_buff_drop { } /* 新内核统一丢包 tracepoint(名称随版本) */
kprobe:kfree_skb { @[kstack] = count(); }
interval:s:10 { print(@); clear(@); }'
/* 输出示例:
@[
kfree_skb+0x1
__udp_enqueue_schedule_skb+0x1a2 <- UDP 接收缓冲满!
udp_queue_rcv_skb+0x88
...
]: 4523
@[
kfree_skb+0x1
tcp_data_queue+0x3f2 <- TCP 乱序队列丢弃
...
]: 12
*/kfree_skb 是内核丢弃 skb(网络包)的统一出口——几乎所有网络丢包最终经过这里(新版本内核有更精确的 drop reason 机制:kfree_skb_reason 携带枚举原因,5.17+ 有 skb:kfree_skb tracepoint 带 reason 字段,比栈聚合更直接)。按 kstack 聚合的输出就是"丢包地图":每个唯一的内核栈代表一个丢弃点,计数是速率。上面的输出显示 UDP 丢弃占绝对主导,栈中的 __udp_enqueue_schedule_skb 是 UDP 入接收队列失败——即 sk_rcvbuf 满,典型受害者是 DNS 服务(查询风暴超过单线程处理能力)或视频流应用。修复:调大 net.core.rmem_max 和应用的 SO_RCVBUF、增加处理线程、或应用层限流。TCP 的 tcp_data_queue 丢弃是乱序包超出窗口/队列——少量正常(乱序网络),大量说明路径有严重的乱序(多路径负载均衡的副作用)。这个方法论的威力在于"无假设扫描":不需要先猜哪里丢包,kfree_skb 栈聚合一次运行就列出全部丢弃点及速率,然后逐个对照 nstat 计数和栈位置分析。老内核上 kfree_skb 调用频率极高(包括正常释放路径的误报——kfree_skb 也用于非丢弃场景的引用释放,consume_skb 才是正常消费),解读时聚焦明确的丢弃栈(enqueue 失败、queue drop 类函数);新内核优先用带 reason 的 tracepoint。实验笔记
本实验构建网络全栈观测能力。预计耗时 3-4 小时。环境:两台 Linux 机器(或虚拟机 + 宿主机,能互相 ping 通即可),安装 bcc-tools、bpftrace、iperf3、sysstat、nstat(iproute2)。root 权限。
【实验一:连接生命周期基线】(1) 用 curl 访问几个网站,同时运行 tcplife -t,记录每个连接的 TX/RX 字节和存活时长;(2) 用 ab 或 wrk(或简单脚本循环 curl)制造每秒 50 个短连接,观察 tcplife 输出的连接速率,用 ss -s 观察 TIME_WAIT 堆积,计算:按此速率多久耗尽 ephemeral 端口(cat /proc/sys/net/ipv4/ip_local_port_range)?(3) 开启 HTTP Keep-Alive(curl 复用连接)对比连接数变化——量化连接复用的价值。
【实验二:重传与丢包制造】(1) 用 tc netem 在测试链路制造丢包:sudo tc qdisc add dev eth0 root netem loss 1%(实验后记得 tc qdisc del dev eth0 root);(2) iperf3 打流量,同时运行 tcpretrans -c,观察重传事件与丢包率的对应关系;(3) nstat 计算重传率,与 netem 设置的 1% 对照;(4) 观察 iperf3 吞吐的下降幅度,理解重传对拥塞窗口的影响;(5) 把 loss 改为 delay 100ms 20ms(延迟抖动),观察 tcpretrans 是否出现 RTO 重传(延迟超过 RTO 阈值时的误判重传)。
【实验三:socket 队列与应用读取】(1) 写一个"慢消费者":TCP 服务端 accept 后故意每秒才 read 一次;客户端持续发送;(2) 用 ss -tn 观察服务端 Recv-Q 增长和客户端 Send-Q 堆积;(3) 用示例4的脚本观察 tcp_recvmsg 模式;(4) 客户端测量发送吞吐——验证"接收端应用慢导致全链路降速"(窗口收缩机制);(5) 讨论:这个实验中"网络"有问题吗?各层指标(重传、RTT)正常吗?
【实验四:accept 队列溢出】(1) 写一个服务端:listen(backlog=5) 后故意不 accept(sleep);(2) 客户端并发发起 20 个连接;(3) 观察 nstat 的 ListenOverflows 增长、客户端的连接状态(哪些成功哪些超时);(4) 用示例5的 BPF 脚本追踪入队与溢出;(5) 调大 somaxconn 和 backlog 重复实验验证修复。记录"连接成功但请求无响应"现象的完整证据链。
【实验五:DNS 与丢包扫描】(1) 配置一个慢 DNS(或用 tc netem 给 UDP 53 加延迟),运行示例3的 getaddrinfo 追踪,观察解析耗时;(2) 修改 /etc/resolv.conf 加多个 search 域,验证查询放大效应;(3) 运行示例6的 kfree_skb 栈聚合,在制造 UDP 洪泛(hping3 --udp 或 dns 查询风暴)时观察 UDP 缓冲丢弃栈的出现;(4) 调大 rmem 验证丢弃消失。
【实验报告要求】五个实验的数据(tcplife 输出、重传率计算、队列状态截图、溢出计数、丢包栈聚合)、每个实验的机制解释。核心交付物:实验二的"丢包率 → 重传率 → 吞吐下降"定量关系表,和实验四的 accept 溢出证据链。附加思考:(1) tcpdump 抓包显示一切正常但应用报超时,列出至少三种可能(提示:网卡层丢弃、accept 队列、应用读取);(2) 为什么 TIME_WAIT 是 60 秒且不能简单调小?(提示:MSL、旧包迷航、RFC 793 的设计考量。)
延伸阅读
- Brendan Gregg, "BPF Performance Tools" 第 9 章——本章原始出处,含网络工具全表(tcplife 家族、tcptop、tcpdrop 等)和 TCP 追踪点清单。
- 《TCP/IP 详解 卷1:协议》第 2 版——TCP 机制的权威教材:重传(RTO/快速重传/SACK)、拥塞控制、窗口管理的完整讲解,理解 tcpretrans 输出的理论基础。
- Linux 内核网络文档 Documentation/networking/ 目录——ip-sysctl.rst(全部网络 sysctl 参数的官方说明)、scaling.rst(RSS/RPS/RFS 多队列扩展)、tcp.rst,网络调优的字典。
- Cilium 文档与 Hubble 项目 (cilium.io, hubble.cilium.io)——BPF 网络数据面的工业级实现,Hubble 的流可见性展示了 BPF 网络观测的产品形态,容器网络排查的利器。
- xdp-tutorial (github.com/xdp-project/xdp-tutorial)——XDP 编程的官方教程,从丢包计数器到重定向的渐进实验,理解 BPF 数据面的最佳动手材料。
- Brendan Gregg, "Systems Performance" 第 2 版第 11 章 (Network)——网络子系统全景:从网卡中断到 TCP 状态机到 DNS,传统工具与 BPF 工具对照,含完整的延迟分解方法论。
- Cloudflare 博客的 XDP 与 DDoS 系列文章 (blog.cloudflare.com, "L4Drop"、"Path to XDP")——XDP 生产实践的第一手经验:每包成本测量、驱动兼容性、与 iptables 的性能对比数据。
- nstat/netstat -s 各计数项的内核源码对照 (net/ipv4/tcp.c 等中的 LINUX_MIB_ 定义)——每个 TCP 统计项对应的精确内核位置,丢包排查地图的底层依据。