2024 年的一个高峰期,一个调用量很大的服务突然开始报错。我登上机器看应用日志,满屏都是同一句异常:Cannot assign requested address。我第一反应是某个地址配错了——可这服务跑了大半年,配置一个字没动过。更怪的是,它不是"连不上对端",而是"连接根本发不出去",像是本机这一端,凭空就没法再建立新连接了。我盯着这句报错想了很久:一台机器,怎么会"没有地址可以用了"?这件事逼着我把 Linux 的 TCP 连接、连接状态机、TIME_WAIT 与端口这一整套彻底理清了。本文复盘这次实战。
问题背景
环境:CentOS 7,一个高频调用下游 HTTP 接口的服务
事故现象:
- 高峰期,服务报 Cannot assign requested address
- 不是连不上对端,是本机【发不出新连接】
- 配置半年没动过
现场排查:
# 1. 统计本机 TCP 连接都处在什么状态
$ ss -ant | awk '{print $1}' | sort | uniq -c | sort -rn
28500 TIME-WAIT # ★ 两万八千个 TIME_WAIT!
1200 ESTAB
60 LISTEN
# 2. 看本机可用的临时端口范围
$ cat /proc/sys/net/ipv4/ip_local_port_range
32768 60999
# —— 可用临时端口一共才 28000 多个
# 3. ★ 端口被 TIME_WAIT 占满了
# 28500 个 TIME_WAIT > 28000 个可用端口
# —— 新连接想找一个空闲端口,找不到了
# 4. 看这些 TIME_WAIT 都连向谁
$ ss -ant state time-wait | awk '{print $5}' | cut -d: -f1 \
| sort | uniq -c | sort -rn | head
28000 10.0.1.50 # ★ 几乎全连向同一个下游
根因(后来想清楚的):
1. 服务每次调下游接口,都【新建一个 TCP 连接】,
用完【主动关闭】它 —— 短连接模式。
2. ★ 主动关闭连接的那一方,关闭后这个连接【不会立即消失】,
它会进入 TIME_WAIT 状态,并停留 2*MSL(Linux 默认约 60 秒)。
3. 高峰期每秒新建几百个短连接,每个关闭后都占着一个
本地端口、停留 60 秒 —— TIME_WAIT 越积越多。
4. 本机可用临时端口只有 ~28000 个,TIME_WAIT 堆到
两万八,把端口池占满了。
5. 新连接要从端口池里取一个空闲端口当源端口,
取不到 —— 内核报 Cannot assign requested address。
不是没地址,是没【端口】了 —— 短连接把端口耗尽了。
修复 1:看连接的工具——ss 与 netstat
# === ss:现在看连接的首选(比 netstat 快得多)===
$ ss -ant
# -a 所有连接 -n 不做域名解析(快) -t 只看 TCP
State Recv-Q Send-Q Local Address:Port Peer Address:Port
ESTAB 0 0 10.0.1.10:43512 10.0.1.50:80
TIME-WAIT 0 0 10.0.1.10:43208 10.0.1.50:80
# === ★ 最常用的一招:统计连接状态分布 ===
$ ss -ant | awk 'NR>1{print $1}' | sort | uniq -c | sort -rn
# 一眼看出:是 TIME-WAIT 多?CLOSE-WAIT 多?还是 ESTAB 多?
# 这是排查连接问题的【第一个动作】。
# === 按状态过滤(ss 的语法很强)===
$ ss -ant state time-wait # 只看 TIME_WAIT
$ ss -ant state close-wait # 只看 CLOSE_WAIT
$ ss -ant state established # 只看已建立的
# === 看某个端口 / 某个对端的连接 ===
$ ss -ant '( dport = :80 )' # 目标端口是 80 的
$ ss -antp dst 10.0.1.50 # 连向 10.0.1.50 的,-p 带进程
# === 看监听端口、以及是哪个进程在听 ===
$ ss -lntp # -l 只看 LISTEN
LISTEN 0 128 0.0.0.0:8080 ... users:(("java",pid=6500,fd=12))
# === netstat:老工具,语法类似,大量机器上还在用 ===
$ netstat -ant | awk '{print $6}' | sort | uniq -c | sort -rn
$ netstat -tnlp # 看监听端口和进程
# ★ netstat 在连接数巨大时会很慢,优先用 ss。
# === 看连接的汇总统计 ===
$ ss -s # 各状态连接数的总览
$ cat /proc/net/sockstat # 内核视角的 socket 统计
修复 2:TCP 连接状态机——每个状态的含义
# === 一条 TCP 连接,一生会经历这些状态 ===
# --- 建立连接(三次握手)---
# LISTEN 服务端在监听端口,等连接进来
# SYN-SENT 客户端发出了 SYN,等对方回应
# SYN-RECV 服务端收到 SYN,回了 SYN+ACK,等最后的 ACK
# ESTABLISHED ★ 连接建立成功,正常收发数据的状态
# --- 关闭连接(四次挥手)---
# 关闭分"主动关闭方"和"被动关闭方",状态完全不同:
#
# 主动关闭的一方(先发 FIN 的):
# FIN-WAIT-1 -> FIN-WAIT-2 -> TIME-WAIT -> (消失)
#
# 被动关闭的一方(后发 FIN 的):
# CLOSE-WAIT -> LAST-ACK -> (消失)
# === ★ 排查中最需要盯的两个状态 ===
# TIME-WAIT ★ 出现在【主动关闭方】。连接已经关了,
# 但还要在这个状态【停留 2*MSL】才彻底消失。
# 为什么要停留?——见修复 3。
# TIME_WAIT 多 = 你这台机器主动关了很多连接。
#
# CLOSE-WAIT ★ 出现在【被动关闭方】。对方已经发来 FIN
# 说"我要关了",你的程序却【迟迟没有调 close()】。
# CLOSE_WAIT 多 = 你的程序有 bug,没关连接(见修复 4)。
# === 一句话区分这两个"堆积" ===
# TIME_WAIT 堆积 -> 通常是【设计问题】:用了大量短连接
# CLOSE_WAIT 堆积 -> 通常是【代码 bug】:忘了 close()
# === 看本机连接的状态分布(再强调一次,第一动作)===
$ ss -ant | awk 'NR>1{print $1}' | sort | uniq -c | sort -rn
修复 3:TIME_WAIT 堆积——这次的根因
# === ★ 先理解:TIME_WAIT 为什么必须存在 ===
# 主动关闭连接的一方,在四次挥手完成后,并不能立刻
# 把连接抹掉,它要在 TIME_WAIT 停留 2*MSL(MSL=报文最大生存时间)。
# 这是 TCP 协议【有意的设计】,为了两件事:
# 1. 保证最后那个 ACK 能可靠到达对方 —— 万一丢了,
# 对方会重发 FIN,你还得能用这个连接回 ACK。
# 2. 让属于这条连接的、还在网络里游荡的旧数据包,
# 在新连接复用同一对地址端口前,彻底自然消亡。
# ★ 所以 TIME_WAIT 本身【不是 bug】,它是 TCP 的正常机制。
# === 那它什么时候变成问题 ===
# 问题不在"有 TIME_WAIT",而在"TIME_WAIT 太多"。
# 而 TIME_WAIT 多,根子在【主动关闭了太多连接】——
# 也就是:大量使用【短连接】(连一次、用一次、关一次)。
# === 看本机 TIME_WAIT 数量,和端口范围对比 ===
$ ss -ant state time-wait | wc -l
$ cat /proc/sys/net/ipv4/ip_local_port_range
# TIME_WAIT 数量逼近"可用端口数" -> 端口要耗尽了。
# === ★ 治本:把短连接改成长连接(连接池)===
# 这是【最根本】的解法 —— 与其每次调用都新建+关闭连接,
# 不如维护一个连接池,连接建好后【复用】,不频繁关。
# 连接不频繁关闭,自然就没有大量 TIME_WAIT。
# (HTTP 客户端开 keep-alive、数据库用连接池,都是这个思路)
# === 治标:内核参数,缓解 TIME_WAIT 压力 ===
# 治本要改代码,改不了时,内核侧可以缓解:
$ sysctl net.ipv4.tcp_tw_reuse=1
# tcp_tw_reuse:允许把处于 TIME_WAIT 的连接,
# 【安全地复用】给新的【对外】连接。对本次这种
# 大量对外短连接的场景,很有效。
$ sysctl net.ipv4.ip_local_port_range="10000 65000"
# 把可用临时端口范围调大 —— 端口池大了,更扛得住。
# ★ 警告:tcp_tw_recycle 这个参数【不要开】。
# 它在 NAT 环境下会导致连接被莫名拒绝,
# 新内核已经把它删除了。别用它。
# === 永久生效:写进 /etc/sysctl.conf ===
$ vim /etc/sysctl.conf
net.ipv4.tcp_tw_reuse = 1
net.ipv4.ip_local_port_range = 10000 65000
$ sysctl -p # 加载生效
修复 4:CLOSE_WAIT 堆积——另一种连接泄漏
# === ★ CLOSE_WAIT 和 TIME_WAIT 是完全不同的问题 ===
$ ss -ant state close-wait | wc -l
# 如果这个数字很大、而且【只增不减】—— 是代码 bug。
# === CLOSE_WAIT 是怎么产生的 ===
# 1. 对端决定关闭连接,向你发来一个 FIN。
# 2. 你的内核收到 FIN,自动回了 ACK,
# 这条连接在你这一侧,就进入了 CLOSE_WAIT 状态。
# 3. CLOSE_WAIT 的含义是:"内核已经知道对方要关了,
# 现在【等你的应用程序调用 close()】来完成关闭。"
# 4. ★ 如果你的程序【永远不调 close()】——
# 这条连接就永远卡在 CLOSE_WAIT,直到进程退出。
# === 所以 CLOSE_WAIT 堆积 = 应用程序忘了关连接 ===
# 典型 bug:
# - 异常路径里没关连接(try 里关,catch 里忘了关)
# - 连接对象没用 try-with-resources / defer 之类兜底
# - 连接池配置错误,坏连接没被回收
# === 定位:是哪个进程在泄漏连接 ===
$ ss -antp state close-wait | awk '{print $6}' | sort | uniq -c | sort -rn
# -p 显示进程 —— 锁定是哪个程序的锅。
# === 看这个进程开了多少 socket(对照它的 fd 上限)===
$ ls /proc/6500/fd | wc -l # 它当前打开的 fd 总数
$ cat /proc/6500/limits | grep 'open files' # 它的 fd 上限
# ★ CLOSE_WAIT 连接也占着 fd,泄漏到一定程度,
# 进程会报 "Too many open files",彻底崩。
# === CLOSE_WAIT 的处理 ===
# ★ 它【没有内核参数能救】—— 内核已经把球踢给应用了,
# 只有应用调 close() 才能推进。
# 治标:重启泄漏连接的那个进程(CLOSE_WAIT 随进程退出清零)。
# 治本:【改代码】,确保所有路径(尤其异常路径)都关闭连接。
修复 5:端口耗尽与文件描述符
# === ★ 这次报错 Cannot assign requested address 的本质 ===
# 本机【主动】发起一条连接时,内核要给它分配一个
# 【源端口】(从 ip_local_port_range 这个范围里取)。
# 当这个范围里的端口,被 ESTABLISHED + TIME_WAIT
# 占得一个不剩时 —— 内核分配不出源端口,
# 就报 Cannot assign requested address。
# === 排查端口够不够用 ===
$ cat /proc/sys/net/ipv4/ip_local_port_range # 可用范围
$ ss -ant | wc -l # 当前连接总数
$ ss -ant state time-wait | wc -l # 其中 TIME_WAIT 多少
# 把范围调大,是立竿见影的缓解手段:
$ sysctl net.ipv4.ip_local_port_range="10000 65000"
# === 另一个常见瓶颈:文件描述符耗尽 ===
# 每条 TCP 连接,在进程里都对应【一个文件描述符】。
# 连接数一多,就会撞上进程的 fd 上限,报:
# Too many open files
$ ulimit -n # 当前 shell/进程的 fd 软上限
$ cat /proc/6500/limits | grep 'open files' # 某进程的实际上限
# === 调高 fd 上限 ===
# 临时(当前会话):
$ ulimit -n 65535
# 永久(写配置):
$ vim /etc/security/limits.conf
* soft nofile 65535
* hard nofile 65535
# ★ systemd 管理的服务,limits.conf 不一定生效,
# 要在 service 文件里单独配:
$ vim /etc/systemd/system/myapp.service
[Service]
LimitNOFILE=65535
$ systemctl daemon-reload && systemctl restart myapp
# === 系统级的连接 / socket 上限 ===
$ sysctl net.ipv4.tcp_max_tw_buckets # TIME_WAIT 桶的上限
$ cat /proc/sys/fs/file-max # 全系统 fd 上限
$ cat /proc/sys/net/core/somaxconn # listen 队列上限
修复 6:网络连接排查纪律
# === 这次事故暴露的网络认知盲区,定几条纪律 ===
# === 1. ★ 连接出问题,第一动作:看状态分布 ===
$ ss -ant | awk 'NR>1{print $1}' | sort | uniq -c | sort -rn
# 是 TIME_WAIT 多、CLOSE_WAIT 多、还是 ESTAB 多 ——
# 三种情况,病因和解法完全不同。
# === 2. 分清 TIME_WAIT 和 CLOSE_WAIT ===
# TIME_WAIT 多 = 你主动关了大量连接,通常是短连接设计问题
# CLOSE_WAIT 多 = 你的程序忘了 close(),是代码 bug
# ★ 别把这两个搞混 —— 一个调内核参数,一个必须改代码。
# === 3. Cannot assign requested address = 端口耗尽 ===
# 先 ss 数连接、看 TIME_WAIT,再看 ip_local_port_range。
# Too many open files = fd 耗尽,看 ulimit -n。
# === 4. TIME_WAIT 治本是连接池,治标才是内核参数 ===
# 改成长连接复用,从源头消灭大量 TIME_WAIT;
# 改不动代码时,才用 tcp_tw_reuse + 调大端口范围缓解。
# === 5. ★ 永远不要开 tcp_tw_recycle ===
# 它在 NAT 环境会丢连接,新内核已删除。tcp_tw_reuse 才是对的。
# === 6. CLOSE_WAIT 没有内核参数能救 ===
# 它只能靠应用调 close()。治标重启进程,治本改代码,
# 确保异常路径也关连接。
# === 7. 排查连接问题的命令链 ===
$ ss -ant | awk 'NR>1{print $1}' | sort | uniq -c # ① 状态分布
$ ss -ant state time-wait | awk '{print $5}' | sort | uniq -c # ② 连向谁
$ cat /proc/sys/net/ipv4/ip_local_port_range # ③ 端口够不够
$ ss -antp state close-wait # ④ 谁在泄漏连接
$ ulimit -n # ⑤ fd 上限够不够
# 按这个顺序,连接问题基本能定位。
命令速查
需求 命令
=============================================================
看连接状态分布 ss -ant | awk '{print $1}' | sort | uniq -c
只看某状态的连接 ss -ant state time-wait
看监听端口和进程 ss -lntp
看连接汇总统计 ss -s
看可用临时端口范围 cat /proc/sys/net/ipv4/ip_local_port_range
允许复用 TIME_WAIT sysctl net.ipv4.tcp_tw_reuse=1
调大临时端口范围 sysctl net.ipv4.ip_local_port_range="10000 65000"
看进程 fd 上限 cat /proc/PID/limits | grep 'open files'
调高 fd 上限 ulimit -n 65535
永久加载内核参数 sysctl -p
口诀:连接报错先 ss 看状态分布 -> TIME_WAIT 多查端口
CLOSE_WAIT 多是代码忘 close -> 治本永远是连接池
避坑清单
- 连接出问题第一动作是 ss -ant 统计状态分布,不同状态病因完全不同
- TIME_WAIT 出现在主动关闭方,是 TCP 正常机制,太多才是问题
- CLOSE_WAIT 出现在被动关闭方,堆积说明程序忘了调 close,是代码 bug
- Cannot assign requested address 是源端口耗尽,不是地址配错
- TIME_WAIT 治本是改长连接连接池,内核参数只是治标缓解
- tcp_tw_reuse 可以安全复用 TIME_WAIT 给对外新连接,该开
- tcp_tw_recycle 在 NAT 环境会丢连接,绝对不要开,新内核已删除
- CLOSE_WAIT 没有内核参数能救,只能靠应用调 close 或重启进程
- 每条连接占一个 fd,连接多会撞 Too many open files,要调 ulimit -n
- systemd 管理的服务,fd 上限要在 service 文件配 LimitNOFILE,limits.conf 不一定生效
总结
这次"本机无法建立新连接"的事故,纠正了我对那句报错 Cannot assign requested address 一个望文生义的误读。看到 "assign address" 和 "address" 这几个字,我下意识就认定是某个 IP 地址配错了,于是把精力全花在核对配置文件上——可这个服务的配置半年没动过,根本无错可改。复盘到根上我才明白,这句报错里的 "address",并不单指 IP 地址,它指的是"本机这一端连接所需要的源端口";内核报这句话,真正的意思是"我没有空闲的源端口可以分配给这条新连接了"。这背后,是一整套我过去从没认真想过的 TCP 机制。当本机要主动向外发起一条 TCP 连接时,内核必须从一个固定的范围——也就是 ip_local_port_range 划定的那两万八千多个临时端口里——取出一个,作为这条连接的源端口。这个端口池,是有限的、会被耗尽的。而耗尽它的,正是 TIME_WAIT。我过去对 TIME_WAIT 的认识相当模糊,只觉得它是个碍眼的东西。这次我才真正搞懂:当本机【主动】关闭一条 TCP 连接时,四次挥手走完,这条连接并不会立刻消失,它会进入 TIME_WAIT 状态,并在那里【停留大约 60 秒】才彻底释放。这不是 bug,而是 TCP 协议有意的设计——它要确保最后那个 ACK 能可靠送达对方,也要让网络里可能还在游荡的旧数据包自然消亡,免得污染之后复用同一对地址端口的新连接。问题从来不在于"有 TIME_WAIT",而在于"TIME_WAIT 太多";而 TIME_WAIT 太多的根子,是我那个服务采用了【短连接】的调用方式——每调用一次下游接口,就新建一条 TCP 连接,用完立刻主动关闭。在高峰期,它每秒主动关闭几百条连接,每一条关闭后都要占着一个本地端口、在 TIME_WAIT 里待满 60 秒。算一笔账就明白了:两万八千多个临时端口,被两万八千个 TIME_WAIT 连接占得满满当当,新连接再想取一个空闲端口,自然就取不到了——Cannot assign requested address 由此而来。想通这一层,解法的层次也清晰了。最治本的,是把短连接改成长连接、用连接池把建好的连接复用起来,从源头上就不再频繁地主动关闭连接,TIME_WAIT 自然消退。在代码一时改不动时,内核侧也有治标的手段:打开 tcp_tw_reuse,允许把 TIME_WAIT 状态的连接安全地复用给新的对外连接,再把临时端口范围调大——这两招配合,能撑过高峰。这次排查还让我顺带彻底厘清了 TIME_WAIT 那个长得很像、却本质迥异的"兄弟":CLOSE_WAIT。TIME_WAIT 出现在主动关闭方,而 CLOSE_WAIT 出现在被动关闭方——当对端发来 FIN 表示要关闭,你的内核回了 ACK 之后,连接就在你这侧进入 CLOSE_WAIT,静静地等着你的应用程序去调用 close()。如果程序永远不调 close(),连接就永远卡在 CLOSE_WAIT。所以这两种堆积的病因南辕北辙:TIME_WAIT 堆积通常是短连接的设计问题,可以靠内核参数缓解;而 CLOSE_WAIT 堆积几乎一定是代码 bug——程序在某条路径(往往是异常路径)上忘了关闭连接,这个,没有任何内核参数能救,只能改代码。这次从一句被我误读的报错出发,我最大的收获,是终于不再把"连接"当成一个用之不竭的东西:本机能主动发起的连接数,被实实在在地卡在一个有限的端口池上;而 TIME_WAIT,就是那个一边维护着 TCP 可靠性、一边悄悄消耗着这个端口池的、必须被认真对待的存在。
—— 别看了 · 2026