明明删了数据,磁盘却满了?

凌晨 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 中,一个文件被"真正删除"需要满足两个条件:

  1. Link Count = 0: 文件名被删除了(你执行 rm 做的事)。
  2. Reference Count = 0: 没有进程正"抓"着这个文件(Open File Descriptor)。

僵尸是如何诞生的?

当你的 Java/Python/Nginx 进程正在疯狂往 application.log 里写日志时,它持有这个文件的 句柄 (Handle)

此时你由外向内执行 rm,只是把"文件名"从目录里抹去了。但在内核眼里,只要进程还抓着句柄,文件就必须活着

数据依然在写入磁盘,占用的空间依然在那里,只是你看不见了(ls 找不到)。

捉鬼实战:
  1. 揪出僵尸:
    使用 lsof 命令查看"已删除但未释放"的文件:
bash 复制代码
lsof | grep deleted

输出:

text 复制代码
java  12345 root  1w  REG  253,1  10737418240  /var/log/app.log (deleted)

看!那个 10GB 的文件还在,状态是 (deleted)

  1. 超度僵尸(解决方法):
  • 方案一(推荐): 重启持有文件的进程(如 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 文件)也不会因此变矮一层。房间空着,但楼还在。

捉鬼实战:
  1. 确认碎片率:
    查询 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),说明你可以回收空间。
  1. 超度僵尸(解决方法):
  • 方案一(推荐): 使用 OPTIMIZE TABLEALTER TABLE ... ENGINE=InnoDB
sql 复制代码
ALTER TABLE orders ENGINE=InnoDB;

原理: MySQL 会悄悄建一个新表,把有效数据紧凑地搬过去,然后删掉旧表。这会彻底释放空洞,把磁盘空间还给 OS。
注意:这是重操作,会消耗大量 I/O,请在低峰期执行或使用 gh-ost / pt-online-schema-change

  • 方案二(暴力): TRUNCATE TABLE
    如果你要把整张表删光,别用 DELETE,直接用 TRUNCATE。它是物理删除,直接重建表文件,空间瞬间释放。

3. 终极避坑:如何防止僵尸复活?

  1. 对于日志文件:
  • 不要直接 rm
  • 使用 Logrotate 工具,配置 copytruncate 模式(先拷贝再清空),或者确保轮转后发送信号(HUP)给进程让其重开日志。
  1. 对于数据库:
  • 冷热分离: 不要把归档数据和热数据混在一起。定期把旧数据迁移到历史表,然后直接 DROPTRUNCATE 旧分区。
  • 分区表 (Partitioning): 按时间分区(如 p202501)。删除数据时直接 ALTER TABLE DROP PARTITION,这是释放空间最快、最彻底的方法。

4. 总结

磁盘满了 不一定是真的满了,可能是"僵尸"在作祟。

  • Linux 僵尸: 文件删了,进程没放手。 -> 重启进程或清空句柄。
  • MySQL 僵尸: 行删了,表空间没缩水。 -> 重建表或使用分区。
相关推荐
014-code1 小时前
Redisson 常用技巧
java·redis
之歆1 小时前
HA 高可用集群指南
java·开发语言
CHANG_THE_WORLD2 小时前
指针入门一
java·前端·网络
时艰.2 小时前
订单系统读写分离方案设计与实现
java
014-code2 小时前
MySQL 事务隔离级别
java·数据库·mysql
hrhcode3 小时前
【Netty】三.ChannelPipeline与ChannelHandler责任链深度解析
java·后端·spring·springboot·netty
invicinble3 小时前
关于学习技术栈的思考
java·开发语言·学习
json{shen:"jing"}4 小时前
分割回文串-暴力法
java·算法
没有bug.的程序员4 小时前
Maven 进阶进阶:依赖优化内核、多模块构建艺术与“依赖地狱”自愈指南
java·maven·构建·多模块·依赖优化