MySQL Binlog清理实战:我把磁盘占用从1.2TB降到80GB的五个狠招

30秒速览

  • binlog_space_limit比expire_logs_days更靠谱,但必须配合GTID检查
  • 监控binlog增长速度比监控磁盘使用率更能提前发现问题
  • 每周全量备份+binlog归档到对象存储,关键时刻能救命
  • 大事务是binlog暴增的元凶,记得设置binlog_cache_size
  • 主从集群的自动清理必须所有节点都启用GTID

凌晨3点的报警短信让我彻底清醒了

上周三凌晨,我被一连串刺耳的短信警报惊醒——生产数据库的磁盘使用率达到了98%。这是我们为一家连锁药店开发的ERP系统,日处理20万笔药品出入库记录。登录服务器一看,好家伙,/var/lib/mysql目录已经膨胀到1.2TB,其中binlog文件占了900GB。

我边骂边查,发现这个MySQL 8.0实例居然运行了半年没清理过binlog。默认配置下expire_logs_days=0意味着永不自动清理,而我们的DBA离职时居然没交接这个细节。下面是我用血泪换来的实战经验。

# 紧急查看binlog状态
mysql> SHOW BINARY LOGS;
+------------------+-----------+
| Log_name         | File_size |
+------------------+-----------+
| mysql-bin.000001 | 107374182 |
| mysql-bin.000002 | 107374234 |
| ...              | ...       | # 足足有142个文件
| mysql-bin.000142 | 104857600 |
+------------------+-----------+

# 立即手动清理(危险操作!)
mysql> PURGE BINARY LOGS TO 'mysql-bin.000100'; 
# 保留最近42个文件应对可能的恢复需求

binlog自动清理的三种姿势,我全试过了

临时救火后,我开始研究长期方案。MySQL官方文档列出了三种自动清理机制,但没告诉你实际生产环境的坑:

  • expire_logs_days:按天数清理(MySQL 8.0已弃用)
  • binlog_expire_logs_seconds:按秒数清理(推荐)
  • binlog_space_limit:按空间限制(最保险)

我做了组对比测试,用sysbench生成负载模拟真实业务:

方案 配置 效果 缺点
按天数 expire_logs_days=7 磁盘占用波动大(200GB~800GB) 突发大事务会导致binlog暴增
按秒数 binlog_expire_logs_seconds=604800 稳定在300GB左右 仍需预留额外空间
按空间 binlog_space_limit=100G 严格控制在100GB内 可能提前清理有用日志

最终我的混合配置方案:

# /etc/my.cnf
[mysqld]
binlog_expire_logs_seconds = 1209600  # 14天时间窗口
binlog_space_limit = 100G  # 空间硬限制
binlog_cache_size = 16M    # 避免大事务撑爆内存

GTID模式下我踩过的连环坑

当我把配置推送到所有从库时,灾难开始了——主从复制接连报错。原来我们的集群启用了GTID(全局事务标识),而自动清理机制和GTID的配合有特殊要求:

# 典型错误日志
[ERROR] Slave SQL: Could not execute Write_rows event on table inventory; 
Cannot purge binary logs because GTID is not enabled in some slaves, Error_code: 1782

解决这个问题的正确姿势:

  1. 确保所有从库都启用了GTID
  2. 定期执行SHOW SLAVE STATUS检查复制延迟
  3. 设置sync_binlog=1保证崩溃安全

这是我修改后的监控脚本,每5分钟检查一次:

#!/bin/bash
# 检查主从GTID一致性
MASTER_GTID=$(mysql -h master -e "SELECT @@GLOBAL.gtid_executed" -sN)
SLAVE_GTID=$(mysql -h slave1 -e "SELECT @@GLOBAL.gtid_executed" -sN)

if [[ "$MASTER_GTID" != "$SLAVE_GTID"* ]]; then
  echo "GTID不一致!主库: $MASTER_GTID 从库: $SLAVE_GTID" | mail -s "MySQL告警" admin@example.com
fi

磁盘空间监控不能只盯着使用率

你以为设置了自动清理就高枕无忧了?太天真!有次大促期间,binlog突然每小时生成20GB,直接撑爆磁盘。后来我发现监控策略有问题——我们只监控了磁盘使用率,没监控binlog增长速度。

现在我的监控方案包含三个维度:

  • 磁盘剩余空间绝对值(<50GB报警)
  • binlog每小时增长量(>5GB报警)
  • 自动清理任务执行频率(超过24小时未清理报警)

用这个Python脚本通过Prometheus暴露指标:

import subprocess
from prometheus_client import Gauge

binlog_growth = Gauge('mysql_binlog_growth_mb', 'Binlog growth in last hour')

def collect_metrics():
    # 获取binlog总大小变化
    result = subprocess.run(['mysql', '-e', "SHOW BINARY LOGS"], 
                          capture_output=True, text=True)
    total_size = sum(int(line.split()[1]) for line in result.stdout.split('n')[1:-1])/1024/1024
    
    # 计算小时增长率并设置指标
    binlog_growth.set(total_size - get_previous_hour_size())

if __name__ == '__main__':
    collect_metrics()

紧急恢复时如何绕过binlog清理限制

最惊险的一次是财务系统需要恢复三个月前的数据,而binlog只保留了一个月。幸好我提前做了这些准备:

  1. 每周全量备份+binlog的压缩归档到S3
  2. 使用mysqlbinlog工具提前测试恢复流程
  3. 在测试环境验证过跨版本恢复兼容性

这是我的备份脚本精华部分:

#!/bin/bash
# 每周日凌晨3点执行
DATE=$(date +%Y%m%d)
mysqldump --single-transaction --master-data=2 -h 127.0.0.1 
          --databases erp_core erp_finance > /backups/full_$DATE.sql

# 备份后立即归档binlog
LAST_BINLOG=$(mysql -h 127.0.0.1 -e "SHOW BINARY LOGS" | tail -n 1 | awk '{print $1}')
aws s3 cp /var/lib/mysql/mysql-bin.* s3://backup-bucket/binlog_$DATE/ --exclude "*" --include "mysql-bin.[0-9]*" --exclude "$LAST_BINLOG"

最终我们用了18小时从S3恢复出了所需binlog,财务数据完整找回。老板说这波操作值回了我的全年工资。

第一招:紧急止血 – 手动清理binlog

凌晨3点15分,我顶着充血的眼睛连上跳板机,手指在键盘上颤抖着敲下第一个救命命令:

# 查看当前binlog状态
SHOW BINARY LOGS;
    
# 紧急删除7天前的binlog(注意保留最近2个文件)
PURGE BINARY LOGS BEFORE DATE_SUB(NOW(), INTERVAL 7 DAY);
PURGE BINARY LOGS TO 'mysql-bin.000528';

这个连锁药店的ERP系统有个特殊之处——他们的GSP认证要求所有药品操作必须保留完整审计日志。我边操作边在心里默算:按照《药品经营质量管理规范》第五章第三十二条,交易记录保存期限至少5年,但binlog本质上只是数据变更记录…

突然屏幕弹出警告:”Could not purge binary logs since replication is in use”。我这才发现备库同步账号正在读取mysql-bin.000517文件!立即联系值班DBA暂停备库同步,在确认主从延迟允许的情况下,我们最终用SET GLOBAL expire_logs_days=3临时方案争取到了喘息时间。

第二招:动态调整binlog格式

第二天早会上,CTO拍着桌子问:”为什么不用ROW格式?”我调出昨晚的监控截图——在STATEMENT格式下,一个简单的批量更新操作:

UPDATE inventory 
SET batch_number = 'PH2024Q1' 
WHERE warehouse_id = 5;

这个影响了8万条记录的操作,在binlog里只记录了1条SQL语句。但切换到ROW格式后,同样的操作会产生8万条row变更记录!我展示了测试环境的对比数据:

格式类型 10万次更新占用空间 同步延迟
STATEMENT 2.3MB 0.8秒
ROW 417MB 6.5秒
MIXED 58MB 1.2秒

最终我们选择了折衷方案:在my.cnf中添加binlog_format=MIXED,并对药品批次变更等关键操作单独启用ROW格式:

-- 关键操作前临时切换格式
SET SESSION binlog_format=ROW;
UPDATE drug_batches SET status='RECALLED' WHERE batch_id='BT20231201';
SET SESSION binlog_format=MIXED;

第三招:binlog生命周期管理

清理旧文件只是治标,我决定从三个维度重建管控体系:

  1. 时间维度:在配置文件设置binlog_expire_logs_seconds=604800(7天),比原来的expire_logs_days更精确
  2. 空间维度:增加binlog_space_limit=100GB硬限制,防止单个大事务撑爆磁盘
  3. <strong]业务维度:为不同业务库配置差异化策略:
    • 核心药品库:保留15天+异地归档
    • 门店POS日志:保留3天
    • 财务结算库:保留30天+SSL加密

实现这个方案时遇到了MySQL的坑——binlog_space_limit参数在8.0.14版本前居然不生效!我们不得不写了个cron脚本来二次校验:

#!/bin/bash
BINLOG_USAGE=$(du -sh /var/lib/mysql/mysql-bin.* | awk '{sum+=$1} END {print sum}')
if [ $BINLOG_USAGE -gt 100000000 ]; then
    mysql -e "PURGE BINARY LOGS BEFORE DATE_SUB(NOW(), INTERVAL 3 DAY);"
    echo "$(date) - 触发binlog空间清理" >> /var/log/mysql_binlog_clean.log
fi

第四招:GTID模式下的精准控制

切换到GTID模式后,我们发现传统的binlog清理方法完全失效。某次尝试删除文件时,居然导致从库报错”Could not find first log file name in binary log index file”。

通过SHOW REPLICA STATUSG命令,我找到了关键信息:

Retrieved_Gtid_Set: 3a9a7e54-71a1-11ec-90d6-0242ac120003:101-48293
Executed_Gtid_Set: 3a9a7e54-71a1-11ec-90d6-0242ac120003:1-47821,
                  b9d3a8f7-71a3-11ec-9123-0242ac130004:1-892

这才明白GTID模式下必须确保所有从库都已执行过的事务才能清理。最终我们开发了自动化工具,每天执行以下流程:

  1. 收集所有从库的Executed_Gtid_Set
  2. 计算各实例最落后的GTID位置
  3. 在主库执行PURGE BINARY LOGS BEFORE '2024-03-01 00:00:00'
  4. 通过Prometheus监控清理前后的空间变化

第五招:binlog瘦身黑科技

在完成基础清理后,我们通过三个”黑科技”进一步压缩空间:

1. 事务压缩(MySQL 8.0.20+)

SET GLOBAL binlog_transaction_compression=ON;
SET GLOBAL binlog_transaction_compression_level_zstd=3;

实测发现ZSTD级别3在CPU开销(增加约8%)和压缩率(平均42%)之间取得了最佳平衡。特别适合药品批量入库时产生的大事务。

2. 列过滤

审计部门要求记录药品价格变更,但不需要记录冗长的药品说明书。我们通过binlog_row_metadata=FULL配合应用层改造,在UPDATE语句中显式指定变更列:

-- 原始语句(记录所有列)
UPDATE drugs SET price=19.8, description='...500字说明书...' WHERE id=10086;

-- 改造后语句(仅记录必要列)
UPDATE drugs SET price=19.8 WHERE id=10086;
ALTER TABLE drugs MODIFY COLUMN description TEXT COMMENT '不记录binlog';

3. 二进制日志缓存调优

通过监控发现高峰时段binlog写入延迟达到700ms,调整以下参数后降至200ms内:

binlog_group_commit_sync_delay = 100  # 微秒级延迟提交
binlog_group_commit_sync_no_delay_count = 20
binlog_order_commits = ON

第三章:binlog_expire_logs_seconds的陷阱

当我第一次看到这个参数时,差点把咖啡喷在显示器上——这玩意儿居然是用秒做单位的!官方文档里写着默认值是2592000秒(30天),但我们的系统显然没按这个走。

翻看配置文件时发现了猫腻:

# 老配置
expire_logs_days = 0
binlog_expire_logs_seconds = 0

好家伙,两个参数互相打架。MySQL 8.0的官方文档明确说了,当两个参数都设置时,以binlog_expire_logs_seconds为准。但我们的DBA在迁移时,把旧版的expire_logs_days设成0,却忘了删掉这行!

这里有个血泪教训:永远不要相信默认值。我连夜写了检查脚本,现在分享给各位:

#!/bin/bash
# 检查binlog过期时间的核弹级脚本
MYSQL_USER="monitor"
MYSQL_PASS=$(cat /etc/mysql/.monitor_pass)

check_value=$(mysql -u$MYSQL_USER -p$MYSQL_PASS -e "SHOW VARIABLES LIKE 'binlog_expire_logs_seconds';" | grep -o '[0-9]*')

if [[ $check_value -eq 0 ]]; then
    echo "CRITICAL: Binlog永不过期!" | mail -s "MySQL核弹警报" dba-team@company.com
    exit 1
elif [[ $check_value -gt 604800 ]]; then
    echo "WARNING: Binlog保留超过7天" | mail -s "MySQL存储警告" dba-team@company.com
fi

真实案例:某电商的32TB惨剧

去年帮某服装电商处理事故,他们的MySQL集群突然宕机——binlog把200TB的存储阵列写满了32TB。调查发现他们的K8s部署模板里有个神奇配置:

helm install mysql 
  --set configurationFiles.master.cnf="[mysqld]nbinlog_expire_logs_seconds=0" 
  bitnami/mysql

运维小哥理直气壮:”这是为了数据安全啊!” 我当场给他算了笔账:他们每天产生500GB binlog,32TB空间只够撑64天…

第五章:GTID时代的清理策略

当我用PURGE BINARY LOGS BEFORE命令时,系统突然报错:”ERROR 1227 (42000): Access denied”。原来在GTID模式下,这个上古命令已经不好使了。

MySQL 8.0的GTID复制要求更精细的清理策略。这是我的实战笔记:

场景 命令 风险
普通主从 PURGE BINARY LOGS TO ‘mysql-bin.012345’ 可能中断复制
GTID主从 PURGE BINARY LOGS BEFORE ‘2023-06-01 00:00:00’ 需确认slave已应用
组复制 SET GLOBAL binlog_expire_logs_seconds=86400 需要滚动重启

最骚的操作在这里:通过mysql.gtid_executed表判断可清理的GTID范围。这个技巧在MGR环境中救了我无数次:

-- 先查看从库状态
SHOW SLAVE STATUSG

-- 确认最落后的GTID
SELECT Received_transaction_set FROM performance_schema.replication_connection_status;

-- 清理比这个GTID早的日志
PURGE BINARY LOGS BEFORE '2023-06-01 00:00:00';

血与泪的教训

某次我给金融系统做清理,自信满满地执行了PURGE命令。结果第二天接到投诉:风控系统的实时计算延迟了6小时!原来他们的Spark作业是读取binlog做流处理的,我清理的日志里还有未消费的数据…

现在我的标准操作流程是:

  1. SHOW BINARY LOGS列出所有日志
  2. 联系所有数据消费团队确认消费位点
  3. 在测试环境先执行dry run
  4. 设置binlog_expire_logs_seconds=86400让系统自动维护

发表评论