“服务明明起来了,怎么访问不了”——这类问题九成要在两条命令之间来回对账:ip addr 回答“这台机器的网卡拿到了什么地址”,ss -tulnp 回答“到底哪个进程在监听哪个地址”。两边对不上时,问题一定出在中间:绑错网卡、监听落在回环、协议栈只开了 IPv6、或者前面还隔着一层 NAT 和容器端口映射。
这篇按“先看地址 → 再看监听 → 最后对账”的顺序拆开,顺带把 ifconfig / netstat 这对旧命令的替代关系讲清楚。
1. ip addr 与 ifconfig:一条已弃用,一条是替代品
$ ip addr show
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
link/ether 52:54:00:8f:1a:2c brd ff:ff:ff:ff:ff:ff
inet 10.0.3.15/24 brd 10.0.3.255 scope global dynamic eth0
valid_lft 86391sec preferred_lft 86391sec
inet6 fe80::5054:ff:fe8f:1a2c/64 scope link
| 维度 | ip addr(iproute2) |
ifconfig(net-tools) |
|---|---|---|
| 是否默认安装 | 是,随 iproute2 装在 Ubuntu 24.04 |
否,需 apt install net-tools |
| 维护状态 | 活跃维护 | 上游事实上停止演进 |
| 能力面 | 地址、路由、链路、邻居、规则、隧道 | 只有地址与链路的一部分 |
| 依赖 | netlink | ioctl |
替代关系不是“新命令更好看”,而是 ifconfig 调用的 ioctl 接口表达不了策略路由、多地址优先级、VRF 这些东西。写脚本时直接用:
$ ip -br addr # 一行一个网卡,机读友好
$ ip -j addr | jq '.[].addr_info' # JSON 输出,配合 jq
$ ip route get 1.1.1.1 # 查“去这个目标走哪张网卡、源地址是谁”
最后一条对排障极有用:它直接给出内核选路结果,比自己猜“应该走 eth0 吧”可靠。
2. 公网 IP 为什么在 ip a 里看不到
ip a 列的永远是网卡上配置的地址。云主机多数是 NAT 出网,公网地址配在云厂商的边缘设备上,本机只有一张私网网卡,所以 ip a 里那串 10.x / 172.16.x / 192.168.x 是真实且完整的——它没漏,是本来就没有。
取公网出口地址只能“问外面”:
$ curl -s4 https://api.ipify.org; echo
$ curl -s https://ifconfig.me/ip
$ dig +short myip.opendns.com @resolver1.opendns.com
| 来源 | 反映的是什么 | 典型偏差 |
|---|---|---|
ip a 的 inet |
本机网卡地址 | 有公网 IP 的机房会直接看到公网地址 |
curl ifconfig.me |
出口 NAT 后的源地址 | 配了代理/多出口时会拿到代理的地址 |
| 云控制台的“公网 IP” | 厂商侧弹性地址 | 可能与本机出口不一致(多网卡、双栈) |
dig +short myip.opendns.com |
DNS 查询的源地址 | 只走 UDP/53,和 TCP 出口可能不同 |
能不能被公网直连看的是入方向:监听地址是 0.0.0.0 还是 127.0.0.1,再叠加安全组,任一不通过外面就连不上。
3. ss -tulnp 与 netstat 的替代关系
$ sudo ss -tulnp
Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
udp UNCONN 0 0 127.0.0.54:53 0.0.0.0:* users:(("systemd-resolve",pid=712,fd=20))
tcp LISTEN 0 4096 0.0.0.0:80 0.0.0.0:* users:(("nginx",pid=1204,fd=6))
tcp LISTEN 0 128 127.0.0.1:8080 0.0.0.0:* users:(("java",pid=5123,fd=98))
tcp LISTEN 0 511 *:443 *:* users:(("nginx",pid=1204,fd=7))
netstat 写法 |
ss 等价写法 |
说明 |
|---|---|---|
netstat -tuln |
ss -tuln |
字母含义一致,ss 更快 |
netstat -tulnp |
ss -tulnp |
-p 需要 root 才能看到别人的进程 |
netstat -an | grep 8080 |
ss -tanp 'sport = :8080' |
过滤表达式比 grep 精确 |
netstat -rn |
ip route |
路由已迁到 ip 命令 |
-t TCP、-u UDP、-l 只听、-n 不解析、-p 带进程。ss 直接读 netlink,netstat 要遍历 /proc/net/tcp,连接上万时差距是秒级。
4. 按端口反查进程:lsof 与 fuser
$ sudo lsof -i:8080 -P -n
java 5123 root 98u IPv4 23117 0t0 TCP *:8080 (LISTEN)
$ sudo fuser -v 8080/tcp
USER PID ACCESS COMMAND
8080/tcp: root 5123 F.... java
| 工具 | 强项 | 短板 |
|---|---|---|
ss -tulnp |
全量一次看完、状态齐全 | 只看 socket,不看进程持有的其他文件 |
lsof -i:PORT |
能同时看 LISTEN 与已建立连接,可加 -p/-u 组合 |
大机器上遍历 /proc 慢,需 root |
fuser -v PORT/tcp |
输出最短,适合塞进脚本判断 | 信息量少,看不到远端地址 |
lsof -p PID |
进程维度的全部句柄 | 不按端口筛时噪音大 |
fuser 对已建立的连接也会命中,别把“在连”读成“在听”。
5. “端口被占用”其实有三种
5.1 同端口、不同绑定地址:并不冲突
$ sudo ss -tlnp 'sport = :8080'
LISTEN 0 128 127.0.0.1:8080 0.0.0.0:* users:(("java",pid=5123,fd=98))
LISTEN 0 511 0.0.0.0:8080 0.0.0.0:* users:(("nginx",pid=1204,fd=9))
能不能共存取决于绑定顺序与 SO_REUSEADDR:先绑具体地址、后绑通配地址,在通配侧开 SO_REUSEADDR 时可以共存;反过来先绑 0.0.0.0 再绑 127.0.0.1 会直接报 Address already in use。具体行为按内核版本核对;更常见的硬冲突是两个进程都绑 0.0.0.0。
5.2 TIME_WAIT 导致的“端口被占”
$ ss -tan state time-wait | wc -l
$ ss -tan state time-wait 'sport = :8080' | head
TIME_WAIT 是主动关闭方保留 2MSL(Linux 固定 60 秒)以防旧报文串进新连接,不是 bug。被动关闭的服务端一般没有;但客户端(反代上游、压测工具)跑大量短连接就会堆出几千个,再想复用同一源端口就会失败。
| 现象 | 判断 | 处理 |
|---|---|---|
ss -s 里 timewait 数万 |
短连接客户端行为 | 改用连接池/keep-alive |
bind 报 EADDRINUSE 但 ss 无 LISTEN |
残留 TIME_WAIT 或别的 netns | ss -tan state time-wait 确认;别急着调 tcp_tw_reuse |
调小 tcp_fin_timeout 无效 |
它管 FIN_WAIT_2,不管 TIME_WAIT | TIME_WAIT 时长不可调 |
5.3 容器端口映射:ss 看到的可能不是你以为的进程
$ docker ps --format '{{.Names}}\t{{.Ports}}'
web 0.0.0.0:8080->8080/tcp
$ sudo ss -tlnp 'sport = :8080'
LISTEN 0 4096 0.0.0.0:8080 0.0.0.0:* users:(("docker-proxy",pid=2210,fd=4))
默认 docker-proxy(userland proxy)会在宿主上占一个监听;关掉它之后改由 iptables DNAT 转发,此时宿主 ss 里可能看不到 8080 的 LISTEN,但流量照样通。所以“ss 里没有这个端口”不能单独作为“服务没起来”的证据,必须再 docker ps 或 iptables -t nat -L -n 对一次。
6. ss 输出里的状态怎么读
| State | 含义 | 排障含义 |
|---|---|---|
LISTEN |
正在等待入连接 | 服务已在听;连不上就是防火墙/路由/绑定地址问题 |
ESTAB |
已建立 | 有活跃连接;数量暴涨要看应用层 |
SYN-SENT |
已发 SYN 等 SYN-ACK | 持续存在 = 对端不回或回程被丢 |
SYN-RECV |
收到 SYN 已回 SYN-ACK | 大量堆积常见于 SYN flood 或回程不通 |
TIME-WAIT |
主动关闭后等待 | 见 5.2,属正常收尾 |
CLOSE-WAIT |
对端已关,本端未 close | 应用没关 fd,是代码问题,堆积会吃满句柄 |
FIN-WAIT-2 |
本端已关,等对端 FIN | 长期停留说明对端异常,受 tcp_fin_timeout 影响 |
Recv-Q / Send-Q 在两列语义不同:LISTEN 状态下它们是已完成三次握手但未被 accept 的队列长度与 backlog 上限。Recv-Q 持续接近上限,说明应用 accept 不过来——这时去查应用,而不是查网络。
$ ss -s # 全局汇总
$ ss -tan state established '( dport = :443 )' | wc -l
7. IPv6 双栈下 ss 的三种写法
| 输出形式 | 含义 |
|---|---|
0.0.0.0:80 |
只绑 IPv4 通配 |
[::]:80 |
绑 IPv6 通配;bindv6only=0 时同时接受 IPv4 |
*:80 |
ss 的合并显示,表示双栈通配 |
127.0.0.1:80 |
仅 IPv4 回环,localhost 解析到 ::1 时会连不上 |
$ ss -4 -tlnp # 只看 IPv4
$ ss -6 -tlnp # 只看 IPv6
$ sysctl net.ipv6.bindv6only
net.ipv6.bindv6only = 0
最容易踩的坑:程序写了 listen("::") 而 bindv6only=1,于是只服务 IPv6;客户端 curl localhost 先试 ::1 成功、curl 127.0.0.1 失败,看起来像“时好时坏”。判断方法就是分别 ss -4 与 ss -6 各看一次。
8. 排错:六种“看不到/连不上”
| 现象 | 原因 | 处理 |
|---|---|---|
ss -p 无 Process 列 |
非 root,无权读他人 fd | sudo ss -tulnp |
服务在跑但 ss 无监听 |
只 bind 未 listen,或崩在 listen 前 | 看应用日志;strace -f -e trace=network 复现 |
| 本机通、外网不通 | 绑了 127.0.0.1,或安全组未放行 |
改绑 0.0.0.0;核安全组 + ufw status |
curl localhost 通、curl 127.0.0.1 不通 |
IPv6 单栈监听 | 见第 7 节 |
| 端口被占但无进程 | 在别的 netns/容器内 | nsenter -t <pid> -n ss -tlnp |
bind: address already in use 但 ss 干净 |
TIME_WAIT 或 SO_REUSEPORT 被占用 |
ss -tan state time-wait;查是否有 SO_REUSEPORT 多进程 |
9. 边界:这些事本就不该用 ip/ss 解决
| 需求 | 更合适的工具 | 理由 |
|---|---|---|
| 端口是否“从外面”可达 | nc -vz / curl --connect-timeout |
本机视角看不到中间链路 |
| 抓包看握手细节 | tcpdump / ss -i |
状态机只给结论,不给报文 |
| 云上安全组规则 | 厂商控制台 / API | 规则不在本机内核里 |
一句话收口:ip a 说“我是谁”,ss -tulnp 说“我在听谁”,lsof/fuser 说“是谁在听”,curl/dig 说“外面看见的是谁”。 四个视角都对上,才叫“端口是通的”。
10. 自测
ip a里只有10.0.3.15/24,说明这台机器没有公网 IP 吗?- 写出
netstat -tulnp的ss等价命令,并说明-p为什么常要加sudo。 - 两个进程分别监听
127.0.0.1:8080与0.0.0.0:8080,哪个先绑会成功? ss输出里CLOSE-WAIT大量堆积,指向网络问题还是应用问题?- 关掉
docker-proxy后宿主ss看不到映射端口,是否等于服务未启动? curl localhost成功而curl 127.0.0.1失败,最可能的内核参数是什么?