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 命令深度解析

ssnetstat 的现代替代品,直接读取内核的 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_wscalercv_wscale。实际窗口大小 = 报文窗口字段 × 2^wscale
rto:204 当前重传超时值(毫秒),由 RTT 和抖动计算:RTO = SRTT + 4×RTTVAR,下限 200ms
rtt:4.5/0.75 <平滑RTT(ms)>/<RTT方差(ms)>,对应内核 tp->srtt_us>>3tp->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.150016 = 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_sendmsgarg2 是应用请求发送的字节数,不等于实际发出的字节数(受发送缓冲区影响),但在排查带宽使用时已足够准确。

脚本四: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

重点看 retransbytes_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-1eth0-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. 增大 somaxconntcp_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,从宿主机到容器内,每层都有专属工具

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