线上告警:某服务的 P99 延迟从 10ms 飙升到 2 秒。你查了应用日志——没有异常。查了 CPU 和内存——都正常。查了数据库——响应时间也没变。
问题在 TCP 层。
这种”什么都正常但就是慢”的故障,根因往往藏在 TCP 的重传、窗口变化或连接异常中。它们不会在应用日志里留下明显痕迹,但会直接影响延迟和吞吐量。
前面几篇文章分别讲了 TCP 的连接管理、可靠传输、流量控制、拥塞控制和调优。这一篇把诊断工具和方法论串起来——面对不同类型的 TCP 问题,用什么工具、看什么指标、怎么定位根因。
一、诊断工具链
ss:最快速的 TCP 状态查看
# ss 比 netstat 快得多(直接读取内核数据结构,不解析 /proc/net/tcp)
# === 基本查看 ===
# 查看所有 TCP 连接
ss -tn
# 查看详细的 TCP 内部信息
ss -ti
# 输出每条连接的:cwnd, ssthresh, rtt, rto, mss, retrans 等
# 查看特定目标的连接
ss -ti dst 10.0.0.2
# 查看指定端口的连接
ss -tn sport = :8080
ss -tn dport = :3306
# === 过滤语法 ===
# 查看建立中的连接
ss -tn state established
# 查看 CLOSE_WAIT 连接(可能的泄漏)
ss -tn state close-wait
# 查看 TIME_WAIT 连接
ss -tn state time-wait
# 查看所有非 ESTABLISHED 状态
ss -tn state all exclude established exclude listen
# 查看队列信息(Recv-Q / Send-Q)
ss -tn | awk '$2 > 0 || $3 > 0'
# Recv-Q > 0: 数据堆积在接收队列(应用读取慢)
# Send-Q > 0: 数据堆积在发送队列(对端接收慢或网络拥塞)
# === 关键字段解读 ===
ss -ti dst 10.0.0.2
# cubic wscale:7,7 rto:204 rtt:1.2/0.5 ato:40 mss:1448
# cwnd:42 ssthresh:38
# bytes_sent:4506880 bytes_retrans:14480 bytes_acked:4492400
# send 406.9Mbps delivery_rate 380.2Mbps
# retrans:0/10 rcv_space:29200 minrtt:0.8
#
# rto:204 — 当前重传超时(ms)
# rtt:1.2/0.5 — 平滑 RTT / RTT 方差
# cwnd:42 — 拥塞窗口
# ssthresh:38 — 慢启动阈值(< 初始值说明有过丢包)
# bytes_retrans — 重传的字节数
# retrans:0/10 — 当前待重传/总重传次数
# minrtt:0.8 — 连接以来最小 RTTnetstat -s:全局 TCP 统计
# netstat -s 提供系统级别的 TCP 统计计数器
# 这些是累计值——需要定期采样计算增量
netstat -s | grep -A 30 "^Tcp:"
# Tcp:
# 12345678 active connection openings ← 主动连接数
# 23456789 passive connection openings ← 被动连接数(accept)
# 12345 failed connection attempts ← 连接失败
# 6789 connection resets received ← 收到的 RST
# 2340 connections established ← 当前建立的连接
# 987654321 segments received
# 876543210 segments sent out
# 12345 segments retransmitted ← 重传段数
# 67 bad segments received ← 校验和错误
# 45678 resets sent ← 发出的 RST
# 更详细的统计
netstat -s | grep -i retrans
# 12345 segments retransmitted
# 678 fast retransmits
# 234 forward retransmits
# 56 retransmits in slow start
# 89 SACK retransmits failed
# 用 nstat 获取增量(比 netstat -s 更好)
nstat -az | grep -i retrans
# TcpRetransSegs 12345 0.0
# TcpExtTCPFastRetrans 678 0.0
# TcpExtTCPSlowStartRetrans 56 0.0
# 计算重传率(每秒采样一次)
while true; do
retrans=$(nstat -az TcpRetransSegs | awk 'NR==2{print $2}')
sent=$(nstat -az TcpOutSegs | awk 'NR==2{print $2}')
if [ "$sent" -gt 0 ]; then
rate=$(echo "scale=4; $retrans / $sent * 100" | bc)
echo "$(date +%H:%M:%S) retrans_rate: ${rate}%"
fi
sleep 1
donetcpdump:精准抓包
# === 抓取重传包 ===
# 方法 1:抓所有包,后用 Wireshark 过滤
tcpdump -i eth0 -nn host 10.0.0.2 -w /tmp/capture.pcap -c 10000
# 方法 2:抓 RST 包
tcpdump -i eth0 -nn 'tcp[tcpflags] & (tcp-rst) != 0'
# 方法 3:抓 SYN 包(分析连接建立)
tcpdump -i eth0 -nn 'tcp[tcpflags] & (tcp-syn) != 0'
# 方法 4:抓特定端口的包
tcpdump -i eth0 -nn port 3306 -w /tmp/mysql.pcap
# === 容器/K8s 环境抓包 ===
# 找到 Pod 的 veth 接口
POD_PID=$(crictl inspect --output json $CONTAINER_ID | jq -r '.info.pid')
nsenter -t $POD_PID -n ip addr # 查看 Pod 的网络命名空间
# 在 Pod 的网络命名空间中抓包
nsenter -t $POD_PID -n tcpdump -i eth0 -nn -w /tmp/pod.pcap -c 5000
# === 线上安全抓包实践 ===
# 1. 限制包数量(-c)或大小(-C)
tcpdump -i eth0 -nn host 10.0.0.2 -w /tmp/cap.pcap -c 50000 -C 100
# -c 50000: 最多抓 5 万个包
# -C 100: 每个文件最大 100MB
# 2. 限制抓取长度(减少磁盘占用)
tcpdump -i eth0 -nn -s 96 host 10.0.0.2 -w /tmp/headers.pcap
# -s 96: 只抓前 96 字节(足以分析 TCP 头和选项)
# 3. 滚动抓包(生产环境常用)
tcpdump -i eth0 -nn -w /tmp/rolling.pcap -C 50 -W 10
# -C 50: 每个文件 50MB
# -W 10: 最多 10 个文件(总计 500MB,然后覆盖最早的文件)二、重传问题诊断
重传率的判断标准
# 重传率 = 重传段数 / 总发送段数
# 判断标准:
# < 0.01% 正常(万分之一)
# 0.01-0.1% 轻微,需要关注
# 0.1-1% 中度,影响延迟,需要排查
# > 1% 严重,影响吞吐和延迟,必须处理
# 但要注意:
# 1. 重传率要看趋势,不是绝对值
# 2. 短暂的高重传率可能是网络抖动,不一定需要处理
# 3. 长期 > 0.1% 的重传率通常意味着有持续性问题定位重传原因
# === 步骤 1:确认重传率和趋势 ===
nstat -az | grep -E 'TcpRetransSegs|TcpOutSegs'
# TcpRetransSegs 12345
# TcpOutSegs 9876543
# === 步骤 2:查看重传类型分布 ===
nstat -az | grep -i retrans
# TcpExtTCPFastRetrans 678 ← 快速重传(3 dup ACK)
# TcpExtTCPSlowStartRetrans 56 ← 慢启动阶段重传
# TcpExtTCPTimeouts 234 ← RTO 超时重传
# TcpExtTCPLossProbes 890 ← TLP(尾丢包探测)
# TcpExtTCPSACKRecovery 123 ← SACK 恢复
# 类型分析:
# 大量 TcpExtTCPTimeouts → 严重拥塞或链路中断
# 大量 TcpExtTCPFastRetrans → 间歇性丢包
# 大量 TcpExtTCPLossProbes → 尾包丢失(常见于小请求)
# === 步骤 3:定位重传来源 ===
# 查看哪些连接在重传
ss -ti | awk '/retrans:[1-9]/' | head -20
# 找到 retrans:N/M 中 N > 0 的连接
# N = 当前待重传段数, M = 历史总重传次数
# === 步骤 4:抓包分析重传模式 ===
tcpdump -i eth0 -nn host PROBLEM_IP -w /tmp/retrans.pcap -c 20000
# 用 tshark 过滤重传
tshark -r /tmp/retrans.pcap -Y 'tcp.analysis.retransmission' \
-T fields -e frame.time_relative -e ip.src -e ip.dst \
-e tcp.srcport -e tcp.dstport -e tcp.len重传的常见根因
# === 根因 1:网络设备丢包 ===
# 特征:
# - 重传均匀分布在多条连接上
# - 不同目标 IP 都有重传
# - tcpdump 看到重传包的间隔与 RTO 一致
#
# 排查:
# 检查交换机/路由器接口错误计数
# 检查链路利用率是否接近上限
# 检查 MTU 不匹配
# === 根因 2:服务器 CPU 或内存瓶颈 ===
# 特征:
# - 重传集中在特定服务器
# - 伴随 CPU 使用率高或内存压力
# - Recv-Q 堆积
#
# 排查:
top -b -n 1 | head -20
cat /proc/net/sockstat | grep TCP
# TCP: inuse X orphan Y tw Z alloc W mem V
# 如果 mem 接近 tcp_mem 压力线 → 内核限制了 TCP 缓冲区
# === 根因 3:拥塞 ===
# 特征:
# - 特定路径上的连接重传率高
# - RTT 明显升高(排队延迟)
# - cwnd 频繁减半
#
# 排查:
ss -ti dst PROBLEM_IP | grep -E 'rtt|cwnd|ssthresh'
# rtt 远大于 minrtt → 排队延迟大
# cwnd 很小且 ssthresh 也很小 → 反复丢包
# === 根因 4:中间设备(防火墙/NAT)问题 ===
# 特征:
# - 重传发生在 TCP 选项被剥离的连接上
# - 特定时间窗口后连接中断
# - conntrack 表满
#
# 排查:
dmesg | grep -i conntrack
# nf_conntrack: table full, dropping packet
# → 增大 net.nf_conntrack_max三、RST 问题诊断
RST 的来源分类
RST 是 TCP 的”紧急中断”信号。收到 RST 的连接会立即关闭,应用层通常看到 “Connection reset by peer” 错误。
# RST 来源的三大类:
# === 类型 1:服务端发送的 RST ===
# 常见场景:
# a) 连接到未监听的端口
# b) Accept 队列满(tcp_abort_on_overflow=1)
# c) 应用在 socket 缓冲区有数据时 close()
# d) SO_LINGER 设置 l_linger=0
# === 类型 2:客户端发送的 RST ===
# 常见场景:
# a) 收到了已关闭连接的数据
# b) 应用崩溃后操作系统清理连接
# === 类型 3:中间设备发送的 RST ===
# 常见场景:
# a) 防火墙规则拒绝连接
# b) NAT 表项过期后收到数据
# c) IDS/IPS 检测到异常流量
# d) 负载均衡器健康检查失败RST 诊断流程
flowchart TD
A["收到 RST"] --> B{"RST 的 TTL"}
B -->|"TTL 与服务端一致"| C["服务端发送"]
B -->|"TTL 明显不同"| D["中间设备发送"]
C --> E{"RST 时机"}
E -->|"SYN 后立即 RST"| F["端口未监听<br>或防火墙规则"]
E -->|"数据传输中 RST"| G["应用异常关闭<br>或缓冲区溢出"]
E -->|"空闲一段时间后 RST"| H["Keepalive 失败<br>或 NAT 超时"]
D --> I{"检查中间设备"}
I --> J["防火墙日志"]
I --> K["NAT/conntrack 状态"]
I --> L["LB 健康检查"]
# === 诊断步骤 ===
# 步骤 1:统计 RST 数量和趋势
netstat -s | grep -i reset
# connection resets received 6789
# resets sent 4567
# 步骤 2:抓取 RST 包
tcpdump -i eth0 -nn 'tcp[tcpflags] & (tcp-rst) != 0' -c 100
# 输出示例:
# 14:23:45.123456 IP 10.0.0.2.3306 > 10.0.0.1.54321: Flags [R.], seq 0, ack 1234, win 0
# 分析:
# 10.0.0.2:3306 → 数据库发的 RST
# ack 1234 → RST 是对特定数据的响应
# win 0 → 标准的 RST 格式
# 步骤 3:通过 TTL 判断 RST 来源
tcpdump -i eth0 -nn 'tcp[tcpflags] & (tcp-rst) != 0' -v | grep ttl
# 如果正常包的 TTL 是 64, RST 的 TTL 也是 64 → 对端发的
# 如果正常包的 TTL 是 64, RST 的 TTL 是 252 → 中间设备发的
# 步骤 4:分析 RST 的时间模式
tshark -r /tmp/capture.pcap -Y 'tcp.flags.reset == 1' \
-T fields -e frame.time_relative -e ip.src -e ip.dst \
-e tcp.srcport -e tcp.dstport
# 如果 RST 集中在某个时间间隔(如 300 秒后)
# → 很可能是 NAT/防火墙超时常见 RST 场景与修复
# === 场景 1:连接池中的 RST ===
# 症状:应用偶发 "Connection reset by peer"
# 原因:连接池中的空闲连接被服务端或中间设备关闭
# 修复:
# a) 配置连接池的空闲超时 < 服务端超时
# b) 启用连接池的验证(validationQuery / testOnBorrow)
# c) 减小 tcp_keepalive_time
# === 场景 2:SYN 后立即 RST ===
# 症状:连接建立失败, "Connection refused"
# 原因:目标端口没有进程监听
# 排查:
ss -ltn | grep PORT
# 如果没有监听 → 服务未启动
# 如果有监听 → 可能是 Accept 队列满
ss -ltn | awk '$2 > 0 && $2 == $3'
# Recv-Q == Send-Q → Accept 队列满
# === 场景 3:NAT 超时导致的 RST ===
# 症状:长连接在空闲一段时间后突然 RST
# 特征:RST 的 TTL 与正常包不同
# 修复:
# a) 减小 Keepalive 间隔 < NAT 超时
sysctl -w net.ipv4.tcp_keepalive_time=60
# b) 应用层心跳(gRPC keepalive / WebSocket ping)
# === 场景 4:SO_LINGER 导致的 RST ===
# 症状:close() 后立即发送 RST, 对端 "Connection reset"
# 原因:设置了 SO_LINGER = {l_onoff=1, l_linger=0}
# 这会让 close() 发送 RST 而不是正常的 FIN
# 修复:去除 SO_LINGER 设置或 l_linger > 0四、窗口异常诊断
零窗口(Zero Window)
# 零窗口 = 接收方通告的窗口大小为 0
# 含义:"我的接收缓冲区满了,你先别发了"
# === 用 tshark 检测零窗口 ===
tshark -r /tmp/capture.pcap -Y 'tcp.analysis.zero_window' \
-T fields -e frame.time_relative -e ip.src -e ip.dst \
-e tcp.window_size
# === 用 ss 检查接收队列 ===
ss -tn | awk '$2 > 100000'
# Recv-Q 很大 → 应用读取慢 → 可能导致零窗口
# === 零窗口的根因 ===
# 1. 应用处理慢(GC, 锁竞争, I/O 阻塞)
# 2. 接收缓冲区太小
# 3. 应用线程池耗尽
# === 诊断流程 ===
# a) 确认零窗口的方向
# 如果是服务端通告零窗口 → 服务端读取慢
# 如果是客户端通告零窗口 → 客户端读取慢
# b) 检查应用层
# Java: 检查 GC 暂停时间(gc.log)
# Go: pprof 看 goroutine 阻塞
# 通用: 检查线程/goroutine 数量和阻塞原因
# c) 检查缓冲区配置
sysctl net.ipv4.tcp_rmem
ss -tim dst TARGET | grep rcv_space
# === 零窗口 vs 窗口满 ===
# Wireshark 会标记两种:
# [TCP Zero Window] — 窗口为 0
# [TCP Window Full] — 发送方的 inflight 数据已达到接收窗口上限
# 两者通常成对出现:接收方零窗口 → 发送方窗口满窗口缩放问题
# 窗口缩放是 TCP 选项,在三次握手时协商
# 如果中间设备剥离了 TCP 选项:
# 症状:
# 1. ss 显示的窗口缩放因子为 -1 或 0
# 2. 最大窗口只有 64KB(未缩放)
# 3. 高 BDP 链路上吞吐量远低于带宽
# 诊断
ss -ti dst TARGET | grep wscale
# wscale:7,7 → 正常(双方都支持, 因子 2^7 = 128)
# wscale:-1,-1 → 异常(窗口缩放被禁用或被剥离)
# 检查系统设置
sysctl net.ipv4.tcp_window_scaling
# 如果为 0 → 开启: sysctl -w net.ipv4.tcp_window_scaling=1
# 如果系统设置正常但 wscale 仍然异常
# → 中间设备问题, 需要检查路径上的防火墙/NAT
# 可以用 traceroute + tcpdump 定位哪个节点剥离了 TCP 选项窗口异常的 Wireshark 分析
# Wireshark Expert Information 中的窗口相关警告:
# [TCP Window Update]
# 接收方单独发一个 ACK 来更新窗口大小(不确认新数据)
# 正常行为,通常发生在应用读取数据后
# [TCP Window Full]
# 发送方发送的数据量 = 接收方通告的窗口
# 如果频繁出现 → 接收方成为瓶颈
# [TCP Zero Window]
# 接收方通告窗口为 0
# 必须排查接收方的应用和缓冲区
# [TCP Zero Window Probe]
# 发送方发送 1 字节探测包检查零窗口是否恢复
# 正常行为
# [TCP Zero Window Probe Ack]
# 对零窗口探测的响应
# 如果窗口仍为 0 → 问题持续
# 用 tshark 统计窗口异常
tshark -r /tmp/capture.pcap -z expert -q | grep -i window
# Warns: 42 TCP Window Full
# Notes: 8 TCP Zero Window
# Chats: 156 TCP Window Update五、连接状态异常
CLOSE_WAIT 堆积
# CLOSE_WAIT = 对端已发 FIN,但本端应用还没有 close()
# 只有应用调用 close() 才能离开 CLOSE_WAIT
# 检测
ss -tn state close-wait | wc -l
# 如果持续增长 → 连接泄漏
# 按进程统计 CLOSE_WAIT
ss -tnp state close-wait | awk '{print $NF}' | sort | uniq -c | sort -rn
# 100 users:(("java",pid=1234,fd=456))
# 找到罪魁祸首进程
# CLOSE_WAIT 的根因:
# 1. 应用 bug: 没有正确关闭 socket(如异常路径缺少 close)
# 2. 连接池 bug: 连接被标记为空闲但没有检测到对端关闭
# 3. 死锁/阻塞: close() 被阻塞了
# 修复:
# a) 检查应用代码中所有获取 socket 的路径是否都有 close/defer close
# b) 使用连接池的 eviction(驱逐)策略
# c) 检查 file descriptor 泄漏
ls -la /proc/PID/fd | wc -l
# 如果 fd 数量持续增长 → fd 泄漏FIN_WAIT_2 堆积
# FIN_WAIT_2 = 本端已发 FIN 并收到 ACK,等待对端的 FIN
# 如果对端不发 FIN(bug 或崩溃),连接会一直停留在 FIN_WAIT_2
# 检测
ss -tn state fin-wait-2 | wc -l
# Linux 的保护机制
sysctl net.ipv4.tcp_fin_timeout
# 60(默认)— FIN_WAIT_2 超时时间
# 超时后连接被内核清理
# 如果 FIN_WAIT_2 堆积严重
sysctl -w net.ipv4.tcp_fin_timeout=15
# 减小超时时间TIME_WAIT 堆积
# TIME_WAIT = 主动关闭方进入的状态,持续 2*MSL(默认 60s)
# TIME_WAIT 本身不是错误,它在保护新连接不受旧包干扰
# 检测
ss -tn state time-wait | wc -l
# 如果超过 2 万,考虑优化
# 按目标 IP 统计(找到高频短连接目标)
ss -tn state time-wait | awk '{print $4}' | cut -d: -f1 | sort | uniq -c | sort -rn | head
# 3512 10.0.0.5
# 2841 10.0.0.6
# → 这两个服务之间大量短连接
# 常见原因:
# 1. HTTP/1.0 短连接(没有 Keep-Alive)
# 2. 数据库连接池频繁建立/销毁
# 3. 健康检查间隔太短(每秒一次 TCP 连接)
# 解决方案优先级:
# a) 使用连接池或 Keep-Alive(治本)
# b) 启用 tw_reuse(仅客户端有效)
sysctl net.ipv4.tcp_tw_reuse
# 1 = 开启,允许 TIME_WAIT 端口被新连接复用
# 前提:必须开启 tcp_timestamps
# c) 减少 TIME_WAIT 持续时间(不推荐,内核没有直接提供此参数)
# 有些文章建议改 tcp_tw_recycle — 该参数已在 Linux 4.12 删除
# 验证 TIME_WAIT 回收是否生效
nstat -z | grep TW
# TcpExtTW 15234 0.0 # 正常关闭进入 TIME_WAIT 的次数
# TcpExtTWRecycled 8921 0.0 # 被 tw_reuse 回收的次数
# TcpExtTWKilled 0 0.0 # 被强制回收的次数SYN_RECV 堆积
# SYN_RECV = 收到 SYN 并回复了 SYN-ACK,等待第三次握手的 ACK
# 正常情况下 SYN_RECV 数量很少(毫秒级完成)
# 如果大量堆积:可能是 SYN Flood 攻击或客户端网络异常
# 检测
ss -tn state syn-recv | wc -l
# 按源 IP 统计
ss -tn state syn-recv | awk '{print $4}' | cut -d: -f1 | sort | uniq -c | sort -rn
# 如果同一 IP 大量 SYN_RECV → 慢客户端
# 如果大量不同 IP → 可能是 SYN Flood
# 检查 SYN Cookie 是否启用
sysctl net.ipv4.tcp_syncookies
# 1 = 启用(推荐生产环境始终开启)
# 检查 SYN 队列溢出
nstat -z | grep SyncookiesSent
# 如果 SyncookiesSent 持续增长 → SYN 队列已满,正在用 Cookie 兜底六、Prometheus TCP 指标采集
关键 TCP 指标
# 使用 node_exporter 的内置 TCP 指标(netstat collector)
# 启用方式:--collector.netstat(通常默认启用)
# 关键指标:
# node_netstat_Tcp_RetransSegs — 重传段数(累计)
# node_netstat_Tcp_OutSegs — 发送段数(累计)
# node_netstat_Tcp_InSegs — 接收段数(累计)
# node_netstat_Tcp_ActiveOpens — 主动连接数(累计)
# node_netstat_Tcp_PassiveOpens — 被动连接数(累计)
# node_netstat_Tcp_CurrEstab — 当前建立的连接数
# 自定义 TCP 指标采集脚本
cat > /opt/tcp_metrics.sh << 'SCRIPT'
#!/bin/bash
FILE="/var/lib/node_exporter/textfile/tcp_extra.prom"
{
# 连接状态分布
for state in established syn-sent syn-recv fin-wait-1 fin-wait-2 \
time-wait close close-wait last-ack listen closing; do
count=$(ss -tn state $state 2>/dev/null | tail -n +2 | wc -l)
label=$(echo $state | tr '-' '_')
echo "tcp_connections_by_state{state=\"$label\"} $count"
done
# Accept 队列使用率(最繁忙的 5 个端口)
ss -ltn | tail -n +2 | awk '{
split($4, a, ":");
port = a[length(a)];
if ($3 > 0) printf "tcp_accept_queue_used{port=\"%s\"} %d\n", port, $2;
printf "tcp_accept_queue_max{port=\"%s\"} %d\n", port, $3;
}' | head -20
# 重传率(用 nstat 计算瞬时值)
nstat -az TcpRetransSegs 2>/dev/null | awk 'NR==2{print "tcp_retrans_segs_total " $2}'
nstat -az TcpOutSegs 2>/dev/null | awk 'NR==2{print "tcp_out_segs_total " $2}'
} > "$FILE.tmp" && mv "$FILE.tmp" "$FILE"
SCRIPT
chmod +x /opt/tcp_metrics.shPrometheus 告警规则
# tcp_alerts.yaml
groups:
- name: tcp_alerts
rules:
- alert: HighTcpRetransmitRate
expr: |
rate(node_netstat_Tcp_RetransSegs[5m]) /
rate(node_netstat_Tcp_OutSegs[5m]) > 0.01
for: 5m
labels:
severity: warning
annotations:
summary: "TCP 重传率超过 1%"
- alert: TcpCloseWaitAccumulation
expr: tcp_connections_by_state{state="close_wait"} > 500
for: 10m
labels:
severity: warning
annotations:
summary: "CLOSE_WAIT 连接超过 500"
- alert: TcpTimeWaitExhaustion
expr: tcp_connections_by_state{state="time_wait"} > 50000
for: 5m
labels:
severity: warning
annotations:
summary: "TIME_WAIT 连接超过 50000"
- alert: AcceptQueueOverflow
expr: tcp_accept_queue_used / tcp_accept_queue_max > 0.8
for: 1m
labels:
severity: critical
annotations:
summary: "Accept 队列使用率超过 80%"七、系统化诊断方法论
TCP 问题诊断决策树
# === 用户报告"慢" ===
#
# 1. 先确认是不是网络问题
# curl -w "DNS:%{time_namelookup} TCP:%{time_connect} TLS:%{time_appconnect} TTFB:%{time_starttransfer}" TARGET
# - DNS 慢 → DNS 问题
# - TCP 连接慢 → 连接建立问题
# - TTFB 慢(TCP 连接后到首字节) → 服务端处理慢或传输慢
#
# 2. 如果 TCP 层有问题
# ss -ti dst TARGET
# 看 rtt vs minrtt → 排队延迟
# 看 cwnd vs ssthresh → 拥塞状态
# 看 retrans → 重传
#
# 3. 如果有重传
# netstat -s | grep retrans → 全局重传率
# tcpdump 抓包 → 分析重传模式
#
# 4. 如果延迟高但不丢包
# 检查 Bufferbloat → mtr 看路径延迟
# 检查接收窗口 → ss -ti 看 rcv_space
# 检查拥塞控制 → 考虑切换 BBR
# === 用户报告"连接失败" ===
#
# 1. 检查监听状态
# ss -ltn | grep PORT
#
# 2. 检查 Accept 队列
# ss -ltn 看 Recv-Q vs Send-Q
# netstat -s | grep overflow
#
# 3. 检查防火墙/安全组
# iptables -L -n | grep PORT
# tcpdump 看 RST 的 TTL
#
# 4. 检查 conntrack
# dmesg | grep conntrack
# conntrack -C vs nf_conntrack_max八、案例:微服务间间歇性超时
# 背景:
# 微服务 A → 微服务 B,gRPC 通信
# 每隔几小时出现一次超时(5 秒),然后自动恢复
# === 第 1 步:收集数据 ===
# 在服务 A 和服务 B 上同时抓包
tcpdump -i eth0 -nn host B_IP -w /tmp/ab.pcap -C 50 -W 20
# 等待问题复现...
# === 第 2 步:分析问题时刻的抓包 ===
# 用 tshark 搜索重传
tshark -r /tmp/ab.pcap -Y 'tcp.analysis.retransmission' \
-T fields -e frame.time -e ip.src -e tcp.srcport -e tcp.analysis.rto
# 发现:问题时刻有大量重传,RTO 从 200ms 指数退避到 3.2s
# 总超时 ≈ 0.2 + 0.4 + 0.8 + 1.6 + 3.2 = 6.2s → 与 5 秒超时吻合
# === 第 3 步:分析重传前发生了什么 ===
# 发现 RST 出现在重传之前
# RST 的 TTL = 253(正常包 TTL = 63)→ 中间设备发的
# === 第 4 步:检查中间设备 ===
# 路径:服务 A → K8s Service → kube-proxy (iptables) → 服务 B
# 检查 conntrack
conntrack -L | grep B_IP | wc -l
cat /proc/sys/net/netfilter/nf_conntrack_count
cat /proc/sys/net/nf_conntrack_max
# 95000 / 65536 → conntrack 表已满!
# === 第 5 步:修复 ===
sysctl -w net.nf_conntrack_max=262144
sysctl -w net.netfilter.nf_conntrack_tcp_timeout_established=3600
# 设置 Prometheus 告警
# 在 nf_conntrack_count / nf_conntrack_max > 0.8 时告警
# 结果:问题消失,再未复现案例二:数据库连接池 CLOSE_WAIT 泄漏
# 背景:
# Java 服务连接 MySQL,运行 3-4 天后出现 "Too many open files"
# 重启后恢复,但几天后再次出现
# === 第 1 步:确认问题 ===
# 检查 fd 数量
ls /proc/$(pgrep -f java)/fd | wc -l
# 28431 — 已接近 ulimit 限制(32768)
# 检查 CLOSE_WAIT 数量
ss -tnp state close-wait | grep java | wc -l
# 24210 — 几乎全是 CLOSE_WAIT
# 按目标端口统计
ss -tn state close-wait | awk '{print $4}' | cut -d: -f2 | sort | uniq -c | sort -rn
# 24103 3306
# 107 6379
# → 绝大多数 CLOSE_WAIT 指向 MySQL(3306)
# === 第 2 步:理解 CLOSE_WAIT 含义 ===
# CLOSE_WAIT 表示:
# 对端(MySQL)已经发了 FIN → MySQL 认为连接应该关闭
# 本端(Java)还没有 close() → 应用没有正确响应
# 核心问题:MySQL 关闭了连接,但 Java 不知道
# === 第 3 步:分析 MySQL 侧 ===
mysql -e "SHOW VARIABLES LIKE 'wait_timeout';"
# wait_timeout = 28800(8 小时)
mysql -e "SHOW VARIABLES LIKE 'interactive_timeout';"
# interactive_timeout = 28800
# 但实际连接池配置:
# maxIdle=50, maxLifetime=不限(默认)
# → 连接池保持空闲连接不释放
# → MySQL 在 wait_timeout 后主动关闭连接(发 FIN)
# → 连接池不知道连接已死,没有 close()
# → CLOSE_WAIT 堆积
# === 第 4 步:验证 ===
# 用 tcpdump 观察 MySQL 发来的 FIN
tcpdump -i eth0 src port 3306 and 'tcp[tcpflags] & tcp-fin != 0' -c 5
# 确认 MySQL 确实在主动关闭连接
# === 第 5 步:修复连接池配置 ===
# HikariCP 配置(推荐):
# maxLifetime=1800000 (30分钟,必须小于 MySQL wait_timeout)
# idleTimeout=600000 (10分钟)
# connectionTestQuery=SELECT 1
# keepaliveTime=300000 (5分钟,定期验活)
# DBCP2/Druid 配置:
# testWhileIdle=true
# timeBetweenEvictionRunsMillis=60000
# minEvictableIdleTimeMillis=300000
# 修复后验证
ss -tn state close-wait | grep java | wc -l
# 0 — 运行 7 天后仍然为 0九、结论
TCP 诊断是一个系统工程——需要应用层、内核层、网络设备层的信息配合才能定位根因。
几个核心原则:
分层排查。 “慢”可能是 DNS、TCP 连接建立、TLS 握手、服务端处理、数据传输中任何一层的问题。先用
curl -w或链路追踪定位到具体层次,再用对应工具深入分析。看指标趋势,不看瞬时值。 一次重传不是问题,持续的高重传率才是。建立基线(
nstat/ Prometheus),通过趋势判断问题的严重程度和时间模式。RST 看 TTL。 RST 包的 TTL 能帮你快速判断它是服务端发的、客户端发的、还是中间设备发的。这决定了你应该排查哪个环节。
conntrack 是容器/K8s 环境的隐形杀手。
nf_conntrack: table full, dropping packet是最常见的容器网络丢包原因之一。生产环境必须监控 conntrack 使用率。别忘了抓包。
ss和netstat提供统计信息,但具体的包交互顺序只有抓包才能看到。线上环境用滚动抓包(-C -W),问题复现后再分析。
TCP 工程系列到此告一段落。下一篇我们来看 TCP 的现代扩展——Fast Open、MPTCP 和 ECN,这些新技术正在塑造 TCP 的未来。
上一篇:TCP 调优实战:内核参数与 socket 选项完全指南
下一篇:TCP 现代扩展:FastOpen、MPTCP 与 ECN
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【网络工程】TCP 可靠传输:序列号、确认与重传机制
从工程视角剖析 TCP 可靠传输的核心机制——序列号与确认的精确语义、RTO 计算的数学基础、快速重传与 SACK 的工程价值、DSACK 的重复检测,以及重传对延迟的放大效应与实际诊断方法。
【网络工程】网络延迟分析方法论:从现象到瓶颈定位
系统讲解网络延迟的分解方法论:从 DNS 到 TCP 到 TLS 到 TTFB 到传输的全链路延迟拆解,Wireshark RTT 测量、mtr/hping3 路径分析、内核协议栈延迟追踪,建立从'用户说慢'到精确定位瓶颈的工程方法。
网络工程索引
汇总本站网络工程系列文章,覆盖分层模型、以太网、IP、TCP、DNS、TLS、HTTP/2/3、CDN、BGP 与故障诊断。
【网络工程】内核网络参数调优:sysctl 全景与实战
Linux 内核网络参数是系统网络性能的基础旋钮。本文从 /proc/sys/net/ 的参数体系出发,系统讲解收发缓冲区自动调优、TCP Backlog 队列、conntrack 连接追踪表、SYN Flood 防护参数、TIME_WAIT 管理,以及参数调优的系统化方法论——先基准、再调整、后验证。