侧边栏壁纸
  • 累计撰写 142 篇文章
  • 累计收到 2 条评论

高并发下报错 Cannot assign requested address?排查 Linux TIME_WAIT 堆积与 TCP 调优我踩过的几个坑

2026-10-1 / 0 评论 / 13 阅读
AI 摘要由 AI 生成

本文分析了Linux系统中在高并发环境下出现的“Cannot assign requested address”报错问题,指出这一问题通常是由于大量TCP连接处于TIME_WAIT状态导致本地临时端口耗尽。文章揭示了TIME_WAIT状态的处理机制,并指出错误的配置如`tcp_tw_recycle`可能导致严重事故。此外,文章还提醒了`tcp_tw_recycle`参数在最新内核版本中已被移除,应…

压测或者反向代理服务跑到高峰期,接口偶发抛出 Cannot assign requested address,很多人第一反应是后端服务崩了或者服务器物理内存见底。我之前排查过两次类似线上故障,实际查下来后端实例负载非常健康,真正被耗尽的是反向代理节点的本地临时端口。大量 TCP 连接处于 TIME_WAIT 状态无法释放,直接导致新发起的连接拿不到可用端口。

网上很多介绍 Linux TCP 调优的博客还停留在十几年前,照搬网上的内核参数经常带来新的故障。这里把几次排查过程中的几个坑点和验证过的方式整理清楚。

故障现场:压测刚跑到两千并发,网关开始间歇性报 99 错误

当时架构很简单,前端流量打到 Nginx 网关,Nginx 再反向代理到内网几台微服务后端。在用压测工具压测一个高频写接口时,QPS 刚推过 2000,客户端就开始收到 502 报错,网关日志里刷出来这样一行错误:

[error] 24151#24151: *148029 connect() to 10.0.1.20:8080 failed (99: Cannot assign requested address) while connecting to upstream

在 Linux 系统里,错误码 99 对应的正是 EADDRNOTAVAIL(Cannot assign requested address)。

登上网关服务器用 ss 检查当前的 TCP 套接字状态:

ss -s

输出里赫然显示:

Total: 34120
TCP:   32840 (estab 420, closed 31920, orphaned 12, timewait 31800)

TIME_WAIT 数量占了三万多。

很多初学者容易搞错一个基本机制:TCP 握手挥手协议里,主动发起关闭连接的一方,才会进入 TIME_WAIT 状态。

如果是客户端先发送 FIN 包断开连接,客户端就会进入 TIME_WAIT;如果是服务端先主动断开,服务端就会进入 TIME_WAIT。在反向代理场景中,Nginx 向后端 upstream 发起 HTTP 请求,如果每次请求结束后是 Nginx 主动关闭这条连接,那么 Nginx 所在的主机就是主动关闭方,大量的 TIME_WAIT 自然全部堆积在 Nginx 网关机器上。

一条 TCP 连接由四元组(源 IP、源端口、目的 IP、目的端口)唯一定位。当 Nginx 访问固定的后端 IP 和端口时,目的 IP 和目的端口是不变的,源 IP 也是网关本机的内网 IP,唯一能够动态变化的只有源端口(即客户端临时端口)。

当短时间内新建和关闭的连接数超过了可用端口的总量,在 TIME_WAIT 还没来得及倒计时结束释放之前,内核的可用端口池被彻底抽干,新连接无法分配到可用源端口,系统就会抛出 Cannot assign requested address。

坑一:病急乱投医开启 tcp_tw_recycle,外部用户大面积连不上

发现 TIME_WAIT 太多,很多运维新人第一反应是去搜索引擎搜「Linux TIME_WAIT 解决办法」,搜出来的很多旧文章会建议在 /etc/sysctl.conf 里加上两行配置:

net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_tw_recycle = 1

如果在生产机器上顺手开启了 tcp_tw_recycle = 1,往往会引发更严重的事故。

tcp_tw_recycle 的机制要求 TCP 连接必须开启时间戳(tcp_timestamps = 1),内核会记录每个 IP 地址最后一次收到数据包的时间戳。在快速回收连接时,如果收到一个 SYN 握手包的时间戳小于内核记录的时间戳,内核会直接认为这是网络延迟送达的旧连接包,进而静默丢弃,连 RST 都不会回。

在真实生产环境中,海量移动端或者办公网络用户往往通过 NAT 网关(SNAT)共用一个公网 IP 出口。不同用户的设备时钟不可能做到纳秒级同步,当用户 A 发送请求后,紧接着用户 B 从同一个公网 IP 发起连接,只要用户 B 设备的时间戳比用户 A 慢了一点点,用户 B 的所有握手包就会被 Linux 内核全部静默丢掉,造成外网用户大面积无法访问网站。

因为这个参数在现代互联网 NAT 环境下破坏性极大,Linux 内核从 4.12 版本开始已经彻底移除了 net.ipv4.tcp_tw_recycle 参数。如果你在 Linux 5.x 或者 6.x 内核执行 sysctl -w net.ipv4.tcp_tw_recycle=1,内核会直接返回 sysctl: cannot stat /proc/sys/net/ipv4/tcp_tw_recycle: No such file or directory。

遇到老教程推荐开启 tcp_tw_recycle,直接跳过即可。

坑二:只开了 tcp_tw_reuse,却忘了调本地端口范围

既然 tcp_tw_recycle 不能开,那么只开 tcp_tw_reuse 行不行?

net.ipv4.tcp_tw_reuse = 1 的作用是:允许内核将处于 TIME_WAIT 状态且持续时间超过 1 秒的 socket 重新分配给新的出站连接。

这里需要注意两个细节:

第一,tcp_tw_reuse 仅对客户端主动向外发起的连接有效,对被动接收外部连接的服务端端口没有任何作用。由于反向代理服务连接 upstream 正好扮演客户端角色,所以在这个场景下是吻合的。

第二,tcp_tw_reuse 生效的前提是必须同时开启 TCP 时间戳支持:

sysctl -w net.ipv4.tcp_timestamps=1
sysctl -w net.ipv4.tcp_tw_reuse=1

在较新的 Linux 内核中,tcp_tw_reuse 还可以设置为 2(loopback traffic only,仅对本地回环流量生效),如果网关和后端在不同机器上,要配置为 1。

但光开这一个参数往往还不够。许多 Linux 发行版默认的临时端口范围相对保守,可以通过下面命令查看:

sysctl net.ipv4.ip_local_port_range

不少默认配置输出是:

net.ipv4.ip_local_port_range = 32768 60999

这意味可分配的端口只有 60999 - 32768 = 28231 个。假设每秒并发量很大,并且每条短连接都需要等待释放,这不到三万个端口很快就会被占满。

应当把端口范围放宽到合法范围的上限:

sysctl -w net.ipv4.ip_local_port_range="10240 65535"

这样可用端口数量立刻扩展到 55000 多个,几乎翻了一倍。

同时配合调整连接在 FIN_WAIT_2 和 TIME_WAIT 相关的回收等待时间:

sysctl -w net.ipv4.tcp_fin_timeout=15

默认的 60 秒对于高并发代理服务来说太长了,缩短到 15 秒可以显著加快断开套接字的释放速度。

坑三:治本之策在 Nginx,反向代理默认用的居然是短连接

修改了上面几条内核参数后,压测期间的 Cannot assign requested address 报错基本消失了,但是观察 ss -s,机器上的 TIME_WAIT 依然在两万上下徘徊。

为什么每次请求都会产生一个 TIME_WAIT?

抓包分析 Nginx 与后端服务的通信流程,发现罪魁祸首在 Nginx 的默认代理协议上:Nginx 在转发请求给 upstream 后端时,默认使用的是 HTTP/1.0 协议,而且头部带有 Connection: close。

这意味着客户端发来的哪怕是 HTTP/1.1 长连接,Nginx 转给后端时也是处理一个请求就断开一次 TCP 连接。一秒钟两千次请求,就等于一秒钟在内网建立两千次 TCP 握手和挥手,系统不堆积 TIME_WAIT 才是怪事。

解决办法是在 Nginx 配置中为 upstream 开启长连接连接池(keepalive)。

修改 Nginx 配置文件:

upstream backend_cluster {
    server 10.0.1.20:8080 max_fails=3 fail_timeout=10s;
    server 10.0.1.21:8080 max_fails=3 fail_timeout=10s;

    # 关键配置:保持每个 worker 与 upstream 之间的空闲长连接数量
    keepalive 256;
}

server {
    listen 80;
    server_name api.example.com;

    location / {
        proxy_pass http://backend_cluster;

        # 关键配置:指定代理协议为 HTTP 1.1 并清理 Connection 头
        proxy_http_version 1.1;
        proxy_set_header Connection "";

        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

配置里的两个细节缺一不可:

  1. proxy_http_version 1.1;:默认是 1.0,必须显式指定为 1.1 才能支持长连接复用。
  2. proxy_set_header Connection "";:清理掉可能由前端客户端传过来的 close 头部,防止后端微服务在响应完单次请求后主动关闭连接。

重新加载 Nginx 配置后再次跑同样的压力测试:

nginx -s reload

压测持续运行 10 分钟,再看 ss -s 的数据:

Total: 1240
TCP:   850 (estab 320, closed 20, orphaned 0, timewait 18)

TIME_WAIT 数量直接从原来的三万多骤降到两位数。因为绝大部分 HTTP 请求都在既有的 TCP 连接池里复用,根本不需要频繁触发三次握手和四次挥手,不仅彻底根治了端口耗尽问题,平均接口延迟也下降了几个毫秒。

坑四:误以为 TIME_WAIT 会吃光系统内存

排查初期,团队里有同学担心三万多个 TIME_WAIT 会不会把服务器内存撑爆。

实际上,在 Linux 2.6 及之后的现代内核中,处于 TIME_WAIT 状态的套接字已经被大幅度轻量化。内核使用的是专门的 struct tcp_timewait_sock,不再维护完整的全状态套接字缓冲区。

一个处于 TIME_WAIT 的套接字在 slab 内存分配器中只占用大概 168 字节左右的内存空间。三万个 TIME_WAIT 连接,消耗的内核内存总量也就是:

$$ 30000 \times 168 \text{ bytes} \approx 5 \text{ MB} $$

加上对应的少量散列链表结构,整体占用不过十几兆内存。

可以通过 /proc/net/sockstat 查看套接字的真实占用:

cat /proc/net/sockstat

输出示例:

sockets: used 1120
TCP: inuse 410 orphan 2 tw 28400 alloc 450 mem 32

其中 mem 32 单位是内存页(Page,通常为 4KB),由此可知内存开销极低。

真正受限制并造成线上灾难的,从来不是内存容量,而是协议栈的端口四元组容量以及在套接字哈希表中查找的 CPU 周期损耗。

关键参数对照与优化清单

把本次涉及的核心网络内核参数整理成速查表:

参数名称 默认常见值 推荐调优值 适用场景与说明 避坑提醒
net.ipv4.tcp_tw_reuse 0 1 允许复用 1 秒以上的 TIME_WAIT 端口用于向外发起的连接 仅对出站客户端有效,必须同时开启时间戳
net.ipv4.tcp_timestamps 1 1 开启 TCP 时间戳,为序列号防重叠与快速复用提供依据 关掉会导致 tcp_tw_reuse 失效
net.ipv4.ip_local_port_range 32768 60999 10240 65535 扩大本地临时端口分配区间 为高并发代理提供五万多个可用源端口
net.ipv4.tcp_fin_timeout 60 15 缩短孤儿套接字保持时间 避免短连接断开后占用端口过久
net.ipv4.tcp_max_tw_buckets 262144 131072 ~ 262144 系统同时保持 TIME_WAIT 套接字的最大上限 超过阈值会被内核暴力销毁并打印警告,勿盲目设太小
net.ipv4.tcp_tw_recycle 已废弃 禁止开启 旧版内核用于快速回收 Linux 4.12 已彻底移除,NAT 网络下会造成严重丢包

生产持久化配置,直接写入 /etc/sysctl.d/99-tcp-tuning.conf:

net.ipv4.tcp_timestamps = 1
net.ipv4.tcp_tw_reuse = 1
net.ipv4.ip_local_port_range = 10240 65535
net.ipv4.tcp_fin_timeout = 15
net.ipv4.tcp_max_tw_buckets = 131072

执行命令使其即时生效:

sysctl --system

遇到高并发场景下的 Cannot assign requested address,排查顺序应当是:先在应用层排查反向代理与后端通信是否为短连接,开启长连接池;随后检查内核的 ip_local_port_range 和 tcp_tw_reuse,确保端口资源池足够宽裕。千万不要随便根据陈旧文档去碰 tcp_tw_recycle。

评论一下?

OωO
取消