upstream 没开 keepalive:一台 Nginx 每秒新建 3000 条连接
压测到 12 分钟开始刷 Cannot assign requested address,TIME_WAIT 六万二。proxy_pass 默认是短连接,补上 keepalive 后还要对齐上下游空闲超时。
大促压测到 12 分钟,入口 Nginx 的 error.log 开始刷同一行:
connect() failed (99: Cannot assign requested address) while connecting to upstream
前面是 502,后面是 504,两条曲线一起翘。CPU 只有 18%,内存余量充足,后端服务自己的监控看着也正常。半分钟后压测停止,一切又恢复。
这种「只在极高并发下出现、负载一撤就消失」的报错,八成不是资源不够,而是连接的用法有问题。我登录机器看的第一条命令是 ss -s,TIME_WAIT 六万二。
一、Cannot assign requested address 到底是什么
这个错误码是 EADDRNOTAVAIL 的字符串形式,字面意思容易让人以为是网卡地址配错了。实际上在 Nginx 反代的场景里,它几乎总是同一件事:本机作为客户端去连上游,需要从本地临时端口范围里挑一个空闲端口做源端口,挑不出来了。
# 本地端口范围与已用情况
sysctl net.ipv4.ip_local_port_range
ss -s
ss -tan state time-wait | wc -l
ss -tan state time-wait | awk '{print $4}' | cut -d: -f2 | sort -u | wc -l
输出 32768 60999,也就是 28231 个端口可用。而 TIME_WAIT 有 6.2 万个,分布在几十个上游地址上,平均每个上游地址 1000 多个端口处于 TIME_WAIT,个别热点上游已经用掉了 8000 多个源端口。
TIME_WAIT 是主动关闭方必然经历的阶段,默认 net.ipv4.tcp_fin_timeout 是 60 秒。这意味着每秒新建 3000 条短连接,就必然有 18 万个 TIME_WAIT 在 60 秒内轮转——端口池根本不够。
二、为什么 Nginx 到上游是短连接
这是最容易被忽略的一点:proxy_pass 默认走 HTTP/1.0,而且不带 Connection: keep-alive。也就是 Nginx 每转发一个请求,都要和上游重新建一次 TCP 连接,用完就关。
location /api/ {
proxy_pass http://backend;
# 下面这两行没写,默认就是短连接
# proxy_http_version 1.1;
# proxy_set_header Connection "";
}
短连接的成本不只是三次握手。每一次关闭,主动关闭方(Nginx)都会进入 TIME_WAIT 并占用一个源端口 60 秒;每一次转发,都要多一次 SYN 往返,在上游 RTT 1.5ms 的内网里,这一来一回就是 3ms 的纯开销。
压测时 QPS 3000、后端 5 个实例,每秒新建连接 3000 条,60 秒就是 18 万条。端口不够,报错就来了。
三、改配置:三步把连接复用起来
第一步,在 upstream 块里声明保活连接池:
upstream backend {
least_conn;
server 10.0.3.11:8080 max_fails=3 fail_timeout=10s;
server 10.0.3.12:8080 max_fails=3 fail_timeout=10s;
server 10.0.3.13:8080 max_fails=3 fail_timeout=10s;
keepalive 128; # 每个 worker 进程最多保持 128 条空闲连接
keepalive_requests 1000; # 单条连接最多复用 1000 次后关闭
keepalive_timeout 65s; # 空闲超过 65 秒则关闭
}
第二步,location 里必须显式把协议版本提到 1.1,并清空 Connection 头。proxy_set_header Connection "" 这一行是最容易漏的——不清空的话,客户端发来的 Connection: close 会被原样转给上游,保活池形同虚设。
location /api/ {
proxy_pass http://backend;
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;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 2s;
proxy_send_timeout 10s;
proxy_read_timeout 30s;
}
第三步,keepalive 的值不是越大越好。它是每个 worker 进程的池子大小,总量等于 worker_processes × keepalive。机器 8 核、worker_processes 8,keepalive 128 意味着最多 1024 条空闲连接常驻,内存代价可以忽略,但每条连接在上游侧也占一个槽位,要确认上游的 maxConnections 扛得住。
四、改完之后的第二个坑:idle 超时不对齐
配好保活,502 少了,但没消失。抓包看,失败的请求都是「连接刚被判断为可用、实际已被上游关掉」。
原因很典型:上游 Tomcat 的 connectionTimeout 是 20 秒,Nginx 这边 keepalive_timeout 设了 65 秒。空闲 25 秒后,Tomcat 主动关掉连接,但 Nginx 池子里这条连接还是「可用」状态,下一个请求复用它,写进去的数据对端已经没了,于是 502。
规则很明确:下游的空闲超时必须短于上游的空闲超时。Nginx 的 keepalive_timeout 要比后端短至少 10 秒。
| 组件 | 参数 | 原值 | 调整后 |
|---|---|---|---|
| Nginx upstream | keepalive_timeout |
65s | 15s |
| Tomcat | connectionTimeout |
20s | 30s |
| 上游 Go 服务 | IdleTimeout |
90s | 90s |
| 内核 | net.ipv4.tcp_keepalive_time |
7200s | 600s |
调整后 502 归零。另外需要注意,proxy_next_upstream 里如果包含 error,重试会把一个已经复用到坏连接的请求再打一次上游,这在非幂等接口上会带来重复提交,我把它收窄成只对 GET/HEAD 生效的 proxy_next_upstream error timeout http_502,并配合应用层的幂等键兜底。
五、还治不了的部分:突发新建
即使全部改成保活,瞬间并发仍然会打满池子。压测从 0 拉到 3000 QPS 的头两秒,池子里没有空闲连接,还是会新建一批,TIME_WAIT 会在那两秒堆起来。
这部分只能靠容量和内核参数兜:
# 扩大本地端口范围(临时生效)
sysctl -w net.ipv4.ip_local_port_range="10240 60999"
# 复用 TIME_WAIT 状态的连接用于新建(谨慎,仅内网可信环境)
sysctl -w net.ipv4.tcp_tw_reuse=1
# 缩短 FIN_WAIT2,加速回收
sysctl -w net.ipv4.tcp_fin_timeout=15
# 提高 somaxconn 与本地端口并发
sysctl -w net.core.somaxconn=32768
tcp_tw_reuse 只对主动发起连接的一方有效,且要求时间戳开启,正好适配 Nginx 作为客户端连上游的场景。它不解决端口耗尽,只是把可复用窗口提前。真正的解法还是保活 + 增加 Nginx 实例数——把 3000 QPS 分摊到 3 台,单台的端口压力就降到了安全范围。
六、怎么验证改对了
改完不能只看「没报错」,要有可量化的对比:
# 1. 保活是否真的生效:ESTABLISHED 数量应该稳定在池子大小附近
ss -tan state established '( dport = :8080 )' | wc -l
# 2. TIME_WAIT 是否下降
ss -tan state time-wait | wc -l
# 3. Nginx 自己的连接统计
curl -s http://127.0.0.1:8080/nginx_status
| 指标 | 改造前 | 改造后 |
|---|---|---|
TIME_WAIT 峰值 |
62 000 | 3 400 |
| 到上游 ESTABLISHED | 波动在 40–900 | 稳定在 980 |
| 上游握手占比 | 100% 请求新建 | 约 0.3% 新建 |
| 接口 P99 | 260ms | 178ms |
| 502 数量 | 压测期 1 800+ | 0 |
P99 降了 80ms 不是玄学:省掉的是每次转发一次 TCP 握手和一次慢启动。
小结
Cannot assign requested address在本机作客户端时几乎等于「本地端口用光了」,先看ss -s的TIME_WAIT。proxy_pass默认是 HTTP/1.0 短连接,要复用必须同时写proxy_http_version 1.1和proxy_set_header Connection ""。keepalive是每个 worker 的池子,总量要乘worker_processes,同时确认上游的最大连接数扛得住。- Nginx 的空闲超时必须短于上游的空闲超时,否则会复用一条已被对端关闭的连接,表现为偶发 502。
tcp_tw_reuse只能缓解,端口耗尽的根因是短连接,改保活才是治本。- 验证要看
TIME_WAIT、ESTABLISHED与握手占比三个数,不能只看错误日志是否干净。
正在加载评论…