适用场景:网络故障排查、连接超时分析、TIME_WAIT 过多问题定位、TCP 参数调优。
环境要求:Linux 4.18+ (RHEL/CentOS 8+ / Ubuntu 20.04+),root 或 sudo 权限,Wireshark 3.0+/tcpdump 4.9+。
1️⃣ 实施步骤(核心内容)
架构与数据流说明
TCP 三次握手流程:
客户端 服务器
│ │
│──────── SYN (seq=x) ────────────→ │ (1) 客户端发起连接
│ │
│←───── SYN-ACK (seq=y, ack=x+1) ────│ (2) 服务器确认并同步
│ │
│──────── ACK (ack=y+1) ───────────→ │ (3) 客户端确认
│ │
│========== 连接建立,开始传输 ==========│
TCP 四次挥手流程:
客户端 服务器
│ │
│──────── FIN (seq=m) ─────────────→ │ (1) 客户端请求关闭
│ │
│←────── ACK (ack=m+1) ───────────────│ (2) 服务器确认
│ │
│←────── FIN (seq=n) ─────────────────│ (3) 服务器请求关闭
│ │
│──────── ACK (ack=n+1) ───────────→ │ (4) 客户端确认
│ │
│ [TIME_WAIT: 2MSL = 120秒] │
│ │
└────────── 连接关闭 ──────────────────┘
关键组件:
- SYN (Synchronize):同步序列号,请求建立连接
- ACK (Acknowledgment):确认号,确认接收到的数据
- FIN (Finish):结束标志,请求关闭连接
- seq (Sequence Number):序列号,标识发送的数据字节
- ack (Acknowledgment Number):确认号,期望接收的下一个字节
- TIME_WAIT:主动关闭方等待 2MSL(最大报文段生存时间),默认 60 秒
Step 1: 环境准备与工具安装
目标: 安装 Wireshark/tcpdump,配置测试环境
环境信息(示例):
- 测试客户端:192.168.1.100 (client / 2C4G / CentOS 8.5)
- 测试服务器:192.168.1.200 (server / 2C4G / CentOS 8.5)
- 测试端口:80 (HTTP) / 443 (HTTPS) / 3306 (MySQL)
- 网络接口:eth0
RHEL/CentOS 命令:
# 1. 安装 tcpdump 和 Wireshark(命令行版 tshark)
yum install -y tcpdump wireshark-cli nc telnet nmap-ncat
# 2. 验证 tcpdump 版本
tcpdump --version
# 预期输出:tcpdump version 4.9.3 或更高
# 3. 检查网络接口
ip link show
# 预期输出:eth0、lo 等接口列表
# 4. 测试抓包权限
tcpdump -D
# 预期输出:
# 1.eth0 [Up, Running]
# 2.lo [Up, Running, Loopback]
# 5. 启动测试 HTTP 服务器(Python 简易服务器)
python3 -m http.server 8080 &
# 预期输出:Serving HTTP on 0.0.0.0 port 8080 (http://0.0.0.0:8080/) ...
# 6. 验证服务监听
ss -tuln | grep 8080
# 预期输出:
# tcp LISTEN 0 5 0.0.0.0:8080 0.0.0.0:*
Ubuntu/Debian 命令:
# 1. 安装 tcpdump 和 Wireshark
apt update && apt install -y tcpdump tshark netcat-openbsd telnet
# 2. 验证安装
tcpdump --version
# 预期输出:tcpdump version 4.99.0 或更高
# 3. 启动测试服务器(使用 nc)
nc -l -p 8080 &
# 或使用 Python
python3 -m http.server 8080 &
# 4. 验证监听
ss -tuln | grep 8080
# 预期输出:tcp LISTEN 0 5 0.0.0.0:8080 0.0.0.0:*
关键参数解释:
tcpdump -D:列出可用网络接口,确认抓包权限
ss -tuln:查看 TCP/UDP 监听端口(-t=TCP, -u=UDP, -l=LISTEN, -n=数字显示)
python3 -m http.server:快速启动 HTTP 服务器用于测试
执行前验证:
# 确认测试端口未被占用
ss -tuln | grep 8080
# 预期输出:无输出(端口空闲)
# 确认 tcpdump 有权限抓包
tcpdump -i eth0 -c 1
# 预期输出:抓到 1 个数据包(按 Ctrl+C 停止)
执行后验证:
# 验证服务启动成功
curl -I http://127.0.0.1:8080/
# 预期输出:
# HTTP/1.0 200 OK
# Server: SimpleHTTP/0.6 Python/3.6.8
# 验证外部可访问(如果防火墙已放行)
curl -I http://192.168.1.200:8080/
# 预期输出:HTTP/1.0 200 OK
常见错误与处理:
# 错误1:tcpdump: eth0: You don't have permission to capture on that device
# 原因:非 root 用户无抓包权限
# 解决:使用 sudo 或切换到 root 用户
# 错误2:Address already in use
# 原因:端口 8080 已被占用
# 解决:更换端口或停止占用进程
lsof -i :8080
kill -9 <PID>
# 错误3:No route to host
# 原因:防火墙阻止连接
# 解决:临时关闭防火墙或放行端口
firewall-cmd --permanent --add-port=8080/tcp
firewall-cmd --reload
幂等性保障:
- 重复安装软件包自动跳过
- 重复启动服务会提示端口占用(可先停止旧进程)
回滚要点:
# 停止测试服务器
pkill -f "http.server 8080"
# 卸载工具(可选)
yum remove -y tcpdump wireshark-cli # RHEL/CentOS
apt remove --purge -y tcpdump tshark # Ubuntu/Debian
Step 2: 抓取三次握手数据包
目标: 使用 tcpdump 抓取完整的 TCP 三次握手过程
RHEL/CentOS 和 Ubuntu/Debian 通用命令:
# 1. 启动 tcpdump 抓包(在服务器端 192.168.1.200 执行)
tcpdump -i eth0 -nn -S -vvv 'tcp and port 8080' -w /tmp/tcp_handshake.pcap &
# 参数说明:
# -i eth0: 指定网络接口
# -nn: 不解析主机名和端口名(显示 IP 和端口号)
# -S: 显示绝对序列号(不使用相对序列号)
# -vvv: 非常详细的输出
# -w: 保存到文件
# 'tcp and port 8080': 过滤规则(仅抓取 TCP 且端口为 8080 的包)
# 2. 在客户端(192.168.1.100)发起连接
curl http://192.168.1.200:8080/
# 或使用 telnet 测试
telnet 192.168.1.200 8080
# 输入 GET / HTTP/1.0 并按两次回车
# 3. 停止抓包(在服务器端)
pkill -INT tcpdump
# 或等待 tcpdump 自动停止
# 4. 查看抓包文件
ls -lh /tmp/tcp_handshake.pcap
# 预期输出:
# -rw-r--r-- 1 root root 2.3K Jan 20 10:30 /tmp/tcp_handshake.pcap
实时查看抓包(不保存文件):
# 方法1:tcpdump 实时输出(终端查看)
tcpdump -i eth0 -nn -S -vvv 'tcp and port 8080'
# 预期输出(三次握手示例):
# 10:30:15.123456 IP 192.168.1.100.45678 > 192.168.1.200.8080: Flags [S], seq 1234567890, win 29200, options [mss 1460,sackOK,TS val 123456 ecr 0,nop,wscale 7], length 0
# 10:30:15.123789 IP 192.168.1.200.8080 > 192.168.1.100.45678: Flags [S.], seq 9876543210, ack 1234567891, win 28960, options [mss 1460,sackOK,TS val 789012 ecr 123456,nop,wscale 7], length 0
# 10:30:15.124012 IP 192.168.1.100.45678 > 192.168.1.200.8080: Flags [.], ack 9876543211, win 229, options [nop,nop,TS val 123457 ecr 789012], length 0
# 方法2:使用 tshark 解析(更易读)
tshark -i eth0 -f 'tcp and port 8080' -Y 'tcp.flags.syn==1 or tcp.flags.fin==1'
# 预期输出:
# 1 0.000000 192.168.1.100 → 192.168.1.200 TCP 74 45678 → 8080 [SYN] Seq=0 Win=29200 Len=0 MSS=1460
# 2 0.000333 192.168.1.200 → 192.168.1.100 TCP 74 8080 → 45678 [SYN, ACK] Seq=0 Ack=1 Win=28960 Len=0 MSS=1460
# 3 0.000612 192.168.1.100 → 192.168.1.200 TCP 66 45678 → 8080 [ACK] Seq=1 Ack=1 Win=229 Len=0
关键字段解读:
- Flags [S]:SYN 标志,表示请求建立连接(第一次握手)
- Flags [S.]:SYN+ACK 标志,表示同步并确认(第二次握手)
- Flags [.]:ACK 标志,表示确认(第三次握手)
- seq 1234567890:初始序列号(ISN,Initial Sequence Number)
- ack 1234567891:确认号 = 对方 seq + 1
- win 29200:TCP 窗口大小(接收缓冲区大小),单位字节
- options [mss 1460]:最大报文段大小(Maximum Segment Size)
执行前验证:
# 确认服务器正在监听
ss -tuln | grep 8080
# 预期输出:tcp LISTEN 0 5 0.0.0.0:8080 0.0.0.0:*
# 确认客户端网络可达
ping -c 3 192.168.1.200
# 预期输出:3 packets transmitted, 3 received, 0% packet loss
执行后验证:
# 统计抓包文件包数量
tcpdump -r /tmp/tcp_handshake.pcap | wc -l
# 预期输出:10-20 个包(包括握手、数据传输、挥手)
# 提取握手包(SYN/SYN-ACK/ACK)
tcpdump -r /tmp/tcp_handshake.pcap -nn 'tcp[tcpflags] & (tcp-syn) != 0'
# 预期输出:2 个包(SYN 和 SYN-ACK)
Step 3: Wireshark 图形化分析(Windows/Linux GUI)
目标: 使用 Wireshark 详细分析三次握手和四次挥手
操作步骤:
# 1. 将抓包文件传输到 Windows 客户端(如有)
scp root@192.168.1.200:/tmp/tcp_handshake.pcap ~/Desktop/
# 或直接在 Linux 图形界面打开 Wireshark
# 2. Wireshark 打开文件
# File → Open → 选择 tcp_handshake.pcap
# 3. 应用过滤器(仅显示握手和挥手)
# 过滤器输入框输入:
tcp.flags.syn==1 or tcp.flags.fin==1
# 4. 查看三次握手详细信息
# 点击第 1 个包(SYN),查看 Packet Details 窗格:
# ▸ Transmission Control Protocol, Src Port: 45678, Dst Port: 8080
# ▸ Flags: 0x002 (SYN)
# .... .... ..1. = Syn: Set
# .... .... ...0 = Fin: Not set
# ▸ Sequence Number: 1234567890 (relative sequence number)
# ▸ Window: 29200
# ▸ Options: (20 bytes)
# ▸ Maximum Segment Size: 1460 bytes
# ▸ Window Scale: 7 (multiply by 128)
# ▸ Timestamps: TSval 123456, TSecr 0
# 5. 查看序列号和确认号关系
# 三次握手序列号验证:
# Packet 1 (SYN): seq=1234567890, ack=0
# Packet 2 (SYN-ACK): seq=9876543210, ack=1234567891 (=Packet1.seq+1 ✅)
# Packet 3 (ACK): seq=1234567891, ack=9876543211 (=Packet2.seq+1 ✅)
Wireshark 快捷操作:
# 快捷键(Windows/Linux 通用)
Ctrl+F # 查找包
Ctrl+G # 跳转到指定包编号
Ctrl+E # 显示/隐藏 Packet Details
Ctrl+Shift+P # 打印包
右键 → Follow → TCP Stream # 查看完整 TCP 会话
# 常用过滤器
tcp.stream eq 0 # 查看第 0 个 TCP 流
tcp.flags.syn==1 and tcp.flags.ack==0 # 仅 SYN 包(第一次握手)
tcp.flags.syn==1 and tcp.flags.ack==1 # 仅 SYN-ACK 包(第二次握手)
tcp.flags.fin==1 # 仅 FIN 包(挥手)
tcp.analysis.retransmission # 重传包
tcp.analysis.window_full # 窗口已满
关键字段详解:
| 字段 |
含义 |
示例值 |
说明 |
| Flags |
TCP 标志位 |
0x002 (SYN) |
SYN=0x02, ACK=0x10, FIN=0x01, PSH=0x08, RST=0x04 |
| Seq |
序列号 |
1234567890 |
标识发送的数据字节位置 |
| Ack |
确认号 |
1234567891 |
期望接收的下一个字节(对方 seq+1) |
| Win |
窗口大小 |
29200 |
接收缓冲区剩余空间(字节) |
| Len |
数据长度 |
0 |
握手包无数据(Len=0) |
| MSS |
最大报文段 |
1460 |
单个 TCP 包最大数据长度(MTU 1500 - IP 头 20 - TCP 头 20) |
| Window Scale |
窗口缩放因子 |
7 |
实际窗口 = Win × 2^7 = 29200 × 128 = 3.7MB |
| Timestamps |
时间戳 |
TSval 123456 |
用于计算 RTT(往返时间) |
执行后验证:
# 验证三次握手完整性(使用 tshark 命令行)
tshark -r /tmp/tcp_handshake.pcap -Y 'tcp.flags.syn==1' -T fields -e frame.number -e ip.src -e ip.dst -e tcp.flags
# 预期输出:
# 1 192.168.1.100 192.168.1.200 0x0002 # SYN
# 2 192.168.1.200 192.168.1.100 0x0012 # SYN-ACK (0x02 | 0x10)
# 验证序列号连续性
tshark -r /tmp/tcp_handshake.pcap -Y 'tcp.stream eq 0' -T fields -e frame.number -e tcp.seq -e tcp.ack
# 预期输出:
# 1 1234567890 0
# 2 9876543210 1234567891
# 3 1234567891 9876543211
Step 4: 抓取四次挥手数据包
目标: 抓取完整的 TCP 连接关闭过程
RHEL/CentOS 和 Ubuntu/Debian 通用命令:
# 1. 启动抓包(在服务器端)
tcpdump -i eth0 -nn -S -vvv 'tcp and port 8080' -w /tmp/tcp_close.pcap &
# 2. 在客户端建立连接并主动关闭
echo "GET / HTTP/1.0" | nc 192.168.1.200 8080
# nc 会自动关闭连接,触发四次挥手
# 3. 停止抓包
pkill -INT tcpdump
# 4. 分析四次挥手
tcpdump -r /tmp/tcp_close.pcap -nn 'tcp[tcpflags] & (tcp-fin) != 0'
# 预期输出(简化版):
# 10:35:20.123456 IP 192.168.1.100.45678 > 192.168.1.200.8080: Flags [F.], seq 1234567901, ack 9876543250, win 229, length 0
# 10:35:20.123789 IP 192.168.1.200.8080 > 192.168.1.100.45678: Flags [.], ack 1234567902, win 227, length 0
# 10:35:20.124012 IP 192.168.1.200.8080 > 192.168.1.100.45678: Flags [F.], seq 9876543250, ack 1234567902, win 227, length 0
# 10:35:20.124234 IP 192.168.1.100.45678 > 192.168.1.200.8080: Flags [.], ack 9876543251, win 229, length 0
# 5. 使用 tshark 详细分析
tshark -r /tmp/tcp_close.pcap -Y 'tcp.flags.fin==1' -T fields -e frame.number -e frame.time_relative -e ip.src -e ip.dst -e tcp.flags
# 预期输出:
# 15 5.234567 192.168.1.100 192.168.1.200 0x0011 # FIN-ACK (第1次挥手)
# 17 5.234890 192.168.1.200 192.168.1.100 0x0011 # FIN-ACK (第3次挥手)
四次挥手详细流程:
包序号 时间(ms) 源地址 → 目标地址 标志 seq ack 说明
------ -------- --------------------------- ----- ----------- ----------- -------------------
15 5234.567 192.168.1.100 → 200:8080 [F.] 1234567901 9876543250 客户端请求关闭(第1次)
16 5234.890 192.168.1.200 → 100:45678 [.] 9876543250 1234567902 服务器确认(第2次)
17 5235.123 192.168.1.200 → 100:45678 [F.] 9876543250 1234567902 服务器请求关闭(第3次)
18 5235.456 192.168.1.100 → 200:8080 [.] 1234567902 9876543251 客户端确认(第4次)
19+ 125234 (客户端进入 TIME_WAIT,等待 2MSL = 120秒)
关键参数解释:
- Flags [F.]:FIN+ACK,请求关闭连接并确认之前的数据
- TIME_WAIT 状态:主动关闭方(客户端)在最后一个 ACK 后等待 2MSL(Maximum Segment Lifetime),默认 120 秒
- 为什么需要 TIME_WAIT?:
- 确保最后一个 ACK 到达服务器(若丢失,服务器会重传 FIN)
- 防止旧连接的延迟数据包影响新连接
执行后验证:
# 验证 TIME_WAIT 状态
ss -tan | grep 8080 | grep TIME-WAIT
# 预期输出:
# TIME-WAIT 0 0 192.168.1.100:45678 192.168.1.200:8080
# 统计 TIME_WAIT 数量
ss -tan | grep TIME-WAIT | wc -l
# 预期输出:1-10 个(取决于并发连接数)
# 查看 TIME_WAIT 超时时间(内核参数)
cat /proc/sys/net/ipv4/tcp_fin_timeout
# 预期输出:60(秒)
Step 5: TCP 状态机与异常连接分析
目标: 理解 TCP 状态转换,排查常见异常状态
TCP 11 种状态:
状态名称 说明 谁持有
----------- ---------------------------------------- --------
LISTEN 监听状态,等待客户端连接 服务器
SYN_SENT 客户端发送 SYN 后,等待 SYN-ACK 客户端
SYN_RECEIVED 服务器收到 SYN,发送 SYN-ACK 后等待 ACK 服务器
ESTABLISHED 连接建立,可以传输数据 双方
FIN_WAIT1 主动关闭方发送 FIN,等待 ACK 主动方
FIN_WAIT2 主动关闭方收到 ACK,等待对方 FIN 主动方
CLOSE_WAIT 被动关闭方收到 FIN,发送 ACK 后等待应用关闭 被动方
CLOSING 双方同时关闭(罕见) 双方
LAST_ACK 被动关闭方发送 FIN,等待最后的 ACK 被动方
TIME_WAIT 主动关闭方收到 FIN 并发送 ACK,等待 2MSL 主动方
CLOSED 连接关闭 双方
查看当前 TCP 连接状态:
# 1. 查看所有 TCP 连接状态统计
ss -tan | awk '{print $1}' | sort | uniq -c | sort -rn
# 预期输出:
# 50 ESTAB
# 20 TIME-WAIT
# 10 LISTEN
# 3 SYN-SENT
# 1 State
# 2. 查看指定端口的连接状态
ss -tan state established '( dport = :8080 or sport = :8080 )'
# 预期输出:
# State Recv-Q Send-Q Local Address:Port Peer Address:Port
# ESTAB 0 0 192.168.1.200:8080 192.168.1.100:45678
# 3. 查看异常状态(SYN_SENT 过多可能遭受 SYN Flood 攻击)
ss -tan state syn-sent | wc -l
# 预期输出:0-5(正常)/ 100+(异常,可能受攻击)
# 4. 查看 CLOSE_WAIT 过多(应用未正常关闭连接)
ss -tan state close-wait | wc -l
# 预期输出:0-10(正常)/ 100+(异常,应用程序 bug)
常见异常状态排查:
# 问题1:TIME_WAIT 过多(>10000)
# 危害:占用端口资源,无法建立新连接
# 排查:
ss -tan | grep TIME-WAIT | wc -l
# 解决方案1:启用 TIME_WAIT 快速回收(内核参数)
sysctl -w net.ipv4.tcp_tw_reuse=1
sysctl -w net.ipv4.tcp_tw_recycle=0 # 注意:tcp_tw_recycle 在 Linux 4.12+ 已废弃
echo "net.ipv4.tcp_tw_reuse = 1" >> /etc/sysctl.conf
sysctl -p
# 解决方案2:增大本地端口范围
sysctl -w net.ipv4.ip_local_port_range="10000 65535"
echo "net.ipv4.ip_local_port_range = 10000 65535" >> /etc/sysctl.conf
# 问题2:CLOSE_WAIT 过多
# 危害:应用未关闭连接,导致资源泄漏
# 排查:
ss -tan state close-wait | head -5
# 预期输出:
# CLOSE-WAIT 0 0 192.168.1.200:8080 192.168.1.100:45678
# 解决:修复应用程序代码,确保调用 close() 关闭连接
# 问题3:SYN_RECV 过多(可能受 SYN Flood 攻击)
# 排查:
ss -tan state syn-recv | wc -l
# 解决:启用 SYN Cookie(内核参数)
sysctl -w net.ipv4.tcp_syncookies=1
echo "net.ipv4.tcp_syncookies = 1" >> /etc/sysctl.conf
执行后验证:
# 验证参数生效
sysctl -a | grep -E 'tcp_tw_reuse|ip_local_port_range|tcp_syncookies'
# 预期输出:
# net.ipv4.tcp_tw_reuse = 1
# net.ipv4.ip_local_port_range = 10000 65535
# net.ipv4.tcp_syncookies = 1
2️⃣ 最小必要原理
核心机制:
TCP 使用 三次握手 建立可靠连接,四次挥手 安全关闭连接。
为什么是三次握手而非两次?
- 防止旧连接请求:若客户端发送的第一个 SYN 延迟到达,服务器回复 SYN-ACK 后建立连接,但客户端认为此连接无效,导致资源浪费。第三次握手让客户端确认“我确实要建立这个连接”。
- 同步双方初始序列号(ISN):客户端和服务器各自生成随机 ISN,三次握手确保双方都知道对方的 ISN。
为什么是四次挥手而非三次?
- TCP 全双工通信:客户端发送 FIN 关闭发送方向,但服务器可能还有数据要发送,因此需要单独发送 FIN。
- 流程:客户端 FIN → 服务器 ACK(此时服务器可继续发送数据)→ 服务器 FIN → 客户端 ACK。
TIME_WAIT 存在的意义:
- 确保最后的 ACK 到达:若最后的 ACK 丢失,服务器会重传 FIN,客户端需保持连接以重新发送 ACK。
- 防止旧数据污染新连接:等待 2MSL(120 秒)确保所有旧数据包在网络中消失。
3️⃣ 可观测性(监控 + 告警 + 性能)
监控指标
Linux 原生监控:
# 1. 实时查看 TCP 连接状态统计
watch -n 1 'ss -tan | awk "{print \$1}" | sort | uniq -c | sort -rn'
# 预期输出:
# Every 1.0s: ss -tan | awk ...
# 150 ESTAB
# 50 TIME-WAIT
# 10 LISTEN
# 5 SYN-SENT
# 2. 监控 TIME_WAIT 数量
watch -n 5 'ss -tan | grep TIME-WAIT | wc -l'
# 预期输出:20-100(正常)/ 1000+(异常)
# 3. 查看网络吞吐量(需安装 iftop)
yum install -y iftop # RHEL/CentOS
apt install -y iftop # Ubuntu/Debian
iftop -i eth0 -n -P
# 预期输出:实时显示每个连接的发送/接收速率
# 4. 监控 TCP 重传率
netstat -s | grep -E 'retransmitted|segments'
# 预期输出:
# 12345678 segments sent
# 567 segments retransmitted
# 重传率 = 567 / 12345678 ≈ 0.0046% (< 1% 为正常)
# 5. 查看 TCP 缓冲区使用情况
ss -tm
# 预期输出:
# ESTAB 0 0 192.168.1.200:8080 192.168.1.100:45678
# ts sack cubic wscale:7,7 rto:204 rtt:3.5/1.5 ato:40 mss:1448 cwnd:10
Prometheus 监控配置:
# /etc/prometheus/prometheus.yml
scrape_configs:
- job_name: 'node_exporter'
static_configs:
- targets: ['192.168.1.200:9100']
# 关键 TCP 指标(node_exporter 自动采集)
# node_netstat_Tcp_CurrEstab # 当前 ESTABLISHED 连接数
# node_netstat_Tcp_ActiveOpens # 主动打开连接数(累计)
# node_netstat_Tcp_PassiveOpens # 被动打开连接数(累计)
# node_netstat_Tcp_RetransSegs # 重传段数(累计)
# node_netstat_TcpExt_TCPTimeouts # 超时次数(累计)
# node_netstat_TcpExt_ListenOverflows # Listen 队列溢出次数
Prometheus 告警规则:
# /etc/prometheus/rules/tcp_alerts.yml
groups:
- name: tcp_alerts
interval: 15s
rules:
# 告警1:TIME_WAIT 过多
- alert: TcpTimeWaitTooMany
expr: node_sockstat_TCP_tw > 5000
for: 2m
labels:
severity: warning
annotations:
summary: "TIME_WAIT 连接过多({{ $value }})"
description: "主机 {{ $labels.instance }} 的 TIME_WAIT 连接数超过 5000,可能影响新连接建立"
# 告警2:TCP 重传率过高
- alert: TcpRetransmitRateHigh
expr: rate(node_netstat_Tcp_RetransSegs[5m]) / rate(node_netstat_Tcp_OutSegs[5m]) > 0.02
for: 3m
labels:
severity: critical
annotations:
summary: "TCP 重传率过高({{ $value | humanizePercentage }})"
description: "主机 {{ $labels.instance }} 的 TCP 重传率超过 2%,网络质量异常"
# 告警3:ESTABLISHED 连接数过多
- alert: TcpEstablishedTooMany
expr: node_netstat_Tcp_CurrEstab > 10000
for: 5m
labels:
severity: warning
annotations:
summary: "TCP ESTABLISHED 连接数过多({{ $value }})"
description: "主机 {{ $labels.instance }} 的 ESTABLISHED 连接数超过 10000"
# 告警4:Listen 队列溢出
- alert: TcpListenQueueOverflow
expr: increase(node_netstat_TcpExt_ListenOverflows[5m]) > 10
for: 2m
labels:
severity: critical
annotations:
summary: "TCP Listen 队列溢出({{ $value }} 次)"
description: "主机 {{ $labels.instance }} 的 Listen 队列在 5 分钟内溢出 {{ $value }} 次,需增大 somaxconn"
性能基准测试
场景1:并发连接性能测试
# 工具:wrk(需提前安装)
wrk -t10 -c1000 -d30s http://192.168.1.200:8080/
# 预期输出(4C8G 节点):
# Running 30s test @ http://192.168.1.200:8080/
# 10 threads and 1000 connections
# Thread Stats Avg Stdev Max +/- Stdev
# Latency 45.23ms 12.34ms 120.45ms 78.90%
# Req/Sec 2.21k 234.56 3.12k 81.23%
# 663000 requests in 30.05s, 545.67MB read
# Requests/sec: 22064.32
# Transfer/sec: 18.15MB
场景2:短连接性能测试(大量 TIME_WAIT)
# 使用 ab 测试短连接
ab -n 100000 -c 1000 http://192.168.1.200:8080/
# 预期输出:
# Requests per second: 15234.56 [#/sec] (mean)
# Time per request: 65.632 [ms] (mean)
# Connection Times (ms)
# min mean[+/-sd] median max
# Connect: 0 15 12.3 12 120
# Processing: 1 45 23.4 42 250
# Waiting: 1 40 21.2 38 240
# Total: 2 60 28.5 55 320
# 测试期间监控 TIME_WAIT 数量
watch -n 1 'ss -tan | grep TIME-WAIT | wc -l'
# 预期输出:1000-5000(短连接场景下正常)
4️⃣ 常见故障与排错
| 故障编号 |
症状 |
诊断命令 |
可能根因 |
快速修复 |
永久修复 |
| #1 |
连接建立缓慢 |
ss -tan state syn-recv | wc -l |
1. SYN 队列满 2. 服务器负载高 3. 网络延迟大 |
增大 SYN 队列:sysctl -w net.ipv4.tcp_max_syn_backlog=8192 |
优化应用性能 增加服务器资源 |
| #2 |
TIME_WAIT 过多 |
ss -tan | grep TIME-WAIT | wc -l |
1. 短连接频繁 2. 主动关闭连接过多 |
启用 tw_reuse:sysctl -w net.ipv4.tcp_tw_reuse=1 |
使用长连接(HTTP Keep-Alive) |
| #3 |
CLOSE_WAIT 过多 |
ss -tan state close-wait | wc -l |
应用未调用 close() 关闭连接 |
重启应用释放连接 |
修复应用代码 确保正确关闭连接 |
| #4 |
Connection refused |
telnet 192.168.1.200 8080 |
1. 服务未启动 2. 端口未监听 3. 防火墙阻止 |
启动服务:systemctl start httpd |
配置防火墙放行端口 |
| #5 |
Connection timeout |
tcpdump -i eth0 'tcp and port 8080' |
1. 网络不通 2. SYN 包被丢弃 3. 服务器无响应 |
检查网络:ping 192.168.1.200 |
检查路由和防火墙规则 |
| #6 |
Connection reset |
ss -tan | grep ESTAB |
1. 服务器主动发送 RST 2. 防火墙中断连接 3. 应用崩溃 |
查看应用日志 |
修复应用程序 bug 调整超时参数 |
| #7 |
半连接队列满 |
ss -tan state syn-recv | wc -l |
SYN Flood 攻击 |
启用 SYN Cookie:sysctl -w net.ipv4.tcp_syncookies=1 |
部署防火墙过滤 增大队列大小 |
详细故障排查案例:
案例1:TIME_WAIT 过多导致无法建立新连接
# 现象:客户端连接失败
curl http://192.168.1.200:8080/
# 输出:curl: (7) Failed to connect to 192.168.1.200 port 8080: Cannot assign requested address
# 排查步骤1:检查 TIME_WAIT 数量
ss -tan | grep TIME-WAIT | wc -l
# 输出:28000(异常!系统端口范围 32768-60999,仅剩 28232-28000=232 个可用)
# 排查步骤2:查看端口范围
cat /proc/sys/net/ipv4/ip_local_port_range
# 输出:32768 60999
# 快速修复:启用 TIME_WAIT 复用
sysctl -w net.ipv4.tcp_tw_reuse=1
# 输出:net.ipv4.tcp_tw_reuse = 1
# 永久修复:扩大端口范围
echo "net.ipv4.ip_local_port_range = 10000 65535" >> /etc/sysctl.conf
sysctl -p
# 预期效果:可用端口从 28232 增加到 55535 个
# 验证修复
curl http://192.168.1.200:8080/
# 输出:HTTP/1.0 200 OK(成功)
案例2:CLOSE_WAIT 过多导致应用无法接受新连接
# 现象:应用无法接受新连接
ss -tan state close-wait | wc -l
# 输出:5000(异常!)
# 排查步骤1:查看哪些进程有 CLOSE_WAIT 连接
ss -tanp state close-wait | grep -oP 'pid=\K\d+' | sort | uniq -c | sort -rn
# 输出:
# 5000 12345 # PID 12345 的进程有 5000 个 CLOSE_WAIT 连接
# 排查步骤2:确认进程名称
ps -p 12345 -o comm=
# 输出:nginx(或其他应用)
# 快速修复:重启应用(临时)
systemctl restart nginx
# 永久修复:修复应用代码
# 原因:应用接收到对方的 FIN 后,未调用 close() 关闭连接
# 解决:在应用代码中确保 socket 正确关闭
# 例如(Python):
# try:
# conn, addr = server_socket.accept()
# # ... 处理请求 ...
# finally:
# conn.close() # 确保连接关闭
5️⃣ 变更与回滚剧本
灰度策略
场景:调整 TCP 内核参数
# 步骤1:备份当前配置
sysctl -a | grep -E 'tcp_tw_reuse|tcp_max_syn_backlog|ip_local_port_range' > /tmp/tcp_params_backup.txt
# 步骤2:在测试环境验证
sysctl -w net.ipv4.tcp_tw_reuse=1
sysctl -w net.ipv4.tcp_max_syn_backlog=8192
# 观察 5-10 分钟,监控连接状态
# 步骤3:生产环境灰度(先在部分节点应用)
# 仅在 192.168.1.200 应用
ssh root@192.168.1.200 "sysctl -w net.ipv4.tcp_tw_reuse=1"
# 步骤4:全量应用
for host in 192.168.1.{200..210}; do
ssh root@$host "sysctl -w net.ipv4.tcp_tw_reuse=1"
done
# 步骤5:持久化配置
echo "net.ipv4.tcp_tw_reuse = 1" >> /etc/sysctl.conf
sysctl -p
回滚条件与命令
回滚触发条件:
- 连接失败率超过 5%
- TCP 重传率超过 5%
- 应用响应时间增加超过 50%
回滚步骤:
# 恢复备份的参数
while IFS= read -r line; do
sysctl -w "$line"
done < /tmp/tcp_params_backup.txt
# 或手动恢复关键参数
sysctl -w net.ipv4.tcp_tw_reuse=0
sysctl -w net.ipv4.tcp_max_syn_backlog=512
# 重启应用(如需)
systemctl restart nginx
6️⃣ 最佳实践
1. 使用 TIME_WAIT 复用而非回收
# 推荐配置(Linux 3.10+)
sysctl -w net.ipv4.tcp_tw_reuse=1
# 不推荐:tcp_tw_recycle(已在 Linux 4.12+ 废弃,NAT 环境下有问题)
2. 调整 SYN 队列大小防止 SYN Flood
sysctl -w net.ipv4.tcp_max_syn_backlog=8192 # 半连接队列
sysctl -w net.core.somaxconn=8192 # 全连接队列
sysctl -w net.ipv4.tcp_syncookies=1 # 启用 SYN Cookie
3. 扩大本地端口范围
sysctl -w net.ipv4.ip_local_port_range="10000 65535"
# 可用端口从 28232 增加到 55535 个
4. 启用 TCP Fast Open(减少握手延迟)
sysctl -w net.ipv4.tcp_fastopen=3 # 3=客户端和服务器都启用
# 效果:第二次连接可跳过三次握手,延迟降低 1 RTT
5. 监控关键指标并设置告警
# 关键指标阈值(Prometheus)
# TIME_WAIT > 5000 → 告警
# 重传率 > 2% → 告警
# CLOSE_WAIT > 1000 → 告警
# SYN_RECV > 500 → 告警(可能受攻击)
6. 使用长连接减少握手开销
# HTTP Keep-Alive 配置(Nginx)
keepalive_timeout 65; # 连接保持 65 秒
keepalive_requests 1000; # 单个连接最多处理 1000 个请求
7. 定期清理 CLOSE_WAIT 连接
# 监控脚本(每 5 分钟检查一次)
*/5 * * * * [ $(ss -tan state close-wait | wc -l) -gt 1000 ] && systemctl restart myapp
7️⃣ FAQ
Q1: 为什么三次握手不是两次或四次?
A: 两次握手无法防止旧 SYN 请求建立无效连接;四次握手冗余(SYN 和 ACK 可合并为 SYN-ACK)。三次握手是效率与可靠性的最佳平衡。
Q2: TIME_WAIT 状态可以关闭吗?
A: 不能完全关闭,但可通过 tcp_tw_reuse=1 快速复用 TIME_WAIT 连接。完全关闭会导致旧数据包污染新连接。
Q3: CLOSE_WAIT 过多如何排查应用 bug?
A:
# 1. 查看哪个进程有 CLOSE_WAIT 连接
ss -tanp state close-wait | grep -oP 'pid=\K\d+' | sort | uniq -c
# 2. 使用 strace 跟踪进程系统调用
strace -p <PID> -e trace=close,shutdown
# 3. 检查代码是否正确调用 close()/shutdown()
Q4: 如何区分主动关闭和被动关闭?
A:
- 主动关闭:先发送 FIN 的一方,最终进入 TIME_WAIT 状态(等待 2MSL)
- 被动关闭:先收到 FIN 的一方,经过 CLOSE_WAIT → LAST_ACK → CLOSED
Q5: 为什么 tcpdump 抓到的序列号和 Wireshark 不一致?
A: tcpdump 默认显示相对序列号,Wireshark 显示绝对序列号。可使用 tcpdump -S 显示绝对序列号。
Q6: 如何处理 SYN Flood 攻击?
A:
# 1. 启用 SYN Cookie(无状态处理 SYN)
sysctl -w net.ipv4.tcp_syncookies=1
# 2. 增大 SYN 队列
sysctl -w net.ipv4.tcp_max_syn_backlog=8192
# 3. 减小 SYN-ACK 重试次数
sysctl -w net.ipv4.tcp_synack_retries=1
# 4. 部署防火墙限速(iptables)
iptables -A INPUT -p tcp --syn -m limit --limit 10/s -j ACCEPT
Q7: 短连接场景如何优化性能?
A:
# 1. 启用 TIME_WAIT 复用
sysctl -w net.ipv4.tcp_tw_reuse=1
# 2. 扩大端口范围
sysctl -w net.ipv4.ip_local_port_range="10000 65535"
# 3. 启用 TCP Fast Open
sysctl -w net.ipv4.tcp_fastopen=3
# 4. 应用层改为长连接(HTTP Keep-Alive)
8️⃣ 附录:关键脚本
脚本1:TCP 连接状态监控脚本
#!/bin/bash
##############################################################################
# 文件名:tcp_monitor.sh
# 版本:v1.0.0
# 用途:监控 TCP 连接状态,输出 Prometheus 格式指标
# 使用:crontab -e → */1 * * * * /usr/local/bin/tcp_monitor.sh
##############################################################################
set -euo pipefail
METRICS_FILE="/var/lib/node_exporter/textfile_collector/tcp_states.prom"
mkdir -p "$(dirname "$METRICS_FILE")"
# 获取各状态的连接数
ESTAB=$(ss -tan state established | wc -l)
TIME_WAIT=$(ss -tan state time-wait | wc -l)
CLOSE_WAIT=$(ss -tan state close-wait | wc -l)
SYN_SENT=$(ss -tan state syn-sent | wc -l)
SYN_RECV=$(ss -tan state syn-recv | wc -l)
FIN_WAIT1=$(ss -tan state fin-wait-1 | wc -l)
FIN_WAIT2=$(ss -tan state fin-wait-2 | wc -l)
LISTEN=$(ss -tan state listen | wc -l)
# 输出 Prometheus 格式
cat > "$METRICS_FILE" << EOF
# HELP tcp_connections_state TCP connections by state
# TYPE tcp_connections_state gauge
tcp_connections_state{state="established"} $ESTAB
tcp_connections_state{state="time_wait"} $TIME_WAIT
tcp_connections_state{state="close_wait"} $CLOSE_WAIT
tcp_connections_state{state="syn_sent"} $SYN_SENT
tcp_connections_state{state="syn_recv"} $SYN_RECV
tcp_connections_state{state="fin_wait1"} $FIN_WAIT1
tcp_connections_state{state="fin_wait2"} $FIN_WAIT2
tcp_connections_state{state="listen"} $LISTEN
EOF
# 计算重传率
RETRANS=$(netstat -s | grep 'segments retransmitted' | awk '{print $1}')
SENT=$(netstat -s | grep 'segments sent out' | awk '{print $1}')
cat >> "$METRICS_FILE" << EOF
# HELP tcp_segments_total TCP segments statistics
# TYPE tcp_segments_total counter
tcp_segments_retransmitted_total $RETRANS
tcp_segments_sent_total $SENT
EOF
echo "[$(date)] TCP metrics updated: ESTAB=$ESTAB, TIME_WAIT=$TIME_WAIT, CLOSE_WAIT=$CLOSE_WAIT"
脚本2:TCP 连接异常检测与告警脚本
#!/bin/bash
##############################################################################
# 文件名:tcp_alert.sh
# 版本:v1.0.0
# 用途:检测 TCP 连接异常状态并发送告警
##############################################################################
set -euo pipefail
# 告警阈值
TIME_WAIT_THRESHOLD=5000
CLOSE_WAIT_THRESHOLD=1000
SYN_RECV_THRESHOLD=500
# 告警方式(修改为实际告警接口)
send_alert() {
local message="$1"
echo "[ALERT] $(date): $message" >> /var/log/tcp_alert.log
# 发送到钉钉/企业微信/邮件等(需自行实现)
# curl -X POST -d "text=$message" https://your-alert-webhook-url
}
# 检查 TIME_WAIT
TIME_WAIT=$(ss -tan state time-wait | wc -l)
if [ "$TIME_WAIT" -gt "$TIME_WAIT_THRESHOLD" ]; then
send_alert "TIME_WAIT 连接数过多:$TIME_WAIT(阈值:$TIME_WAIT_THRESHOLD)"
fi
# 检查 CLOSE_WAIT
CLOSE_WAIT=$(ss -tan state close-wait | wc -l)
if [ "$CLOSE_WAIT" -gt "$CLOSE_WAIT_THRESHOLD" ]; then
send_alert "CLOSE_WAIT 连接数过多:$CLOSE_WAIT(阈值:$CLOSE_WAIT_THRESHOLD),可能应用未正确关闭连接"
fi
# 检查 SYN_RECV(可能受攻击)
SYN_RECV=$(ss -tan state syn-recv | wc -l)
if [ "$SYN_RECV" -gt "$SYN_RECV_THRESHOLD" ]; then
send_alert "SYN_RECV 连接数过多:$SYN_RECV(阈值:$SYN_RECV_THRESHOLD),可能遭受 SYN Flood 攻击"
fi
脚本3:自动化抓包脚本
#!/bin/bash
##############################################################################
# 文件名:tcp_capture.sh
# 版本:v1.0.0
# 用途:自动抓取指定端口的 TCP 连接(三次握手+四次挥手)
# 使用:./tcp_capture.sh 8080 60 # 抓取 8080 端口,持续 60 秒
##############################################################################
set -euo pipefail
PORT=${1:-80}
DURATION=${2:-30}
OUTPUT_DIR="/tmp/tcp_captures"
OUTPUT_FILE="${OUTPUT_DIR}/tcp_${PORT}_$(date +%Y%m%d_%H%M%S).pcap"
mkdir -p "$OUTPUT_DIR"
echo "[$(date)] 开始抓包:端口=$PORT,持续时间=$DURATION 秒"
echo "[$(date)] 输出文件:$OUTPUT_FILE"
# 启动 tcpdump 抓包
timeout "$DURATION" tcpdump -i any -nn -S -vvv "tcp and port $PORT" -w "$OUTPUT_FILE" 2>&1 | tee "${OUTPUT_FILE}.log" &
TCPDUMP_PID=$!
# 等待抓包结束
wait "$TCPDUMP_PID" || true
# 分析抓包结果
echo "[$(date)] 抓包完成,开始分析..."
# 统计包数量
TOTAL_PACKETS=$(tcpdump -r "$OUTPUT_FILE" 2>/dev/null | wc -l)
SYN_PACKETS=$(tcpdump -r "$OUTPUT_FILE" 'tcp[tcpflags] & (tcp-syn) != 0' 2>/dev/null | wc -l)
FIN_PACKETS=$(tcpdump -r "$OUTPUT_FILE" 'tcp[tcpflags] & (tcp-fin) != 0' 2>/dev/null | wc -l)
echo "========== 抓包统计 =========="
echo "总包数:$TOTAL_PACKETS"
echo "SYN 包数:$SYN_PACKETS(包括 SYN 和 SYN-ACK)"
echo "FIN 包数:$FIN_PACKETS"
echo "文件路径:$OUTPUT_FILE"
echo "文件大小:$(du -h "$OUTPUT_FILE" | cut -f1)"
# 导出可读的文本格式
tcpdump -r "$OUTPUT_FILE" -nn -tttt > "${OUTPUT_FILE}.txt"
echo "文本格式已导出:${OUTPUT_FILE}.txt"
9️⃣ 扩展阅读
官方文档:
深入技术博客:
工具下载:
社区资源:
希望这篇结合了实战抓包、故障排查与内核调优的详细指南,能帮助您更深入地理解 TCP/IP 协议栈。如果您在实践过程中遇到任何问题,或者有更深入的见解,欢迎到云栈社区与广大开发者交流讨论。