"启动服务报错 Address already in use"、"端口通了但连不上"、"连接数莫名其妙涨到几万",这类问题每天都在服务器上发生。它们的根因往往不在应用本身,而在 TCP 连接的状态上。要看懂这些,必须掌握端口和连接的查看方法。
这篇文章以 ss 为主,讲清楚 ss -lntp、ss -tunp 各参数的含义,配合 lsof -i 和 fuser 做双向查询,并说明为什么 netstat 已经不推荐继续使用。更重要的篇幅会放在 TCP 状态机上:什么是 LISTEN、ESTABLISHED、TIME_WAIT、CLOSE_WAIT,为什么 TIME_WAIT 多不一定是问题,而 CLOSE_WAIT 多一定是代码问题。
先分清两种"端口被占用"
这是排查前必须想清楚的第一件事,因为两者的处理方式完全不同。
第一种是监听冲突。 你想让 Nginx 监听 80 端口,但已经有别的进程占着。服务启动失败,报错 bind() to 0.0.0.0:80 failed (98: Address already in use)。这种情况查的是 LISTEN 状态的 socket,处理方式是停掉占用者或者给你的服务换个端口。
第二种是连接堆积。 端口是同一个,但问题是连接数过多、状态异常,表现为服务能启动但有用户连不上、响应变慢。这种情况查的是 ESTABLISHED、TIME_WAIT、CLOSE_WAIT 这些非监听状态的连接,处理方式涉及内核参数和应用代码,绝不是杀进程能解决的。
用一句话概括:前者看 -l,后者看状态分布。搞清楚这一点,能省掉一半的无效操作。
ss:现代首选工具
ss 来自 iproute2 软件包,所有现代发行版默认自带,它直接读取内核的 socket 信息,比 netstat 快得多,尤其是在连接数上万时差距非常明显。
ss -lntp
这是最高频的一条命令,参数逐个拆解:
| 参数 | 含义 |
|---|---|
-l |
只看监听状态的 socket |
-n |
不解析服务名和主机名,直接显示数字,速度更快 |
-t |
TCP |
-u |
UDP |
-p |
显示持有该 socket 的进程 |
-a |
显示所有状态,不只监听 |
-s |
输出统计摘要 |
-r |
解析主机名(默认尝试解析,通常加 -n 关掉) |
-4 / -6 |
只显示 IPv4 或 IPv6 |
常用组合:
ss -lntp # 查看所有 TCP 监听及进程
ss -lntup # 同时看 TCP 和 UDP
ss -lntp '( sport = :80 )' # 精确查 80 端口被谁监听
ss -lntp | grep -E ':80|:443'
ss -tunp # 所有 TCP 与 UDP 连接,含进程
ss -tan # 所有 TCP 连接,不解析名字
ss -s # 连接总数与状态分布摘要
ss -tn state established # 只看已建立连接
ss -tn state time-wait # 只看 TIME_WAIT
ss -tnp dst 10.0.0.5 # 看与某个地址的连接
ss -tnp dst 10.0.0.5:443 # 精确到对端端口
ss -tnm # 显示 socket 内存用量
ss -tn -o state established # 显示定时器信息
-p 参数需要 root 权限才能看到其他用户的进程信息,普通用户只能看到自己的。所以排查时记得加 sudo,否则会看到进程列为空,误以为没人占用。
查看某个端口被谁占用,最实用的一条:
sudo ss -lntp '( sport = :8080 )'
注意 ss 的过滤表达式要加引号,否则 shell 会把它解释错。sport 是源端口也就是本地端口,dport 是目的端口即对端端口。查本地监听用 sport,查出去的连接用 dport。
按状态统计数量,这是判断连接堆积的第一步:
ss -tan state all | awk 'NR>1 {print $1}' | sort | uniq -c | sort -rn
TCP 状态机:必须看懂的几个状态
看 ss 输出时,第一列就是状态。理解它们的含义,才能判断是否正常。
| 状态 | 含义 | 出现位置 | 异常判断 |
|---|---|---|---|
LISTEN |
正在监听,等待连接请求 | 服务端 | 数量应与服务数匹配 |
SYN-SENT |
已发出 SYN,等待对方回应 | 客户端 | 大量堆积说明对端不通 |
SYN-RECV |
收到 SYN 并回复,等待最后确认 | 服务端 | 大量堆积可能是 SYN 洪泛 |
ESTABLISHED |
连接已建立,正在传输数据 | 双方 | 远超基线要查来源 |
FIN-WAIT-1 |
主动关闭方已发 FIN | 主动关闭方 | 一般短暂 |
FIN-WAIT-2 |
已收到对方 ACK 确认 | 主动关闭方 | 长时间停留说明对端不关 |
TIME-WAIT |
主动关闭方等待 2MSL | 主动关闭方 | 几万以内通常正常 |
CLOSE-WAIT |
被动关闭方等待应用调用 close | 被动关闭方 | 数量持续增长就是泄漏 |
LAST-ACK |
被动关闭方等待最后的 ACK | 被动关闭方 | 一般短暂 |
CLOSED |
连接已完全关闭 | — | 不显示在 ss 中 |
关键点在于谁主动关闭。主动发起关闭的一方会进入 TIME-WAIT 并等待两倍 MSL(Maximum Segment Lifetime),Linux 上通常约六十秒。被动关闭的一方收到 FIN 后进入 CLOSE-WAIT,停留多久完全取决于应用程序什么时候调用 close()——内核不会替你关。
所以一条经验法则:TIME-WAIT 多在服务端通常说明这台机器主动关闭了大量连接,可能是短连接服务;CLOSE-WAIT 多则几乎一定是本机的应用程序没有正确关闭 socket,是代码缺陷。
lsof -i:从进程和端口两个方向查
lsof 全称 list open files,在 Linux 中 socket 也是文件,所以它能查网络连接。它的优势是双向查询很自然。
# 安装
# RHEL、Rocky、CentOS
sudo dnf install -y lsof
# Debian、Ubuntu
sudo apt update && sudo apt install -y lsof
# 查某个端口被谁占用
sudo lsof -i :80
sudo lsof -i :80 -sTCP:LISTEN
sudo lsof -iTCP -sTCP:LISTEN -P -n
# 查某个进程打开了哪些连接
sudo lsof -i -a -p 1234
sudo lsof -p 1234 | grep -i 'sock\|TCP'
# 查与某个主机的连接
sudo lsof -i @10.0.0.5
sudo lsof -i @10.0.0.5:443
# 按协议
sudo lsof -iTCP
sudo lsof -iUDP
sudo lsof -iTCP:1-1024 # 端口范围
-a 表示多个条件取交集,不加的话默认是并集,这是个容易搞错的地方。-P 不解析端口名,-n 不解析主机名,两者一起加能显著加快输出。
lsof 的输出列包括 COMMAND、PID、USER、FD(文件描述符)、TYPE、DEVICE、SIZE、NODE、NAME。FD 列可以看到 socket 在进程里的描述符编号,配合 /proc/ 能进一步确认。
fuser:直接定位并可结束占用者
fuser 能显示使用某个文件或 socket 的进程,还可以直接发送信号,但后者要非常谨慎。
# 安装
# RHEL、Rocky、CentOS:psmisc 包
sudo dnf install -y psmisc
# Debian、Ubuntu
sudo apt update && sudo apt install -y psmisc
# 查看占用 80 端口的进程
sudo fuser -n tcp 80
sudo fuser -v -n tcp 80
sudo fuser -v 80/tcp
输出会直接给出 PID 列表。-v 显示详细信息,包括进程名和用户名。
风险提示: fuser -k 会直接杀掉占用者,这是高危操作。用之前必须先确认这些 PID 是什么、杀了会有什么后果。生产环境上用它杀数据库、杀关键服务的事故并不少见。正确做法是先 -v 看清楚,再用 systemctl stop 优雅停止对应服务。
# 危险用法,务必先确认 PID 再执行
sudo fuser -k -n tcp 80
# 相对温和的用法:发送 TERM 而不是默认的 KILL
sudo fuser -k -TERM -n tcp 80
netstat:为什么不建议再用
netstat 来自 net-tools 软件包,这个包自 2001 年后基本停止维护。它读取 /proc/net/tcp 并做文本解析,在连接数多时非常慢,而且不支持很多新的内核特性。
| 对比项 | ss |
netstat |
|---|---|---|
| 所属包 | iproute2,默认安装 |
net-tools,需单独装 |
| 数据来源 | netlink socket,直接读内核 | 解析 /proc 文本 |
| 速度 | 快,万级连接秒级返回 | 慢,万级连接可能数秒到十几秒 |
| 维护状态 | 活跃维护 | 基本停止维护 |
| 过滤能力 | 支持表达式过滤 | 有限 |
| 输出信息 | 支持定时器、内存、拥塞窗口等 | 较少 |
很多新装的系统已经没有 netstat,执行会报 command not found。如果需要安装:
# RHEL、Rocky、CentOS
sudo dnf install -y net-tools
# Debian、Ubuntu
sudo apt update && sudo apt install -y net-tools
老命令与新命令的对照,方便过渡:
netstat -lntp → ss -lntp
netstat -tunp → ss -tunp
netstat -an → ss -an
netstat -s → ss -s 或 nstat -a
netstat -r → ip route
netstat -i → ip -s link
最后一个对照尤其重要:netstat -r 和 netstat -i 的功能已经完全被 ip route 和 ip -s link 取代,后者信息更全。
TIME_WAIT 过多的成因与处理
TIME-WAIT 的存在是有意义的:它防止旧连接的延迟数据包被新连接误接收,同时保证最后的 ACK 能被对方收到。所以它不是垃圾,是协议正确性的保障。
什么时候算多?经验上,几千个是正常的,几万个需要关注,超过十万且持续不降才需要处理。判断标准不是绝对数量,而是是否影响了新连接的建立以及是否占用过多内存。每个 TIME-WAIT socket 大约占用几百字节到几 KB 内核内存,十万级别的连接大约消耗几十 MB,一般不是瓶颈。
常见成因:这台机器上有大量短连接,比如 HTTP 请求没有启用 keepalive、爬虫程序频繁建连、健康检查间隔极短、或者本机作为客户端去调用大量外部接口。
可调整的内核参数(修改前必须评估风险):
sysctl net.ipv4.ip_local_port_range
sysctl net.ipv4.tcp_tw_reuse
sysctl net.ipv4.tcp_max_tw_buckets
sysctl net.ipv4.tcp_fin_timeout
tcp_tw_reuse 设为 1 允许将 TIME-WAIT socket 用于新的出站连接,对客户端场景有效,对服务端监听场景没有作用。注意这个参数只影响主动发起的连接,不要指望它能解决服务端的问题。
tcp_max_tw_buckets 限制 TIME-WAIT 总数,超出后内核会直接销毁最早的 socket 并在日志里留下 TCP: time wait bucket table overflow。这个参数是把双刃剑,调太小会破坏协议正确性,一般不建议动。
现代内核(4.12 之后)已经彻底移除了 tcp_tw_recycle,任何教程里让你设置这个参数的都是过时内容。它在 NAT 环境下会导致大面积连接失败,是被废弃的直接原因。
临时修改与持久化:
sudo sysctl -w net.ipv4.tcp_tw_reuse=1
echo 'net.ipv4.tcp_tw_reuse = 1' | sudo tee -a /etc/sysctl.d/99-tcp.conf
sudo sysctl -p /etc/sysctl.d/99-tcp.conf
风险提示: 内核参数会影响整机所有连接,不要在没搞清楚含义的情况下照抄网上的"优化脚本"。很多所谓的优化参数在新内核中已经默认启用或已被移除。
真正治本的做法是在应用侧:为 HTTP 服务开启 keepalive,让连接复用;检查负载均衡的健康检查频率;确认客户端是否正确关闭连接。
CLOSE_WAIT 泄漏:真正的代码问题
CLOSE-WAIT 表示对端已经关闭了连接(发来 FIN 并收到我们的 ACK),但本机的应用程序还没有调用 close()。内核会一直等,直到应用主动关闭。
所以 CLOSE-WAIT 数量持续增长,几乎可以确定是程序缺陷:可能是异常分支没有关闭 socket、线程池里的连接没有归还、框架配置的连接未正确释放,或者程序卡在某个阻塞操作上没走到关闭逻辑。
定位步骤:
# 第一步:确认数量是否在增长
ss -tan state close-wait | wc -l
sleep 60
ss -tan state close-wait | wc -l
# 第二步:找出是哪些进程
sudo ss -tanp state close-wait | awk '{print $NF}' | sort | uniq -c | sort -rn | head
# 第三步:看该进程打开了多少连接
sudo lsof -i -a -p PID | wc -l
ls /proc/PID/fd | wc -l
处理方式是修复代码或重启服务释放连接。/proc/ 里的句柄数量如果接近 ulimit -n 的上限,说明连接泄漏已经很严重,进程很快会无法接受新连接。
顺便提一下文件描述符上限,它决定了单进程最多能开多少连接:
ulimit -n # 当前 shell 的限制
cat /proc/PID/limits | grep 'open files'
cat /proc/sys/fs/file-max # 系统级上限
cat /proc/sys/fs/file-nr # 已用/上限
systemd 管理的服务可以在 unit 文件里设置 LimitNOFILE=65535,这不是内核参数,是服务级的限制,修改更安全可控。
端口起不来的标准排查流程
# 第一步:确认是谁在监听
sudo ss -lntp '( sport = :8080 )'
sudo lsof -i :8080 -sTCP:LISTEN
# 第二步:确认进程身份
PID=上面查到的PID
sudo ps -o pid,user,etime,args -p $PID
sudo ls -l /proc/$PID/exe
# 第三步:决定处理方式
sudo systemctl status 服务名 --no-pager
sudo systemctl stop 服务名 # 优雅停止,优先
# 仅在确认安全后
sudo kill -TERM $PID
# 第四步:确认释放后重启目标服务
sudo ss -lntp '( sport = :8080 )'
sudo systemctl start 你的服务
sudo journalctl -u 你的服务 -n 30 --no-pager
还有几种特殊情况:IPv6 与 IPv4 双栈监听时,::8080 通常会覆盖 IPv4,看到 tcp 和 tcp6 两行不要奇怪;如果服务配置为监听 127.0.0.1:8080,外部就访问不到,表现为"端口没开",实际是绑定地址不对;容器环境里宿主机看不到容器内部的监听,需要用 ss -lntp 在容器网络命名空间里查,或者看 docker ps 的端口映射。
常见问题
ss -lntp 看到的进程列是空的?
没有 root 权限就看不到别人的进程。加 sudo 再执行。容器环境里也可能因为 PID 命名空间隔离而无法显示。
服务显示已监听,但外部 telnet 不通?
按顺序排查:监听地址是不是 127.0.0.1 而非 0.0.0.0;云安全组是否放行;系统防火墙是否放行;中间是否有 NAT 或负载均衡。用 ss -lntp 看 Local Address:Port 那一列能确认第一项。
TIME_WAIT 几万个,需要立即处理吗?
先看业务有没有受影响,再看成因。如果连接数稳定、没有新连接失败、内存正常,可以只观察。优先在应用侧启用连接复用,而不是上来就调内核参数。
为什么内核参数里没有 tcp_tw_recycle?
该参数在 Linux 4.12 之后被移除,因为它在 NAT 环境下会错误地丢弃合法连接。任何建议设置它的教程都已过时。
netstat 和 ss 结果不一致怎么办?
两者的数据来源和刷新时机不同,瞬时结果有差异是正常的。以 ss 为准,它是当前推荐的工具。如果差异持续且巨大,可能是采样时刻不同,可以用 ss -s 看总数核对。
总结
- 排查端口问题先分清两类:监听冲突看
-l,连接堆积看状态分布,两者的处理手段完全不同。 - 日常用
ss -lntp查监听、ss -tan查连接、ss -s看总量,-p参数必须配sudo才能看到进程。 lsof -i :端口与ss互补,前者从进程角度查更方便;fuser -k能直接杀占用者但风险很高,生产环境优先用systemctl stop。netstat所属的net-tools已停止维护且速度慢,应迁移到ss和ip系列命令。TIME-WAIT多是短连接的正常表现,优先改应用启用连接复用;CLOSE-WAIT持续增长是代码未关闭 socket 的泄漏,必须从应用侧修复。
企业QQ咨询




