Linux 网络内核协议栈深度剖析(十):网络诊断工具与实战排查

网络问题是线上故障中最难定位的一类。症状千变万化——P99 延迟突然抖动、某个服务间歇性超时、容器之间偶发丢包——而根因可能藏在协议栈的任何一层:网卡驱动的 ring buffer 溢出、内核 backlog 队列打满、TCP 重传引发的滑动窗口收缩、iptables 规则误命中,乃至 NUMA 拓扑导致的中断不均衡。本文是本系列第十篇,聚焦于工具链与实战:从 ss 的每个输出字段到 bpftrace 脚本,从 /proc/net 的原始数字到三个完整的排查案例,构建一套系统化的网络诊断方法论。


一、网络问题分类与排查框架

1.1 问题三分类

在动手之前,先把问题分类,可以大幅缩小排查范围:

连通性问题(Connectivity):两端完全不通,ping 无回应,TCP 连接无法建立。根因通常在路由、防火墙、ARP/ND 解析失败或物理链路故障。

性能问题(Performance):能连通但慢,表现为吞吐低、延迟高或抖动大。根因多在 TCP 拥塞控制参数不当、重传率过高、缓冲区过小或 CPU/中断不均衡。

应用层问题(Application Layer):底层网络正常,但应用层报错,如 HTTP 502/504、gRPC deadline exceeded。根因往往是 backlog 队列满、TIME_WAIT 端口耗尽、连接池配置不当或 TLS 握手超时。

1.2 系统化排查流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
问题报告
│
▼
┌─────────────────────────────────┐
│ 1. 确认问题范围 │
│ - 单点 or 全量? │
│ - 单向 or 双向? │
│ - 特定时间段? │
└────────────┬────────────────────┘
│
▼
┌─────────────────────────────────┐
│ 2. 收集宏观指标 │
│ ss -s / nstat -az │
│ /proc/net/sockstat │
│ ethtool -S eth0 │
└────────────┬────────────────────┘
│
┌─────────┴──────────┐
│ │
▼ ▼
连通性异常 性能/应用异常
│ │
▼ ▼
ping/traceroute ss -tienpm (socket 详情)
arp -n nstat (重传/超时计数)
ip route show tcpdump 抓包分析
iptables -L -v bpftrace 精确追踪
│ │
└─────────┬──────────┘
│
▼
┌─────────────────────────────────┐
│ 3. 定位协议栈层次 │
│ L1/L2: ethtool, ip link │
│ L3: ip route, conntrack │
│ L4: ss, tcp 状态机 │
│ App: strace, lsof │
└────────────┬────────────────────┘
│
▼
┌─────────────────────────────────┐
│ 4. 根因确认与修复 │
│ - 内核参数调整 (sysctl) │
│ - 驱动/固件更新 │
│ - 应用配置修改 │
└─────────────────────────────────┘

二、ss 命令深度解析

ss 是 netstat 的现代替代品,直接读取内核的 socket 数据结构,速度更快、信息更丰富。核心命令:

1
2
3
4
5
6
7
ss -tienpm
# -t 只显示 TCP
# -i 显示内部 TCP 信息(拥塞窗口、RTT 等)
# -e 显示扩展信息(uid、inode 等)
# -n 不解析主机名
# -p 显示进程信息
# -m 显示 socket 内存使用(skmem)

2.1 Recv-Q 与 Send-Q 的双重语义

这两个字段在不同 socket 状态下含义截然不同,是理解内核队列模型的关键:

状态 Recv-Q 含义 Send-Q 含义
LISTEN 全连接队列(accept queue)当前长度 全连接队列最大长度(backlog)
ESTABLISHED 已收到但应用尚未 read() 的字节数 已发送但尚未被对端 ACK 的字节数

LISTEN 状态:当 Recv-Q 接近 Send-Q 时,说明全连接队列即将打满。内核会开始丢弃新完成的三次握手连接,客户端表现为连接超时。此时需要扩大 net.core.somaxconn 并让应用更快地调用 accept()。

ESTABLISHED 状态:Recv-Q 非零说明应用读取速度跟不上网络接收速度(读慢写快场景);Send-Q 持续增大说明对端接收窗口收缩或网络拥塞,数据在发送缓冲区堆积。

2.2 skmem 字段逐字段解析

skmem 展示 socket 的内存使用全景,格式如下:

1
skmem:(r:0,rb:87380,t:0,tb:16384,f:1024,w:0,o:0,bl:0,d:0)
字段 内核变量 含义
r:<rcv_alloc> sk->sk_rmem_alloc 接收队列中已分配的 skb 内存总量(字节),即实际占用的接收缓冲区
rb:<sk_rcvbuf> sk->sk_rcvbuf 接收缓冲区上限,由 net.ipv4.tcp_rmem[2] 或 SO_RCVBUF 设置
t:<snd_alloc> sk->sk_wmem_alloc 发送队列中已分配但尚未释放的 skb 内存(含飞行中的数据)
tb:<sk_sndbuf> sk->sk_sndbuf 发送缓冲区上限,由 net.ipv4.tcp_wmem[2] 或 SO_SNDBUF 设置
f:<fwd_alloc> sk->sk_forward_alloc 预分配但尚未使用的内存额度,来自 proto 的内存预算机制
w:<wmem_alloc> sk->sk_wmem_queued 发送队列(未发送 + 已发送未 ACK)中的 skb 总内存
o:<opt_mem> sk->sk_optmem_alloc socket 选项(如 IP_OPTIONS、cmsg)消耗的内存
bl:<back_log> sk->sk_backlog.len 软中断上下文来不及处理、暂存在 backlog 队列中的数据量
d:<drops> sk->sk_drops 因缓冲区满等原因丢弃的包计数(累计)

诊断要点:当 r 接近 rb 时,说明接收缓冲区即将满,下一个数据包将触发 sk_rmem_schedule 失败进而丢包;当 bl 持续非零时,说明内核软中断处理不及时,可检查 /proc/net/softnet_stat 中的 time_squeeze 列。

2.3 TCP 内部信息字段解析

1
2
3
4
5
6
7
cubic wscale:7,7 rto:204 rtt:4.5/0.75 ato:40 mss:1448 pmtu:1500
cwnd:10 ssthresh:7 bytes_sent:12345 bytes_retrans:0 bytes_acked:12345
bytes_received:9876 segs_out:100 segs_in:95 data_segs_out:80
data_segs_in:75 send 257.1Mbps lastsnd:10 lastrcv:20 lastack:15
pacing_rate 514.3Mbps delivery_rate 257.1Mbps delivered:80
app_limited busy:100ms retrans:0/1 dsack_dups:0 reordering:3
rcv_rtt:4.5 rcv_space:14480 rcv_ssthresh:87380 minrtt:3.2
字段 含义与来源
cubic 当前使用的拥塞控制算法,对应 tp->icsk_ca_ops->name
wscale:7,7 <本端窗口缩放因子>,<对端窗口缩放因子>,来自 tp->rx_opt.snd_wscale 和 rcv_wscale。实际窗口大小 = 报文窗口字段 × 2^wscale
rto:204 当前重传超时值(毫秒),由 RTT 和抖动计算:RTO = SRTT + 4×RTTVAR,下限 200ms
rtt:4.5/0.75 <平滑RTT(ms)>/<RTT方差(ms)>,对应内核 tp->srtt_us>>3 和 tp->mdev_us>>2
ato:40 ACK 延迟超时(ms),TCP 延迟确认定时器,通常 40ms,对应 icsk->icsk_ack.ato
mss:1448 当前有效 MSS(本端发送的最大段大小),受 MTU、PMTU 和对端通告 MSS 共同限制
cwnd:10 拥塞窗口(段数),tp->snd_cwnd,限制飞行中的数据量为 cwnd × mss 字节
ssthresh:7 慢启动阈值(段数),tp->snd_ssthresh,超过此值进入拥塞避免阶段
bytes_retrans 累计重传字节数,tp->bytes_retrans
retrans:0/1 <当前飞行中的重传段数>/<累计重传段数>,前者非零说明正在重传
pacing_rate TCP Pacing 发送速率,由 BBR 或 FQ 调度器控制,避免突发
delivery_rate 最新测量到的实际交付速率,用于 BBR 带宽估算
rcv_rtt 接收端测量的 RTT(ms),用于接收缓冲区自动调优
rcv_space 接收端滑动窗口大小,由接收速率动态调整

2.4 timer 字段

1
timer:(on,200ms,0)

格式为 (<timer_type>,<expires>,<retransmits>):

timer_type 含义
on 重传定时器运行中
keepalive Keepalive 探测定时器
timewait TIME_WAIT 定时器(2MSL)
persist 零窗口探测定时器
off 无定时器

expires 是定时器剩余时间;retransmits 是已重传次数,达到 tcp_retries2(默认 15)后连接将被强制关闭。


三、/proc/net 虚拟文件深度解析

3.1 /proc/net/tcp 字段解析

1
2
3
cat /proc/net/tcp
# sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt
# 0: 0F02000A:0016 C802000A:C4E8 01 00000000:00000000 00:00000000 00000000

各列含义:

列 说明
sl socket 在哈希桶中的槽位编号
local_address 本地 IP:Port,十六进制,小端序:0F02000A = 10.0.2.15,0016 = 22
rem_address 远端 IP:Port,同上编码方式
st TCP 状态,十六进制枚举(见下表)
tx_queue:rx_queue 发送队列字节数:接收队列字节数(十六进制)
tr:tm->when 重传定时器是否在运行(0/1):定时器剩余 jiffies
retrnsmt 已重传次数
uid socket 所属用户 uid
inode socket 对应的 inode 号,可通过 ls -la /proc/<pid>/fd/ 映射到进程

TCP 状态枚举表:

十六进制 状态名
01 ESTABLISHED
02 SYN_SENT
03 SYN_RECV
04 FIN_WAIT1
05 FIN_WAIT2
06 TIME_WAIT
07 CLOSE
08 CLOSE_WAIT
09 LAST_ACK
0A LISTEN
0B CLOSING
1
2
# 统计各状态连接数
awk 'NR>1 {print $4}' /proc/net/tcp | sort | uniq -c | sort -rn

3.2 /proc/net/sockstat

1
2
3
4
5
6
7
cat /proc/net/sockstat
# sockets: used 1024
# TCP: inuse 512 orphan 3 tw 128 alloc 520 mem 42
# UDP: inuse 16 mem 2
# UDPLITE: inuse 0
# RAW: inuse 0
# FRAG: inuse 0 memory 0
字段 含义
TCP: inuse 当前使用中的 TCP socket 数
orphan 孤儿 socket 数:已调用 close() 但尚未完成四次挥手,不属于任何进程。过多会消耗内存,受 net.ipv4.tcp_max_orphans 限制
tw TIME_WAIT socket 数,过多时检查 tcp_tw_reuse 参数
alloc 已分配(含未绑定)的 TCP socket 总数
mem TCP socket 占用的内存页数,乘以页大小(通常 4KB)得字节数

3.3 nstat 关键指标解析

1
nstat -az | grep -E 'Retrans|Timeout|OFO|Backlog|Listen|Drop'
指标 内核来源 告警意义
TcpExtTCPFastRetrans 快速重传触发次数(收到 3 个重复 ACK),相对正常 持续增长说明网络存在随机丢包
TcpExtTCPTimeouts RTO 超时重传次数 比 FastRetrans 严重,说明丢包率高或 RTT 剧烈抖动
TcpExtTCPOFOQueue 收到乱序包放入 OFO 队列的次数 大量乱序可能触发不必要的重传
TcpExtTCPBacklogDrop socket 的 backlog 队列满而丢弃的包数 应用处理 socket 太慢,增大 backlog 或优化应用
TcpExtListenDrops 全连接队列满时丢弃 SYN 的次数 需增大 somaxconn 或加快 accept()
TcpExtListenOverflows 全连接队列溢出次数(与 ListenDrops 相似但计数时机不同) 同上
TcpExtTCPSACKDiscard SACK 信息被丢弃(通常因重复或无效) —
TcpExtTCPAbortOnTimeout 因连接超时(达到最大重试次数)而中止的连接数 说明有连接长时间无响应
TcpExtTCPSynRetrans SYN 包重传次数 持续增长说明服务端 backlog 满或存在 SYN flood

3.4 /proc/net/snmp 关键字段

1
cat /proc/net/snmp | grep -E '^Tcp|^Ip|^Udp'

重要字段:

1
Tcp: ... RetransSegs ... InErrs ... OutRsts ...
字段 含义
Ip/InDiscards IP 层因内存不足丢弃的入包数
Ip/OutDiscards IP 层因无路由或资源不足丢弃的出包数
Tcp/ActiveOpens 主动建立连接次数(客户端发起)
Tcp/PassiveOpens 被动建立连接次数(服务端接受)
Tcp/AttemptFails 连接建立失败次数(SYN_SENT/SYN_RECV 中途失败)
Tcp/EstabResets ESTABLISHED 状态下被 RST 关闭的连接数
Tcp/RetransSegs 重传段总数,除以 OutSegs 得重传率
Tcp/InErrs 收到的错误 TCP 段数(checksum 失败等)
Tcp/OutRsts 发出的 RST 包数,突增说明有异常断连
Udp/RcvbufErrors UDP 接收缓冲区满丢包数
Udp/SndbufErrors UDP 发送缓冲区满丢包数

四、tcpdump 高级用法

4.1 BPF 捕获过滤器语法精华

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 基础:捕获特定主机和端口
tcpdump -i eth0 -nn host 10.0.0.1 and port 8080

# 协议过滤
tcpdump -i eth0 -nn proto tcp
tcpdump -i eth0 -nn udp and port 53

# 仅捕获 TCP 控制包(SYN/FIN/RST),减少抓包量
tcpdump -i eth0 -nn 'tcp[tcpflags] & (tcp-syn|tcp-fin|tcp-rst) != 0'

# 仅捕获 SYN(过滤 SYN-ACK):bit 1 set, bit 4 not set
tcpdump -i eth0 -nn 'tcp[tcpflags] == tcp-syn'

# 捕获 RST
tcpdump -i eth0 -nn 'tcp[tcpflags] & tcp-rst != 0'

# 捕获 TCP 零窗口(window size = 0)
tcpdump -i eth0 -nn 'tcp[14:2] == 0'

# 捕获小窗口(< 1000 字节,可能即将触发零窗口)
tcpdump -i eth0 -nn 'tcp[14:2] < 1000 and tcp[14:2] > 0'

# 捕获 ICMP 不可达(可以发现 PMTU 问题)
tcpdump -i eth0 -nn 'icmp[icmptype] == icmp-unreach'

# 复合过滤:特定主机的 HTTP 流量或 DNS
tcpdump -i eth0 -nn '(host 10.0.0.1 and port 80) or port 53'

# 排除 SSH,避免抓包流量干扰自身
tcpdump -i eth0 -nn 'not port 22'

# 按包大小过滤(大包,可能是数据传输)
tcpdump -i eth0 -nn 'greater 1400'

4.2 TCP 握手异常分析

SYN 重传:在抓包中看到同一个 (src_ip, src_port, seq_num) 的 SYN 包出现多次,时间间隔通常是 1s、2s、4s(指数退避)。

1
2
3
4
5
# 抓 SYN 包,写入文件后用 tshark 分析重传
tcpdump -i eth0 -nn -w /tmp/syn.pcap 'tcp[tcpflags] == tcp-syn' &

# 用 tshark 统计 SYN 重传
tshark -r /tmp/syn.pcap -q -z io,stat,1,"tcp.flags.syn==1 && tcp.analysis.retransmission"

RST 原因判断:

1
2
# 抓 RST 包,同时捕获前后各 5 个包提供上下文
tcpdump -i eth0 -nn -A 'tcp[tcpflags] & tcp-rst != 0'

RST 来源分析:

  • 服务端主动 RST:通常因为对端发来的包不符合预期(如连接已关闭但收到数据),或调用了 SO_LINGER 选项
  • 防火墙/中间盒 RST:RST 包的 TTL 与正常通信包不同,seq number 恰好在窗口边界,可疑
  • 客户端 RST:收到 SYN-ACK 但本地没有对应连接(端口复用冲突),OutRsts 增加

4.3 零窗口与拥塞分析

1
2
3
4
5
6
7
8
9
10
11
# 捕获完整会话,包含零窗口通告
tcpdump -i eth0 -nn -w /tmp/full.pcap host 10.0.0.1 and port 8080

# 用 tshark 提取零窗口事件
tshark -r /tmp/full.pcap -Y "tcp.window_size == 0" -T fields \
-e frame.time -e ip.src -e ip.dst -e tcp.srcport -e tcp.dstport

# 查看窗口大小随时间的变化趋势
tshark -r /tmp/full.pcap -T fields \
-e frame.time_relative -e tcp.window_size -e tcp.analysis.zero_window \
-Y "ip.src == 10.0.0.1"

4.4 典型诊断命令集

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 1. 快速查看流量概况(每秒统计包数和字节数)
tcpdump -i eth0 -nn -q 2>&1 | pv -l -i 1 > /dev/null

# 2. 抓取并实时展示 TCP 握手过程(含 SYN/SYN-ACK/ACK)
tcpdump -i eth0 -nn -S 'tcp[tcpflags] & (tcp-syn|tcp-ack) != 0 and not port 22'

# 3. 排查 DNS 超时(捕获无响应的查询)
tcpdump -i eth0 -nn 'udp port 53' -w /tmp/dns.pcap

# 4. MTU 探测问题排查(DF 位设置的大包)
tcpdump -i eth0 -nn 'ip[6:2] & 0x4000 != 0 and greater 1400'

# 5. 统计各连接的包数(离线分析)
tcpdump -r /tmp/full.pcap -nn -q | awk '{print $3}' | cut -d. -f1-4 | sort | uniq -c | sort -rn | head

# 6. 捕获 TCP Fast Open(TFO)握手
tcpdump -i eth0 -nn 'tcp[tcpflags] == tcp-syn' -A | grep -A2 'Fast Open'

# 7. 检测 TCP 时间戳选项是否启用
tcpdump -i eth0 -nn 'tcp[tcpflags] == tcp-syn' -v 2>&1 | grep -i 'timestamp\|nop'

五、bpftrace 网络诊断脚本集

bpftrace 基于 eBPF,可以在不修改内核的情况下动态追踪任意内核函数,开销极低。以下五个脚本覆盖网络诊断最常见场景。

脚本一:TCP 重传追踪(含源目的 IP:Port)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/usr/bin/env bpftrace
#include <linux/tcp.h>
#include <net/sock.h>

/*
* 追踪 TCP 重传事件,打印进程、本地端口、远端端口
* 可用于快速确认哪些连接在重传、重传频率
*/
kprobe:tcp_retransmit_skb {
$sk = (struct sock *)arg0;
$lport = $sk->__sk_common.skc_num;
$dport = $sk->__sk_common.skc_dport;
/* dport 在内核中是网络字节序,需要手动转换 */
$dport = ($dport >> 8) | (($dport << 8) & 0xff00);
printf("%-6d %-12s TCP retransmit: lport=%d dport=%d\n",
pid, comm, $lport, $dport);
}

运行:sudo bpftrace retrans.bt,输出示例:

1
2
12345  nginx        TCP retransmit: lport=80 dport=54321
12345 nginx TCP retransmit: lport=80 dport=54322

若某个 dport 频繁出现,说明该客户端连接质量差,可结合 ss 查看该连接的 RTT 和重传计数确认。

脚本二:TCP 连接建立延迟分布

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/usr/bin/env bpftrace
/*
* 测量 tcp_v4_connect() 调用耗时,以直方图展示
* 用于量化 DNS 解析 + 三次握手的端到端延迟
*/
kprobe:tcp_v4_connect { @start[tid] = nsecs; }

kretprobe:tcp_v4_connect /@start[tid]/ {
@connect_lat_us = hist((nsecs - @start[tid]) / 1000);
delete(@start[tid]);
}

END {
printf("\nTCP connect latency distribution (us):\n");
print(@connect_lat_us);
}

输出示例:

1
2
3
4
5
6
@connect_lat_us:
[256, 512) 5 |@@ |
[512, 1K) 42 |@@@@@@@@@@@@@@@@@@ |
[1K, 2K) 89 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|
[2K, 4K) 23 |@@@@@@@@@ |
[4K, 8K) 3 |@ |

延迟集中在 1ms-2ms 为正常本地网络;若出现 [100K, 200K) 的长尾,说明存在严重的连接建立延迟,需检查是否有 SYN 丢包导致的 1s 重传。

脚本三:按进程统计发送/接收字节

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/usr/bin/env bpftrace
/*
* 每 5 秒打印一次各进程的 TCP 收发字节统计
* 快速定位带宽消耗大户
*/
kprobe:tcp_sendmsg {
/* arg2 是 size_t size,即本次发送请求的字节数 */
@sent_bytes[comm, pid] += arg2;
}

kprobe:tcp_recvmsg {
/* arg2 是 size_t size,即本次接收请求的缓冲区大小 */
@recv_bytes[comm, pid] += arg2;
}

interval:s:5 {
printf("\n=== Top senders (5s) ===\n");
print(@sent_bytes);
printf("\n=== Top receivers (5s) ===\n");
print(@recv_bytes);
clear(@sent_bytes);
clear(@recv_bytes);
}

注意:tcp_sendmsg 的 arg2 是应用请求发送的字节数,不等于实际发出的字节数(受发送缓冲区影响),但在排查带宽使用时已足够准确。

脚本四:Socket 接收队列积压监控

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/usr/bin/env bpftrace
#include <net/sock.h>
/*
* 监控各进程的 socket backlog 积压情况
* sk_backlog.len 是软中断来不及处理的包的总大小
* 持续非零说明 CPU 处理 softirq 不及时
*/
kprobe:tcp_add_backlog {
$sk = (struct sock *)arg0;
$backlog_len = $sk->sk_backlog.len;
if ($backlog_len > 0) {
@backlog[comm] = max($backlog_len);
@backlog_hist = hist($backlog_len);
}
}

interval:s:1 {
if (@backlog) {
printf("\n--- Backlog snapshot ---\n");
print(@backlog);
}
}

若 @backlog_hist 中出现大量高值,需检查:

  1. napi_schedule 是否不均匀(多队列网卡的 RSS 配置)
  2. ksoftirqd CPU 占用率
  3. /proc/net/softnet_stat 中的 time_squeeze 列是否在增长

脚本五:追踪 TCP 状态变化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/usr/bin/env bpftrace
#include <net/sock.h>
/*
* 追踪 TCP 状态机转换,打印每次状态变化
* 对于排查异常断连(直接从 ESTABLISHED 跳到 CLOSE)非常有用
*/
kprobe:tcp_set_state {
$sk = (struct sock *)arg0;
$newstate = (int32)arg1;
$oldstate = (int32)$sk->__sk_common.skc_state;

$state_names = ("UNKNOWN", "ESTABLISHED", "SYN_SENT", "SYN_RECV",
"FIN_WAIT1", "FIN_WAIT2", "TIME_WAIT", "CLOSE",
"CLOSE_WAIT", "LAST_ACK", "LISTEN", "CLOSING",
"NEW_SYN_RECV");

/* 只关注从 ESTABLISHED 开始的断连过程 */
if ($oldstate == 1 || $newstate == 7 || $newstate == 8) {
printf("%-12s pid=%-6d %s -> %s\n",
comm, pid,
$state_names[$oldstate],
$state_names[$newstate]);
}
}

输出示例:

1
2
3
nginx        pid=12345  ESTABLISHED -> FIN_WAIT1     # 正常主动关闭
nginx pid=12345 ESTABLISHED -> CLOSE_WAIT # 对端主动关闭
nginx pid=12345 ESTABLISHED -> CLOSE # 异常!RST 导致

第三行的 ESTABLISHED -> CLOSE 跳过了正常的四次挥手,通常由 RST 触发,需结合 tcpdump 进一步定位 RST 原因。


六、实战案例一:排查 P99 延迟抖动(TCP 重传)

场景:监控系统显示某微服务 P99 延迟从 50ms 上升到 800ms,P50 正常,怀疑存在 TCP 重传。

步骤一:找出重传多的连接

1
ss -tienp | grep ESTAB | sort -k5 -n | tail -10

重点看 retrans 和 bytes_retrans 字段:

1
2
3
ESTAB  0  0  10.0.0.1:8080  10.0.0.2:54321
cubic wscale:7,7 rto:412 rtt:105/82.5 ato:40 mss:1448
cwnd:4 ssthresh:4 bytes_retrans:204800 retrans:2/45

解读:rtt:105/82.5 说明平均 RTT 105ms 且方差极大(正常应为 4.5/0.75),retrans:2/45 说明累计重传 45 次,ssthresh:4 说明拥塞控制已经将阈值压到很低,吞吐受到严重限制。

步骤二:确认重传计数增长趋势

1
watch -d -n 1 'nstat -az | grep -E "Retrans|Loss|Timeout"'
1
2
3
4
TcpExtTCPFastRetrans          1234    0.0   # 快速重传,网络有随机丢包
TcpExtTCPTimeouts 87 0.0 # RTO 超时重传,更严重
TcpExtTCPSynRetrans 3 0.0 # SYN 重传,建立连接也慢
TcpExtTCPLostRetransmit 12 0.0 # 重传包本身也丢了

TcpExtTCPTimeouts 持续增长是核心告警信号,说明 RTO 触发导致 800ms 的延迟峰值(默认 RTO 下限 200ms,第一次超时等 200ms,若再丢就等 400ms)。

步骤三:抓包确认丢包位置

1
2
3
4
5
6
# 抓取所有控制包和小窗口包
tcpdump -i eth0 -nn -w /tmp/retrans.pcap \
'tcp[tcpflags] & (tcp-syn|tcp-fin|tcp-rst) != 0 or (tcp[14:2] < 1000)'

# 同时抓取目标端口的完整流量(限制大小避免磁盘打满)
tcpdump -i eth0 -nn -w /tmp/full.pcap -C 100 -W 5 'port 8080'
1
2
# 离线分析:统计重传包数量
tshark -r /tmp/full.pcap -q -z io,stat,1,"tcp.analysis.retransmission"

步骤四:bpftrace 精确追踪重传触发点

1
2
3
4
5
6
sudo bpftrace -e '
kprobe:tcp_retransmit_skb {
@[comm, pid, kstack] = count();
}
interval:s:10 { print(@); clear(@); }
'

通过内核调用栈可以判断重传是由 RTO 定时器触发(tcp_write_timer_handler)还是由 SACK 驱动的快速重传(tcp_fastretrans_alert)。

步骤五:检查网卡错误统计

1
ethtool -S eth0 | grep -iE 'error|drop|miss|fifo'
1
2
3
rx_dropped: 1523
rx_fifo_errors: 891
tx_errors: 0

rx_fifo_errors 说明网卡 ring buffer 已满,包在驱动层被丢弃,不经过协议栈,因此 nstat 里看不到,但 tcpdump 也抓不到。需要增大 ring buffer:

1
ethtool -G eth0 rx 4096

步骤六:检查 CPU 中断分布

1
cat /proc/interrupts | grep eth0
1
2
3
45:  28000000         0         0         0   PCI-MSI  eth0-0
46: 500 1200000 0 0 PCI-MSI eth0-1
47: 1000 0 1800000 0 PCI-MSI eth0-2

发现 eth0-1 和 eth0-2 的中断都压在 CPU 1 和 CPU 2 上,CPU 0 和 CPU 3 几乎没有。调整 IRQ 亲和性:

1
2
3
4
echo "f" > /proc/irq/45/smp_affinity   # 允许所有 CPU 处理
echo "f" > /proc/irq/46/smp_affinity
# 或使用 irqbalance
systemctl restart irqbalance

七、实战案例二:排查 SYN 丢包(连接建立失败)

场景:新上线的服务在高并发时出现大量连接超时,客户端报 Connection refused 或持续 SYN_SENT。

步骤一:确认全局 SYN 丢包情况

1
netstat -s | grep -iE 'syn|listen|backlog|overflow'
1
2
3456 SYNs to LISTEN sockets dropped
1234 times the listen queue of a socket overflowed

若这两个数字在持续增长(用 watch 监测差值),确认是 backlog 满导致的 SYN 丢包。

步骤二:检查全连接队列占用

1
ss -lntp
1
2
State   Recv-Q  Send-Q  Local Address:Port
LISTEN 512 512 0.0.0.0:8080

Recv-Q == Send-Q == 512 说明全连接队列已经打满(Recv-Q = 当前队列长度,Send-Q = 队列最大值)。当队列满时,新完成三次握手的连接会被直接丢弃,客户端的 SYN 需要等到 RTO 超时(1s)后重传。

步骤三:查看当前 backlog 配置

1
2
3
sysctl net.ipv4.tcp_max_syn_backlog net.core.somaxconn
# net.ipv4.tcp_max_syn_backlog = 128 <- 半连接队列(SYN_RECV 状态上限)
# net.core.somaxconn = 128 <- 全连接队列上限

两个值都是 128,远不够用。调整:

1
2
3
sysctl -w net.core.somaxconn=65535
sysctl -w net.ipv4.tcp_max_syn_backlog=65535
# 应用层也需要增大 listen() 的 backlog 参数并重启服务

重要:somaxconn 是系统级上限,但应用调用 listen(fd, backlog) 时传入的值才是实际生效的全连接队列大小(取两者中的较小值)。Nginx 对应配置 listen 8080 backlog=65535,Java 应用需修改 ServerSocket 构造参数。

1
2
sysctl net.ipv4.tcp_syncookies
# net.ipv4.tcp_syncookies = 1

SYN Cookie 是在半连接队列满时的保护机制:服务端不记录 SYN 状态,而是把连接信息编码进 SYN-ACK 的 ISN 中。优点是防 SYN flood,缺点是无法使用 TCP 选项(SACK、窗口缩放等)。若已开启但仍然大量丢包,说明是全连接队列满的问题,SYN Cookie 此时不起作用。

1
2
3
4
5
# 通过 nstat 确认 SYN Cookie 使用情况
nstat -az | grep -i 'cookie'
# TcpExtSyncookiesSent 1234 # 发出了 SYN cookie
# TcpExtSyncookiesRecv 987 # 收到有效 SYN cookie 响应
# TcpExtSyncookiesFailed 12 # SYN cookie 验证失败(可能是伪造包)

步骤五:排查 conntrack 表满

如果服务器部署了 iptables NAT 或防火墙规则,连接跟踪表(conntrack)满也会导致新连接被丢弃:

1
2
3
sysctl net.netfilter.nf_conntrack_count net.netfilter.nf_conntrack_max
# net.netfilter.nf_conntrack_count = 131070
# net.netfilter.nf_conntrack_max = 131072 <- 几乎打满!
1
2
3
4
5
6
7
8
9
# 临时扩大 conntrack 表
sysctl -w net.netfilter.nf_conntrack_max=524288

# 查看 conntrack 超时配置(TIME_WAIT 连接占用时间)
sysctl net.netfilter.nf_conntrack_tcp_timeout_time_wait
# net.netfilter.nf_conntrack_tcp_timeout_time_wait = 120

# 缩短超时,加快 conntrack 条目回收
sysctl -w net.netfilter.nf_conntrack_tcp_timeout_time_wait=30

步骤六:修复建议汇总

  1. 增大 somaxconn 和 tcp_max_syn_backlog 至少到 65535
  2. 更新应用 listen() backlog 参数并重启
  3. 扩大 conntrack 表(如使用了 NAT)
  4. 检查并优化应用的 accept() 速度(避免 accept 线程成为瓶颈)
  5. 开启 tcp_syncookies 作为最后防线

八、实战案例三:容器网络丢包排查

场景:Kubernetes 集群中,Pod A 访问 Pod B 时出现间歇性丢包(ping 丢包率约 0.5%)。

L1/L2:物理网卡层

1
2
3
4
5
6
7
# 检查网卡错误计数
ethtool -S eth0 | grep -iE 'error|drop|miss|crc|fifo'

# 检查接口统计(含错误帧)
ip -s link show eth0
# RX: bytes packets errors dropped missed mcast
# 1.2TB 8000M 0 1523 891 0

dropped 是驱动层丢包(ring buffer 满),missed 是硬件来不及 DMA 的丢包。两者都需扩大 ring buffer 并检查 CPU 中断亲和性。

veth 对层

1
2
3
4
5
6
7
8
# 找到 Pod 对应的 veth 接口名(通过 nsenter 或 crictl)
POD_PID=$(crictl inspect <container_id> | jq '.info.pid')
nsenter -t $POD_PID -n ip link show

# 在宿主机检查对应 veth 的统计
ip -s link show veth3a8b2c1d
# RX: bytes packets errors dropped
# 50GB 200M 0 450 <- dropped 非零!

veth 对的 dropped 通常因为:

  1. 容器内的接收缓冲区满(sk_rcvbuf 耗尽)
  2. iptables 规则丢包(下一步检查)
  3. qdisc 队列满(检查 tc qdisc show dev veth3a8b2c1d)

Bridge / OVS 层

1
2
3
4
5
6
7
8
9
10
# 检查 Linux bridge 转发统计
bridge -s link
bridge fdb show br0

# 检查是否有 MAC 地址漂移(导致包发到错误端口)
bridge fdb show | grep <pod_mac>

# 若使用 OVS
ovs-vsctl show
ovs-ofctl dump-flows br0 | grep drop

iptables/nftables 层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 查看带计数的 NAT 规则(重点关注 DROP 和计数增长快的规则)
iptables -t nat -L -n -v --line-numbers | grep -v "0 0"

# 查看 filter 表
iptables -t filter -L FORWARD -n -v | grep -v "0 0"

# 检查 KUBE-FORWARD 链(Kubernetes 常见丢包点)
iptables -L KUBE-FORWARD -n -v

# 使用 nftables(如果集群用 nftables)
nft list ruleset | grep -A5 'drop\|reject'

# 追踪一个包通过 iptables 的路径
iptables -t raw -I PREROUTING -s <src_ip> -j TRACE
iptables -t raw -I OUTPUT -d <dst_ip> -j TRACE
dmesg | grep 'TRACE:'
# 排查完毕后删除 TRACE 规则!
iptables -t raw -D PREROUTING -s <src_ip> -j TRACE

容器内部

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 进入容器命名空间
nsenter -t $POD_PID -n bash

# 检查路由表
ip route show
# default via 169.254.1.1 dev eth0
# 169.254.1.1 dev eth0 scope link

# 检查 ARP 表
arp -n
# Address HWtype HWaddress Flags
# 169.254.1.1 ether ee:ee:ee:ee:ee:ee C <- 正常

# 检查 conntrack 在容器命名空间内的状态
conntrack -L | wc -l
conntrack -S # 查看统计,重点看 drop 和 insert_failed

# 检查容器内的 socket 缓冲区
ss -tienm | head -20

常见根因:

  • Calico/Flannel 的 IPIPtunnel 或 VXLAN 的 MTU 比物理网卡小 50/20 字节,导致大包触发 IP 分片或 PMTU 黑洞
  • kube-proxy 的 iptables 规则链过长(超过 10000 条),导致规则匹配 CPU 占用高
  • conntrack 表在高并发时插入冲突(insert_failed 计数增长),导致包被 DROP
1
2
3
4
# MTU 排查
ip link show | grep mtu
# 物理网卡 mtu 1500,tunl0 mtu 1480(IPIP 隧道开销 20 字节)
# 容器内 eth0 需要配置 mtu 1480,否则大包无法传输

九、诊断工具速查表

工具 主要用途 核心命令示例
ss Socket 状态、队列深度、TCP 内部参数 ss -tienpm
nstat 内核网络计数器(比 netstat -s 更快) nstat -az | grep Retrans
tcpdump 抓包,支持 BPF 过滤 tcpdump -i eth0 -nn -w file.pcap 'port 80'
tshark tcpdump 离线分析,支持协议解析 tshark -r file.pcap -Y "tcp.analysis.retransmission"
bpftrace 内核动态追踪,低开销 eBPF 脚本 bpftrace -e 'kprobe:tcp_retransmit_skb { printf("%s\n", comm); }'
perf CPU 性能分析,支持网络子系统 perf stat -e 'net:*' -p <pid>
ethtool 网卡配置和统计 ethtool -S eth0 | grep drop
ip 网络接口、路由、统计 ip -s link show eth0
conntrack 连接跟踪表查询和统计 conntrack -S
tc 流量控制,查看 qdisc 丢包 tc -s qdisc show dev eth0
netstat 连接状态统计(老工具,可被 ss 替代) netstat -s | grep -i retrans
sar 历史网络统计(sysstat 包) sar -n DEV 1 10
iptraf-ng 实时流量监控,交互式界面 iptraf-ng -i eth0
mtr 结合 traceroute 和 ping,持续路径探测 mtr --report --tcp -P 80 10.0.0.1
nmap 端口扫描,确认服务监听状态 nmap -sS -p 8080 10.0.0.1
strace 追踪进程系统调用,定位 socket API 调用 strace -e trace=network -p <pid>
lsof 查看进程打开的 socket lsof -i :8080 -n -P
systemtap 更强大的内核动态追踪(需编译) stap -e 'probe tcp.sendmsg { ... }'

小结

本文系统地介绍了 Linux 网络诊断的完整工具链和方法论:

  1. 分类先于动手:区分连通性、性能和应用层问题,选择正确的工具切入点,避免盲目抓包
  2. ss 是第一现场:ss -tienpm 的输出几乎涵盖了 TCP 连接的完整内核状态,能快速定位重传、队列积压、缓冲区不足等问题
  3. 计数器是趋势指标:nstat、/proc/net/snmp 提供全局视角,通过观察计数器的增长速率判断问题的严重程度和类型
  4. tcpdump 提供证据:当计数器告诉你”有问题”,tcpdump 告诉你”具体是什么包出了问题”
  5. bpftrace 提供根因:当 tcpdump 看到了现象,bpftrace 深入内核调用栈,精确定位代码路径
  6. 逐层排查容器网络:从物理网卡到 veth,从 bridge 到 iptables,从宿主机到容器内,每层都有专属工具

掌握这套工具链,绝大多数线上网络问题都可以在分钟级定位到根因。下一篇将进入网络性能调优的主题,探讨如何通过内核参数和硬件配置最大化网络吞吐。