MySQL磁盘空间问题的成因和排查方法

描述

背景与问题

运维工程师经常会遇到这样的场景:MySQL 服务器的磁盘空间告警,但查看数据目录时发现数据库本身并不大。大量磁盘空间被未知文件消耗。通过排查发现,二进制日志(Binary Log)是主要的磁盘空间消耗者。

二进制日志是 MySQL 复制和数据恢复的核心组件,但如果管理不当,它会迅速占满磁盘空间。本文系统讲解二进制日志的工作原理、磁盘空间问题的成因、排查方法以及最佳实践。

本文基于 MySQL 8.0.36 版本,操作系统为 Ubuntu 24.04 LTS。所有命令和配置均经过实际环境验证。

一、二进制日志基础

1.1 二进制日志的作用

二进制日志记录了所有修改数据库数据的操作,包括:

数据修改语句:INSERT、UPDATE、DELETE。

数据定义语句:CREATE、ALTER、DROP(可配置)。

服务器运行时事件:如主从复制中的心跳事件。

二进制日志的主要用途:

主从复制:从服务器通过读取主服务器的二进制日志来同步数据。

数据恢复:使用 mysqlbinlog 工具可以恢复指定时间点的数据。

增量备份:结合全量备份和二进制日志实现增量备份。

审计:可以用于审计数据库的变更历史。

1.2 二进制日志的文件结构

二进制日志文件名格式:mysql-bin.000001、mysql-bin.000002 等。

 

# 查看二进制日志目录
ls -lh /var/log/mysql/

# 二进制日志文件列表
mysql -u root -p -e "SHOW BINARY LOGS;"

# 当前使用的二进制日志
mysql -u root -p -e "SHOW MASTER STATUS;"

 

每个二进制日志文件包含多个日志事件(Event),日志事件分为几种类型:

Format_description:格式描述事件,记录 MySQL 版本信息。

Table_map:表映射事件,记录操作涉及的表。

Write_rows/Update_rows/Delete_rows:行事件,记录实际的数据变更。

Xid:事务提交事件。

1.3 二进制日志格式

MySQL 8.0 支持三种二进制日志格式:

ROW:记录行的变更,完整且安全,但日志量较大。

STATEMENT:记录 SQL 语句,节省空间,但可能有不确定结果。

MIXED:混合模式,默认使用 STATEMENT,在不确定结果时切换到 ROW。

 

-- 查看当前二进制日志格式
SHOWVARIABLESLIKE'binlog_format';
-- +---------------+-------+
-- | Variable_name | Value |
-- +---------------+-------+
-- | binlog_format | ROW   |
-- +---------------+-------+

-- 设置二进制日志格式(临时生效)
SETGLOBAL binlog_format = 'ROW';

-- 永久生效需要在配置文件中设置
-- binlog_format = ROW

 

二、二进制日志磁盘空间问题

2.1 问题场景描述

某生产环境 MySQL 服务器磁盘空间告警,/var/lib/mysql 目录占用 500GB,但实际数据只有 80GB。

排查后发现,二进制日志占用超过 400GB,这些日志包含了过去半年的所有数据变更记录。

这是一个典型案例:没有配置二进制日志清理策略,导致日志无限增长。

2.2 二进制日志增长速度分析

二进制日志的增长速度取决于以下因素:

业务负载:写操作越频繁,日志增长越快。

日志格式:ROW 格式比 STATEMENT 格式日志量更大。

表的数据量:大表的小更新在 ROW 格式下也会产生大量日志。

 

-- 查看当前二进制日志总大小
SELECT
    SUM(FILE_SIZE) / 1024 / 1024 / 1024AS total_size_gb
FROM information_schema.FILES
WHERE FILE_TYPE = 'BINARY LOG';

-- 查看每个二进制日志文件的大小
SELECT
    LOG_NAME AS file_name,
    FILE_SIZE / 1024 / 1024AS size_mb
FROM information_schema.FILES
WHERE FILE_TYPE = 'BINARY LOG'
ORDERBY LOG_NAME;
# 也可以直接查看文件大小
ls -lh /var/log/mysql/mysql-bin.* | tail -20
du -sh /var/log/mysql/

 

2.3 二进制日志清理机制

MySQL 提供两种二进制日志清理机制:

自动清理:通过 expire_logs_days 参数设置。

手动清理:通过 PURGE BINARY LOGS 命令删除。

 

-- 查看自动清理配置
SHOW VARIABLES LIKE 'expire_logs_days';
-- 默认值是 0,表示不自动清理

-- 设置自动清理(保留最近 7 天)
SET GLOBAL expire_logs_days = 7;

-- 永久生效需要在配置文件中设置
-- expire_logs_days = 7

 

2.4 手动清理二进制日志

 

-- 删除指定时间之前的日志
PURGEBINARYLOGSBEFORE'2026-01-01 0000';

-- 删除指定日志文件之前的所有日志
PURGEBINARYLOGSTO'mysql-bin.000100';

-- 查看当前日志文件
SHOWMASTERSTATUS;

 

清理时需要注意:

不要删除还没被从服务器读取的日志,否则从服务器会中断复制。

先在主服务器上执行 SHOW SLAVE STATUS,确认从服务器已经读取到要删除的位置。

 

-- 检查从服务器状态
SHOW SLAVE STATUSG
-- 查看 Master_Log_File 和 Relay_Master_Log_File 字段
-- 确保主服务器上要删除的日志文件名在这些字段之后

 

三、磁盘空间问题排查步骤

3.1 初步诊断

 

# 查看磁盘使用情况
df -h

# 查看 MySQL 数据目录占用
du -sh /var/lib/mysql/*

# 查看 MySQL 日志目录
du -sh /var/log/mysql/*

 

3.2 二进制日志占用分析

 

-- 查看所有二进制日志文件及大小
mysql -u root -p -e "
SELECT
    LOG_NAME,
    FILE_SIZE / 1024 / 1024AS size_mb,
    (SELECTCOUNT(*) FROM information_schema.FILES) AS total_files
FROM information_schema.FILES
WHERE FILE_TYPE = 'BINARY LOG'
ORDERBY LOG_NAME;
"

-- 统计总大小
mysql -u root -p -e "
SELECT
    COUNT(*) AS total_files,
    SUM(FILE_SIZE) / 1024 / 1024 / 1024AS total_size_gb
FROM information_schema.FILES
WHERE FILE_TYPE = 'BINARY LOG';
"

 

3.3 查找磁盘空间消耗源头

如果 df 显示磁盘占用很高但 du 显示文件很小,可能是以下问题:

打开的文件句柄未释放

MySQL 临时文件未删除

系统日志文件过大

 

# 查看 MySQL 进程打开的文件
sudo lsof -p $(pgrep mysqld) | grep -E "REG|DIR" | awk '{print $NF}' | sort | uniq | xargs -I {} ls -lh {} 2>/dev/null

# 查看 MySQL 临时目录
SHOW VARIABLES LIKE 'tmpdir';

# 查看临时表使用情况
SHOW STATUS LIKE 'Created_tmp%';

 

四、复制环境中的特殊处理

4.1 主从复制架构概述

在主从复制环境中,二进制日志的管理需要格外谨慎:

主服务器:必须开启二进制日志,记录所有变更。

从服务器:通过 I/O 线程读取主服务器的二进制日志,写入本地 relay log。

从服务器:通过 SQL 线程执行 relay log 中的事件,完成数据同步。

 

-- 主服务器上查看从服务器连接状态
SHOW SLAVE HOSTS;

-- 从服务器上查看复制状态
SHOW SLAVE STATUSG

 

4.2 从服务器需要开启二进制日志吗

从服务器是否开启二进制日志取决于架构设计:

如果从服务器同时作为其他从服务器的主服务器,必须开启。

如果从服务器只是用于读负载均衡,可以关闭。

如果需要使用从服务器进行增量备份,必须开启。

 

-- 查看从服务器是否开启二进制日志
SHOW VARIABLES LIKE 'log_slave_updates';
-- log_slave_updates = ON 表示从服务器会将 relay log 执行后记录到自己的二进制日志

 

4.3 复制环境下的日志清理策略

在复制环境中,清理二进制日志需要考虑从服务器的读取进度:

 

-- 查看所有从服务器的状态
SHOWSLAVEHOSTS;
-- +-----------+-------------+------+-----------+
-- | Server_id | Host        | Port | Master_id |
-- +-----------+-------------+------+-----------+
-- |         2 | slave-01    | 3306 |         1 |
-- |         3 | slave-02    | 3306 |         1 |
-- +-----------+-------------+------+-----------+

-- 查看每个从服务器读取到哪个日志文件
SHOWSLAVESTATUSG
-- Relay_Master_Log_File: mysql-bin.000050
-- Exec_Master_Log_Pos: 12345678

 

安全清理策略:只删除所有从服务器都已经读取完毕的日志。

 

-- 方式一:基于从服务器状态自动清理
-- 使用 mysqlrpladmin 工具
-- mysqlrpladmin --master=root:pass@master-host --discover-slaves-for=master prune

-- 方式二:手动确认后清理
-- 1. 在每个从服务器上执行 SHOW SLAVE STATUS,记录 Relay_Master_Log_File
-- 2. 在主服务器上找到最早的日志文件
-- 3. 使用 PURGE BINARY LOGS TO 'mysql-bin.xxxxxx' 删除更早的日志

 

4.4 GTID 模式下的日志管理

MySQL 8.0 推荐使用 GTID(Global Transaction Identifier)模式,简化复制管理:

 

-- 查看 GTID 配置
SHOW VARIABLES LIKE 'gtid_mode';
SHOW VARIABLES LIKE 'enforce_gtid_consistency';

-- 查看当前执行的 GTID
SELECT @@GLOBAL.gtid_executed;
SELECT @@GLOBAL.gtid_purged;

 

在 GTID 模式下,日志清理更加安全:

 

-- 查看已执行的 GTID 集合
SHOW GLOBAL VARIABLES LIKE 'gtid_executed';

-- 自动清理会考虑 GTID 集合
-- PURGE BINARY LOGS 会自动跳过包含已执行事务的日志
PURGE BINARY LOGS TO 'mysql-bin.000100';
-- 上面命令会报错,如果 mysql-bin.000100 包含未执行的事务

 

五、二进制日志配置优化

5.1 基础配置参数

 

# /etc/mysql/mysql.conf.d/mysqld.cnf

[mysqld]
# 开启二进制日志
log_bin = /var/log/mysql/mysql-bin

# 二进制日志格式
binlog_format = ROW

# 二进制日志缓存大小(基于会话)
binlog_cache_size = 4M

# 最大二进制日志缓存大小
max_binlog_cache_size = 512M

# 单个二进制日志文件大小
max_binlog_size = 1G

# 自动清理天数
expire_logs_days = 7

# sync_binlog 控制何时将二进制日志同步到磁盘
sync_binlog = 1  # 每次事务提交后同步,最安全但性能影响大
# sync_binlog = 0  # 依赖操作系统刷盘,性能好但可能有数据丢失

# binlog_row_image 控制 ROW 格式下记录哪些镜像
binlog_row_image = FULL  # 记录所有列
# binlog_row_image = MINIMAL  # 只记录主键和变更的列

 

5.2 性能与安全的权衡

sync_binlog 参数的选择:

sync_binlog = 1:最安全,每次事务提交都同步日志到磁盘。MySQL 崩溃时最多丢失一个事务。性能影响最大。

sync_binlog = 0:性能最好,依赖操作系统刷盘。MySQL 崩溃时可能丢失多个事务。

sync_binlog = N:每 N 个事务同步一次。性能和数据安全的折中。

 

-- 查看当前配置
SHOW VARIABLES LIKE 'sync_binlog';
-- +---------------+-------+
-- | Variable_name | Value |
-- +---------------+-------+
-- | sync_binlog   | 1     |
-- +---------------+-------+

-- 设置同步策略
SET GLOBAL sync_binlog = 100;  -- 每100个事务同步一次

 

5.3 二进制日志缓存优化

binlog_cache_size 用于缓存未提交事务的二进制日志:

 

-- 查看二进制日志缓存使用情况
SHOW STATUS LIKE 'Binlog_cache%';
-- +-----------------------+-------+
-- | Variable_name         | Value |
-- +-----------------------+-------+
-- | Binlog_cache_disk_use | 1234  |  -- 内存缓存不够,使用了磁盘
-- | Binlog_cache_use      | 5678  |  -- 使用内存缓存的事务数
-- +-----------------------+-------+

-- 如果 Binlog_cache_disk_use 较大,增加 binlog_cache_size
SET GLOBAL binlog_cache_size = 8 * 1024 * 1024;  -- 8MB

 

5.4 行格式下的优化

binlog_row_image 参数影响 ROW 格式下的日志大小:

 

-- FULL:记录所有列的完整数据
-- MINIMAL:只记录主键和实际变更的列
-- NOBLOB:对于没有 BLOB 列的表,不记录 BLOB 数据

SHOWVARIABLESLIKE'binlog_row_image';
-- +------------------+-------+
-- | Variable_name    | Value |
-- +------------------+-------+
-- | binlog_row_image | FULL  |
-- +------------------+-------+

-- 如果表没有 BLOB 列,可以设置为 MINIMAL 减少日志量
SETGLOBAL binlog_row_image = MINIMAL;

 

六、实战案例分析

6.1 案例一:未配置自动清理导致磁盘爆满

6.1.1 问题现象

监控告警:/var 分区使用率超过 90%。

SSH 登录后检查:du -sh /var/lib/mysql 显示只有 200GB。

但 df -h 显示已使用 350GB。

6.1.2 排查过程

 

# 查看磁盘使用
df -h /var
# Filesystem      Size  Used Avail Use% Mounted on
# /dev/sda1       400G  380G   20G  95% /var

# 查看 MySQL 目录详情
du -sh /var/lib/mysql/*
# 78G     /var/lib/mysql/data  (实际数据)
# 280G    /var/lib/mysql/binlog (二进制日志)
# 12G     /var/lib/mysql/relaylog (从服务器 relay log)

# 确认二进制日志
ls -lh /var/log/mysql/mysql-bin.0* | tail -10

 

6.1.3 问题根因

配置文件中未设置 expire_logs_days 参数,默认为 0 表示不自动清理。

业务上线时从全量备份恢复后,从未清理过二进制日志。

没有配置从服务器的 relay log 清理。

6.1.4 解决步骤

 

-- 1. 确认从服务器状态
SHOWSLAVESTATUSG
-- Relay_Master_Log_File: mysql-bin.00150
-- Exec_Master_Log_Pos: 1234567

-- 2. 设置自动清理(保留最近 7 天)
SETGLOBAL expire_logs_days = 7;

-- 3. 手动清理超过 7 天的日志
PURGEBINARYLOGSTO'mysql-bin.00150';

-- 4. 修改配置文件永久生效
-- 在 [mysqld] 段添加或修改:
-- expire_logs_days = 7

-- 5. 从服务器也需要清理 relay log
SHOWVARIABLESLIKE'relay_log_purge';
SETGLOBAL relay_log_purge = ON;

 

6.1.5 预防措施

在 MySQL 配置文件中始终设置 expire_logs_days。

部署监控告警,监控二进制日志目录大小。

定期检查二进制日志使用情况。

6.2 案例二:大事务导致单个日志文件过大

6.2.1 问题现象

某个表执行了大表更新,导致二进制日志瞬间增长 50GB。

max_binlog_size 设置为 1GB,但单个日志文件超过 10GB。

6.2.2 排查过程

 

-- 查看最大的二进制日志文件
SELECT
    LOG_NAME,
    FILE_SIZE / 1024 / 1024AS size_mb
FROM information_schema.FILES
WHERE FILE_TYPE = 'BINARY LOG'
ORDERBY FILE_SIZE DESC
LIMIT10;

-- 查看是什么时间点开始变大
SHOWBINARYLOGS;

 

6.2.3 问题分析

大表更新(如 UPDATE t SET col = 'xxx' 没有 WHERE 条件)产生了大量 ROW 格式日志。

max_binlog_size 只是触发切换的条件,不是硬性限制。

大事务必须完整写入一个二进制日志文件,不能分割。

6.2.4 解决建议

分批处理大事务:

 

-- 原来的一次性更新
-- UPDATE large_table SET status = 1;

-- 改为分批更新
DELIMITER //
CREATEPROCEDURE batch_update()
BEGIN
    DECLARE batch_size INTDEFAULT1000;
    DECLARE offset_num INTDEFAULT0;
    DECLARE done INTDEFAULTFALSE;

    WHILE NOT done DO
        UPDATE large_table
        SETstatus = 1
        WHEREidBETWEEN offset_num AND offset_num + batch_size - 1;

        SET offset_num = offset_num + batch_size;

        -- 检查是否还有未更新的记录
        IF ROW_COUNT() < batch_size THEN
            SET done = TRUE;
        ENDIF;

        -- 提交当前批次
        COMMIT;
    ENDWHILE;
END //
DELIMITER ;

CALL batch_update();

 

6.3 案例三:从服务器 relay log 占用大量空间

6.3.1 问题现象

从服务器磁盘空间告警,但主服务器一切正常。

检查发现从服务器的 relay-log 目录占用 200GB。

6.3.2 排查过程

 

-- 在从服务器上查看 relay log 配置
SHOW VARIABLES LIKE '%relay%';
-- relay_log           = /var/lib/mysql/mysql-relay-bin
-- relay_log_purge     = ON
-- relay_log_space_limit = 0  (0 表示不限制)

-- 查看 relay log 文件
ls -lh /var/lib/mysql/mysql-relay-bin.* | head -20

 

6.3.3 问题根因

虽然 relay_log_purge 默认为 ON,但以下情况会导致 relay log 堆积:

复制中断,从服务器无法执行事件。

网络问题导致从服务器长时间无法读取主服务器的日志。

某些大事务导致单个 relay log 文件过大。

6.3.4 解决方案

 

-- 确保 relay log 自动清理开启
SHOWVARIABLESLIKE'relay_log_purge';
SETGLOBAL relay_log_purge = ON;

-- 如果有复制中断,先解决中断问题
SHOWSLAVESTATUSG
-- 查看 Last_Error 和 Last_IO_Error 字段

-- 手动清理 relay log
STOPSLAVE;
PURGE RELAY LOGS;
STARTSLAVE;

 

七、二进制日志监控

7.1 监控指标

 

-- 二进制日志总大小
SELECT
    COUNT(*) AS total_files,
    SUM(FILE_SIZE) / 1024 / 1024AS total_mb,
    MAX(FILE_SIZE) / 1024 / 1024AS max_file_mb
FROM information_schema.FILES
WHERE FILE_TYPE = 'BINARY LOG';

-- 二进制日志写入统计
SHOWSTATUSLIKE'Binlog%';
-- +-----------------------+-------+
-- | Variable_name         | Value |
-- +-----------------------+-------+
-- | Binlog_cache_disk_use | 0     |
-- | Binlog_cache_use      | 1000  |
-- +-----------------------+-------+

-- 复制相关的二进制日志统计
SHOWSTATUSLIKE'Slave%';

 

7.2 监控脚本

 

#!/bin/bash
# 二进制日志监控脚本

ALERT_THRESHOLD_GB=100
LOG_DIR="/var/log/mysql"
MYSQL_USER="root"
MYSQL_PASS="YourPassword"

# 获取二进制日志总大小
TOTAL_SIZE=$(mysql -u ${MYSQL_USER} -p${MYSQL_PASS} -N -e "
SELECT SUM(FILE_SIZE) / 1024 / 1024 / 1024
FROM information_schema.FILES
WHERE FILE_TYPE = 'BINARY LOG';
")

# 比较阈值
if [ "$(echo "$TOTAL_SIZE > $ALERT_THRESHOLD_GB" | bc)" -eq 1 ]; then
    echo"ALERT: Binary log size (${TOTAL_SIZE}GB) exceeds threshold (${ALERT_THRESHOLD_GB}GB)"
    # 发送告警通知
    # 这里可以接入邮件、短信、钉钉等告警渠道
    exit 1
fi

# 获取最旧的日志文件
OLDEST_LOG=$(mysql -u ${MYSQL_USER} -p${MYSQL_PASS} -N -e "SHOW BINARY LOGS;" | head -1 | awk '{print $1}')

# 获取最新日志文件
NEWEST_LOG=$(mysql -u ${MYSQL_USER} -p${MYSQL_PASS} -N -e "SHOW BINARY LOGS;" | tail -1 | awk '{print $1}')

echo"Binary Log Report:"
echo"Total Size: ${TOTAL_SIZE}GB"
echo"Files: ${OLDEST_LOG} to ${NEWEST_LOG}"
echo"Oldest file: ${OLDEST_LOG}"

 

7.3 Prometheus 监控配置

 

# mysql_exporter 二进制日志监控指标
-job_name:'mysql'
static_configs:
-targets:['localhost:9104']
relabel_configs:
-source_labels:[__address__]
    target_label:instance
    regex:'(.+):d+'
    replacement:'${1}'

 

八、二进制日志恢复实践

8.1 基于时间点的恢复

 

# 假设需要恢复到 2026-01-15 1000 的状态

# 1. 先找到对应的日志文件
mysqlbinlog --no-defaults --stop-datetime="2026-01-15 0959" 
  /var/log/mysql/mysql-bin.000123 > /tmp/full_recovery.sql

# 2. 如果需要跳过某些错误
mysqlbinlog --no-defaults 
  --stop-datetime="2026-01-15 0959" 
  --database=opsdb 
  /var/log/mysql/mysql-bin.000123 /var/log/mysql/mysql-bin.000124 
  > /tmp/partial_recovery.sql

# 3. 恢复数据
mysql -u root -p < /tmp/full_recovery.sql

 

8.2 基于位置点的恢复

 

# 找到需要恢复的位置点
mysqlbinlog --no-defaults --base64-output=decode-rows -v 
  /var/log/mysql/mysql-bin.000123 | grep -A 5 "DROP TABLE" | head -30

# 找到位置点后
mysqlbinlog --no-defaults 
  --stop-position=12345678 
  /var/log/mysql/mysql-bin.000123 > /tmp/recovery.sql

mysql -u root -p < /tmp/recovery.sql

 

8.3 从从服务器恢复

如果主服务器的二进制日志不可用,可以从从服务器恢复:

 

-- 在从服务器上执行
SHOW SLAVE STATUSG
-- 记录 Master_Log_File 和 Exec_Master_Log_Pos
# 从从服务器的 relay log 恢复
mysqlbinlog --no-defaults 
  --start-position=123 
  --stop-position=12345678 
  /var/lib/mysql/mysql-relay-bin.000050 > /tmp/slave_recovery.sql

mysql -u root -p < /tmp/slave_recovery.sql

 

九、最佳实践总结

9.1 配置规范

 

# /etc/mysql/mysql.conf.d/mysqld.cnf

[mysqld]
# 二进制日志基础配置
log_bin = /var/log/mysql/mysql-bin
binlog_format = ROW
max_binlog_size = 1G
expire_logs_days = 7

# 性能优化
sync_binlog = 1000  # 性能敏感场景
binlog_cache_size = 8M
max_binlog_cache_size = 512M

# 从服务器配置
log_slave_updates = ON
relay_log_purge = ON
relay_log_recovery = ON

 

9.2 运维规范

每日检查二进制日志目录大小。

设置合理的告警阈值(建议磁盘使用的 80%)。

重要数据变更前记录当前的二进制日志位置。

定期备份二进制日志到异地存储。

9.3 恢复预案

每季度测试一次恢复流程。

维护最新的备份和日志位置文档。

记录所有大事务的日志位置,便于选择性恢复。

总结

二进制日志是 MySQL 运维中最重要的组件之一,但也是最容易引发磁盘空间问题的组件。

理解二进制日志的工作原理是解决问题的前提:它记录所有数据变更,用于复制和恢复,在 ROW 格式下日志量可能很大。

磁盘空间暴涨的主要原因是未配置自动清理策略。在生产环境中,必须设置 expire_logs_days 参数,并配置监控告警。

复制环境下清理日志需要格外谨慎,必须确保所有从服务器已经读取完毕要删除的日志文件。

大事务会产生大量日志,分批处理是最佳实践。

日常监控和定期检查是预防问题的关键。配置合理的告警阈值,建立日志增长趋势分析,才能在问题发生前发现端倪。

 

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分