RSS
菜单
全部文章快讯开发科技深度热点

第 66 章 磁盘与容量踩坑

内容摘要

磁盘是数据库最容易"慢性出问题"的资源——它不会突然挂,但会慢慢被 binlog、临时文件、膨胀的 undo log 占满,等告警时已经写不进去。本篇基于 MySQL 8.0,梳理数据/binlog 分盘、水位线、undo 膨胀、inode、SSD 选型等 11 个高频坑。前置知识是 InnoDB 架构与备份。学完后你应能规划磁盘布局、识别磁盘增长来源、并提前做容量规划。

📖 本篇简介:磁盘是数据库最容易"慢性出问题"的资源——它不会突然挂,但会慢慢被 binlog、临时文件、膨胀的 undo log 占满,等告警时已经写不进去。本篇基于 MySQL 8.0,梳理数据/binlog 分盘、水位线、undo 膨胀、inode、SSD 选型等 11 个高频坑。前置知识是 InnoDB 架构与备份。学完后你应能规划磁盘布局、识别磁盘增长来源、并提前做容量规划。

目录

  • 坑 1:数据文件和 binlog 放在同一块盘
  • 坑 2:不设磁盘水位线等告警才处理
  • 坑 3:delete 后表空间不回收
  • 坑 4:undo log 异常膨胀吃满磁盘
  • 坑 5:只监控磁盘容量不监控 inode
  • 坑 6:临时文件/排序文件占满磁盘
  • 坑 7:binlog 保留时间过长吃盘
  • 坑 8:不做磁盘扩容预案
  • 坑 9:SSD 选型不看写入放大与寿命
  • 坑 10:大表不做分区导致单文件膨胀
  • 坑 11:忽略 OS page cache 对容量的影响
  • 磁盘容量规划思路
  • 速查表:磁盘相关关键项
  • 自测练习

坑 1:数据文件和 binlog 放在同一块盘

场景 / 错误示范:

datadir 和 binlog 都放在同一块磁盘上。高峰期 binlog 写入和数据文件读写争抢 IO,磁盘 IO 打满,主从复制和业务查询一起变慢。

# 错误:数据和 binlog 同盘,IO 互相争抢
[mysqld]
datadir = /var/lib/mysql          # 数据文件
log_bin = /var/lib/mysql/mysql-bin  # binlog 也在同一块盘

为什么是坑:数据文件(读写混合)和 binlog(顺序写)的 IO 特征不同,放同一块盘会互相争抢 IO 带宽。更危险的是——同盘一旦写满,数据文件和 binlog 一起写不进去,数据库直接 hang。生产环境应该把数据、binlog、redo log 分到不同磁盘,隔离 IO 也隔离故障。

正确做法:

数据、binlog、redo 分盘部署。

# 正确:不同数据放不同磁盘(本示例基于 MySQL 8.0)
[mysqld]
datadir = /data/mysql            # 数据文件,独立磁盘
log_bin = /binlog/mysql-bin      # binlog,独立磁盘
innodb_log_group_home_dir = /redolog   # redo log,低延迟磁盘

加分点:redo log 对延迟最敏感,建议放最快的盘(NVMe SSD);binlog 顺序写,普通 SSD 即可。


坑 2:不设磁盘水位线等告警才处理

场景 / 错误示范:

磁盘默默增长,DBA 没设水位线告警。某天磁盘 100%,数据库写不进去,业务大面积报错才发现。

# 错误:不监控磁盘增长,写满才发现
df -h   # 从来没人定期看,直到数据库报错

为什么是坑:磁盘满是数据库致命故障——无法写数据、无法写 binlog、甚至实例 crash。磁盘增长是渐进的,不设水位线就只能"等出事"。水位线能在磁盘还充裕时就预警,给 DBA 留出清理/扩容的从容时间。

正确做法:

分级水位线 + 增长速率监控。

# 正确:多级水位(本示例基于 Linux)
# 70% 通知关注 / 80% Warning 开始清理 / 85% Critical 紧急处理
# 同时监控增长速率,预测何时打满
df -h /data
-- 定期清理过期 binlog,是控制磁盘增长的日常操作(本示例基于 MySQL 8.0)
PURGE BINARY LOGS BEFORE '2026-10-01 00:00:00';

加分点:磁盘监控要同时看"水位"和"增速",后者能提前预测"几天后打满"。


坑 3:delete 后表空间不回收

场景 / 错误示范:

运营删了历史 3 亿行订单,du -sh 看表文件却一点没小。磁盘还是占着,因为 InnoDB 删除数据只是标记删除,不把磁盘还给 OS。

-- 错误:delete 大量历史数据,以为磁盘立刻释放
DELETE FROM t_order WHERE create_time < '2024-01-01';
-- 删完表文件 .ibd 大小不变,磁盘空间没还给 OS

为什么是坑:InnoDB 删除数据只是在 B+ 树中标记删除,页空闲了但不归还操作系统。表文件还是那么大。DBA 以为"删了数据磁盘就空了",结果磁盘没降,空间被这些"已删除但未归还"的页占着,长期碎片化。要真正回收磁盘,得 OPTIMIZE TABLE 或重建表。

正确做法:

大批量删数据后,用重建表或 OPTIMIZE 回收空间。

-- 正确:删除后回收表空间(本示例基于 MySQL 8.0)
-- 方式 1:OPTIMIZE TABLE(会重建表,锁表,低峰执行)
OPTIMIZE TABLE t_order;

-- 方式 2:更平滑的方式,新建表交换(pt-online-schema-change 思路)
-- 大批量删数据优先用分区 DROP,而不是 DELETE
ALTER TABLE t_order DROP PARTITION p2023;   -- 删分区瞬间回收,远快于 delete

加分点:历史数据清理优先用分区表 DROP PARTITION,比 DELETE+OPTIMIZE 快几个数量级。


坑 4:undo log 异常膨胀吃满磁盘

场景 / 错误示范:

一个大事务长时间不提交,InnoDB 要保留 undo log 供 MVCC 回滚,undo 目录越涨越大,最后把磁盘吃满。业务一直没察觉这个长事务。

-- 错误:长事务不提交,undo 无法 purge,持续膨胀
START TRANSACTION;
SELECT * FROM t_big ORDER BY id;   -- 开了事务一直不 commit
-- 期间其他事务的旧版本都要靠 undo 保留,undo 目录暴涨

为什么是坑:InnoDB 的 undo log 用于事务回滚和 MVCC 一致性读。只要有长事务还没提交,它"看到的旧版本"就不能被 purge 清理。长事务开着不提交,undo log 就一直膨胀,越积越多最后吃满磁盘。这是"长事务拖垮磁盘"的经典路径。

正确做法:

监控长事务,及时结束;并配置 undo 独立表空间自动回收。

-- 1) 找出最长事务(本示例基于 MySQL 8.0)
SELECT trx_id, trx_started, trx_mysql_thread_id, trx_query
FROM information_schema.INNODB_TRX
ORDER BY trx_started ASC LIMIT 5;
-- 找出跑了很久的事务,KILL 掉或催业务提交

-- 2) undo 独立表空间并自动回收
SET GLOBAL innodb_undo_log_truncate = ON;
# 正确:undo 独立表空间,避免污染系统表空间
[mysqld]
innodb_undo_tablespaces = 2
innodb_undo_log_truncate = ON

加分点:监控里专门加一条"最老事务持续时间",超过阈值告警,是防 undo 膨胀的关键。


坑 5:只监控磁盘容量不监控 inode

场景 / 错误示范:

磁盘容量还剩 50%,但 MySQL 报错"No space left on device"。排查发现是 inode 耗尽了——目录里有几十万个小文件(临时文件、慢日志),inode 用光,即使还有磁盘容量也创建不了新文件。

# 错误:只 df -h 看容量,不看 inode
df -h /data        # 容量还 50%,看似正常
df -i /data        # inode 已 100%,无法创建新文件

为什么是坑:Unix 文件系统里每个文件都要占一个 inode。海量小文件会把 inode 耗尽,此时即使磁盘还有容量也无法新建文件。数据库的临时文件、日志、表文件都是小文件,数量多了 inode 先于容量耗尽。只监控容量不监控 inode,就会遇到"有空间却写不了"的怪事。

正确做法:

inode 也要监控,及时清理小文件。

# 正确:同时监控容量和 inode
df -h /data    # 容量水位
df -i /data    # inode 水位,接近 100% 也要告警
# 定位哪个目录小文件最多
du -sh /data/mysql/#sql* 2>/dev/null   # 临时文件残留
find /data -type f | wc -l

加分点:临时表、临时文件残留是 inode 杀手,定期清理 #sql* 临时文件。


坑 6:临时文件/排序文件占满磁盘

场景 / 错误示范:

一个大 ORDER BY 或 ALTER TABLE 产生巨大的临时排序文件,写到了数据盘,瞬间占满磁盘。

-- 错误:大排序/DDL 在数据盘产生巨大临时文件
SELECT * FROM t_big ORDER BY text_col;   -- 内存放不下,落盘临时表占满磁盘

为什么是坑:排序、join、临时表超过内存阈值会落盘成临时文件,大查询能产生几个 G 甚至几十 G 的临时文件。这些文件默认放在数据目录,瞬间占满磁盘。大促或批量任务跑这类查询,是磁盘突发占满的常见原因。

正确做法:

临时目录单独挂载大磁盘,并控制大查询。

# 正确:临时文件目录单独放一块盘(本示例基于 MySQL 8.0)
[mysqld]
tmpdir = /tmpfs_or_separate_disk   # 临时文件单独大盘,不挤数据盘
-- 监控临时表落盘(本示例基于 MySQL 8.0)
SHOW GLOBAL STATUS LIKE 'Created_tmp_disk_tables';
-- 突增时及时定位是哪条大查询

加分点:大表 ALTER 用在线 DDL 工具(pt-osc/gh-ost),临时中间文件可控,不直接在数据盘爆量。


坑 7:binlog 保留时间过长吃盘

场景 / 错误示范:

expire_logs_days 设成 30 天,binlog 每天增长 20G,30 天就是 600G,磁盘被 binlog 吃掉一大半。

# 错误:binlog 保留太久,只增不删
[mysqld]
expire_logs_days = 30    -- 30 天 binlog 累积,占盘严重

为什么是坑:binlog 用于主从复制和数据恢复,但不是越久越好。保留太久会持续吃磁盘。保留策略要平衡"复制/恢复需要"和"磁盘成本"——从库都正常、备份也全的情况下,没必要留几十天 binlog。

正确做法:

合理设置 binlog 过期时间,配合自动清理。

# 正确:8.0 用 binlog_expire_logs_seconds(本示例基于 MySQL 8.0)
[mysqld]
binlog_expire_logs_seconds = 604800   -- 保留 7 天,足够复制和常规恢复
-- 手动清理也可(本示例基于 MySQL 8.0)
PURGE BINARY LOGS BEFORE '2026-10-01 00:00:00';
-- 注意:清理前确认从库已经拉取过这些 binlog,别把从库还需要的删了

加分点:从库延迟大时别盲目 purge,先确认从库的 Relay_Master_Log_File 已经追过要删的位点。


坑 8:不做磁盘扩容预案

场景 / 错误示范:

磁盘用了 85%,DBA 才发现,想扩容。结果云盘扩容要停机、要迁数据,业务高峰期根本没法操作,只能临时删数据应急。

# 错误:磁盘快满了才临时想扩容,没有预案
# 临时删 binlog / 删数据应急,慌中出错

为什么是坑:磁盘扩容不是随时都能做——块设备扩容可能需要停机、文件系统扩容需要窗口、数据迁移需要时间。等到快满了才临时弄,必然手忙脚乱。容量规划的意义就是"提前知道何时要扩、怎么扩",而不是等报警。

正确做法:

提前做容量预测和扩容预案。

# 正确:提前预测磁盘何时打满,预约扩容窗口
# 按当前增速预测:现有剩余 / 每天增长 = 还能用几天
# 提前几周走扩容流程,选低峰操作

加分点:云上用可在线扩容的云盘,提前扩好容量,比等满了再动手从容。


坑 9:SSD 选型不看写入放大与寿命

场景 / 错误示范:

DBA 图便宜买了消费级 SSD 跑数据库,高写入下两年就写寿命耗尽,磁盘突然掉盘,数据丢失。

# 错误:用消费级 SSD 跑高写入数据库
# 消费级 SSD 的 TBW(总写入字节)远低于企业级,高写入很快写爆寿命

为什么是坑:SSD 有写入寿命(TBW/DWPD 指标),数据库尤其是写密集场景,每天写入量巨大。消费级 SSD 寿命短、无掉电保护,跑高写数据库迟早掉盘。数据库磁盘选型不能只看容量和价格,要看写入寿命和掉电保护(电容)。

正确做法:

写密集数据库选企业级 SSD,看 TBW 和掉电保护。

| 盘类型 | 适用场景 | 寿命/可靠性 |

|---|---|---|

| 消费级 SSD | 读多写少、非核心 | TBW 低,无掉电保护 |

| 企业级 SSD | 大多数生产库 | TBW 高,带电容掉电保护 |

| NVMe SSD | redo log、高并发 | 低延迟,最贵 |

| HDD | 冷备份、归档 | 便宜慢 |

加分点:写密集库重点看"每天写入量 × 使用寿命要求"是否在 SSD 的 TBW 范围内。


坑 10:大表不做分区导致单文件膨胀

场景 / 错误示范:

一张订单表 5 亿行,单表单文件,查询要扫巨大的 B+ 树,DDL 维护一次要锁几小时,磁盘文件碎片化严重。

-- 错误:超大表单表,不分区
CREATE TABLE t_order (
  id BIGINT PRIMARY KEY,
  create_time DATETIME
);   -- 5 亿行全在一个表,维护困难

为什么是坑:单表数据量过大,B+ 树层级变深、查询变慢、DDL 维护时间长、磁盘文件碎片化。按时间分区后,查询只扫相关分区、删除历史数据用 DROP PARTITION 瞬间完成,磁盘管理也清晰。大表不分区,等于把所有问题堆在一个文件里。

正确做法:

大表按时间/范围分区。

-- 正确:按时间分区,查询和清理都按分区走(本示例基于 MySQL 8.0)
CREATE TABLE t_order (
  id BIGINT,
  create_time DATETIME,
  PRIMARY KEY (id, create_time)   -- 分区键必须进主键
)
PARTITION BY RANGE (UNIX_TIMESTAMP(create_time)) (
  PARTITION p202609 VALUES LESS THAN (UNIX_TIMESTAMP('2026-10-01')),
  PARTITION p202610 VALUES LESS THAN (UNIX_TIMESTAMP('2026-11-01'))
);
-- 删历史:ALTER TABLE t_order DROP PARTITION p202609; 瞬间回收

加分点:分区键要进主键/唯一键,否则 8.0 建表报错。


坑 11:忽略 OS page cache 对容量的影响

场景 / 错误示范:

free -m 看到 available 很少,DBA 慌了以为内存不够。其实是 OS 把空闲内存都拿来做 page cache 了,这是正常且有益的。

# 误读:把 page cache 当成"内存耗尽"
free -m
# total used free 里 free 很少,但 buff/cache 很大,其实是磁盘缓存

为什么是坑:Linux 会把空闲内存自动用作磁盘 page cache,加速数据读取。free 列少不代表内存不够——关键看 available。误读这个指标会做出错误的扩容判断。理解 page cache 是 OS 在帮你加速,而不是占内存。

正确做法:

看 available 判断真正可用内存,page cache 是好事。

# 正确:看 available 列判断内存是否真紧张
free -m
# 关键看 available:它是应用真正能拿到的内存
# buff/cache 大是 OS 在做磁盘缓存,有益无害

加分点:DB 机器上 page cache 和 buffer pool 是两层缓存,合理配合能大幅减少磁盘读。


磁盘容量规划思路

磁盘问题绝大多数是"规划不足"而非"突发故障"。以下是一套可落地的容量规划方法。

| 规划项 | 怎么做 | 频率 |

|---|---|---|

| 现状盘点 | 各目录占用、表大小、binlog 量 | 每月 |

| 增长预测 | 按历史增速预测 3/6 个月后容量 | 每月更新 |

| 水位告警 | 70/80/85% 分级 + inode | 持续 |

| 清理机制 | 定期 purge binlog、清理临时文件 | 每周 |

| 扩容预案 | 提前选好扩容方式和窗口 | 每季度演练 |

| 磁盘布局 | 数据/binlog/redo/临时分盘 | 架构评审时 |

口诀:数据 binlog redo 分盘,水位提前预警,删数据要回收空间,长事务防 undo 膨胀,inode 也要监控。

真实事故串联:磁盘类故障很少单点爆发,往往是"长事务撑大 undo + binlog 没及时 purge + 临时文件残留"三件事叠加,一个大查询就把本来还有余量的盘瞬间写满。因此日常巡检要把这三项放在一起看,而不是各自孤立监控。遇到磁盘告急,第一优先级是"先保命"——停掉大查询、purge 过期 binlog、清临时文件,把磁盘拉出危险区,再从容做长期扩容。

日常巡检三件套(建议每周跑一遍):

1. \df -h\ 看各盘水位,\df -i\ 看 inode,两个都不能逼近红线。

2. \SHOW BINARY LOGS\ 看 binlog 总量,对照保留策略 purge 过期部分。

3. \information_schema.INNODB_TRX\ 找最老事务,发现长事务及时 KILL。

这三件事各花一分钟,能挡掉绝大多数磁盘类夜间告警。


速查表:磁盘相关关键项

| 项目 | 错误做法 | 正确做法 |

|---|---|---|

| 磁盘布局 | 数据 binlog 同盘 | 数据/binlog/redo 分盘 |

| 水位线 | 95% 才告警 | 70/80/85% 分级 |

| 删数据 | delete 后以为释放空间 | 分区 DROP / OPTIMIZE 回收 |

| undo log | 不防长事务 | 监控最老事务 + 自动 truncate |

| inode | 只看容量 | 容量+inode 双监控 |

| binlog 保留 | 30 天太久 | 7 天,确认从库追过再 purge |

| SSD 选型 | 消费级跑高写库 | 企业级,看 TBW 和掉电保护 |


自测练习

1. 为什么数据文件和 binlog 要分盘?

<details>

<summary>参考答案</summary>

一是 IO 隔离(数据读写混合与 binlog 顺序写互相争抢),二是故障隔离(同盘写满时两者一起写不进去导致实例 hang)。redo log 对延迟最敏感应放最快的盘。

</details>

2. DELETE 删了大量数据,为什么磁盘空间没释放?

<details>

<summary>参考答案</summary>

InnoDB 删除只是标记删除,页空闲但不归还 OS。要 OPTIMIZE TABLE 重建回收,或用分区表 DROP PARTITION 瞬间回收。历史清理优先用分区。

</details>

3. 长事务为什么会撑爆 undo log 和磁盘?

<details>

<summary>参考答案</summary>

长事务未提交时,它需要的旧版本不能被 purge,undo log 持续累积膨胀。应监控 INNODB_TRX 最老事务及时 KILL,并开启 innodb_undo_log_truncate 自动回收。

</details>

4. inode 耗尽是什么现象?如何避免?

<details>

<summary>参考答案</summary>

磁盘容量还有剩余,却报"No space left on device"——inode 被海量小文件(临时文件、日志)占满。要同时监控 df -i,定期清理 #sql* 临时文件。

</details>

5. 清理 binlog 前要注意什么?

<details>

<summary>参考答案</summary>

先确认所有从库已经拉取过即将删除的 binlog(看从库的 Relay_Master_Log_File/Executed_Gtid_Set),否则会导致从库复制中断。用 binlog_expire_logs_seconds 自动管理比手动 purge 安全。

</details>

下一篇预告:第 67 章 连接与连接池踩坑

— 全文完 —回到顶部 ↑
下载推广海报

文章推广海报

《第 66 章 磁盘与容量踩坑》完整推广海报
DISCUSSION

文章回复

0 条公开回复
未登录回复需要审核后公开
还没有回复,欢迎参与讨论。