土法炼钢兴趣小组的算法知识备份

【网络工程】TCP 问题诊断实战:重传、RST 与窗口异常

文章导航

分类入口
network
标签入口
#tcp#diagnostics#retransmission#rst#wireshark

目录

线上告警:某服务的 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     — 连接以来最小 RTT

netstat -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
done

tcpdump:精准抓包

# === 抓取重传包 ===
# 方法 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.sh

Prometheus 告警规则

# 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 诊断是一个系统工程——需要应用层、内核层、网络设备层的信息配合才能定位根因。

几个核心原则:

  1. 分层排查。 “慢”可能是 DNS、TCP 连接建立、TLS 握手、服务端处理、数据传输中任何一层的问题。先用 curl -w 或链路追踪定位到具体层次,再用对应工具深入分析。

  2. 看指标趋势,不看瞬时值。 一次重传不是问题,持续的高重传率才是。建立基线(nstat / Prometheus),通过趋势判断问题的严重程度和时间模式。

  3. RST 看 TTL。 RST 包的 TTL 能帮你快速判断它是服务端发的、客户端发的、还是中间设备发的。这决定了你应该排查哪个环节。

  4. conntrack 是容器/K8s 环境的隐形杀手。 nf_conntrack: table full, dropping packet 是最常见的容器网络丢包原因之一。生产环境必须监控 conntrack 使用率。

  5. 别忘了抓包。 ssnetstat 提供统计信息,但具体的包交互顺序只有抓包才能看到。线上环境用滚动抓包(-C -W),问题复现后再分析。

TCP 工程系列到此告一段落。下一篇我们来看 TCP 的现代扩展——Fast Open、MPTCP 和 ECN,这些新技术正在塑造 TCP 的未来。


上一篇:TCP 调优实战:内核参数与 socket 选项完全指南

下一篇:TCP 现代扩展:FastOpen、MPTCP 与 ECN

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-04-22 · network

网络工程索引

汇总本站网络工程系列文章,覆盖分层模型、以太网、IP、TCP、DNS、TLS、HTTP/2/3、CDN、BGP 与故障诊断。

2025-07-30 · network

【网络工程】内核网络参数调优:sysctl 全景与实战

Linux 内核网络参数是系统网络性能的基础旋钮。本文从 /proc/sys/net/ 的参数体系出发,系统讲解收发缓冲区自动调优、TCP Backlog 队列、conntrack 连接追踪表、SYN Flood 防护参数、TIME_WAIT 管理,以及参数调优的系统化方法论——先基准、再调整、后验证。


By .