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

发布时间:2026/10/21 15:32:46
磁盘空间充足却无法写入?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。登录数据库执行几条 insertMySQL 直接返回磁盘空间不足。但同一时刻 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 pagedmesg 里可能出现 ext4 文件系统相关告警dmesg -T | grep -i ext4 | tail -20这一步的核心目的是区分“文件系统本身坏了”“容量问题”“inode 问题”三类方向。不要一上来就重启数据库否则如果文件系统已经处于异常状态重启可能是雪上加霜。2.2 系统层“四查”容量、inode、挂载点、文件句柄确认日志后我习惯按顺序跑四个命令每个命令解决一类问题。第一查容量用 df -hT 而不是 df -lhT 参数能把文件系统类型也显示出来df -hT第二查 inodedf -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 -iinode 使用率从 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 bs1M count100然后进 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 坏了之前先确认你看到的剩余空间真的是它可以用得了的剩余空间。