ss -s 一看,几千个 TIME_WAIT 连接,要不要处理?删掉了几个 G 的大日志,df 显示磁盘却一点没释放,空间去哪了?这两个问题,一个考验对 ss 输出里 TCP 状态的理解,一个考验对 lsof “一切皆文件”哲学的运用——都超出了”查个端口占用”的基本盘。

这是 Linux 命令系列的一篇,承接 Linux 常见命令与工具地图。先一句话对齐两个工具是干什么的:ss(socket statistics)查看系统里所有网络连接的状态,是 netstat 的现代替代;lsof(list open files)列出进程打开的所有”文件”——Linux 里网络连接、管道也都算文件,所以它既能查端口也能查文件占用。两者查端口监听/占用的基本用法在 服务器排查那篇 讲过了,本篇不再重复,直接进深水区。

ss 按状态统计和过滤

ss(socket statistics)除了列连接,还能按 TCP 状态统计和过滤——排查连接类问题时,先看全局分布再钻具体状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ss -s
# -s:按状态汇总统计所有 socket——estab(已建立)、time-wait 等各有多少
# 一眼看出连接的整体健康状况,是排查的第一步

ss -tn state time-wait
# -t:TCP;-n:不解析域名和服务名(快很多)
# state 后面跟状态名,只列出处于这个状态的连接

ss -tn state close-wait
# 同理,专门看 close-wait 状态的连接——后文会讲为什么要特别盯这个

ss -tnp state established '( dport = :443 )'
# 还能按端口过滤:只看目标端口 443 的已建立连接
# -p:显示是哪个进程持有这条连接(需要相应权限)

TIME_WAIT:正常现象,别急着动内核参数

TCP 连接关闭时,主动发起关闭的一方会进入 TIME_WAIT 状态,等待一段固定时间(2 倍 MSL,通常总共几十秒到几分钟)后自动消失——这是 TCP 协议设计的一部分,用来确保最后的确认包送达、避免旧连接的残留数据包串进新连接。

所以看到一批 TIME_WAIT 不用慌:它们不占进程资源(连接已经关了),会自己过期,是协议在正常工作。数量大到几千上万,说明的也不是故障,而是”短连接建立和拆除得太频繁”——比如服务间调用没用连接池、HTTP 客户端没开 keep-alive。这时候正确的方向是改用长连接/连接池,从源头减少连接的建拆次数,而不是上来就调内核参数硬压。

CLOSE_WAIT:这个才需要警惕

CLOSE_WAIT 的含义完全不同:对端已经关闭了连接,但本端的应用程序还没有调用 close() 把连接关掉。内核收到了对方的挥手、在等你的程序把连接收尾,你的程序却一直没动作——连接就卡在 CLOSE_WAIT。

零星几个是瞬时现象,但 CLOSE_WAIT 持续堆积基本可以断定是应用代码的 bug:某个分支漏了关闭连接(常见于异常路径——请求出错提前 return,忘了释放连接)。特征也很好认:

1
2
3
4
5
ss -tn state close-wait | wc -l
# 隔一分钟跑一次,数量只涨不跌,就是泄漏没跑了

ss -tnp state close-wait
# -p 看这些连接挂在哪个进程上,直接定位到该背锅的服务

重启进程能把 CLOSE_WAIT 清零,但那只是止痛——代码不修,堆积会再来一遍。这是”监控指标能直接指向代码 bug”的少数场景之一,值得把 CLOSE_WAIT 数量放进告警。

lsof:一切皆文件的排查方式

Linux 里普通文件、目录、网络连接、管道全是”文件”,lsof(list open files)因此成了万能反查工具——从进程查文件,或从文件反查进程:

1
2
3
4
5
6
7
8
9
10
11
12
lsof -p 12345
# -p:列出指定进程打开的所有文件——配置读的是哪份、日志写到哪去了,一目了然

lsof /var/log/app.log
# 参数直接给文件路径:反查谁在用这个文件

lsof +D /mnt/data
# +D:反查谁在用这个目录(递归)——umount 报 "target is busy" 时,
# 用它找出占着挂载点不放的进程

lsof -u deploy
# -u:列出指定用户打开的所有文件

lsof +L1:已删除但还占着磁盘的文件

回收 ls/du/df 那篇 埋的伏笔——df 显示磁盘快满,du 把目录加起来却对不上,差的那块空间在哪?

要理解这个现象,先把三个东西分开:文件内容(磁盘上的数据块,真正占空间的是它)、目录里的文件名(指向内容的入口)、进程的文件句柄(进程打开文件后拿到的访问通道)。关键规则是:内容被释放的条件,是”没有任何名字指向它”且”没有任何进程打开着它”——两个条件要同时满足

rm 只做了一件事:删掉目录里的名字。如果这时还有进程开着这个文件(典型场景:删了正在被服务写入的日志),第二个条件不满足,数据块就原地不动——于是出现了诡异的一幕:

  • du 是沿着目录树把文件加起来统计的,名字没了,它就统计不到这份内容
  • df 统计的是文件系统层面数据块的实际占用,这份”无名内容”照样算在内

你的理解方向是对的:df 显示的确实还包含着”删除前”的那份空间——因为那份内容根本没被删掉,被删掉的只是名字。lsof +L1 就是把这些”有内容、有句柄、没名字”的文件找出来:

1
2
3
4
5
6
lsof +L1
# +L1:列出链接数小于 1 的打开文件——也就是"已删除但仍被进程占用"的文件
# 输出里 SIZE/OFF 列能看到文件多大,NAME 列会标注 (deleted)

lsof +L1 /var/log
# 限定只查某个路径下的,范围小输出更干净

找到之后有两种处理,原理不同但殊途同归:

1
2
3
4
5
6
7
8
9
10
11
systemctl restart myapp
# 方式一:重启持有句柄的进程——句柄释放后,"两个条件"凑齐,内容被回收,空间回来
# 进程优雅退出的机制见 kill 信号那篇——systemctl stop 会先发 SIGTERM

: > /proc/12345/fd/4
# 方式二(不能重启服务时):不动句柄,直接把内容清空
# /proc/PID/fd/ 目录下,内核把进程打开的每个文件句柄都暴露成一个入口,
# 编号 N 从 lsof 输出的 FD 列拿(比如 4w 就是 4)——文件在目录里的名字虽然没了,
# 但通过这个入口依然能访问到那份内容
# : 是 shell 的空命令(什么都不输出),> 是重定向——"把空输出写入这个文件",
# 效果就是把文件内容截断成 0 字节

回答一个自然的疑问:方式二清空后句柄确实还开着,为什么空间能释放?因为占磁盘的是数据块,不是句柄——句柄只是访问通道,本身不占什么空间。内容被截断成 0 字节,数据块立刻归还给文件系统,df 马上就能看到下降;进程后续再写日志,从 0 开始占用新的块,服务全程不受影响。

预防比补救简单:要清空正在被写入的日志,用 truncate -s 0 文件: > 文件 原地清空内容(名字和句柄都不动,只清数据块),别用 rm——rm 删的是名字、留下的恰恰是那份占空间的内容,方向完全反了。

常用命令速查表

命令 作用
ss -s 按状态汇总统计所有连接
ss -tn state 状态名 只看指定状态的 TCP 连接
ss -tnp state close-wait 查 CLOSE_WAIT 挂在哪个进程
TIME_WAIT 大量出现 短连接太频繁,上连接池/keep-alive,通常不是故障
CLOSE_WAIT 持续堆积 应用漏了 close(),修代码,重启只是止痛
lsof -p PID 进程打开了哪些文件
lsof 文件路径 / +D 目录 反查谁在用这个文件/目录(umount busy 排查)
lsof +L1 已删除但仍被占用的文件,df/du 对不上时查它
: > /proc/PID/fd/N 不重启进程直接释放已删除文件的空间
清空活跃日志 truncate -s 0: >,别用 rm

写到这里

ss 的进阶在于读懂状态:TIME_WAIT 是协议正常运转(多了优化连接复用),CLOSE_WAIT 堆积是代码 bug 的直接证据。lsof 的进阶在于反向思维:从文件反查进程,+L1 专治”删了文件磁盘不释放”。这两个工具吃透,网络连接和文件句柄两类”隐形”问题就都有了抓手。