news 2026/10/21 15:32:44

磁盘空间充足却无法写入?MySQL inode耗尽排查指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
磁盘空间充足却无法写入?MySQL inode耗尽排查指南

长假回来第一天,某生产库的报警群就先炸了。业务方贴了一串截图:MySQL 写入报错,订单明细表插不进数据,应用日志里明晃晃写着 No space left on device。可我登录机器敲 df -lh 一看,根分区用了 58%,数据目录所在挂载点也剩着十几个G。df -lh 显示空间充足,MySQL 却无法写入,这种矛盾放谁身上都容易慌,甚至会怀疑是数据库文件损坏、表空间坏了。先别急着动库,这类问题其实不罕见,而且九成有固定套路可查。

按我个人的处理习惯,遇到“磁盘明明没满但写入失败”,第一个动作永远是拉开一张清单,把系统层和数据库层分开排查,而不是一上来就 kill、重启、删日志。这个坑我踩过不止一次,尤其是在节后复产这个特殊时间点:假期积攒的任务日志、备份残留、临时文件,加上日志轮转偶尔没跑干净,很容易把系统的 inode 或某个特定挂载点打满,而表面上的容量使用率却欺骗性十足。下面把这次完整的排查过程和恢复操作写出来,给同样踩坑的人一个参考。

1. 先把现象和影响范围界定清楚

1.1 故障现象:磁盘指标和写入行为打架

当天我们拿到机器权限后,先做了一件事:复现问题。应用侧报错是:

SQLSTATE[HY000]: General error: 1114 (HY000): The table 'order_detail_2024' is full

应用日志里还出现了操作系统的 errno 28,也就是 No space left on device。登录数据库执行几条 insert,MySQL 直接返回磁盘空间不足。但同一时刻 df -lh 看到的是 / 分区 58%,/data 分区 72%,怎么看都不像“满”。更诡异的是,同一台机器上的其他应用写入正常,只有 MySQL 报错。这种组合很容易给人一个暗示:问题出在 MySQL 内部,和操作系统没关系。

其实我当时瞥了一眼 df -i,心里基本有底了,inode 使用率几乎 100%。系统层指标分“容量”和“可用文件索引”两个维度,df -lh 只看前者,后者要用 df -i 才看得到。MySQL 写入表数据,不只是往已有的数据文件里追加内容,它还要频繁创建临时文件、扩展表空间、分配新文件块。任何一个环节拿不到“编号”,都会直接返回 no space left on device。

1.2 为什么 df -lh 会给你“错误的安全感”

df -lh 这个命令展示的是文件系统块的使用情况,也就是以 KB/GB 为单位统计的容量。它本身没错,但它只回答一个问题:存储块还有多少空闲。而文件系统里还有另一套资源叫 inode,中文一般叫索引节点,每个文件或目录都占一个 inode。创建新文件、创建新临时文件,都要消耗一个 inode。inode 一旦耗尽,哪怕磁盘上还有几十G空块,你也新建不了文件。

我用一个类比解释给当时在线的同学听:文件系统就像一个停车场,存储块是车位,inode 是闸机口的停车票。df -lh 告诉你停车场里还有多少个空车位,但它不告诉你票已经发完了。MySQL 现在不是停不进车位,而是压根进不了闸机,它在闸机口拿不到票,系统就回一句“没有空位了”。这就是为什么 df -lh 显示空间充足,MySQL 却无法写入。

还有两个隐藏的“欺骗点”。第一,普通用户写文件还会受 root reserved blocks 限制,ext4 文件系统默认保留 5% 的块给 root,普通用户的可写容量不等于 df 显示的总可用量。第二,MySQL 的 datadir、binlog 目录、tmpdir 可能分布在不同的挂载点上,你以为查了数据目录所在的分区,实际上写满的是日志所在的分区。排查时两个维度都要看,单独盯着一个 df -lh 很容易漏。

1.3 “写入失败”和“空间不足”并不完全等价

MySQL 的一次普通 insert,可能涉及三类磁盘动作:往表空间数据文件写入数据页、在 tmpdir 创建排序或临时表文件、写 binlog 和 redo/undo 日志。任何一个“必须新建文件”的环节失败,都可能回传 errno 28。所以排查问题时,我一般把“数据库报错”当成结果,把“哪个文件、哪个目录、哪个进程要新建文件”当成真正的排查目标。

这也决定了处理顺序:先搞清楚是哪个环节分配文件失败,再决定清哪里、怎么清。直接重启 MySQL 或盲目删大文件,往往会把现场破坏掉,甚至让问题更难定位。下面第二部分的排查顺序就是围绕这个思路展开的。

2. 排查顺序:先系统层,后 MySQL 层

2.1 拿到故障先稳住:看错误日志和系统消息

我处理这类问题的第一步从来不是执行命令刷屏,而是先确认三样东西:MySQL 自身的 error log、系统 dmesg 里的文件系统报错、当前所有挂载点的状态。

MySQL error log 默认路径一般在 datadir 下面,文件名通常是主机名加 .err,可以用下面命令找到:

show variables like 'log_error';

重点看最近有没有这类内容:

[ERROR] InnoDB: Write to file /data/mysql/ibtmp1 failed with error 28 [ERROR] InnoDB: Could not allocate a new page

dmesg 里可能出现 ext4 文件系统相关告警:

dmesg -T | grep -i ext4 | tail -20

这一步的核心目的是区分“文件系统本身坏了”“容量问题”“inode 问题”三类方向。不要一上来就重启数据库,否则如果文件系统已经处于异常状态,重启可能是雪上加霜。

2.2 系统层“四查”:容量、inode、挂载点、文件句柄

确认日志后,我习惯按顺序跑四个命令,每个命令解决一类问题。

第一查容量,用 df -hT 而不是 df -lh,T 参数能把文件系统类型也显示出来:

df -hT

第二查 inode:

df -i

如果某个挂载点的 IUse% 到了 100% 或者接近 100%,那基本可以确认问题方向了。

第三查挂载关系,确认 MySQL 数据目录、日志目录、临时目录到底落在哪个分区:

lsblk mount | grep -E ' / |/data|/var|/tmp'

这一步能揪出“查错分区”的坑:比如数据目录是 /data/mysql,但你刚才 df -lh 只看了根分区;又比如 binlog 写在单独挂载的 /var/log/mysql 分区,这个分区悄悄满了。

第四查文件句柄,看有没有“已被删除但仍被进程占用”的大文件:

lsof +L1 | head -30

如果这行命令输出很多 deleted 文件,或者里面出现了 mysqld、java、nginx 等进程名,说明有进程握着已经删除的文件句柄,磁盘空间实际上被占着,但你用常见命令在目录里找不到这些文件。这种情况在 logrotate 轮转日志后尤其常见。

2.3 MySQL 层“三查”:目录配置、进程状态、引擎状态

系统层查完没发现问题,再进 MySQL 层。我一般做三件事。

第一,确认目录配置:

show variables like '%dir%'; show variables like 'log_bin_basename'; show variables like 'tmpdir';

拿到的 datadir、tmpdir、log_bin_basename,再和 df -hT、mount 的结果做匹配,确认每个目录落在哪个挂载点、剩余容量是多少。

第二,看进程状态:

show full processlist;

重点看有没有长时间 running 的查询、临时表排序、阻塞事务。长事务在假期期间可能积压过大量 undo 日志和临时文件,节后释放时容易造成磁盘瞬时写满。

第三,看引擎状态:

show engine innodb status\G show variables like 'innodb_data_file_path'; show variables like 'innodb_file_per_table';

InnoDB 的共享表空间 ibdata1 如果开启了自动扩展,理论上会持续增长;如果系统表空间或临时表空间所在分区满了,也会出现写入失败。这个环节的目标是把“是不是 MySQL 内部逻辑导致的分区写满”和“是不是外部文件系统问题”彻底分开。

3. 完整排查实录与恢复操作

3.1 第一刀:df -i 暴露了 inode 100%

我们的第一步确认就是 df -i。输出大概是这样的:

Filesystem Inodes IUsed IFree IUse% Mounted on /dev/sda3 19922944 19922810 134 100% /

根分区 inode 已经 100% 耗尽。mysql 的 datadir 在 /data 上,但运行过程中产生的临时文件、日志碎片、进程间的 socket 文件、以及系统层面的各种小文件,都可能落在 / 分区下。MySQL 在写文件时如果需要在 / 分区下新建临时文件(比如 tmpdir 或进程写日志),就会撞上 inode 耗尽。

接下来定位 inode 消耗大户。我用的命令是:

find / -xdev -type f -printf '%h\n' 2>/dev/null | sort | uniq -c | sort -rn | head -20

这条命令的思路是:进入根分区(-xdev 防止跨文件系统),把所有文件的目录名打印出来,按目录统计文件数量,排在前面的就是 inode 消耗重灾区。我们当时的前几名里有 /tmp、/var/spool、/var/tmp。打开一看,/tmp 下有几十万个 session 文件,/var/spool 下也积压了大量临时任务文件。

节后出现这个状态不奇怪:放假期间定时任务、临时脚本、监控探测都在持续运行,有些任务只写不删,小文件一多,inode 就先撑爆了。容量看起来还够,但新的文件已经建不出来了。

3.2 第二刀:lsof 揪出删除后未释放的旧句柄

清理小文件之前,我又顺手查了一遍 lsof,这一步后来证明非常关键:

lsof +L1 | grep -iE 'deleted|mysqld' | head -30

输出里出现了一行类似这样的记录:

mysqld 12345 mysql 11u REG 8,3 4294967296 0 /tmp/mysql_sort_XXXX (deleted)

这个文件是一个 MySQL 创建后就删除的排序临时文件,大小约 4G,虽然文件已被删除,但 mysqld 进程仍然持有句柄。也就是说,如果在 inode 清理完之前重启 MySQL,这个句柄会被正常释放;但如果我们把注意力全放在删小文件上,这 4G 并不会消失,而且如果在删除文件后、重启前持续写入,占用的空间还可能继续增长。

我这里用的验证命令是:

ls -l /proc/$(pgrep -x mysqld)/fd 2>/dev/null | grep deleted

如果看到 deleted 文件,处理办法通常是:确认这个进程可以重启后,选一个维护窗口重启服务;或者至少记录下进程名、文件大小,纳入后续处理计划。MySQL 排序临时文件产生的 deleted 句柄,在重启 mysqld 后就会释放。生产环境要不要马上重启,要看业务容忍度,但方向一定要明确:不释放句柄,它占的空间永远回不来。

3.3 第三刀:清理、验证、恢复写入

整个恢复过程分四步。

第一步先清理小文件。我们当时把 /tmp 和 /var/tmp 下超过 7 天且无进程占用的临时文件删了一批,并清理了 /var/spool 下已经处理完的残留任务。清理后立刻再看一次 df -i,inode 使用率从 100% 跌到 78%,这说明方向正确。清理临时文件的命令要谨慎,先看再删,删除时建议带 mtime 条件:

find /tmp -type f -mtime +7 -delete

第二步处理 deleted 句柄。因为当时是上午业务高峰,直接重启 MySQL 影响太大,我们选择在低峰窗口重启了一次 mysqld,重启后那 4G 空间立刻释放。重启前确认了主从同步状态和连接数正常,避免重启造成二次故障。

第三步验证写入。验证包括三件事:再看一遍 df -i 和 df -hT,确认两个维度都健康;在临时目录做一个写入测试:

dd if=/dev/zero of=/tmp/testfile bs=1M count=100

然后进 MySQL 执行一条简单 insert,确认不再报 1114 错误。测试文件记得删掉,别留下新的垃圾。

第四步,把这次故障涉及到的问题点全部加固一遍,后面单开一节讲。

3.4 事后加固:把这台机器拉回“健康基线”

故障恢复只是上半场,下半场是防止它再次发生。我们当时做了四件事。

第一,监控里补上 inode 指标。Prometheus 的 node_exporter 本身就暴露了 node_filesystem_files_free、node_filesystem_inodes 这些指标,直接加一条告警规则就行;Zabbix 对应的是 vfs.fs.inode 监控项。告警阈值设在 80% 就要开始关注,别等 100%。

第二,把 /tmp 分区独立出来,并限制大小。当时 /tmp 在根分区上,依赖根分区的 inode 资源。后来我们给 /tmp 单独挂了一个 2G 的 tmpfs,重启后自动清空,既能保证不写满根分区,也让临时文件的清理变得无脑。注意 tmpfs 会占用内存,大小要按业务实际情况评估。

第三,检查 MySQL 的 tmpdir 配置。临时表排序文件最好写到独立挂载点,并且和生产数据目录分开,避免相互挤占。这个改动需要重启 MySQL 生效,建议好好规划和验证。

第四,binlog 过期时间确认一下。MySQL 8 默认用 binlog_expire_logs_seconds,旧版本是 expire_logs_days。如果 binlog 长期不清理,日志分区被写满只是时间问题。检查命令:

show variables like 'binlog_expire_logs_seconds';

顺带说一下,在确认从库都跟得上进度的情况下,可以用 PURGE BINARY LOGS 清理已经过期的 binlog,不要直接手工 rm binlog 文件,否则会破坏 binlog index 文件的一致性。

4. 常见原因速查表与快速定位

4.1 典型问题对照表

这次故障只是“磁盘显示充足但写不进去”里最常见的一种。实际生产环境中还有好几类原因也会造成同样的表面现象,我把它们整理成一张对照表,遇到问题可以直接按表格方向排查。

表面现象可能原因快速确认方式处理方式
df -h 显示满容量真的耗尽df -hT 看具体挂载点清日志、清 binlog、扩容
df -h 有剩余,df -i 100%inode 耗尽df -i清理小文件,优化目录规划
删除大文件后 df -h 不变进程持有已删除文件句柄lsof +L1 / /proc/*/fd重启对应进程,或记录后处理
df -h 还剩 5%+,普通用户写入失败ext4 保留块耗尽tune2fs -l /dev/xxx调整 reserved-blocks 比例
mount 显示 ro文件系统异常或强制只读mount、dmesg检查修复文件系统,避免异常重启
单表 insert 报 1114,磁盘正常表空间满或共享表空间达到限制show table status、检查 innodb_data_file_path清理表数据、调整表空间策略
大排序、临时表导致写入失败tmpdir 空间不足show variables like 'tmpdir'清理临时文件,调整 tmpdir 位置
binlog 目录写满但数据目录正常日志与数据目录分离对比 datadir 和 log_bin_basename清理过期 binlog,调整保留天数
应用报 no space 但文件系统正常磁盘配额 quota 限制quota 相关状态查询调整配额或清理文件
能读不能写、报权限错误SELinux 或属主问题audit2why、ls -lZ调整上下文或权限设置
网络盘场景下写入卡死NFS 服务端故障或锁问题mount、nfsstat联系存储侧处理,切换副本

这张表不能覆盖所有场景,但能覆盖生产环境里 90% 以上的“假空间不足”问题。遇到新问题,先按表里的顺序逐项排除,别急着重启数据库。

4.2 容易被误判的“伪空间不足”

这里补充两个我在实际中经常看到的误判。

第一个是 ext4 保留块。ext系列文件系统默认给 root 用户保留约 5% 的块,用来防止磁盘写满后系统无法完成关键写操作。这 5% 在 df 输出里是计入“已用”还是“可用”?实际表现是:非 root 用户写文件到 95% 时就会报 no space left on device,但 root 还能继续写。如果 MySQL 以普通用户运行,遇到 df -h 显示还剩 5% 但写入失败,就要往这个方向想。

查看和调整保留比例的命令:

tune2fs -l /dev/sda3 | grep -i 'reserved block count' tune2fs -m 1 /dev/sda3

把保留比例从 5% 降到 1% 能让普通用户可写容量变大,但会降低磁盘满时的系统可用空间,生产环境调整前要评估风险和团队规范,不要私自乱改。

第二个是文件系统被强制重新挂载成只读。Linux 在文件系统出现异常时,有时会主动把挂载状态切成只读,防止数据进一步损坏。此时 df -h 显示正常,MySQL 写入却报错,而且报错可能不是 errno 28,而是 “read-only file system” 或 “Permission denied”。遇到这种,先看 mount 输出:

mount | grep -E ' / | /data'

如果出现了 ro,优先确认服务器物理状态、磁盘健康度,并尽快准备切换或迁移,不要试图直接在 ro 状态下去改数据。

4.3 节后复产专项检查清单

节后第一天是生产事故高发时段,主要原因是假期里各种定时任务照常跑,但很少有人在现场盯告警。建议按照下面的清单花十到十五分钟快速过一遍,特别是数据库机器。

  • 第一屏看监控:磁盘容量使用率、inode 使用率、进程文件句柄数,三个指标放一起看。
  • df -hT 和 df -i 两条命令必须同时跑,只有 df 没有 df -i 等于只看了一半。
  • 检查 MySQL error log 里有没有近期出现 error 28、1114、read-only 等关键词。
  • 查看 logrotate 状态,确认假期轮转任务是否正常完成。
systemctl status logrotate.timer cat /var/lib/logrotate/status | tail -20
  • 检查 /tmp、/var/tmp、备份暂存目录的文件数量和大小。
  • 看一下 show full processlist,找有没有跨假期未结束的长事务。
  • 确认 binlog 目录大小和过期策略是否正常。
  • 节后第一周,数据库变更和重启操作尽量收敛,除非有明确故障,否则不折腾。

这个过程用不了多少时间,但能把我们这次踩的坑提前挡在门外。

5. 踩过几次坑之后,我留下的几个习惯

这个坑我前前后后遇到过三四次,印象最深的不是 inode 到底消耗在哪,而是屏幕上那个刺眼的 100% 和 df -lh 的从容剩余同时出现时,人很容易被误导。现在我处理生产告警有个固定习惯:任何 disk 相关报警,第一屏永远同时放 df -h 和 df -i;监控面板上磁盘使用率必须挂 inode 曲线。这俩指标是孪生兄弟,看一个不看另一个一定会出事。

另一个习惯是,遇到 MySQL 写入失败,我一定会先问自己一句:它要写的那个文件,落在哪个目录、哪个挂载点、谁能新建文件?把这个想清楚,排查范围就锁死了一大半。对 MySQL 临时目录、日志目录和 binlog 目录,平时就做好 80% 预警线和自动化清理,比出了事再抢修舒服太多。最后留一句经验:怀疑 MySQL 坏了之前,先确认你看到的剩余空间,真的是它可以用得了的剩余空间。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/21 15:32:14

内网渗透全流程:从信息收集到横向移动的实战攻击链路

我一直觉得,内网渗透是最能拉开安全从业者差距的方向之一。外网打点拼的是漏洞基础和运气,进了内网之后,比的就是信息收集的细度、权限维持的耐心,以及横向移动时对不同环境的判断。今天我把自己平时在授权靶场里反复验证过的路径…

作者头像 李华
网站建设 2026/10/21 15:31:15

MTK Camera Metadata Size配置详解:从原理到实战排查

直接把“配置size”这件事讲透。这篇文章不打算从头讲MTK camera整体架构,那些HAL层怎么流转、pipeline怎么构建,网上文档不少,但唯独“metadata里的size到底怎么配、配错了会发生什么、为什么这么配”这件事,很少有人掰开揉碎讲清…

作者头像 李华
网站建设 2026/10/21 15:29:35

Python五子棋实战源码:Tkinter GUI开发与坐标映射详解

简介:本资源是一份面向计算机专业本科生的Python毕业设计完整实践材料,聚焦五子棋游戏开发全流程,帮助学习者掌握GUI编程、游戏逻辑设计与软件工程规范。压缩包含1个253KB的Word文档(.doc),完整涵盖摘要、需…

作者头像 李华
网站建设 2026/10/21 15:29:11

把个人技能当系统来维护:从盘点、评分到高效成长的完整方法

1. "skills"不是简历上那三行字,而是一个需要维护的系统前两年我做过一次很尴尬的复盘。当时想跳槽,打开简历准备更新技能栏,盯着屏幕想了二十分钟,写出来的是"熟悉办公软件、掌握某种编程语言、了解数据库"。…

作者头像 李华
网站建设 2026/10/21 15:28:15

Halcon血管支架视觉检测:高精度测量与缺陷识别全解析

血管支架直径只有几毫米,上面的金属网丝细得像头发丝,公差却常常要求在几十微米以内。这种零件放到产线上做全检,靠人眼盯显微镜不现实,靠传统接触式测量又容易划伤表面。所以这几年很多医疗器械厂家在找机器视觉方案,…

作者头像 李华