TCP问题排查实战
章节概述
在生产环境中,TCP相关的问题是最常见的网络故障之一。本章将通过实际案例,教你如何系统地排查和解决各类TCP问题,包括连接失败、TIME_WAIT堆积、延迟抖动、重传等常见场景。
学习目标:
- 掌握TCP问题的系统排查方法
- 学会使用工具快速定位问题
- 能够解决常见的TCP连接和性能问题
- 建立TCP故障排查的知识体系
核心概念
1. TCP问题分类
┌─────────────────────────────────┐
│ 连接建立问题 │
│ - 无法连接 │
│ - 连接超时 │
│ - 连接拒绝 │
└─────────────────────────────────┘
┌─────────────────────────────────┐
│ 连接状态问题 │
│ - TIME_WAIT堆积 │
│ - CLOSE_WAIT泄漏 │
│ - SYN_RECV攻击 │
└─────────────────────────────────┘
┌─────────────────────────────────┐
│ 性能问题 │
│ - 延迟高 │
│ - 吞吐量低 │
│ - 重传频繁 │
└─────────────────────────────────┘
┌─────────────────────────────────┐
│ 数据传输问题 │
│ - 丢包 │
│ - 乱序 │
│ - 窗口阻塞 │
└─────────────────────────────────┘
2. 排查工具链
快速诊断工具:
- ping: 测试连通性
- telnet/nc: 测试端口
- ss/netstat: 查看连接状态
深入分析工具:
- tcpdump: 抓包分析
- wireshark: 可视化分析
- ss -i: 查看TCP详细信息
- netstat -s: 查看统计信息
内核追踪工具:
- strace: 系统调用追踪
- perf: 性能分析
- bpftrace: eBPF追踪
案例分析
案例1:连接拒绝(Connection Refused)
症状:
$ telnet server_ip 8080
telnet: Unable to connect to remote host: Connection refused
排查流程:
# 步骤1:确认服务是否在监听
ss -ltn | grep 8080
# 或
netstat -ltn | grep 8080
# 预期输出:
# LISTEN 0 128 *:8080 *:*
# 如果没有输出 → 服务未启动或监听错误端口/地址
# 步骤2:检查防火墙
# Ubuntu/Debian
sudo ufw status
# CentOS/RHEL
sudo firewall-cmd --list-all
# iptables
sudo iptables -L -n | grep 8080
# 步骤3:检查SELinux(CentOS/RHEL)
getenforce
# 如果是Enforcing,临时关闭测试
sudo setenforce 0
# 步骤4:检查服务日志
# 查看应用日志
tail -f /var/log/app.log
# 查看系统日志
journalctl -u service_name -f
常见原因和解决方案:
原因 | 解决方案 |
---|---|
服务未启动 | 启动服务 |
监听地址错误 | 修改为0.0.0.0或具体IP |
防火墙阻止 | 添加防火墙规则 |
SELinux阻止 | 配置SELinux策略 |
端口被占用 | 更换端口或停止占用进程 |
案例2:TIME_WAIT堆积
症状:
$ ss -s
TCP: 15000 (estab 1000, timewait 12000, closed 12000)
影响:
- 端口耗尽(客户端)
- 连接表满
- QPS下降
排查流程:
# 步骤1:确认TIME_WAIT数量
ss -tan | grep TIME-WAIT | wc -l
# 步骤2:查看端口范围
sysctl net.ipv4.ip_local_port_range
# 输出:net.ipv4.ip_local_port_range = 32768 60999
# 步骤3:计算可用端口
# 可用端口 = 60999 - 32768 = 28231
# 如果TIME_WAIT接近此值,会端口耗尽
# 步骤4:查看连接来源
ss -tan | grep TIME-WAIT | awk '{print $4}' | cut -d: -f1 | sort | uniq -c | sort -rn | head
# 找出产生最多TIME_WAIT的源IP
解决方案:
# 方案1:启用TIME_WAIT重用
sudo sysctl -w net.ipv4.tcp_tw_reuse=1
# 方案2:减少TIME_WAIT等待时间
sudo sysctl -w net.ipv4.tcp_fin_timeout=30
# 方案3:扩大端口范围
sudo sysctl -w net.ipv4.ip_local_port_range="1024 65535"
# 方案4:使用连接池(应用层)
# - HTTP Keep-Alive
# - 数据库连接池
# - gRPC连接复用
永久生效:
sudo tee -a /etc/sysctl.conf << EOF
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 30
net.ipv4.ip_local_port_range = 1024 65535
EOF
sudo sysctl -p
案例3:CLOSE_WAIT泄漏
症状:
$ ss -tan | grep CLOSE-WAIT | wc -l
5000
# 连接数持续增长,不会减少
原因分析:
CLOSE_WAIT表示:
- 对端已发送FIN
- 本端收到并回复ACK
- 但本端未调用close()
这通常是应用程序bug!
排查流程:
# 步骤1:定位进程
ss -tanp | grep CLOSE-WAIT | awk '{print $NF}' | sort | uniq -c
# 输出:users:(("app_name",pid=12345,...))
# 步骤2:查看进程打开的fd
lsof -p 12345 | grep -i close
ls -l /proc/12345/fd | wc -l
# 步骤3:追踪系统调用
sudo strace -p 12345 -e trace=close,read,write -T
# 观察是否有close()调用
代码检查:
// 错误示例(Go)
func handleConnection(conn net.Conn) {
// 忘记close
data, _ := ioutil.ReadAll(conn)
process(data)
// 缺少: defer conn.Close()
}
// 正确示例
func handleConnection(conn net.Conn) {
defer conn.Close() // 确保关闭
data, _ := ioutil.ReadAll(conn)
process(data)
}
应急措施:
# 找出并重启问题进程
kill -HUP <pid> # 优雅重启
# 或
kill -9 <pid> # 强制重启(最后手段)
案例4:高延迟和抖动
症状:
# 延迟突然升高
# p50: 10ms
# p99: 500ms (异常)
# p999: 2000ms
排查流程:
# 步骤1:网络层延迟
ping server_ip
# 观察RTT是否稳定
mtr -rwzbc100 server_ip
# 查看路由上的丢包和延迟
# 步骤2:TCP层统计
ss -ti dst server_ip
# 输出:
# cubic wscale:7,7 rto:240 rtt:31.5/2.4 ato:40
# cwnd:10 bytes_acked:12345 segs_out:100
# send 22.8Mbps lastsnd:1000 lastrcv:1000
关键指标:
rtt: 往返时延(越小越好)
rto: 重传超时(过大说明网络不稳定)
cwnd: 拥塞窗口(太小限制吞吐)
retrans: 重传次数(应该为0或很小)
# 步骤3:检查重传
netstat -s | grep -i retrans
# 步骤4:抓包分析
sudo tcpdump -i eth0 -nn host server_ip -w delay.pcap
# 用wireshark分析:
# 1. Statistics -> TCP Stream Graph -> Time Sequence
# 2. 查找DUP ACK、Retransmission
常见原因和解决方案:
原因 | 表现 | 解决方案 |
---|---|---|
网络拥塞 | RTT高,重传多 | 启用BBR算法 |
缓冲区小 | 窗口小 | 调大tcp_rmem/wmem |
应用阻塞 | Recv-Q积压 | 优化应用处理速度 |
CPU高负载 | 处理慢 | 增加worker或优化代码 |
跨机房 | 基线RTT高 | 考虑CDN或就近部署 |
优化示例:
# 启用BBR
sudo sysctl -w net.ipv4.tcp_congestion_control=bbr
# 调大缓冲区
sudo sysctl -w net.ipv4.tcp_rmem="4096 87380 6291456"
sudo sysctl -w net.ipv4.tcp_wmem="4096 16384 4194304"
# 启用TCP fast open
sudo sysctl -w net.ipv4.tcp_fastopen=3
案例5:高重传率
症状:
$ netstat -s | grep retrans
1234 segments retransmited
# 重传率 > 1% 需要关注
排查流程:
# 步骤1:实时监控重传
sudo /usr/share/bcc/tools/tcpretrans
# 输出:
# TIME PID LADDR:LPORT RADDR:RPORT STATE
# 10:20:15 12345 10.0.0.1:80 10.0.0.2:54321 ESTABLISHED
# 步骤2:查看连接详情
ss -ti | grep retrans
# 输出:
# retrans:1/5 表示当前重传1个包,总共重传5个
# 步骤3:抓包分析重传原因
sudo tcpdump -i eth0 -nn 'tcp[tcpflags] & tcp-syn != 0' -w syn.pcap
# wireshark分析:
# 过滤器: tcp.analysis.retransmission
# 查看:
# 1. 是超时重传还是快速重传
# 2. RTT变化趋势
# 3. 窗口大小变化
常见重传原因:
- 网络丢包
# 检查网卡丢包
ethtool -S eth0 | grep -i drop
# 检查队列溢出
netstat -s | grep -i overflow
- 缓冲区不足
# 检查缓冲区溢出
netstat -s | grep -i "buffer full"
- 拥塞窗口太小
ss -ti | grep cwnd
# cwnd持续很小 → 切换拥塞控制算法
解决方案:
# 1. 调整网卡队列
sudo ethtool -G eth0 rx 4096 tx 4096
# 2. 增大socket缓冲区
sudo sysctl -w net.core.rmem_max=16777216
sudo sysctl -w net.core.wmem_max=16777216
# 3. 切换拥塞控制算法
sudo sysctl -w net.ipv4.tcp_congestion_control=bbr
# 4. 调整队列长度
sudo sysctl -w net.core.netdev_max_backlog=16384
️ 排查工具箱
1. 快速诊断脚本
#!/bin/bash
# tcp_quick_check.sh - TCP快速诊断
echo "===== TCP连接状态统计 ====="
ss -tan | awk '{print $1}' | sort | uniq -c | sort -rn
echo ""
echo "===== TOP 10 TIME_WAIT 连接 ====="
ss -tan | grep TIME-WAIT | awk '{print $4" "$5}' | sort | uniq -c | sort -rn | head -10
echo ""
echo "===== TOP 10 CLOSE_WAIT 连接 ====="
ss -tan | grep CLOSE-WAIT | awk '{print $4" "$5}' | sort | uniq -c | sort -rn | head -10
echo ""
echo "===== TCP统计信息 ====="
netstat -s | grep -E "segments|retrans|failed|reset"
echo ""
echo "===== 当前TCP参数 ====="
sysctl -a | grep -E "tcp_tw_reuse|tcp_fin_timeout|tcp_max_syn_backlog|ip_local_port_range"
2. 连接追踪脚本
#!/bin/bash
# connection_tracker.sh - 追踪指定端口的连接
PORT=$1
if [ -z "$PORT" ]; then
echo "用法: $0 <port>"
exit 1
fi
echo "追踪端口 $PORT 的连接..."
while true; do
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
TOTAL=$(ss -tan | grep ":$PORT" | wc -l)
ESTAB=$(ss -tan | grep ":$PORT" | grep ESTAB | wc -l)
TIMEWAIT=$(ss -tan | grep ":$PORT" | grep TIME-WAIT | wc -l)
CLOSEWAIT=$(ss -tan | grep ":$PORT" | grep CLOSE-WAIT | wc -l)
printf "%s | Total: %4d | ESTAB: %4d | TIME-WAIT: %4d | CLOSE-WAIT: %4d\n" \
"$TIMESTAMP" "$TOTAL" "$ESTAB" "$TIMEWAIT" "$CLOSEWAIT"
sleep 1
done
3. 性能测试脚本
#!/bin/bash
# tcp_perf_test.sh - TCP性能测试
SERVER=$1
PORT=$2
if [ -z "$SERVER" ] || [ -z "$PORT" ]; then
echo "用法: $0 <server> <port>"
exit 1
fi
echo "测试服务器: $SERVER:$PORT"
# 1. 连通性测试
echo "1. 连通性测试..."
nc -zv $SERVER $PORT 2>&1 | grep succeeded && echo " 端口可达" || echo " 端口不可达"
# 2. RTT测试
echo ""
echo "2. RTT测试..."
ping -c 10 $SERVER | tail -1
# 3. 带宽测试(需要iperf3)
echo ""
echo "3. 带宽测试..."
if command -v iperf3 &> /dev/null; then
iperf3 -c $SERVER -p $PORT -t 10 2>/dev/null | grep sender
else
echo "未安装iperf3,跳过"
fi
# 4. TCP握手时间
echo ""
echo "4. TCP握手时间..."
curl -o /dev/null -s -w "Connect: %{time_connect}s\nTotal: %{time_total}s\n" http://$SERVER:$PORT/
实战演练
演练1:模拟TIME_WAIT堆积
# 创建大量短连接
for i in {1..1000}; do
(echo "test" | nc localhost 8080 &)
done
# 观察TIME_WAIT
watch -n 1 'ss -tan | grep TIME-WAIT | wc -l'
演练2:模拟CLOSE_WAIT泄漏
# server_leak.py - 模拟CLOSE_WAIT泄漏
import socket
import time
sock = socket.socket()
sock.bind(('0.0.0.0', 9999))
sock.listen(100)
while True:
conn, addr = sock.accept()
# 故意不关闭连接
print(f"Accept from {addr}")
time.sleep(0.1)
# 客户端:创建连接后立即关闭
for i in {1..100}; do
echo "test" | nc localhost 9999
sleep 0.1
done
# 观察CLOSE_WAIT
ss -tan | grep CLOSE-WAIT
演练3:抓包分析重传
# 启动抓包
sudo tcpdump -i lo -nn port 8080 -w retrans.pcap &
# 使用tc模拟丢包
sudo tc qdisc add dev lo root netem loss 10%
# 发送数据
echo "test data" | nc localhost 8080
# 停止模拟
sudo tc qdisc del dev lo root
# 分析抓包
wireshark retrans.pcap
# 过滤器: tcp.analysis.retransmission
常见问题
Q1: 如何判断TIME_WAIT是否过多?
A: 判断标准:
- 绝对数量:超过10000个需要关注
- 相对比例:超过可用端口的50%
- 增长趋势:持续增长不回落
- 业务影响:出现端口耗尽错误
Q2: CLOSE_WAIT和TIME_WAIT有什么区别?
A:
- TIME_WAIT: 主动关闭方的正常状态,会自动消失
- CLOSE_WAIT: 被动关闭方,需要应用调用close()才会消失
- TIME_WAIT是正常的,CLOSE_WAIT堆积是bug
Q3: 如何快速定位是哪个服务产生的连接问题?
A: 使用命令:
# 查看连接最多的进程
ss -tanp | awk '/ESTAB/{print $NF}' | cut -d'"' -f2 | sort | uniq -c | sort -rn
# 查看TIME_WAIT最多的目标IP
ss -tan | grep TIME-WAIT | awk '{print $5}' | cut -d: -f1 | sort | uniq -c | sort -rn
Q4: 生产环境如何安全地调整TCP参数?
A: 安全步骤:
- 在测试环境先验证
- 使用sysctl临时调整
- 观察监控指标
- 确认无问题后写入sysctl.conf
- 保留回退方案
复习题
选择题
TIME_WAIT状态出现在哪一方?
- A. 主动关闭方
- B. 被动关闭方
- C. 双方都有
- D. 随机
CLOSE_WAIT堆积通常是什么原因?
- A. 网络问题
- B. 应用未调用close()
- C. 内核bug
- D. 配置错误
以下哪个工具不能查看TCP连接状态?
- A. ss
- B. netstat
- C. ifconfig
- D. lsof
tcp_tw_reuse参数的作用是?
- A. 删除TIME_WAIT
- B. 重用TIME_WAIT端口
- C. 增加连接数
- D. 提高速度
查看TCP重传次数使用什么命令?
- A. ping
- B. netstat -s
- C. ifconfig
- D. route
简答题
解释TIME_WAIT堆积的原因和解决方法。
如何排查CLOSE_WAIT泄漏问题?给出完整的步骤。
描述如何使用tcpdump和wireshark分析TCP重传问题。
在什么情况下应该调整tcp_rmem和tcp_wmem参数?
如何判断TCP性能问题是网络层还是应用层导致的?
实战题
故障排查题: 线上服务突然无法建立新连接,ss显示大量TIME_WAIT。请给出完整的排查和解决方案。
性能优化题: 一个高并发HTTP服务,客户端经常报连接超时。通过分析发现服务端有大量CLOSE_WAIT。请分析原因并给出修复方案。
综合分析题: 使用tcpdump抓包发现大量TCP重传,如何进一步分析确定是网络问题还是配置问题?给出详细步骤。
扩展阅读
推荐资源
深入方向
- TCP性能调优高级技巧
- TCP拥塞控制算法对比
- 内核TCP参数详解
- eBPF在TCP调试中的应用
下一章预告: 我们将深入探讨网络性能优化,包括BBR算法、内核参数调优、以及各种网络优化技术的对比和选择。