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
解决这个问题的正确姿势:
- 确保所有从库都启用了GTID
- 定期执行SHOW SLAVE STATUS检查复制延迟
- 设置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只保留了一个月。幸好我提前做了这些准备:
- 每周全量备份+binlog的压缩归档到S3
- 使用mysqlbinlog工具提前测试恢复流程
- 在测试环境验证过跨版本恢复兼容性
这是我的备份脚本精华部分:
#!/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生命周期管理
清理旧文件只是治标,我决定从三个维度重建管控体系:
- 时间维度:在配置文件设置
binlog_expire_logs_seconds=604800(7天),比原来的expire_logs_days更精确 - 空间维度:增加
binlog_space_limit=100GB硬限制,防止单个大事务撑爆磁盘 - <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模式下必须确保所有从库都已执行过的事务才能清理。最终我们开发了自动化工具,每天执行以下流程:
- 收集所有从库的
Executed_Gtid_Set - 计算各实例最落后的GTID位置
- 在主库执行
PURGE BINARY LOGS BEFORE '2024-03-01 00:00:00' - 通过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做流处理的,我清理的日志里还有未消费的数据…
现在我的标准操作流程是:
- 用
SHOW BINARY LOGS列出所有日志 - 联系所有数据消费团队确认消费位点
- 在测试环境先执行dry run
- 设置
binlog_expire_logs_seconds=86400让系统自动维护