三个命令为什么经常给出三个答案
线上出现「端口被占用」「服务起不来」的时候,多数人的第一反应是跑一条命令看谁占着 80。但 ss -lntp、lsof -i :80、netstat -tlnp 这三个命令的结果经常不一致:有的显示 PID,有的显示空,有的干脆什么都没输出。于是就开始怀疑「是不是权限问题」「是不是内核 bug」。
实际上这三条命令查的是不同层次的东西。netstat 在较新的发行版里已经被 ss 取代,它读的是 /proc/net/tcp,对已经是 TIME_WAIT、CLOSE_WAIT 这类半死状态的连接会显示得比较全;ss 走 netlink,能拿到 socket 的 inode 再反查进程,速度更快;lsof 是遍历 /proc/*/fd,只有当持有 socket 的进程对你可见时才显示 PID。
所以「ss 有 LISTEN 但没有进程名」通常只有两个原因:进程在另一个网络命名空间(容器里),或者你当前不是 root 且内核开了 kernel.yama.ptrace_scope。先把这两种可能性排除,再去查应用配置,能省掉大量时间。
第一步:确认是「端口被占」还是「根本没监听」
判断顺序我固定为:先看监听表,再看连接表,最后才看进程。
ss -lntup | grep -E ':(80|443|3306)\b'
ss -s第一条列出 TCP 与 UDP 的监听套接字。-p 显示进程,非 root 执行时这一列可能是空的,需要 sudo。如果没有输出,说明根本没有进程在那个端口监听,问题不在占用,而在服务没起来或者监听地址不对。
「监听地址不对」是极易被忽略的一类。配置文件里写 listen 127.0.0.1:80 与 listen 0.0.0.0:80 是完全不同的两件事。容器里尤其常见:应用监听 127.0.0.1,宿主机做了端口映射却连不通,因为容器的 loopback 不对外。用 ss -lnt 的输出第三列看得很清楚:
LISTEN 0 511 127.0.0.1:8080 0.0.0.0:*
LISTEN 0 511 0.0.0.0:80 0.0.0.0:*第二行才是所有网卡都在听。看到 [::]:80 也不用紧张,那是 IPv6 的通配地址,在 net.ipv6.bindv6only=0 的情况下也接受 IPv4 连接。
第二个常见误判是「端口被占」其实是 Address already in use 但占用者是同一程序的上一个实例残留。这时 ss -lntp 会显示一个你并不认识的 PID,ps -p PID -o cmd= 一看还是自己的程序,只是被 systemd 重拉了一次而老进程没死透。这种情况要查 Restart= 的配置和程序的优雅退出逻辑,不是 `kill -9` 一把梭能解决的。
第二步:连接表里的状态才是真正的线索
端口能 listen 但访问不通,问题往往出在连接状态上。看一眼状态分布:
ss -tan state established '( sport = :80 )' | wc -l
ss -tan | awk 'NR>1{print $1}' | sort | uniq -c | sort -rn如果 TIME_WAIT 数量很大(几千以上),说明是本机主动关闭连接造成的。TIME_WAIT 本身不是故障,它就是 TCP 协议规定的 2MSL 等待,作用是保证最后一个 ACK 能重传、防止旧连接的延迟包污染新连接。60 秒内积累几千个属于正常量级,尤其在有健康检查、短连接 API 的场景下。
Linux 上真正会造成麻烦的是两类情况。第一是本地端口耗尽:客户端连接目标地址的后半段是本地临时端口,范围由 net.ipv4.ip_local_port_range 决定,默认 32768~60999 只有约 28000 个。如果服务每秒建立大量短连接,TIME_WAIT 会把可用端口压到很低,报错是 Cannot assign requested address。第二是 CLOSE_WAIT 堆积,那个说明是应用层没关 socket,属于程序 bug,调内核参数完全没用。
区分两者的命令是同一个,看数量增长:
watch -n 2 "ss -tan | awk 'NR>1{print \$1}' | sort | uniq -c | sort -rn | head -6"TIME_WAIT 会在 60 秒后自动消失,是锯齿形;CLOSE_WAIT 只增不减,那就得去查应用代码里 curl_close / fclose / 连接池的归还逻辑。
至于网上常见的 net.ipv4.tcp_tw_reuse=1,在 Linux 4.12 之后已经改成只对**主动发起连接**且时间戳能保证单调的场景生效,开着风险不大;但 tcp_tw_recycle 在 4.12 就被移除了,NAT 环境下它会丢包,看到有人教你在 sysctl.conf 里加这一项,可以直接忽略。真正值得做的是把 ip_local_port_range 放宽到 10240 65535,以及确认服务端开启了 keep-alive,让连接可以复用而不是不停重建。
第三步:UDP 与 Unix socket 常被整条链路漏掉
上面五步都在聊 TCP,但实际排障里还有两类套接字经常把人绕晕。第一类是 UDP。ss -lunp 才会列出 UDP 监听,很多服务在 TCP 上一切正常,却在 UDP 上悄悄失败,典型的是 DNS、syslog、以及自建的监控 agent。如果只跑 ss -lntp,看不到 UDP 端口,就会误判成「服务没监听」。比如本机 DNS 解析慢,先要确认 53 端口 UDP 是否有人在听:一条 ss -lunp 'sport = :53' 就够了。
UDP 没有连接状态,也没有三次握手,所以「端口在听但收不到包」的判断要看队列。当 ss -un 的 Recv-Q 持续增长而不归零,说明应用读取速度跟不上发包速度,内核缓冲区被填满,后续的包会直接被丢弃,应用侧完全无感。这类问题在日志采集与 DNS 上最常见,处理方式是先加应用侧读取并发,再考虑调大 net.core.rmem_max,顺序反了只是把问题推迟。
第二类是 Unix domain socket。Nginx 连 PHP-FPM、MySQL 本机连接、Docker daemon,走的都是这条路,它们根本不占用任何 TCP 端口。所以「端口都正常但 502」的时候,去看 TCP 表完全是浪费时间。正确的检查是列出所有 unix socket:
ss -lxnp | grep -E 'php|mysql|docker'这里能看到 socket 的路径与归属进程。ss -lx 的第一列 u_str 是流式 socket,u_seq 是顺序包 socket。如果 Nginx 配置里写的 fastcgi_pass unix:/run/php/php8.2-fpm.sock; 与实际存在的 socket 路径不一致(升级 PHP 版本后路径变了是很常见的),nginx -t 依然通过,只有访问时才会报 502,错误日志里写的是 connect() to unix:/run/php/php8.1-fpm.sock failed (2: No such file or directory)。看到 No such file 就去对路径,看到 Permission denied 就去查 socket 文件的权限与属组,这两个错误的具体含义要分清楚。
还有一个细节容易被误判:Unix socket 文件存在于磁盘上并不代表服务活着。进程崩溃后 .sock 文件常常残留,ls 看得到,ss -lx 却列不出来。判断标准永远是 ss 或能否 connect,不是文件是否存在。我现在的习惯是看到残留 socket 先 rm 再重启服务,否则 FPM 有时会因为 listen.owner 相关的检查直接拒绝启动,日志里只留一句很含混的 bind 失败。
第四步:容器与命名空间里的端口
如果宿主机 ss -lntp 看到端口有人听但没有进程名,或者端口被 docker-proxy 占着,说明流量走的是 Docker 的 NAT 链路。判断真实情况要进网络命名空间看:
docker inspect -f '{{.State.Pid}}' 容器名
nsenter -t <pid> -n ss -lntpnsenter 是排查容器网络最直接的工具,不需要进容器内部装任何软件。进去之后看到的监听地址是容器自己的视角,这时候再对照 docker port 容器名 就能判断映射是否一致。
另一个经典现象是宿主机端口被占用的报错:bind: address already in use,但 ss -lntp 里什么都没有。原因通常是 iptables 的 NAT 规则里还有残留的 DNAT 到已删除容器,或者某个进程用 SO_REUSEADDR 绑定了一个非 listening 的连接。ss -ant 不带 -l 能看到这些「非监听但占端口」的条目,它们的 Local Address 末端会带端口号。
还有一种只在云主机上出现的情况:绑定的 IP 已经不在网卡上了。程序配置写死 listen 10.0.0.5:80,机器浮动 IP 漂走后这个地址不存在,bind 会直接失败。ip addr show 确认地址,再用 ss -lnt 看当前实际绑定,两步就能定位。
第五步:从端口反查进程的可靠方式
lsof -i :80 在多数场景下好用,但它有个已知缺陷:对处于 TIME_WAIT 的 socket 有时不显示,而且在大并发机器上会跑得很慢,因为它要遍历所有进程的 fd。我的顺序是先用 ss 拿到 inode,再去 /proc 里精确比对:
ss -lntp 'sport = :80'
ss -lnt -e 'sport = :80'-e 会输出 socket 的 inode 号,拿到之后可以在 /proc/*/fd 里找哪个进程持有它:
find /proc/[0-9]*/fd -lname 'socket:\[12345\]' 2>/dev/null路径里出现的进程号就是答案,即使该进程对 lsof 不可见也能找到(前提是 root)。这招在排查「ss 显示占用但 lsof 查不到」时特别有用,也是排查木马类进程的思路之一。
另外要记住 /proc/net/tcp 里端口是十六进制的。0x0050 就是 80,0x1F90 是 8080。有些精简镜像里没有 ss 也没有 lsof,只能靠 cat /proc/net/tcp6 加换算,知道这个细节能避免到处装包。
第六步:不是占用问题,而是内核与安全策略
定位完进程之后如果发现端口确实空闲、服务也确实起来了,但外面连不上,问题通常在这三处:
一是监听队列溢出。ss -lnt 的 Recv-Q 列在 LISTEN 状态下表示「已完成三次握手但还没被 accept 的连接数」,如果持续大于 0 且接近 Send-Q(即 backlog),说明应用 accept 得太慢。net.core.somaxconn 与 Nginx 的 listen ... backlog=、PHP-FPM 的 listen.backlog 要注意三者取小值,只改一项没有意义。
二是防火墙。ufw status、nft list ruleset、云厂商的安全组是三套独立的规则,任何一层拒绝都表现为「连不上但端口在听」。用 ss 确认本地监听正常之后,先从本机 curl 127.0.0.1:端口 测,再从同网段另一台机器测,最后从公网测,三段测试能把范围切得很干净。
三是 SELinux 或 AppArmor。Debian 上 AppArmor 给 nginx 设的 profile 如果限制了可绑定端口,改配置让它监听 8080 会直接被拒,dmesg | tail 或 journalctl -u apparmor 里能看到 DENIED 记录,而 nginx -t 完全不会报错。这是「配置文件没问题、服务起不来」最容易被忽略的一类原因。
把排查顺序固定成「监听表 → 连接状态 → 命名空间 → 反查进程 → 安全策略」,绝大多数端口类故障能在五分钟内收敛到唯一原因,而不是在 lsof 和 ss 之间来回猜。