网络知识
Production Dossiers · Linux Networking

网络实战案例集

协议告诉你系统应该怎样工作,证据链告诉你它究竟在哪里偏离。九个生产故障,从现场现象一路追到内核队列、报文与应用。

9 个 生产案例6 层 观测路径逐项 版本边界
阅读约定:命令输出必须看“故障窗口内的增量”,不能只看历史累计值。TTL、IPv4 ID、单点抓包等只能提供旁证;改内核参数前,先证明对应计数器正在增长并准备回滚。

案例索引

01TCP 半连接 / 全连接队列溢出握手像是成功了,应用却迟迟 accept 不到+

现现象

服务启动、流量突增或应用停顿时,大批客户端连接超时;有的客户端已经进入 ESTABLISHED,服务端业务线程却没有处理到连接。机器 CPU、内存甚至应用处理耗时都可能看似正常。

根根因

accept 变慢才是常见上游原因。已完成握手、尚未被应用 accept() 的连接塞满 accept queue;也可能是 SYN 洪泛或资源压力挤满 request queue。只调大 backlog 会延后爆点,却不会修复线程泄漏、事件循环卡顿或冷启动。

理原理

一句话:Linux 为监听 socket 分别保存“握手中”的 request socket 与“握手完成等待应用接走”的连接;应用给 listen(backlog) 的值还会被 net.core.somaxconn 封顶。

证排查证据链

① 先看监听 socket 的 accept queue

$ ss -lnt '( sport = :8080 )'
State  Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 129    128    0.0.0.0:8080      0.0.0.0:*

仅对监听 socket:Recv-Q 是等待 accept() 的当前连接数,Send-Q 是 accept queue 上限。二者持续贴近,不等于“网络慢”,而是应用没及时接走连接。它们不直接显示半连接队列。

② 分开看 SYN_RECV 与累计计数器的增量

$ ss -Hnt state syn-recv '( sport = :8080 )' | wc -l
317
$ nstat -az | egrep 'TcpExt(ListenOverflows|ListenDrops|TCPReqQFullDoCookies|TCPReqQFullDrop|SyncookiesSent)'
TcpExtListenOverflows          842
TcpExtListenDrops              901
TcpExtTCPReqQFullDoCookies     56
TcpExtTCPReqQFullDrop          0

ListenOverflows 强烈指向 accept queue 溢出;ListenDrops 范围更宽,内存分配失败等也可能令它单独增加。计数按丢包/重试累计,不是失败连接数。SYN cookies 开启时,不能只拿 tcp_max_syn_backlog 推算半连接容量。

③ 把内核容量、应用 backlog 和线程现场对齐

$ sysctl net.core.somaxconn net.ipv4.tcp_max_syn_backlog \
          net.ipv4.tcp_abort_on_overflow
$ ps -L -p "$PID" -o pid,tid,stat,pcpu,wchan:24,comm
$ tcpdump -ni any -s 0 -w queue.pcap 'tcp port 8080'

抓包用于确认 SYN/SYN+ACK/ACK 的重试时间线;同时查事件循环、线程栈、GC/停顿和 accept 速率。只抓物理口可能漏掉 Nginx→本机应用的 lo 流量。

界预防与参数边界

Linux 版本相关不要写死“队列满一定丢第三次 ACK”。Linux 4.10 前后处理序列有差异,syncookies 也会改变现场。somaxconn 默认值在 Linux 5.4 起从 128 提高到 4096;最终仍以本机 sysctl 和应用实际 backlog 为准。tcp_abort_on_overflow=1 会把慢 accept 变成 RST,可能直接伤害客户端,只能在理解重试语义后采用,不是扩容开关。预防重点是监控队列利用率与溢出增量、控制启动突发,并修复 accept 延迟。

参考来源:就是要你懂 TCP:半连接队列和全连接队列 · TCP 队列实战案例

↑ 返回顶部
02RST 来源鉴别端点发的,还是 LB / 防火墙伪造的?+

现现象

连接在某条请求、某个响应大小或固定空闲时间后被 Connection reset by peer 中断。接收端抓到一个地址、端口和序号都“像对端”的 RST,但对端日志找不到关闭动作。

根根因

地址能伪造,生成位置会留下模式差异。应用的 abortive close、进程退出、内核发现无 socket、LB 空闲超时、IPS/防火墙内容拦截都可能发 RST;中间设备甚至会向两端各伪造一份。

理原理

一句话:有效 RST 首先要匹配连接并通过 TCP 序号校验;TTL、IPv4 ID 和二层源 MAC 只反映包的路径与生成实现,必须和双端时间线共同使用。

证排查证据链

① 两端同时抓完整流,而不是只抓 RST

# 客户端与服务端都执行;先校准时钟并记录抓包点
$ tcpdump -ni any -s 0 -vvv -w side-a.pcap \
  'host 203.0.113.20 and tcp port 443'

# 快速巡检所有 RST
$ tcpdump -ni any -s 0 -vvv \
  'tcp[tcpflags] & tcp-rst != 0'

若 RST 只在接收端出现、发送端抓包没有,而时钟和抓包范围可靠,链路中间注入的可能性显著上升。双端都看到则继续查主机内核与应用。

② 导出旁证字段,和同方向正常包比较

$ tshark -r side-a.pcap -Y 'tcp.flags.reset == 1' -T fields \
  -e frame.time_epoch -e ip.src -e ip.dst -e ip.ttl -e ip.id \
  -e eth.src -e tcp.srcport -e tcp.dstport -e tcp.seq -e tcp.ack
字段正确读法
TTL与同方向正常包明显不同才是线索;初始 TTL 只能猜测,不能单独定案。
IPv4 ID序列突变可提示伪造,但它也可能为 0、随机或按流生成;IPv6 没有该字段。
Seq / Ack确认是否落入接收窗口。比较两端时关闭相对序号显示,或统一换算。
源 MAC只代表当前抓包点相邻二层发送者;经过路由后不再是原始发送主机。

③ 主机内核计数与设备日志交叉验证

$ nstat -az | egrep 'TcpOutRsts|TcpExtTCPAbortOn(Data|Close|Memory|Timeout|Linger)'
$ traceroute 203.0.113.20
# 再按时间、五元组查询 LB / 防火墙 / IPS 日志

TcpOutRsts 表示本机内核尝试发送 RST,不保证已经上线路,也不能直接定位进程。固定空闲时长、固定内容或响应大小,是查询中间设备策略的关键条件。

界预防与参数边界

四类旁证都不是密码学证明。任何结论都应写成“由双端包序列 + TTL/IP ID 模式 + 设备日志共同支持”。SO_LINGER {on,0} 是 abortive close:可能丢弃未发送数据并发 RST,不是普通的“快速关闭”。生产环境应统一 LB/NAT 空闲超时与连接池寿命,并保留可按五元组检索的策略日志。

参考来源:到底是谁 reset 了你的连接 · iptables 监控 reset 的连接信息

↑ 返回顶部
03PMTUD 黑洞与隧道 MTU小包能通,大文件或 TLS 握手却卡死+

现现象

ping、TCP 握手和小请求正常;SCP、大响应或某个 TLS 报文开始后,同一大段不断重传,小 ACK/keepalive 仍能通过。容器、VPN、VXLAN/GRE/IPsec 场景尤其常见。

根根因

路径中某一跳 MTU 小于端点认知,报文又设置 DF;路由器返回的 ICMP “需要分片”或 ICMPv6 Packet Too Big 被过滤,发送端无法降低包长。隧道新增外层首部却没有同步收缩内层 MTU,是高频诱因。

理原理

一句话:MSS 只声明 TCP 端点愿意接收的最大段,不证明整条路径都容得下;PMTUD 依靠 ICMP 反馈学习路径瓶颈,反馈消失就会形成“黑洞”。

证排查证据链

① 先画实际出接口与每层 MTU

$ ip route get 203.0.113.20
$ ip -d link show
$ tracepath 203.0.113.20
# 重点对比 pod eth0 / veth / bridge / tunnel / 物理口

VXLAN、GRE、IPsec 等封装会占用额外字节。只看容器内的 1500,不能排除宿主或底层网络只有 1450/1400。

② 用 DF ping 二分可通过的 payload

$ ping -4 -M do -s 1472 -c 3 203.0.113.20
From 192.0.2.1: icmp_seq=1 Frag needed and DF set (mtu = 1450)
$ ping -4 -M do -s 1422 -c 3 203.0.113.20
64 bytes from 203.0.113.20: icmp_seq=1 ttl=52 time=18.4 ms

-s 是 ICMP payload,不是 IP 总长:无 IPv4 选项时约再加 28 字节;IPv6 约加 48 字节。远端禁 Echo 时以 tracepath 和实际 TCP 证据为准。

③ 多点抓包找“最后出现、首次消失”的位置

$ tcpdump -ni any -s 0 -vvv -w mtu.pcap \
  'icmp or icmp6 or (host 203.0.113.20 and tcp port 443)'
# IPv4 关键反馈:ICMP type 3 / code 4
# IPv6 关键反馈:ICMPv6 type 2 Packet Too Big

宿主抓包受 TSO/GSO/GRO 影响,可能显示超过 MTU 的大 skb 或假 checksum error;这不等于线上真的发出了巨帧,应在隧道外或外部抓包点复核。

界预防与参数边界

IPv4 / IPv6 不同IPv4 黑洞常涉及 ICMP type 3/code 4;IPv6 路由器不会替端点分片,ICMPv6 Packet Too Big 是基本依赖。正确修复优先级是:统一端点/隧道 MTU → 放行必要 ICMP/ICMPv6 → 在明确的 TCP 转发边界审计双向 MSS clamp。
# 仅示意转发边界;上线前审计方向、表和回滚
iptables -t mangle -A FORWARD -p tcp --tcp-flags SYN,RST SYN \
  -j TCPMSS --clamp-mss-to-pmtu

# TCP PLPMTUD:0 关闭;1 检测到 ICMP 黑洞后启用;2 始终启用
sysctl net.ipv4.tcp_mtu_probing net.ipv4.tcp_base_mss
MSS clamp 只改 TCP SYN 中的 MSS,修不了 UDP/QUIC,也不能替代 ICMPv6。模式 2 以 tcp_base_mss 为初始 MSS;tcp_mtu_probing 仅覆盖 TCP,不是全协议修复。

参考来源:通过案例来理解 MSS、MTU 等 TCP 概念

↑ 返回顶部
04DNS resolver 路径分叉dig 正常,应用却解析失败或去了错误地址+

现现象

dig name @server 能得到正确答案,ping、curl 或业务进程却报 unknown host、命中另一个地址,甚至没有任何 53 端口流量。加上或去掉域名末尾的点,耗时和答案又变了。

根根因

“查 DNS”不是单一路径。应用可能依次经过 NSS、/etc/hosts、本地缓存、systemd-resolved 或 glibc stub resolver;也可能由 JVM、Go、musl、c-ares 自己实现。rotate、search/ndots 和负缓存会继续放大差异。

理原理

一句话:dig 是直接构造 DNS 查询的诊断工具,而多数原生应用调用 getaddrinfo(),其数据源与顺序由 NSS 和运行库决定。

证排查证据链

① 先复现“应用路径”,再对照 DNS 工具

$ getent ahosts api.example.internal
192.0.2.40  STREAM api.example.internal
$ dig api.example.internal
$ dig api.example.internal. @192.0.2.53
# 末尾点号表示绝对域名,避免附加 search 后缀

getent ahosts 更接近应用的 getaddrinfo()。getent hosts、nslookup 与 dig 也不应被当成完全等价的实现。

② 沿 NSS 配置逐站核对

$ grep '^hosts:' /etc/nsswitch.conf
hosts: files myhostname resolve [!UNAVAIL=return] dns
$ grep -n 'api.example.internal' /etc/hosts
$ readlink -f /etc/resolv.conf
/run/systemd/resolve/stub-resolv.conf
$ sed -n '1,80p' /etc/resolv.conf
search svc.cluster.local cluster.local
options ndots:5 timeout:2 attempts:2 rotate

rotate 会轮换 nameserver 的起点,不是“第一台失败再切换”的同义词。ndots:n 决定先尝试原名还是先附加 search;后缀越多、重试越多,单次逻辑查询可能膨胀成很多网络查询。

③ 用系统调用和抓包确认“有没有发、发给谁”

$ strace -f -tt -s 200 \
  -e trace=openat,connect,sendto,recvfrom,poll,ppoll \
  curl -v https://api.example.internal/
$ tcpdump -ni any -s 0 -vvv 'port 53'

若先从 hosts/缓存返回,抓不到 DNS 包是正常现象;若请求发给 127.0.0.53,继续查本地代理的上游。抓包时同时看 A/AAAA、EDNS OPT、RCODE、TC 位、重试间隔和是否 TCP fallback。

④ 分清权威链与本机路径

$ dig +trace api.example.com
# 适合检查委派链;不会重现 hosts、NSS、缓存、search
# 或应用内置 resolver 的行为

界预防与参数边界

先确认应用到底用哪套 resolver。Go 的纯 Go resolver、JVM DNS 缓存、musl 与 glibc 的行为和选项并不完全相同;不能看到 Linux 就默认 nscd 生效。内部域名不要把互不具备相同视图的 nameserver 简单放进同一轮询组。缩短 TTL、清缓存或删除 search 只能针对已证明的根因;dig +trace 成功不代表业务解析链健康。

参考来源:就是要你懂 DNS:域名解析相关问题 · nslookup OK but ping fail · EDNS 不兼容案例

↑ 返回顶部
05Kubernetes DNS 典型约 5 秒A / AAAA + ClusterIP DNAT + conntrack 的历史竞态+

现现象

Pod 内解析偶发多出一段接近 5 秒的等待,重试后成功;抓包可能看到 A 与 AAAA 同时发出,却只收到其中一个响应。它不是 DNS 协议规定的“固定 5 秒”,只是 glibc 默认超时常产生的典型形状。

根根因

这是一个需要证据才能成立的历史故障模型:glibc 复用同一 UDP socket 并发发 A/AAAA;查询经 kube-dns/CoreDNS 的 ClusterIP DNAT 后,在特定内核、kube-proxy 模式与并发时序下发生 conntrack 插入冲突,一包被丢,resolver 等待缺失响应超时后重试。

理原理

一句话:UDP connect() 本身不发包,A/AAAA 两个报文可能用相同源端点几乎同时穿过 Service DNAT;连接跟踪要为转换后的流建立无冲突状态,竞态失败就可能丢掉后插入的一条。

证排查证据链

① 先量出等待发生在哪个系统调用

$ cat /etc/resolv.conf
nameserver 10.96.0.10
search app.svc.cluster.local svc.cluster.local cluster.local
options ndots:5
$ strace -f -tt -s 200 \
  -e trace=connect,sendto,sendmmsg,recvfrom,poll,ppoll \
  getent ahosts redis.app.svc.cluster.local
12:00:00.101 sendmmsg(... A ..., ... AAAA ...) = 2
12:00:00.104 recvfrom(... AAAA response ...) = 141
12:00:00.105 poll(..., 5000) = 0 (Timeout)

等待值可被 options timeout:、libc、Go/JVM 或应用超时修改;所以正文只称“典型约 5 秒”。

② 在 Pod、Node 与 CoreDNS 侧对同一事务多点抓包

$ tcpdump -ni any -s 0 -vvv -w dns.pcap 'udp port 53'
$ tshark -r dns.pcap -Y dns -T fields \
  -e frame.time_epoch -e ip.src -e udp.srcport \
  -e dns.id -e dns.qry.name -e dns.qry.type -e dns.flags.response
# QTYPE 1=A,28=AAAA;按 dns.id 和源端口配对问答

只在 Pod 抓到“一问无答”不够;要确认请求进入节点后是否经过 DNAT、CoreDNS 是否收到,以及响应在哪一段消失。

③ 用 conntrack 增量区分“插入冲突”与“表满”

$ conntrack -S
cpu=0 found=... insert=... insert_failed=37 drop=...
$ sysctl net.netfilter.nf_conntrack_count \
          net.netfilter.nf_conntrack_max
$ dmesg -T | grep -i 'nf_conntrack.*table full'

insert_failed 在复现窗口同步增加,才支持插入冲突假设。nf_conntrack_count 接近 max 且出现 table full 是容量耗尽;两者不能混称“conntrack 表满”。

④ 同时排除更常见的 5 秒来源

  • ndots:5 + 多个 search 域扩增查询;
  • CoreDNS CPU/队列过载或上游超时;
  • 网络只丢 AAAA 响应、MTU/分片问题;
  • 应用连接超时恰好也是 5 秒,而 DNS 已经很快完成。

界预防与参数边界

glibc / 内核 / 拓扑相关glibc 自 2.9 起通常并发 A/AAAA;single-request(2.10+)改为串行,single-request-reopen(2.9+)在同一查找的 A/AAAA 同端口应答异常时关闭旧 socket 再发第二问,但并非所有 libc 支持。它们可作为经验证的缓解,不是跨平台标准。NodeLocal DNSCache 能减少 Pod→Service DNAT/UDP conntrack 路径,通常降低相关竞态概率,但不保证治好所有 5 秒。优先升级已修复相关竞态的内核/平台,并保留查询延迟、CoreDNS 和 conntrack 指标。

参考来源:Docker 中的 DNS 解析过程 · Kubernetes service 与 kube-proxy

↑ 返回顶部
06分层丢包定位NIC ring → softnet → netfilter → socket → listen+

现现象

应用超时、重传或吞吐下降;tcpdump 有时能看到请求,应用却没收到,有时连 tcpdump 都抓不到。笼统地“调大 buffer”后,问题可能短暂缓解,也可能只是把丢包变成长排队。

根根因

包可能在网卡硬件 FIFO/ring、NAPI/每 CPU backlog、XDP/netfilter/路由、协议栈、socket 接收缓冲、conntrack 或监听队列任一处被丢。每层都有自己的容量、上下文和计数器。

理原理

一句话:收包是一条有多个队列的流水线;只有“某层的丢弃计数在故障窗口持续增加”,才能把根因落在那一层。

证排查证据链

① 从最靠近硬件的一层开始

$ ip -s -s link show dev eth0
$ ethtool -g eth0
$ ethtool -S eth0 | egrep -i 'drop|miss|over|fifo|error|no.?buf'
$ cat /proc/interrupts
$ cat /proc/softirqs | grep NET_RX

ethtool -S 字段由驱动定义,rx_missed_errors、rx_no_buffer 等并非每块网卡都有。先查驱动文档,再解释哪个环节增加。

② 看每 CPU softnet 队列

$ head -4 /proc/net/softnet_stat
0004f938 00000000 00000000 ...
0012a7b1 0000002a 00000019 ...
# 每行对应一个 CPU,字段为十六进制
# 前三列:processed / backlog 满而 dropped / budget 或时间用尽的 time_squeeze

第二列增长才支持调整 net.core.netdev_max_backlog;第三列增长提示 NAPI 一轮预算/时间不足,可再结合 net.core.netdev_budget、IRQ 分布和 CPU si%。不要硬背后续列,它们随内核演进。

③ 到协议栈、socket 与应用消费速度

$ nstat -az | egrep 'Udp(InErrors|RcvbufErrors)|TcpExt(TCPRcvQDrop|TCPBacklogDrop|TCPMemoryPressures|IPReversePathFilter)'
$ ss -tinm '( sport = :8080 )'
skmem:(r123456,rb131072,t0,tb2626560,f0,w0,o0,bl0,d18)

建立连接的 Recv-Q 是应用未读取的字节,不等于已经丢包。ss -m 中 r 接近 rb 表示接收缓冲吃紧,d 是 socket drop;还要确认应用线程是否在及时读取。

④ 最后核对状态防火墙与监听队列

$ sysctl net.netfilter.nf_conntrack_count net.netfilter.nf_conntrack_max
$ conntrack -S
$ dmesg -T | grep -i conntrack
$ ss -lnt '( sport = :8080 )'
$ nstat -az | egrep 'TcpExtListen(Overflows|Drops)'

⑤ 正确理解抓包点

tcpdump 抓到入站包,只能证明它到达 packet tap;包仍可能在 netfilter、路由、TCP 或 socket 后续被丢。反过来,NIC ring 满、XDP drop 或 tap 之前的丢包可能完全抓不到。多点抓包要找“最后出现”和“首次消失”,而不是用一个抓包点证明端到端送达。

界预防与参数边界

队列越大不等于越快。ring、backlog、budget、socket buffer 和 conntrack 上限都会消耗内存,且可能增加排队延迟。只有对应增量计数器持续增长时才调整,并同时观察延迟、CPU 和丢包是否改善。RSS/RPS/IRQ 绑核需要结合 NUMA 和业务 CPU 分布验证,优先建立基线,不要照搬固定掩码。

参考来源:网络丢包 · Linux Network Stack · nginx 性能和软中断

↑ 返回顶部
07用 ss -ti 读 TCP 慢问题cwnd、RTT、RTO、ssthresh、BDP 与零窗口+

现现象

链路标称 1 Gbit/s,单连接却只有几 Mbit/s;或业务时快时慢、Send-Q 堆积。ping 看起来尚可,盲目放大 socket buffer 或更换拥塞算法后也未必改善。

根根因

在途数据可能被拥塞窗口 cwnd、对端接收窗口、发送缓冲任一项限制;高 RTT 令窗口增长和丢包恢复变慢。还可能是应用供数不足、对端不及时读取形成零窗口、pacing 限速,或真正的丢包/RTO。

理原理

一句话:发送上限可粗略看作 min(cwnd×MSS, 对端接收窗口, 可用发送缓冲) ÷ RTT;BDP 是跑满链路所需的目标在途量,不是吞吐保证。

证排查证据链

① 对同一连接连续采样,而非只截一帧

$ ss -tinm '( dst 203.0.113.20 and dport = :443 )'
ESTAB 0 524288 192.0.2.10:49152 203.0.113.20:443
 cubic wscale:7,7 rto:420 rtt:82.1/11.4 mss:1448
 cwnd:10 ssthresh:18 bytes_sent:... bytes_acked:...
 retrans:0/7 send 1.4Mbps pacing_rate 2.8Mbps delivery_rate 1.2Mbps
 skmem:(r0,rb131072,t0,tb2626560,f0,w524288,o0,bl0,d0)
字段怎么读
rtt:a/b平滑 RTT / RTT 方差,单位 ms;82 ms 链路上 cwnd=10、MSS=1448,仅约 14 KiB 在途。
rto重传超时估计,单位 ms。没有丢包时它不参与正常发送,不能仅凭“大”判为根因。
cwnd通常以 segment 计,乘当前 MSS 才近似换算为字节。
ssthresh慢启动与拥塞避免的分界,不是永久吞吐上限;cwnd 之后仍可线性增长。
send / delivery_rate内核估算,需连续采样并和 bytes_acked、应用吞吐对照。

② 用 BDP 检查窗口量级

# 100 Mbit/s × 80 ms = 8 Mbit ≈ 1 MB BDP
$ python3 -c 'print(100e6/8*0.080)'
1000000.0
# 若 cwnd × MSS 只有约 14 KiB,就不可能仅靠该连接跑满 100 Mbit/s

仍要同时确认对端通告窗口、窗口扩大协商、发送/接收缓冲和应用是否持续供数。BDP 只给量级,不说明为什么窗口小。

③ 区分快重传、超时与 loss probe

$ nstat -az | egrep 'TcpRetransSegs|TcpExt(TCPTimeouts|TCPLossProbes)'
TcpRetransSegs        1842
TcpExtTCPTimeouts     97
TcpExtTCPLossProbes   63
$ tcpdump -ni any -s 0 -w slow.pcap 'host 203.0.113.20 and tcp port 443'

计数器要看流量归一化后的增量。抓包再区分重复 ACK/SACK 后的快速恢复、等待 RTO 后的重传、乱序,以及抓包点丢包造成的“伪重传”。

④ 识别零窗口:网络没堵,是接收应用没读

$ tshark -r slow.pcap \
  -Y 'tcp.analysis.zero_window || tcp.analysis.zero_window_probe' \
  -T fields -e frame.time_relative -e ip.src -e tcp.window_size_value
$ ss -o state established '( dst 203.0.113.20 )'
# timer:persist 表示发送端正在做零窗口探测

发送端常见 Send-Q 堆积;接收端则可能看到 Recv-Q 或 skmem r 接近 rb。根因通常回到接收应用消费慢,而非扩大拥塞窗口。

界预防与参数边界

先判瓶颈属于流量控制、拥塞控制、缓冲还是应用。CUBIC、BBR 等算法会改变 cwnd/pacing 行为,不能套用 Reno 的固定曲线;改算法前需在相同 RTT、带宽、丢包与并发条件下比较吞吐、重传、公平性和排队延迟。Linux 默认 TCP 缓冲会自动调优;应用显式设置过小的 SO_SNDBUF/SO_RCVBUF 反而可能关闭或限制自动调优。

参考来源:就是要你懂 TCP:性能问题 · TCP 窗口与 Buffer · TCP 传输速度案例

↑ 返回顶部
08TIME_WAIT、CLOSE_WAIT 与临时端口先分清“谁在等谁”,再谈连接耗尽+

现现象

ss -s 显示成千上万 TIME_WAIT 或 CLOSE_WAIT;代理向单一上游建立新连接时报 Cannot assign requested address,服务重启则可能报 Address already in use。团队把它们统称为“端口不够”。

根根因

  • TIME_WAIT:通常是本端主动关闭,短连接模型令相同方向的状态大量积累;它本身是可靠关闭与旧报文隔离机制。
  • CLOSE_WAIT:本端已经收到 FIN,正在等本地应用 close;短暂出现正常,持续增长才强烈指向生命周期泄漏或线程卡住。
  • 临时端口耗尽:常发生在“单一本地 IP → 单一目标 IP:Port”的四元组空间,或代理提前 bind(ip,0) 让端口无法跨目标复用。

理原理

一句话:TCP 连接由本地 IP/端口与远端 IP/端口组成的四元组区分;同一源端口可以连接不同远端,所以 65535 从来不是“整机总连接数上限”。

证排查证据链

① 按状态、进程与对端聚合,而不是只看总数

$ ss -s
$ ss -Htan | awk '{s[$1]++} END {for (k in s) print k,s[k]}' | sort
$ ss -Htanp state close-wait | head
$ ss -Htan | awk '$1=="TIME-WAIT"{print $5}' | sort | uniq -c | sort -nr | head
# 注意:ss 加 state 过滤后会省略 State 列,届时对端地址在 $4 而非 $5

先确认哪一侧主动关闭、集中到哪些目标、由哪个进程持有。TIME_WAIT 出现在服务器并不自动等于异常,也可能是服务器主动关闭大量短连接。

② 核对可分配端口与保留区间

$ sysctl net.ipv4.ip_local_port_range net.ipv4.ip_local_reserved_ports
net.ipv4.ip_local_port_range = 32768 60999
$ ss -Htan state established '( dst 203.0.113.20 and dport = :443 )' | wc -l
$ ss -Htan state time-wait '( dst 203.0.113.20 and dport = :443 )' | wc -l

直接 connect() 在单目标空间耗尽时常返回 EADDRNOTAVAIL;显式 bind() 冲突更常见 EADDRINUSE,但语言框架可能改写错误文本。

③ 不要漏掉文件描述符和应用 close 路径

$ cat /proc/$PID/limits | grep 'open files'
$ find /proc/$PID/fd -maxdepth 1 -type l 2>/dev/null | wc -l
$ ss -Htanp state close-wait '( sport = :8080 )'

CLOSE_WAIT 的关键不是 TCP 参数,而是找到没有走到 close 的代码、阻塞线程或失效的连接池回收逻辑。

④ 检查代理是否过早占用源端口

Linux 4.2+ 提供 IP_BIND_ADDRESS_NO_PORT:先绑定源地址但推迟到 connect() 时才选端口,使相同源端口在四元组不同的连接间复用。它适用于必须先选源 IP 的代理,不是所有客户端的必需选项。

界预防与参数边界

  • tcp_fin_timeout 控制孤儿 FIN_WAIT_2,不控制 TIME_WAIT。
  • tcp_max_tw_buckets 是 DoS 防护上限,调低会直接销毁 TIME_WAIT socket,不是常规优化。
  • tcp_tw_reuse 在现代内核是整数 0/1/2,旧内核默认值与语义可能不同;不要给跨版本统一“设 1”的建议。
  • SO_REUSEADDR/SO_REUSEPORT 主要影响 bind/listen 语义,不是客户端临时端口耗尽的通用修复。
预防优先使用连接池/长连接、平滑关闭、多个源/目标地址与合理端口范围,并监控每个目标的状态分布。

参考来源:就是要你懂 TCP:握手和挥手 · 一台服务器最多能创建多少 TCP 连接

↑ 返回顶部
09tcp_tw_recycle + NAT + PAWS“参数越激进越好”的历史反面教材+

现现象

旧 Linux 服务端在 NAT/LVS 后出现概率性 SYN 无响应:同一公网出口下有些客户端能连、有些不能,停流量约一分钟后又恢复;端口监听、路由和防火墙规则均正常。

根根因

旧内核启用 net.ipv4.tcp_tw_recycle=1 后,为加速 TIME_WAIT 回收而使用按远端地址缓存的 timestamp/PAWS 假设。NAT 后多个真实客户端共享一个公网源 IP,但它们的 TCP TSval 不保证跨主机单调,合法 SYN 因而被当成旧报文拒绝。

理原理

一句话:PAWS 用 TCP timestamp 防止序号回绕后的旧段混入新数据;把“每连接有效”的时间戳假设激进扩大到共享源 IP,就破坏了 NAT 场景。

证排查证据链

① 先确认它确实是老内核和旧参数

$ uname -r
3.10.0-1160.el7.x86_64
$ sysctl net.ipv4.tcp_tw_recycle net.ipv4.tcp_timestamps
net.ipv4.tcp_tw_recycle = 1
net.ipv4.tcp_timestamps = 1
# Linux 4.12+ 应返回参数不存在;不要凭空“创建”它

② 看 PAWS 被动拒绝计数的故障窗口增量

$ nstat -az | grep -i paws
TcpExtPAWSPassive      960535
$ netstat -s | grep -i 'passive connections rejected because of time stamp'
960535 passive connections rejected because of time stamp

计数器名称和展示文本随内核/iproute2 版本变化,应以本机 nstat -az 实际输出为准。历史累计值很大没有意义,必须与失败请求同步增长。

③ 把 NAT 后的 SYN timestamp 与重试时间线对齐

$ tcpdump -ni any -s 0 -vvv -w paws.pcap \
  'host 198.51.100.40 and tcp[tcpflags] & tcp-syn != 0'
$ tshark -r paws.pcap -Y 'tcp.flags.syn == 1' -T fields \
  -e frame.time_epoch -e ip.src -e tcp.srcport \
  -e tcp.options.timestamp.tsval -e tcp.analysis.retransmission

服务端看到相同源 IP 的不同连接携带回退的 TSval,随后该 SYN 无 SYN+ACK,并与 PAWSPassive 增量重合,才形成完整证据。仅看到 SYN 重传还可能是队列、ACL 或普通丢包。

界预防与参数边界

Linux 2.4–4.11tcp_tw_recycle 在 Linux 4.12 被删除;现代内核里它不是“默认关闭但还能打开”,而是根本不存在。老系统的正确处理是关闭该参数并计划升级。不要用关闭 tcp_timestamps、强行缩短 TIME_WAIT 或 SO_LINGER=0 来掩盖问题。tcp_tw_reuse 是另一项功能,删除 recycle 不等于删除 reuse。这起事故的通用教训是:任何减少状态保留的优化,都必须验证它依赖的唯一性假设是否会被 NAT、LB 和多路径打破。

参考来源:为什么你的 SYN 包被丢:net.ipv4.tcp_tw_recycle

↑ 返回顶部