
凌晨 3 点,生产环境报警:/var/log 分区使用率 99%。
你登录服务器,发现 application.log 竟然有 100G。
你的操作: rm -rf application.log。
你的期待: 磁盘空间释放,报警解除,继续睡觉。
残酷现实: df -h 显示磁盘依然是 99%。报警短信还在狂响。
更诡异的场景:
MySQL 数据库报警磁盘满了。你执行 DELETE FROM orders WHERE date < '2025-01-01'; 删了 500G 数据。
结果:磁盘空间一点都没变少 ,甚至可能因为写日志还变多了。
1. 嫌疑人 A:操作系统的"幽灵文件" (OS Level)
这是 Linux 系统中最经典的坑。
核心原理:文件名 vs 文件句柄
在 Linux 中,一个文件被"真正删除"需要满足两个条件:
- Link Count = 0: 文件名被删除了(你执行
rm做的事)。 - Reference Count = 0: 没有进程正"抓"着这个文件(Open File Descriptor)。
僵尸是如何诞生的?
当你的 Java/Python/Nginx 进程正在疯狂往 application.log 里写日志时,它持有这个文件的 句柄 (Handle) 。
此时你由外向内执行 rm,只是把"文件名"从目录里抹去了。但在内核眼里,只要进程还抓着句柄,文件就必须活着 。
数据依然在写入磁盘,占用的空间依然在那里,只是你看不见了(ls 找不到)。
捉鬼实战:
- 揪出僵尸:
使用lsof命令查看"已删除但未释放"的文件:
bash
lsof | grep deleted
输出:
text
java 12345 root 1w REG 253,1 10737418240 /var/log/app.log (deleted)
看!那个 10GB 的文件还在,状态是 (deleted)。
- 超度僵尸(解决方法):
- 方案一(推荐): 重启持有文件的进程(如
systemctl restart nginx)。进程一死,句柄释放,空间瞬间回来。 - 方案二(不重启): 如果不能停服,可以使用重定向清空文件内容:
bash
# 找到进程 ID 和文件描述符 (比如 pid 12345, fd 1)
ls -l /proc/12345/fd/1
# 清空它
> /proc/12345/fd/1
这会让文件大小变为 0,空间释放,且进程不会报错。
2. 嫌疑人 B:数据库的"瑞士奶酪" (InnoDB Level)
这是 MySQL 数据库中最常被误解的机制。
核心原理:高水位线 (High Water Mark)
InnoDB 的表空间(.ibd 文件)就像一个总是只扩不缩的仓库。
当你执行 DELETE 时,InnoDB 只是把这些数据页标记为 "可复用" (Marked as Deleted)。
- 它就像切掉了一块奶酪,在这个位置留下了空洞 (Hole)。
- 如果有新数据插入,MySQL 会尝试填入这些空洞。
- 但是: 它绝不会自动把文件尾部缩回去还给操作系统。
比喻:
你住酒店。就算这一层的客人都退房了(DELETE),酒店大楼(ibd 文件)也不会因此变矮一层。房间空着,但楼还在。
捉鬼实战:
- 确认碎片率:
查询information_schema.TABLES:
sql
SELECT table_name, data_length, data_free
FROM information_schema.TABLES
WHERE table_schema = 'mydb' AND data_free > 0;
data_length: 实际数据大小。data_free: 碎片大小(空洞)。如果这个值很大(比如 500G),说明你可以回收空间。
- 超度僵尸(解决方法):
- 方案一(推荐): 使用
OPTIMIZE TABLE或ALTER TABLE ... ENGINE=InnoDB。
sql
ALTER TABLE orders ENGINE=InnoDB;
原理: MySQL 会悄悄建一个新表,把有效数据紧凑地搬过去,然后删掉旧表。这会彻底释放空洞,把磁盘空间还给 OS。
注意:这是重操作,会消耗大量 I/O,请在低峰期执行或使用 gh-ost / pt-online-schema-change。
- 方案二(暴力):
TRUNCATE TABLE。
如果你要把整张表删光,别用DELETE,直接用TRUNCATE。它是物理删除,直接重建表文件,空间瞬间释放。
3. 终极避坑:如何防止僵尸复活?
- 对于日志文件:
- 不要直接
rm。 - 使用 Logrotate 工具,配置
copytruncate模式(先拷贝再清空),或者确保轮转后发送信号(HUP)给进程让其重开日志。
- 对于数据库:
- 冷热分离: 不要把归档数据和热数据混在一起。定期把旧数据迁移到历史表,然后直接
DROP或TRUNCATE旧分区。 - 分区表 (Partitioning): 按时间分区(如
p202501)。删除数据时直接ALTER TABLE DROP PARTITION,这是释放空间最快、最彻底的方法。
4. 总结
磁盘满了 不一定是真的满了,可能是"僵尸"在作祟。
- Linux 僵尸: 文件删了,进程没放手。 -> 重启进程或清空句柄。
- MySQL 僵尸: 行删了,表空间没缩水。 -> 重建表或使用分区。