TL;DR: MySQL崩溃恢复不是简单的"重启服务",而是涉及InnoDB引擎、Binlog、备份、HA架构的完整防御体系。本文从内核原理到生产实践,系统拆解崩溃恢复的完整方案设计。
适合人群 : DBA、运维工程师、后端架构师
阅读时间 : 30-45分钟
难度等级: ⭐⭐⭐⭐ (需要MySQL基础知识)
目录
- MySQL崩溃场景全景图
- InnoDB崩溃恢复机制:从Redo到Undo
- Binlog与数据一致性:XID的奥秘
- 物理备份恢复:Xtrabackup实战
- 逻辑备份恢复:mysqldump与Mydumper
- 时间点恢复(PITR):从Binlog重放
- 高可用架构下的恢复策略
- 实战:一次生产Crash恢复全记录
- 方案设计:RPO/RTO与成本权衡
- 最佳实践与避坑指南
🚀 快速导航索引
按故障类型快速定位
| 故障类型 | 定位章节 | 核心技术 | 预期恢复时间 |
|---|---|---|---|
| 实例Crash | [第2章](#故障类型 定位章节 核心技术 预期恢复时间 实例Crash 第2章 Redo/Undo机制 1-10分钟 硬件故障 第4章 Xtrabackup恢复 30-120分钟 误删数据 第6章 PITR Binlog重放 15-180分钟 主库故障 第7章 主从切换/MGR 1-5分钟) | Redo/Undo机制 | 1-10分钟 |
| 硬件故障 | [第4章](#故障类型 定位章节 核心技术 预期恢复时间 实例Crash 第2章 Redo/Undo机制 1-10分钟 硬件故障 第4章 Xtrabackup恢复 30-120分钟 误删数据 第6章 PITR Binlog重放 15-180分钟 主库故障 第7章 主从切换/MGR 1-5分钟) | Xtrabackup恢复 | 30-120分钟 |
| 误删数据 | [第6章](#故障类型 定位章节 核心技术 预期恢复时间 实例Crash 第2章 Redo/Undo机制 1-10分钟 硬件故障 第4章 Xtrabackup恢复 30-120分钟 误删数据 第6章 PITR Binlog重放 15-180分钟 主库故障 第7章 主从切换/MGR 1-5分钟) | PITR Binlog重放 | 15-180分钟 |
| 主库故障 | [第7章](#故障类型 定位章节 核心技术 预期恢复时间 实例Crash 第2章 Redo/Undo机制 1-10分钟 硬件故障 第4章 Xtrabackup恢复 30-120分钟 误删数据 第6章 PITR Binlog重放 15-180分钟 主库故障 第7章 主从切换/MGR 1-5分钟) | 主从切换/MGR | 1-5分钟 |
核心组件速查表
| 组件 | 作用 | 关键参数 | 避坑指南 |
|---|---|---|---|
| Redo Log | 崩溃恢复基石 | innodb_flush_log_at_trx_commit=1 |
别设置太小 |
| Binlog | PITR和主从 | sync_binlog=1 |
必须保留足够时间 |
| Undo Log | 事务回滚 | innodb_undo_tablespaces |
独立表空间 |
| Double Write | 防止半写 | innodb_doublewrite=ON |
别关闭! |
恢复时间预估
| 恢复方式 | RTO范围 | 影响因素 | 建议场景 |
|---|---|---|---|
| 自动恢复 | 1-10分钟 | Redo Log大小 | 实例意外Crash |
| 主从切换 | 1-5分钟 | 人工操作熟练度 | 主库硬件故障 |
| Xtrabackup | 30-120分钟 | 数据量、IO性能 | 数据文件损坏 |
| PITR恢复 | 15-180分钟 | Binlog大小、时间跨度 | 误操作恢复 |
成本与SLA对应关系
| RPO/RTO目标 | 架构要求 | 年度成本估算 | 适用业务 |
|---|---|---|---|
| RPO=0, RTO<1分钟 | MGR三节点+异地灾备 | ¥100万+ | 金融核心 |
| RPO<5分钟, RTO<5分钟 | 半同步主从+监控 | ¥30-50万 | 电商订单 |
| RPO<30分钟, RTO<30分钟 | 主从+定时备份 | ¥10-20万 | 企业应用 |
| RPO<1天, RTO<2小时 | 单实例+每日备份 | ¥5-10万 | 数据分析 |
📚 章节速览图谱
核心原理篇 (1-3章) 实战操作篇 (4-6章) 架构设计篇 (7-10章)
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 1️⃣ 崩溃场景全景 │ │ 4️⃣ 物理备份恢复 │ │ 7️⃣ 高可用架构 │
│ • 4级崩溃分类 │ │ • Xtrabackup │ │ • 主从/MGR │
│ • CAP工程实践 │────────────│ • 全量+增量 │────────────│ • 1分钟切换 │
│ • RTO/RPO量化 │ │ • 50GB+方案 │ │ • 成本分析 │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 2️⃣ InnoDB机制 │ │ 5️⃣ 逻辑备份恢复 │ │ 8️⃣ 生产实战案例 │
│ • Redo/Undo │ │ • mysqldump │ │ • 磁盘满Crash │
│ • Double Write │ │ • Mydumper │ │ • 8分钟恢复 │
│ • 恢复流程 │ │ • 决策树 │ │ • 改进措施 │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 3️⃣ Binlog一致性 │ │ 6️⃣ PITR时间点恢复│ │ 9️⃣ 方案成本设计 │
│ • 2PC协议 │ │ • Binlog重放 │ │ • SLA分析 │
│ • XID机制 │ │ • 误删恢复 │ │ • TCO计算 │
│ • 闪回工具 │ │ • 15分钟实战 │ │ • 选型矩阵 │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│
▼
┌─────────────────┐
│ 🔟 最佳实践避坑 │
│ • 验证守则 │
│ • 监控告警 │
│ • 演练计划 │
└─────────────────┘
建议阅读路径
🚀 快速入门路径 (新手): 1章 → 2章 → 6章 → 10章
🔧 实战操作路径 (DBA): 4章 → 5章 → 6章 → 8章
🏗️ 架构设计路径 (架构师): 1章 → 7章 → 9章 → 10章
🔬 深度原理路径 (内核研究): 2章 → 3章 → 完整阅读
🔍 术语表与概念解释
核心技术术语
LSN (Log Sequence Number): 日志序列号,InnoDB内部用来标识Redo Log位置的唯一标识符,类似于"时间戳+偏移量"的组合。
XID (Transaction ID): 事务标识符,连接Binlog与Redo Log的桥梁,确保两阶段提交的一致性。
Checkpoint: 检查点,InnoDB记录的一个安全点,表示此点之前的Redo Log已经被应用到数据文件中。
Dirty Page: 脏页,内存中被修改但尚未写入磁盘的数据页,需要通过后台线程异步刷盘。
Double Write Buffer: 双写缓冲区,InnoDB用来防止页半写损坏的连续内存区域,类似"备份页"机制。
业务关键概念
RPO (Recovery Point Objective): 恢复点目标,表示系统故障后可接受的最大数据丢失时间窗口。例如RPO=5分钟意味着最多丢失5分钟的数据。
RTO (Recovery Time Objective): 恢复时间目标,表示系统故障后可接受的最大停机时间。例如RTO=15分钟意味着15分钟内必须恢复服务。
SLA (Service Level Agreement): 服务等级协议,通常99.95%可用性意味着年停机时间不超过4.38小时。
PITR (Point-In-Time Recovery): 时间点恢复,能够将数据库恢复到任意指定时间点的状态,依赖Binlog重放。
架构组件术语
HA (High Availability): 高可用,通过冗余和故障转移机制保证系统持续运行能力。
MGR (MySQL Group Replication): MySQL组复制,提供自动故障转移和数据强一致性保证。
Semi-Sync Replication: 半同步复制,主库提交事务时等待至少一个从库确认,保证数据不丢失。
GTID (Global Transaction ID): 全局事务ID,MySQL 5.6+引入的全局唯一事务标识符,简化主从切换。
MySQL崩溃场景全景图
崩溃不是"宕机"那么简单
MySQL崩溃场景按严重程度分为4级:
MySQL Crash Scenarios Level 1: 实例Crash Level 2: OS Crash Level 3: 硬件故障 Level 4: 人为误操作 内存访问越界 死锁/死循环 OOM killed 内核Panic 断电 磁盘损坏 RAID卡故障 DROP DATABASE UPDATE无WHERE
不同场景恢复策略对比:
| 崩溃等级 | 典型场景 | 恢复方案 | 恢复时间 | 数据丢失 | 发生概率 |
|---|---|---|---|---|---|
| Level 1 | OOM killed、段错误 | InnoDB自动恢复 | 1-10分钟 | 0(理想) | 90% |
| Level 2 | 断电、内核Panic | 物理备份+Binlog | 30-60分钟 | <5分钟 | 7% |
| Level 3 | 磁盘损坏、RAID故障 | 异地备份+HA切换 | 60-120分钟 | <30分钟 | 2% |
| Level 4 | 误删库、UPDATE无WHERE | 延迟从库+审计日志 | 15-180分钟 | 0(可回滚) | 1% |
崩溃恢复的核心目标
python
# 恢复三要素:CAP理论的工程实践
recovery_goals = {
"Consistency": "事务要么全提交,要么全回滚,不能中间状态",
"Availability": "RTO(恢复时间)满足业务SLA",
"Durability": "RPO(数据丢失)控制在分钟级"
}
# 真实生产SLA要求
production_sla = {
"RTO": 15, # 15分钟内恢复服务
"RPO": 5, # 最多丢失5分钟数据
"HA": 99.95 # 年度可用性
}
InnoDB崩溃恢复机制:从Redo到Undo
Redo Log:崩溃恢复的基石
为什么需要Redo Log?
python
# 假设没有Redo Log,直接刷脏页
def write_data_without_redo(data_page, disk):
# 修改内存页
data_page.modify()
# 随机IO刷盘(耗时10ms)
disk.write(data_page)
# 崩溃发生在write过程中 → 页损坏!
# 有Redo Log的机制
def write_data_with_redo(data_page, disk, redo_log):
# 1. 先记Redo(顺序IO,50μs)
redo_log.append("修改页X,偏移Y,值Z")
# 2. 修改内存页
data_page.modify()
# 3. 后台线程异步刷脏(不影响事务提交)
disk.write_later(data_page)
# 即使崩溃,通过Redo可以重放
Redo Log的物理结构:
ib_logfile0 (50M) ib_logfile1 (50M)
[Header] [Header]
[Checkpoint LSN] [Checkpoint LSN]
[Redo Records...] [Redo Records...]
[Dirty Page Table] [Dirty Page Table]
关键参数配置:
ini
# my.cnf
innodb_log_file_size = 2G # Redo文件大小,影响恢复速度
innodb_log_files_in_group = 2 # Redo文件数量
innodb_flush_log_at_trx_commit = 1 # 关键!保证持久性
# 参数详解
innodb_flush_log_at_trx_commit:
=1: 每次提交都fsync (安全,性能略低) ✨
=2: 提交到OS缓存 (可能丢1秒数据)
=0: 每秒flush一次 (可能丢1秒数据)
Undo Log:事务回滚的保障
Undo的作用:
sql
-- 事务示例
BEGIN;
UPDATE students SET gpa = 4.0 WHERE id = 1; -- 原gpa=3.5
-- 此时Undo记录:页X,偏移Y,旧值3.5
-- 事务未提交,实例Crash
-- 恢复时:发现事务未提交 → 通过Undo回滚 → gpa恢复为3.5
Undo存储位置:
- MySQL 5.6及之前:存储在系统表空间(ibdata1)
- MySQL 5.7+:可配置为独立表空间
ini
# my.cnf
innodb_undo_tablespaces = 3 # 3个Undo表空间
innodb_max_undo_log_size = 1G # Undo文件最大1G
innodb_undo_log_truncate = ON # 自动清理
Double Write Buffer:防止页半写
什么是半写?
python
# 16KB页,刷盘过程中只写了8KB,断电
disk_page = b"前8KB新数据 + 后8KB旧数据" # 页损坏!无法修复
# Double Write机制
def write_with_doublewrite(data_page, disk, doublewrite_buf):
# 1. 先写Double Write Buffer(连续空间)
doublewrite_buf.write(data_page)
doublewrite_buf.fsync() # 原子性保证
# 2. 再写真实数据页(随机IO)
disk.write(data_page)
# 崩溃恢复时:检查数据页checksum
# 如果损坏 → 从Double Write Buffer恢复
性能影响与权衡:
ini
# MySQL 5.7+ 可关闭Double Write(有风险)
innodb_doublewrite = ON # 默认开启,不要关闭!
崩溃恢复流程详解
InnoDB自动恢复完整流程:
┌─────────────────────────────────────────────────────────────┐
│ MySQL启动检测 │
│ 检查:最后一次正常关闭标志 (shutdown_flag != CLEAN) │
└────────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 第1阶段:扫描Redo Log │
│ • 读取checkpoint LSN: 88823345 │
│ • 扫描到最新LSN: 88829123 │
│ • 构建待恢复事务列表 │
│ • 日志量:5778 bytes ≈ 5.6KB │
└────────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 第2阶段:重做已提交事务 (Redo) │
│ • 重放物理日志记录到内存页 │
│ • Progress: [████████████████████] 100% │
│ • 恢复123个数据页修改 │
│ • 时间:2-8秒 (取决于日志量) │
└────────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 第3阶段:回滚未提交事务 (Undo) │
│ • 扫描活跃事务列表:trx_id [0x12345, 0x12348] │
│ • 读取Undo日志,逆向回滚 │
│ • Rolling back: 2 transactions │
│ • 时间:1-3秒 │
└────────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 第4阶段:清理与验证 │
│ • 删除临时表 (temp_*) │
│ • 重建数据字典缓存 │
│ • 验证系统表空间完整性 │
│ • 重置shutdown标志 → CLEAN │
└────────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 恢复完成,正常启动 │
│ [InnoDB] Recovery from Crash complete. │
│ [Server] MySQL is ready for connections. │
│ 总耗时:3-15秒 (典型场景) │
└─────────────────────────────────────────────────────────────┘
实际日志示例:
bash
# 1. MySQL启动,检测到异常关闭
[InnoDB] Starting crash recovery.
[InnoDB] Reading redo log from LSN 88823345
# 2. 分析Redo Log,重放已提交事务
[InnoDB] redo log: 15000 bytes
[InnoDB] 8.8.23.345 -> 8.8.29.123 (Progress: 0-100%)
# 3. 回滚未提交事务(使用Undo)
[InnoDB] Rolling back trx 0x12345
# 4. 清理临时表
[InnoDB] Removing temporary table
# 5. 恢复完成,正常启动
[InnoDB] Crash recovery finished.
恢复时间估算:
python
def estimate_recovery_time(redo_log_size_gb, disk_iops):
"""
恢复时间 ≈ Redo Log重放 + Undo回滚 + 脏页刷盘
Redo重放:1GB Redo ≈ 30-60秒(取决于CPU)
Undo回滚:活跃事务数 × 10ms/事务
脏页刷盘:脏页数 × 10ms/页
"""
redo_replay_time = redo_log_size_gb * 45 # 秒
# 活跃事务数(从show engine innodb status)
active_transactions = 50
undo_rollback_time = active_transactions * 0.01 # 秒
# 脏页数量(从buffer pool)
dirty_pages = 10000
flush_time = dirty_pages * 0.01 / (disk_iops / 1000) # 秒
return redo_replay_time + undo_rollback_time + flush_time
# 示例:2GB Redo,100活跃事务,1万脏页,SSD 5000 IOPS
# 恢复时间 ≈ 90秒 + 0.5秒 + 20秒 = 110秒
Binlog与数据一致性:XID的奥秘
Binlog与Redo的一致性挑战
两阶段提交(2PC)机制:
python
# 事务提交流程(保证Binlog与Redo一致)
def commit_transaction(trx_id, binlog_cache, redo_log):
# 阶段1:Prepare
# 1.1 写Redo Log(标记事务Prepare状态)
redo_log.write(f"TRX_PREPARE: {trx_id}")
redo_log.fsync()
# 1.2 写Binlog(内存缓存)
binlog_cache.write(trx_id, "COMMIT")
# 阶段2:Commit
# 2.1 刷Binlog到磁盘(原子性保证)
binlog_cache.fsync()
# 2.2 写Redo Log(标记事务Commit)
redo_log.write(f"TRX_COMMIT: {trx_id}")
redo_log.fsync()
# 崩溃恢复时:
# 若Redo是Prepare但Binlog不存在 → 回滚
# 若Redo是Prepare且Binlog已写 → 提交
# XID:连接Redo与Binlog的桥梁
XID = {
"trx_id": "0x12345",
"binlog_pos": "mysql-bin.000123:456789",
"redo_lsn": "88823345"
}
Binlog格式选择:
ini
# my.cnf
binlog_format = ROW # 推荐!数据最准确
# 三种格式对比
ROW格式:
优点: 记录每行变更,恢复精确 ✨
缺点: 文件较大
适用: 金融、电商(数据一致性要求高)
STATEMENT格式:
优点: 文件小
缺点: 某些函数可能不一致(如UUID())
适用: 读多写少的分析场景
MIXED格式:
折中方案,MySQL自动选择
从Binlog恢复数据
场景 :误操作 DELETE FROM students WHERE 1=1(忘记加WHERE)
bash
# 步骤1:找到误操作的Binlog位置
mysqlbinlog --base64-output=decode-rows -v \
--start-datetime="2024-01-15 14:00:00" \
--stop-datetime="2024-01-15 14:05:00" \
mysql-bin.000123 | grep -i delete
# 输出示例(ROW格式)
### DELETE FROM `test`.`students`
### WHERE
### @1=1 /* INT meta=0 nullable=0 is_null=0 */
### @2='Alice' /* VARSTRING(100) meta=100 nullable=1 is_null=0 */
### @3=3.5 /* FLOAT meta=4 nullable=1 is_null=0 */
# 步骤2:生成反向SQL
# 工具:binlog2sql(美团开源)
pip install binlog2sql
binlog2sql -h127.0.0.1 -uroot -p'xxx' \
--flashback \\ # 生成回滚SQL
--start-file=mysql-bin.000123 \
--start-pos=456789 \
--stop-pos=456999 \
-d test -t students
# 输出(反向INSERT)
INSERT INTO `test`.`students` (`id`, `name`, `gpa`) VALUES (1, 'Alice', 3.5);
# 步骤3:执行恢复
mysql -uroot -p < flashback.sql
Binlog恢复的价值:
- PITR(时间点恢复):恢复任意时刻数据
- 误操作闪回:快速回滚错误SQL
- 审计:追踪数据变更历史
物理备份恢复:Xtrabackup实战
Xtrabackup vs mysqldump
| 维度 | Xtrabackup | mysqldump |
|---|---|---|
| 备份方式 | 物理(ibd文件) | 逻辑(SQL) |
| 备份速度 | 快(顺序IO) | 慢(逐行查询) |
| 恢复速度 | 快(文件拷贝) | 慢(重放SQL) |
| 一致性 | 完美 | 可能锁表 |
| 增量备份 | 支持 | 不支持 |
| 表级别恢复 | 困难 | 容易 |
| 影响业务 | 极低 | 高(锁表) |
结论:10GB+数据量,Xtrabackup是唯一选择。
Xtrabackup全量备份
bash
# 安装
wget https://downloads.percona.com/downloads/Percona-XtraBackup-8.0/Percona-XtraBackup-8.0.35-31/binary/tarball/percona-xtrabackup-8.0.35-Linux-x86_64.glibc2.17.tar.gz
tar -xzf percona-xtrabackup-8.0.35-Linux-x86_64.glibc2.17.tar.gz
# 全量备份
xtrabackup --defaults-file=/etc/my.cnf \
--user=root --password='your_pass' \
--backup --target-dir=/data/backup/full_20240115 \
--parallel=4 --compress --compress-threads=4
# 关键参数详解
--backup: 备份模式
--target-dir: 备份目录(每个实例一个)
--parallel: 并行线程数(加速备份)
--compress: 压缩(节省空间70%)
--compress-threads: 压缩线程(不占主线程)
备份过程解析:
bash
# 1. 连接MySQL,记录LSN
xtrabackup: Connected to MySQL
xtrabackup: Starting LSN: 88823345
# 2. 拷贝InnoDB表空间(不锁表!)
xtrabackup: Copying ./ibdata1
xtrabackup: Copying ./test/students.ibd
xtrabackup: Copying ./test/courses.ibd
# 3. 备份期间Redo Log持续写入
xtrabackup: Log copying: Last LSN: 88824567
# 4. 全局锁(短暂,仅拷贝非InnoDB表)
xtrabackup: Executing FLUSH TABLES WITH READ LOCK...
xtrabackup: Finished backing up non-InnoDB tables
# 5. 备份完成,记录Binlog位置
xtrabackup: MySQL binlog position: mysql-bin.000123:456789
xtrabackup: Backup successful
Xtrabackup增量备份
优点:每天只备份变化的数据,节省空间90%。
bash
# 步骤1:周日全量备份
xtrabackup --backup --target-dir=/data/backup/full_sunday
# 步骤2:周一增量(基于周日)
xtrabackup --backup \
--target-dir=/data/backup/incr_monday \
--incremental-basedir=/data/backup/full_sunday
# 步骤3:周二增量(基于周一)
xtrabackup --backup \
--target-dir=/data/backup/incr_tuesday \
--incremental-basedir=/data/backup/incr_monday
# 关键:基于前一天的增量,形成链条
# 恢复时需要全量 + 所有增量
增量备份原理:
python
# Xtrabackup通过LSN判断哪些页被修改
class PageTracker:
def __init__(self):
self.last_lsn = 0
def is_page_modified(self, page_lsn):
# 如果页的LSN > 上次备份的LSN → 该页被修改
return page_lsn > self.last_lsn
# 备份时只拷贝modified_pages
modified_pages = [p for p in all_pages if is_page_modified(p.lsn)]
Xtrabackup恢复演练
恢复步骤:
bash
# 步骤1:准备全量备份(应用Redo)
xtrabackup --prepare \
--apply-log-only \
--target-dir=/data/backup/full_sunday
# --prepare: 准备恢复(必须)
# --apply-log-only: 只应用Redo,不滚回未提交事务(增量恢复需要)
# 步骤2:应用周一增量
xtrabackup --prepare \
--apply-log-only \
--target-dir=/data/backup/full_sunday \
--incremental-dir=/data/backup/incr_monday
# 步骤3:应用周二增量(最后一次不加--apply-log-only)
xtrabackup --prepare \
--target-dir=/data/backup/full_sunday \
--incremental-dir=/data/backup/incr_tuesday
# 步骤4:停止MySQL,恢复数据
systemctl stop mysql
rm -rf /data/mysql/*
# 步骤5:拷贝备份文件
xtrabackup --copy-back \
--target-dir=/data/backup/full_sunday \
--datadir=/data/mysql
# 步骤6:修改权限,启动MySQL
chown -R mysql:mysql /data/mysql
systemctl start mysql
# 步骤7:检查Binlog位置,确定是否需要增备
# 如果备份期间有业务写入,需要从Binlog位置开始重放
Xtrabackup生产脚本
bash
#!/bin/bash
# backup_mysql.sh
# MySQL备份脚本(生产级)
set -e
# 配置
MYSQL_USER="backup_user"
MYSQL_PASS="your_secure_pass"
BACKUP_BASE="/data/backup/mysql"
RETENTION_DAYS=7
# 创建备份目录
FULL_DIR="$BACKUP_BASE/full_$(date +%Y%m%d)"
INCR_DIR="$BACKUP_BASE/incr_$(date +%Y%m%d)"
mkdir -p $FULL_DIR $INCR_DIR
# 判断:周日全量,其他天增量
if [ "$(date +%u)" == "7" ]; then
# 周日:全量备份
echo "[$(date)] 开始全量备份..."
xtrabackup --defaults-file=/etc/my.cnf \
--user=$MYSQL_USER --password=$MYSQL_PASS \
--backup --target-dir=$FULL_DIR \
--parallel=4 --compress --compress-threads=4 \
2>&1 | tee $FULL_DIR/backup.log
# 清理旧备份(保留7天)
find $BACKUP_BASE -name "full_*" -type d -mtime +$RETENTION_DAYS | xargs rm -rf
# 记录Binlog位置(用于PITR)
cat $FULL_DIR/xtrabackup_binlog_info >> $BACKUP_BASE/binlog_history.log
echo "[$(date)] 全量备份完成:$FULL_DIR"
else
# 其他天:增量备份
# 找到昨天备份(全量或增量)
LAST_BACKUP=$(ls -td $BACKUP_BASE/*/ | head -1)
echo "[$(date)] 开始增量备份(基于:$LAST_BACKUP)..."
xtrabackup --defaults-file=/etc/my.cnf \
--user=$MYSQL_USER --password=$MYSQL_PASS \
--backup --target-dir=$INCR_DIR \
--incremental-basedir=$LAST_BACKUP \
--parallel=2 --compress \
2>&1 | tee $INCR_DIR/backup.log
echo "[$(date)] 增量备份完成:$INCR_DIR"
fi
# 备份验证
if [ -f "$FULL_DIR/xtrabackup_info" ] || [ -f "$INCR_DIR/xtrabackup_info" ]; then
echo "[$(date)] 备份成功!"
# 发送到监控
curl -X POST https://monitoring.company.com/api/alerts \
-d "message=MySQL备份成功&type=backup&status=success"
else
echo "[$(date)] 备份失败!"
# 发送告警
curl -X POST https://monitoring.company.com/api/alerts \
-d "message=MySQL备份失败&type=backup&status=failure"
exit 1
fi
逻辑备份恢复:mysqldump与Mydumper
mysqldump:传统但可靠
适用场景:
- 数据量 < 10GB
- 表级别备份恢复
- 跨版本迁移
- 生成测试数据
bash
# 全库备份
mysqldump -uroot -p'xxx' \
--all-databases \
--single-transaction \
--master-data=2 \
--routines --triggers \
--default-character-set=utf8mb4 \
> /data/backup/full_dump_$(date +%Y%m%d).sql
# 关键参数
--single-transaction: InnoDB一致性备份(不锁表)✨
--master-data=2: 记录Binlog位置(用于主从)
--routines: 备份存储过程
--triggers: 备份触发器
--default-character-set: 指定字符集
问题:mysqldump一个50GB库需要2小时,期间业务性能下降40%。
Mydumper:并发加速
原理:多线程并行导出表。
bash
# 安装
yum install -y mydumper
# 并发备份(10线程)
mydumper -u root -p 'xxx' \
-B test \\ # 指定库
-t 10 \\ # 10个线程
-c \\ # 压缩
-o /data/backup/mydumper_test \
-v 3 # 详细日志
# 备份结果结构
/data/backup/mydumper_test/
├── metadata # Binlog位置
├── test-schema-create.sql # 建库SQL
├── test.students-schema.sql # 建表SQL
├── test.students.sql # 数据(多个chunk)
├── test.courses-schema.sql
└── test.courses.sql
恢复:
bash
# Myloader并发导入(10线程)
myloader -u root -p 'xxx' \
-d /data/backup/mydumper_test \
-t 10 \
-v 3
# 50GB数据:mysqldump需2小时 → Mydumper 20分钟 ✨
逻辑备份 vs 物理备份决策树
python
def choose_backup_method(data_size_gb, backup_window_minutes, recovery_time_sla):
"""
备份方案选择逻辑
"""
if data_size_gb > 100:
return "xhysical_backup" # Xtrabackup
if recovery_time_sla < 30:
return "physical_backup" # 恢复快
if backup_window_minutes < 60:
return "mydumper" # 并行备份快
return "mysqldump" # 简单可靠
# 真实案例
decision = choose_backup_method(
data_size_gb=50,
backup_window_minutes=30, # 只允许30分钟备份窗口
recovery_time_sla=60 # 要求1小时内恢复
)
# 返回:mydumper(备份快,恢复可接受)
时间点恢复(PITR):从Binlog重放
PITR原理
python
# 恢复流程
def point_in_time_recovery(full_backup, binlog_files, target_time):
"""
1. 恢复全量备份(到某个时间点T1)
2. 从T1开始重放Binlog(到目标时间T2)
3. 跳过错误SQL(如果是指定位置)
"""
# 步骤1:恢复全量
xtrabackup --copy-back --target-dir=full_backup
# 步骤2:应用Redo(保证一致性)
xtrabackup --prepare --target-dir=full_backup
# 步骤3:启动MySQL(此时数据状态为T1)
start_mysql()
# 步骤4:重放Binlog(T1 -> T2)
mysqlbinlog \
--start-datetime="T1" \
--stop-datetime="T2" \
mysql-bin.000123 | mysql
return "恢复完成,数据状态:T2"
# 关键:必须记录Binlog位置
# Xtrabackup备份时记录:xtrabackup_binlog_info
PITR生产实战
场景 :1月15日14:30,误删students表,需恢复到14:29。
bash
# 步骤1:找到最近全量备份
ls -lt /data/backup/full_* | head -1
# full_20240115_020000(凌晨2点全量)
# 步骤2:恢复全量
systemctl stop mysql
rm -rf /data/mysql/*
xtrabackup --copy-back --target-dir=/data/backup/full_20240115_020000
xtrabackup --prepare --target-dir=/data/backup/full_20240115_020000
chown -R mysql:mysql /data/mysql
systemctl start mysql
# 步骤3:确定Binlog位置
cat /data/backup/full_20240115_020000/xtrabackup_binlog_info
# mysql-bin.000123:456789
# 步骤4:重放Binlog(2:00 -> 14:29)
mysqlbinlog \
--start-position=456789 \
--stop-datetime="2024-01-15 14:29:00" \
/var/lib/mysql/mysql-bin.000123 \
/var/lib/mysql/mysql-bin.000124 \
/var/lib/mysql/mysql-bin.000125 | mysql
# 步骤5:验证数据
mysql -e "SELECT COUNT(*) FROM test.students"
# 恢复成功!数据回到14:29状态
PITR注意事项:
- Binlog必须完整(
expire_logs_days设置合理) - 恢复前必须备份当前数据(防止二次损坏)
- 大事务Binlog重放可能很慢(100MB+需30分钟+)
高可用架构下的恢复策略
主从架构的恢复捷径
原理:从库数据实时同步,主库Crash后直接提升从库。
bash
# 主库Crash,检测主库状态
mysqladmin -h master -u root -p ping
# mysqld is alive ✗
# 步骤1:确认从库同步状态
mysql -h slave -u root -p -e "SHOW SLAVE STATUS\G"
# Seconds_Behind_Master: 0 # 必须=0,无延迟
# Master_Log_File: mysql-bin.000123
# Read_Master_Log_Pos: 456789
# 步骤2:从库停止复制
mysql -h slave -u root -p -e "STOP SLAVE;"
# 步骤3:提升从库为主库
# 修改应用配置,指向从库IP
# 或将从库IP漂移(VIP方案)
# 步骤4:原主库恢复后作为新从库
# 原主库恢复后,CHANGE MASTER TO指向新主库
优点:
- RTO < 1分钟(人工切换)
- RPO ≈ 0(无数据丢失)
- 无需备份恢复
前提:
- 从库必须无延迟
- 半同步复制(
rpl_semi_sync)保证数据不丢
MGR(MySQL Group Replication)自动恢复
MGR架构:
Primary (写) + 2个Secondary (读)
↓
自动Failover:Primary Crash → Secondary自动提升
配置:
ini
# my.cnf
plugin-load = group_replication.so
group_replication_group_name = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
group_replication_start_on_boot = ON
group_replication_local_address = "10.0.0.1:33061"
group_replication_group_seeds = "10.0.0.1:33061,10.0.0.2:33061,10.0.0.3:33061"
group_replication_bootstrap_group = OFF # 只在第一台开启
group_replication_single_primary_mode = ON # 单主模式
恢复流程:
bash
# Primary节点Crash
[Primary] mysqld stopped
# MGR自动选举(5秒内完成)
[Secondary2] 提升为Primary
[Secondary2] 日志: This member is the PRIMARY
# 应用无感知(Proxy自动路由)
异地容灾架构
跨机房复制:
上海可用区B 北京可用区A 机房断电 灾备从库 Slave2 主库 Master 从库 Slave1
配置:
ini
# 上海从库配置
group_replication_group_seeds = "北京IP:33061,上海IP:33061"
故障切换:
bash
# 北京机房整体故障
# 提升上海从库为主库
# 步骤1:在上海启动新MGR集群
mysql -h 上海IP -e "
SET GLOBAL group_replication_bootstrap_group=ON;
START GROUP_REPLICATION;
SET GLOBAL group_replication_bootstrap_group=OFF;
"
# 步骤2:应用切换到上海IP
# 步骤3:北京恢复后,作为从库加入
实战:一次生产Crash恢复全记录
事故背景
时间 :2024-01-15 23:45(业务高峰期)
现象:MySQL主库响应缓慢,监控系统报警"Too many connections",随后实例Crash。
环境:
- MySQL 8.0.32
- 数据量:120GB
- 主从架构:1主1从
- 备份策略:每日3点Xtrabackup全量
事故现场
bash
# 错误日志
[ERROR] InnoDB: Database page corruption on disk or a failed
[ERROR] InnoDB: file read of page 12345 in file ./ibdata1
[ERROR] mysqld got signal 11 (Segmentation fault)
[Warning] disk is full. Aborting
分析:磁盘满导致InnoDB写入失败 → 页损坏 → Crash
恢复决策
python
# 评估选项
recovery_options = {
"option_1": {
"desc": "从库提升为主库",
"rto": 5, # 分钟
"rpo": 0, # 无丢失
"risk": "低"
},
"option_2": {
"desc": "Xtrabackup恢复",
"rto": 120, # 分钟
"rpo": 20 * 60, # 20分钟(从3:00备份到3:20 Crash)
"risk": "中"
},
"option_3": {
"desc": "PITR恢复",
"rto": 180, # 分钟
"rpo": 0, # 无丢失(Binlog完整)
"risk": "高(操作复杂)"
}
}
# 决策:选方案1(从库提升)
# 原因:RTO短、RPO=0、风险低
执行恢复
bash
# 步骤1:检查从库状态
mysql -h 10.0.0.2 -u root -p -e "SHOW SLAVE STATUS\G"
# Seconds_Behind_Master: 0 ✨
# Master_Log_File: mysql-bin.000125
# Read_Master_Log_Pos: 789012
# 步骤2:停止从库复制
mysql -h 10.0.0.2 -e "STOP SLAVE;"
# 步骤3:确认无写入
mysql -h 10.0.0.2 -e "SHOW PROCESSLIST" | grep -v "Sleep"
# 步骤4:修改应用配置(切换数据库IP)
# 配置文件:/app/config/database.yml
# 修改:host: 10.0.0.1 → 10.0.0.2
ansible app_servers -m lineinfile \
-a "path=/app/config/database.yml regexp='host: 10.0.0.1' line='host: 10.0.0.2'"
# 步骤5:重启应用
ansible app_servers -m shell -a "systemctl restart app"
# 步骤6:验证业务
curl -X POST https://api.company.com/health
# {"status": "ok", "db": "connected"} ✨
# 步骤7:原主库修复
# 清理磁盘空间
rm -rf /var/log/mysql/*.log
# 重启MySQL
systemctl start mysql
# 作为从库加入
mysql -e "
CHANGE MASTER TO
MASTER_HOST='10.0.0.2',
MASTER_USER='repl',
MASTER_PASSWORD='repl_pass',
MASTER_LOG_FILE='mysql-bin.000125',
MASTER_LOG_POS=789012;
START SLAVE;
"
# 步骤8:验证主从同步
mysql -h 10.0.0.2 -e "SHOW SLAVE STATUS\G" | grep "Seconds_Behind_Master"
# Seconds_Behind_Master: 0 ✨
恢复结果
python
recovery_result = {
"total_time": 8, # 分钟(从Crash到恢复)
"data_loss": 0, # 无数据丢失
"business_impact": "5分钟",
"user_complaints": 3,
"post_mortem": [
"改进1:磁盘空间监控告警(80%阈值)",
"改进2:自动切换脚本(减少人工操作)",
"改进3:从库延迟监控(>5秒告警)"
]
}
方案设计:RPO/RTO与成本权衡
RPO/RTO定义
python
# RPO(恢复点目标):最大可接受数据丢失
rpo_targets = {
"金融核心系统": "RPO=0秒", # 不能丢数据 → 同步复制
"电商平台": "RPO=5分钟", # 可接受少量订单丢失 → 半同步
"内容管理": "RPO=1小时", # 可接受 → 异步复制
}
# RTO(恢复时间目标):最大可接受停机时间
rto_targets = {
"金融核心系统": "RTO=1分钟", # 高可用切换
"电商平台": "RTO=15分钟", # 快速恢复
"内容管理": "RTO=4小时" # 可容忍长时间停机
}
成本模型
python
def calculate_tco(data_size_gb, rpo_minutes, rto_minutes):
"""
总拥有成本(3年)
"""
# 基础成本(单实例)
hardware = 5000 # 服务器
mysql_license = 0 # 社区版免费
# 高可用成本(与RPO/RTO相关)
if rpo_minutes <= 1 and rto_minutes <= 5:
# 三节点MGR + 异地灾备
ha_cost = 50000 # 硬件*3 + 网络
elif rpo_minutes <= 5 and rto_minutes <= 15:
# 主从 + 半同步
ha_cost = 20000 # 主+从
else:
# 单实例 + 备份
ha_cost = 5000
# 存储成本
storage_cost = (data_size_gb * 3) * 0.1 * 36 # 3副本,3年
# 人力成本
dba_cost = 20000 # 年维护成本
# 备份存储成本
backup_cost = (data_size_gb * 0.1) * 36 # 每天备份,保留30天
return {
"hardware": hardware + ha_cost,
"storage": storage_cost,
"backup": backup_cost,
"dba": dba_cost * 3,
"total": hardware + ha_cost + storage_cost + backup_cost + dba_cost * 3
}
# 示例:100GB数据,RPO=5分钟,RTO=15分钟
cost = calculate_tco(100, 5, 15)
# {
# "hardware": 25000,
# "storage": 10800,
# "backup": 360,
# "dba": 60000,
# "total": 96,160(约10万)
# }
方案选型矩阵
| 业务类型 | 数据量 | RPO | RTO | 推荐方案 | 成本(3年) |
|---|---|---|---|---|---|
| 个人博客 | <10GB | 1小时 | 4小时 | 单机+mysqldump | 5,000 |
| SMB应用 | 10-100GB | 5分钟 | 30分钟 | 主从+Xtrabackup | 50,000 |
| 电商平台 | 100GB-1TB | <1分钟 | <5分钟 | MGR+异地备 | 200,000 |
| 金融核心 | >1TB | 0秒 | <1分钟 | 三地五中心 | 1,000,000 |
最佳实践与避坑指南
崩溃恢复黄金守则
1. 备份必须验证
bash
# 错误:备份后不验证,恢复时发现备份损坏
# 正确:定期恢复演练
0 3 * * 0 /root/backup_mysql.sh && /root/verify_backup.sh
# verify_backup.sh
xtrabackup --prepare --target-dir=/data/backup/latest
if [ $? -eq 0 ]; then
echo "备份验证通过"
else
echo "备份损坏,立即告警!"
fi
2. Binlog必须保留足够时间
ini
# my.cnf
# 错误设置(3天)
expire_logs_days = 3
# 正确(至少30天,配合备份策略)
binlog_expire_logs_seconds = 2592000 # 30天
3. Redo Log大小要合理
ini
# 错误:innodb_log_file_size = 50M(太小,频繁切换)
# 正确:根据写入量调整
innodb_log_file_size = 2G # 一般2-4G
innodb_log_files_in_group = 2
4. 监控必须到位
python
# Prometheus告警规则
- alert: MySQLCrash
expr: mysql_up == 0
for: 1m
labels:
severity: critical
annotations:
summary: "MySQL实例宕机: {{ $labels.instance }}"
- alert: MySQLReplicationLag
expr: mysql_slave_lag_seconds > 10
for: 5m
labels:
severity: warning
annotations:
summary: "从库延迟超过10秒"
action: "检查从库状态,准备切换"
常见错误与解决方案
| 错误现象 | 根本原因 | 解决方案 |
|---|---|---|
| 恢复后数据不一致 | Binlog与Redo不一致 | 检查sync_binlog=1 |
| Xtrabackup恢复失败 | 备份损坏 | 备份时加--check-privileges |
| Crash恢复时间太长 | Redo Log太大 | 减小innodb_log_file_size |
| 从库提升后主从不一致 | 未停止复制就写入 | 提升前STOP SLAVE |
| PITR找不到Binlog | Binlog被清理 | 延长binlog_expire_logs_seconds |
恢复演练计划
python
recovery_drill_plan = {
"frequency": "每月一次",
"scenarios": [
{
"name": "主库Crash",
"steps": ["检查从库", "提升从库", "切换VIP"],
"expected_rto": 10 # 分钟
},
{
"name": "误删表",
"steps": ["确定时间", "PITR恢复", "验证数据"],
"expected_rto": 60 # 分钟
},
{
"name": "磁盘损坏",
"steps": ["Xtrabackup恢复", "应用Binlog"],
"expected_rto": 120 # 分钟
}
],
"success_criteria": "RTO和RPO均达标"
}
📊 监控配置模板
Prometheus + Grafana监控方案
1. Prometheus告警规则
yaml
# mysql_alerts.yml
groups:
- name: mysql_critical
rules:
- alert: MySQLInstanceDown
expr: mysql_up == 0
for: 1m
labels:
severity: critical
annotations:
summary: "MySQL实例宕机: {{ $labels.instance }}"
description: "MySQL实例 {{ $labels.instance }} 已经宕机超过1分钟"
- alert: MySQLReplicationLag
expr: mysql_slave_lag_seconds > 10
for: 5m
labels:
severity: warning
annotations:
summary: "从库延迟超过10秒: {{ $labels.instance }}"
action: "检查从库状态,准备主从切换"
- alert: MySQLDiskSpaceLow
expr: (mysql_global_status_innodb_data_files_bytes - mysql_global_status_innodb_data_free_bytes) / mysql_global_status_innodb_data_files_bytes > 0.85
for: 5m
labels:
severity: warning
annotations:
summary: "MySQL磁盘使用率超过85%: {{ $labels.instance }}"
description: "实例 {{ $labels.instance }} 磁盘使用率 {{ $value | humanizePercentage }}"
- alert: MySQLTooManyConnections
expr: mysql_global_status_threads_connected > mysql_global_variables_max_connections * 0.8
for: 2m
labels:
severity: critical
annotations:
summary: "MySQL连接数过高: {{ $labels.instance }}"
description: "连接数 {{ $value }} 超过最大连接数80%"
- alert: MySQLReplicationBroken
expr: mysql_slave_status_slave_io_running == 0 or mysql_slave_status_slave_sql_running == 0
for: 1m
labels:
severity: critical
annotations:
summary: "主从复制中断: {{ $labels.instance }}"
description: "从库 {{ $labels.instance }} 复制线程停止"
- alert: MySQLBinaryLogDiskUsage
expr: mysql_global_status_binlog_cache_disk_use > 0
for: 5m
labels:
severity: warning
annotations:
summary: "Binlog缓存溢出到磁盘: {{ $labels.instance }}"
description: "实例 {{ $labels.instance }} Binlog缓存使用磁盘,可能影响性能"
- alert: MySQLInnoDBCheckpointAgeTooOld
expr: mysql_global_status_innodb_checkpoint_max_age > mysql_global_variables_innodb_log_file_size * 0.75
for: 10m
labels:
severity: warning
annotations:
summary: "InnoDB Checkpoint年龄过老: {{ $labels.instance }}"
description: "实例 {{ $labels.instance }} Checkpoint年龄超过Redo Log大小75%"
2. Grafana仪表板配置
json
{
"dashboard": {
"title": "MySQL Crash Recovery Monitor",
"panels": [
{
"title": "实例状态总览",
"type": "stat",
"targets": [
{
"expr": "sum(mysql_up)",
"legendFormat": "正常运行实例数"
},
{
"expr": "sum(mysql_global_status_threads_connected)",
"legendFormat": "当前连接数"
}
]
},
{
"title": "主从复制延迟",
"type": "graph",
"targets": [
{
"expr": "mysql_slave_lag_seconds",
"legendFormat": "{{instance}} - 从库延迟"
}
],
"thresholds": [
{
"color": "green",
"value": null
},
{
"color": "yellow",
"value": 10
},
{
"color": "red",
"value": 60
}
]
},
{
"title": "磁盘使用率",
"type": "graph",
"targets": [
{
"expr": "(mysql_global_status_innodb_data_files_bytes - mysql_global_status_innodb_data_free_bytes) / mysql_global_status_innodb_data_files_bytes * 100",
"legendFormat": "{{instance}} - 磁盘使用率%"
}
],
"thresholds": [
{
"color": "green",
"value": null
},
{
"color": "yellow",
"value": 70
},
{
"color": "red",
"value": 85
}
]
}
]
}
}
3. 备份状态监控脚本
bash
#!/bin/bash
# backup_monitor.sh - 备份状态监控脚本
MYSQL_USER="monitor_user"
MYSQL_PASS="secure_password"
BACKUP_DIR="/data/backup/mysql"
RETENTION_DAYS=7
# 检查备份目录权限
if [ ! -d "$BACKUP_DIR" ]; then
echo "CRITICAL: 备份目录不存在: $BACKUP_DIR"
exit 1
fi
# 检查今日备份是否存在
TODAY_BACKUP=$(find $BACKUP_BASE -name "*$(date +%Y%m%d)*" -type d | head -1)
if [ -z "$TODAY_BACKUP" ]; then
echo "WARNING: 今日备份不存在: $(date +%Y%m%d)"
# 发送告警
curl -X POST https://monitoring.company.com/api/alerts \
-H "Content-Type: application/json" \
-d '{
"alert_type": "backup_missing",
"severity": "warning",
"message": "MySQL备份缺失 - 日期: '$(date +%Y%m%d)'",
"timestamp": "'$(date -Iseconds)'"
}'
else
# 验证备份完整性
if [ -f "$TODAY_BACKUP/xtrabackup_info" ]; then
BACKUP_SIZE=$(du -sh $TODAY_BACKUP | cut -f1)
echo "SUCCESS: 今日备份正常 - 大小: $BACKUP_SIZE"
else
echo "ERROR: 备份文件损坏"
exit 1
fi
fi
# 检查备份保留策略
OLD_BACKUPS=$(find $BACKUP_BASE -name "*full_*" -type d -mtime +$RETENTION_DAYS)
if [ -n "$OLD_BACKUPS" ]; then
echo "WARNING: 发现过期备份需要清理"
echo "$OLD_BACKUPS" | while read backup; do
echo "清理过期备份: $backup"
rm -rf "$backup"
done
fi
# 验证主从复制延迟
SLAVE_LAG=$(mysql -u$MYSQL_USER -p$MYSQL_PASS -h slave-host -e "SHOW SLAVE STATUS\G" | grep "Seconds_Behind_Master" | awk '{print $2}')
if [ "$SLAVE_LAG" -gt 60 ]; then
echo "CRITICAL: 从库延迟超过60秒: $SLAVE_LAG"
# 发送紧急告警
curl -X POST https://monitoring.company.com/api/alerts \
-H "Content-Type: application/json" \
-d '{
"alert_type": "replication_lag",
"severity": "critical",
"message": "从库延迟严重: '${SLAVE_LAG}'秒",
"timestamp": "'$(date -Iseconds)'"
}'
elif [ "$SLAVE_LAG" -gt 10 ]; then
echo "WARNING: 从库延迟: $SLAVE_LAG 秒"
fi
echo "备份监控检查完成: $(date)"
4. 健康检查端点
python
#!/usr/bin/env python3
# health_check.py - MySQL健康检查端点
import mysql.connector
import sys
import json
from datetime import datetime
def check_mysql_health():
"""MySQL实例健康检查"""
config = {
'host': 'localhost',
'user': 'health_check_user',
'password': 'health_check_pass',
'database': 'information_schema'
}
try:
conn = mysql.connector.connect(**config)
cursor = conn.cursor()
# 检查基本连接
cursor.execute("SELECT 1")
result = cursor.fetchone()
if not result:
return {"status": "unhealthy", "error": "Basic query failed"}
# 检查复制状态
cursor.execute("SHOW SLAVE STATUS")
slave_status = cursor.fetchone()
if slave_status:
io_running = slave_status[10] # Slave_IO_Running
sql_running = slave_status[11] # Slave_SQL_Running
lag = slave_status[32] # Seconds_Behind_Master
replication_healthy = (io_running == 'Yes' and sql_running == 'Yes')
else:
replication_healthy = True # 非从库
lag = 0
# 检查InnoDB状态
cursor.execute("SHOW ENGINE INNODB STATUS")
innodb_status = cursor.fetchone()
# 检查连接数
cursor.execute("SHOW STATUS LIKE 'Threads_connected'")
threads_connected = cursor.fetchone()[1]
cursor.execute("SHOW VARIABLES LIKE 'max_connections'")
max_connections = cursor.fetchone()[1]
connection_ratio = int(threads_connected) / int(max_connections)
# 综合健康评估
health_score = 100
issues = []
if not replication_healthy:
health_score -= 30
issues.append("主从复制中断")
if int(lag) > 60:
health_score -= 20
issues.append(f"复制延迟过高: {lag}秒")
if connection_ratio > 0.8:
health_score -= 15
issues.append(f"连接数过高: {connection_ratio:.1%}")
# 确定整体状态
if health_score >= 90:
status = "healthy"
elif health_score >= 70:
status = "warning"
else:
status = "critical"
return {
"status": status,
"score": health_score,
"timestamp": datetime.now().isoformat(),
"metrics": {
"replication_healthy": replication_healthy,
"replication_lag_seconds": int(lag) if lag else 0,
"connection_ratio": round(connection_ratio, 3),
"threads_connected": int(threads_connected)
},
"issues": issues
}
except mysql.connector.Error as e:
return {
"status": "unhealthy",
"error": str(e),
"timestamp": datetime.now().isoformat()
}
finally:
if 'conn' in locals():
conn.close()
if __name__ == "__main__":
health = check_mysql_health()
print(json.dumps(health, indent=2))
# 返回适当的退出码
if health["status"] == "healthy":
sys.exit(0)
elif health["status"] == "warning":
sys.exit(1)
else:
sys.exit(2)
Zabbix监控模板
xml
<!-- mysql_crash_recovery.xml -->
<zabbix_export>
<version>6.0</version>
<templates>
<template>
<name>MySQL Crash Recovery Monitor</name>
<description>MySQL崩溃恢复相关监控模板</description>
<items>
<item>
<name>MySQL Uptime</name>
<key>mysql.uptime</key>
<type>0</type>
<value_type>3</value_type>
<units>s</units>
<preprocessing>
<step>
<type>1</type>
<params>mysql.global.status.Uptime</params>
</step>
</preprocessing>
</item>
<item>
<name>Replication Lag</name>
<key>mysql.replication.lag</key>
<type>0</type>
<value_type>3</value_type>
<units>s</units>
<preprocessing>
<step>
<type>1</type>
<params>mysql.slave.status.Seconds_Behind_Master</params>
</step>
</preprocessing>
</item>
<item>
<name>Connection Usage %</name>
<key>mysql.connections.usage</key>
<type>0</type>
<value_type>0</value_type>
<units>%</units>
<preprocessing>
<step>
<type>1</type>
<params>mysql.global.status.Threads_connected</params>
</step>
<step>
<type>7</type>
<params></params>
</step>
<step>
<type>1</type>
<params>mysql.global.variables.max_connections</params>
</step>
<step>
<type>1</type>
<params>100</params>
</step>
</preprocessing>
</item>
</items>
<triggers>
<trigger>
<name>MySQL Instance Down</name>
<expression>{MySQL Crash Recovery Monitor:mysql.uptime.nodata(60)}=1</expression>
<priority>5</priority>
</trigger>
<trigger>
<name>Replication Lag High</name>
<expression>{MySQL Crash Recovery Monitor:mysql.replication.lag.last()}>60</expression>
<priority>4</priority>
</trigger>
<trigger>
<name>Connection Usage High</name>
<expression>{MySQL Crash Recovery Monitor:mysql.connections.usage.last()}>80</expression>
<priority>3</priority>
</trigger>
</triggers>
</template>
</templates>
</zabbix_export>
🏗️ 系统架构图和故障转移流程
MySQL高可用架构演进图
Level 4: 异地灾备 Level 3: MGR组复制 Level 2: 主从架构 Level 1: 单机架构 北京主集群
MGR 3节点 上海从集群
MGR 3节点 跨地域复制 Primary 写 Secondary1 读 Secondary2 读 自动故障转移 主库 Master 从库 Slave 半同步复制 Xtrabackup MySQL 8.0 磁盘
RAID1 备份
mysqldump
主从故障转移流程图
应用层 代理层 主库 从库 管理员 故障检测 心跳失败 告警通知 人工决策 检查复制状态 Seconds_Behind_Master=0 执行切换 STOP SLAVE 更新路由配置 流量切换 指向新主库 验证业务 写入测试 成功响应 恢复原主库 修复故障 CHANGE MASTER TO START SLAVE 应用层 代理层 主库 从库 管理员
MGR自动故障转移流程
Primary Crash 其他节点检测 Secondary1 Secondary2 选举算法 Secondary1当选 自动提升为主 更新路由 应用无感知 原Primary恢复 作为新节点加入 自动数据同步 恢复正常MGR
Xtrabackup恢复流程图
是 否 灾难发生 选择恢复方案 单实例恢复 主从切换 PITR恢复 停止MySQL 清理数据目录 准备全量备份 准备增量备份 准备最终增量 拷贝备份文件 修改权限 启动MySQL 验证数据 检查从库延迟 延迟<10秒? 提升从库 执行C方案 切换应用 恢复全量备份 应用Binlog重放 跳转到指定时间 验证恢复
PITR时间点恢复流程
异地灾备切换架构图
应用层 代理层 上海可用区B MGR集群2 北京可用区A MGR集群1 应用实例1 应用实例2 应用实例3 读写分离
VIP 192.168.1.100 Primary
10.0.2.1 Secondary
10.0.2.2 Secondary
10.0.2.3 Primary
10.0.1.1 Secondary
10.0.1.2 Secondary
10.0.1.3
RPO/RTO决策矩阵
成本投入 技术方案匹配 业务需求分析 硬件: 15万
年维护: 6万 硬件: 6万
年维护: 2万 硬件: 2万
年维护: 0.5万 三节点MGR
- 同步复制 主从架构
- 半同步 单机 + 备份 RPO=0秒 财务系统 RTO=1分钟 RPO=5分钟 电商平台 RTO=15分钟 RPO=1小时 内容系统 RTO=4小时
🛠️ 自动化脚本工具集
1. 一键故障转移脚本
bash
#!/bin/bash
# failover_master.sh - MySQL主从一键故障转移
set -e
# 配置
MASTER_HOST="10.0.0.1"
SLAVE_HOST="10.0.0.2"
MYSQL_USER="root"
MYSQL_PASS="your_password"
VIP="192.168.1.100"
LOG_FILE="/var/log/mysql_failover.log"
# 日志函数
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a $LOG_FILE
}
# 检查参数
check_prerequisites() {
if [ -z "$MYSQL_PASS" ]; then
echo "错误: 请设置 MYSQL_PASS 环境变量"
exit 1
fi
if ! command -v mysql &> /dev/null; then
echo "错误: mysql 客户端未安装"
exit 1
fi
if ! command -v ip &> /dev/null; then
echo "错误: ip 命令未找到"
exit 1
fi
}
# 检查主库状态
check_master_status() {
log "检查主库状态..."
if mysql -h$MASTER_HOST -u$MYSQL_USER -p$MYSQL_PASS -e "SELECT 1" &>/dev/null; then
log "警告: 主库仍然在线,是否继续切换?"
read -p "输入 'yes' 继续: " confirm
if [ "$confirm" != "yes" ]; then
log "操作已取消"
exit 0
fi
fi
}
# 检查从库状态
check_slave_status() {
log "检查从库状态..."
# 检查复制延迟
SLAVE_LAG=$(mysql -h$SLAVE_HOST -u$MYSQL_USER -p$MYSQL_PASS -e "SHOW SLAVE STATUS\G" | grep "Seconds_Behind_Master" | awk '{print $2}')
if [ "$SLAVE_LAG" != "0" ]; then
log "错误: 从库延迟 $SLAVE_LAG 秒,不能切换"
exit 1
fi
# 检查复制状态
IO_RUNNING=$(mysql -h$SLAVE_HOST -u$MYSQL_USER -p$MYSQL_PASS -e "SHOW SLAVE STATUS\G" | grep "Slave_IO_Running" | awk '{print $2}')
SQL_RUNNING=$(mysql -h$SLAVE_HOST -u$MYSQL_USER -p$MYSQL_PASS -e "SHOW SLAVE STATUS\G" | grep "Slave_SQL_Running" | awk '{print $2}')
if [ "$IO_RUNNING" != "Yes" ] || [ "$SQL_RUNNING" != "Yes" ]; then
log "错误: 从库复制状态异常: IO=$IO_RUNNING, SQL=$SQL_RUNNING"
exit 1
fi
log "从库状态正常,延迟: $SLAVE_LAG 秒"
}
# 停止复制
stop_slave_replication() {
log "停止从库复制..."
mysql -h$SLAVE_HOST -u$MYSQL_USER -p$MYSQL_PASS -e "STOP SLAVE; RESET SLAVE ALL;"
log "复制已停止"
}
# 切换VIP
switch_vip() {
log "切换VIP到新主库..."
# 移除VIP从旧主库
ip addr del $VIP/24 dev eth0 2>/dev/null || true
# 将VIP添加到新主库
ip addr add $VIP/24 dev eth0
# 更新路由(如果需要)
# ip route add default via <gateway> dev eth0
log "VIP切换完成: $SLAVE_HOST"
}
# 更新应用配置
update_app_config() {
log "更新应用数据库配置..."
# 示例:更新nginx配置文件
if [ -f "/etc/nginx/conf.d/database.conf" ]; then
sed -i "s/server.*3306/server $SLAVE_HOST:3306/" /etc/nginx/conf.d/database.conf
nginx -s reload
fi
# 示例:更新应用配置文件
if [ -f "/app/config/database.yml" ]; then
sed -i "s/host:.*/host: $SLAVE_HOST/" /app/config/database.yml
fi
# 示例:重启应用
# systemctl restart app
}
# 验证切换结果
verify_failover() {
log "验证故障转移结果..."
# 检查新主库可写
if mysql -h$SLAVE_HOST -u$MYSQL_USER -p$MYSQL_PASS -e "CREATE DATABASE IF NOT EXISTS failover_test; DROP DATABASE failover_test;"; then
log "✓ 新主库写入测试通过"
else
log "✗ 新主库写入测试失败"
return 1
fi
# 检查应用连接
sleep 5
if curl -f http://localhost/health &>/dev/null; then
log "✓ 应用健康检查通过"
else
log "✗ 应用健康检查失败"
fi
log "故障转移完成!"
log "新主库: $SLAVE_HOST"
log "VIP: $VIP"
}
# 主函数
main() {
log "=== MySQL故障转移开始 ==="
check_prerequisites
check_master_status
check_slave_status
stop_slave_replication
switch_vip
update_app_config
verify_failover
log "=== 故障转移完成 ==="
}
# 执行主函数
main "$@"
2. 自动化备份恢复脚本
bash
#!/bin/bash
# auto_recovery.sh - MySQL自动恢复脚本
set -e
# 配置
BACKUP_DIR="/data/backup/mysql"
MYSQL_DATA_DIR="/data/mysql"
MYSQL_USER="root"
MYSQL_PASS="your_password"
LOG_FILE="/var/log/mysql_recovery.log"
# 日志函数
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a $LOG_FILE
}
# 选择恢复策略
choose_recovery_strategy() {
echo "请选择恢复策略:"
echo "1) 主从切换 (最快,需要从库)"
echo "2) Xtrabackup恢复 (较慢,需要备份)"
echo "3) PITR恢复 (最精确,需要Binlog)"
read -p "选择 [1-3]: " choice
case $choice in
1)
STRATEGY="failover"
;;
2)
STRATEGY="xtrabackup"
;;
3)
STRATEGY="pitr"
;;
*)
echo "无效选择"
exit 1
;;
esac
}
# 主从切换恢复
failover_recovery() {
log "执行主从切换恢复..."
SLAVE_HOST="10.0.0.2" # 从库地址
# 检查从库状态
SLAVE_LAG=$(mysql -h$SLAVE_HOST -u$MYSQL_USER -p$MYSQL_PASS -e "SHOW SLAVE STATUS\G" | grep "Seconds_Behind_Master" | awk '{print $2}')
if [ "$SLAVE_LAG" = "0" ]; then
mysql -h$SLAVE_HOST -u$MYSQL_USER -p$MYSQL_PASS -e "STOP SLAVE; RESET SLAVE ALL;"
log "主从切换完成,新主库: $SLAVE_HOST"
else
log "错误: 从库延迟 $SLAVE_LAG 秒,不能切换"
exit 1
fi
}
# Xtrabackup恢复
xtrabackup_recovery() {
log "执行Xtrabackup恢复..."
# 选择备份
echo "可用备份:"
ls -la $BACKUP_BASE/full_* | tail -5
read -p "选择备份目录: " BACKUP_PATH
if [ ! -d "$BACKUP_PATH" ]; then
log "错误: 备份目录不存在: $BACKUP_PATH"
exit 1
fi
# 停止MySQL
log "停止MySQL服务..."
systemctl stop mysql
# 清理数据目录
log "清理数据目录..."
rm -rf $MYSQL_DATA_DIR/*
# 恢复备份
log "恢复备份数据..."
xtrabackup --copy-back --target-dir=$BACKUP_PATH --datadir=$MYSQL_DATA_DIR
# 修改权限
chown -R mysql:mysql $MYSQL_DATA_DIR
# 启动MySQL
log "启动MySQL服务..."
systemctl start mysql
log "Xtrabackup恢复完成"
}
# PITR恢复
pitr_recovery() {
log "执行PITR恢复..."
read -p "恢复到时间点 (YYYY-MM-DD HH:MM:SS): " TARGET_TIME
# 找到最近的备份
LATEST_BACKUP=$(ls -td $BACKUP_BASE/full_* | head -1)
log "使用备份: $LATEST_BACKUP"
# 停止MySQL
systemctl stop mysql
rm -rf $MYSQL_DATA_DIR/*
# 恢复全量备份
xtrabackup --copy-back --target-dir=$LATEST_BACKUP --datadir=$MYSQL_DATA_DIR
chown -R mysql:mysql $MYSQL_DATA_DIR
# 准备恢复
xtrabackup --prepare --target-dir=$LATEST_BACKUP
# 启动MySQL
systemctl start mysql
# 获取Binlog位置
BINLOG_INFO=$(cat $LATEST_BACKUP/xtrabackup_binlog_info)
BINLOG_FILE=$(echo $BINLOG_INFO | awk '{print $1}')
BINLOG_POS=$(echo $BINLOG_INFO | awk '{print $2}')
# 重放Binlog
log "重放Binlog到 $TARGET_TIME..."
mysqlbinlog --start-position=$BINLOG_POS --stop-datetime="$TARGET_TIME" $BINLOG_FILE | mysql
log "PITR恢复完成"
}
# 验证恢复结果
verify_recovery() {
log "验证恢复结果..."
# 检查MySQL状态
if systemctl is-active --quiet mysql; then
log "✓ MySQL服务运行正常"
else
log "✗ MySQL服务启动失败"
exit 1
fi
# 检查数据库连接
if mysql -u$MYSQL_USER -p$MYSQL_PASS -e "SELECT 1" &>/dev/null; then
log "✓ 数据库连接正常"
else
log "✗ 数据库连接失败"
exit 1
fi
# 检查关键表
TABLES=$(mysql -u$MYSQL_USER -p$MYSQL_PASS -e "SHOW TABLES" | wc -l)
log "✓ 发现 $TABLES 个表"
log "恢复验证完成"
}
# 主函数
main() {
log "=== MySQL自动恢复开始 ==="
choose_recovery_strategy
case $STRATEGY in
failover)
failover_recovery
;;
xtrabackup)
xtrabackup_recovery
;;
pitr)
pitr_recovery
;;
esac
verify_recovery
log "=== 恢复完成 ==="
}
# 执行主函数
main "$@"
3. MySQL健康检查脚本
bash
#!/bin/bash
# mysql_health_check.sh - MySQL全面健康检查
set -e
# 配置
MYSQL_USER="health_check_user"
MYSQL_PASS="health_check_password"
HOSTS=("10.0.0.1" "10.0.0.2" "10.0.0.3")
LOG_FILE="/var/log/mysql_health_check.log"
# 日志函数
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a $LOG_FILE
}
# 检查MySQL连接
check_connection() {
local host=$1
log "检查 $host 连接状态..."
if mysql -h$host -u$MYSQL_USER -p$MYSQL_PASS -e "SELECT 1" &>/dev/null; then
echo "✓"
return 0
else
echo "✗"
return 1
fi
}
# 检查主从复制状态
check_replication() {
local host=$1
log "检查 $host 复制状态..."
SLAVE_STATUS=$(mysql -h$host -u$MYSQL_USER -p$MYSQL_PASS -e "SHOW SLAVE STATUS\G" 2>/dev/null)
if [ -z "$SLAVE_STATUS" ]; then
echo "ℹ️ (非从库)"
return 0
fi
IO_RUNNING=$(echo "$SLAVE_STATUS" | grep "Slave_IO_Running" | awk '{print $2}')
SQL_RUNNING=$(echo "$SLAVE_STATUS" | grep "Slave_SQL_Running" | awk '{print $2}')
LAG=$(echo "$SLAVE_STATUS" | grep "Seconds_Behind_Master" | awk '{print $2}')
if [ "$IO_RUNNING" = "Yes" ] && [ "$SQL_RUNNING" = "Yes" ]; then
echo "✓ (延迟: ${LAG}s)"
return 0
else
echo "✗ (IO: $IO_RUNNING, SQL: $SQL_RUNNING, 延迟: ${LAG}s)"
return 1
fi
}
# 检查关键性能指标
check_performance() {
local host=$1
log "检查 $host 性能指标..."
# 连接数使用率
CONNECTIONS=$(mysql -h$host -u$MYSQL_USER -p$MYSQL_PASS -e "SHOW STATUS LIKE 'Threads_connected'" | grep "Threads_connected" | awk '{print $2}')
MAX_CONN=$(mysql -h$host -u$MYSQL_USER -p$MYSQL_PASS -e "SHOW VARIABLES LIKE 'max_connections'" | grep "max_connections" | awk '{print $2}')
CONN_RATIO=$(echo "scale=2; $CONNECTIONS * 100 / $MAX_CONN" | bc)
# InnoDB缓冲池使用率
BUFFER_POOL_USED=$(mysql -h$host -u$MYSQL_USER -p$MYSQL_PASS -e "SHOW STATUS LIKE 'Innodb_buffer_pool_pages_data'" | grep "Innodb_buffer_pool_pages_data" | awk '{print $2}')
BUFFER_POOL_TOTAL=$(mysql -h$host -u$MYSQL_USER -p$MYSQL_PASS -e "SHOW STATUS LIKE 'Innodb_buffer_pool_pages_total'" | grep "Innodb_buffer_pool_pages_total" | awk '{print $2}')
BUFFER_RATIO=$(echo "scale=2; $BUFFER_POOL_USED * 100 / $BUFFER_POOL_TOTAL" | bc)
# 查询缓存命中率
QUERIES=$(mysql -h$host -u$MYSQL_USER -p$MYSQL_PASS -e "SHOW STATUS LIKE 'Queries'" | grep "Queries" | awk '{print $2}')
CACHE_HITS=$(mysql -h$host -u$MYSQL_USER -p$MYSQL_PASS -e "SHOW STATUS LIKE 'Qcache_hits'" | grep "Qcache_hits" | awk '{print $2}')
echo " 连接使用率: ${CONN_RATIO}% ($CONNECTIONS/$MAX_CONN)"
echo " 缓冲池使用率: ${BUFFER_RATIO}%"
echo " 总查询数: $QUERIES"
}
# 检查磁盘空间
check_disk_space() {
local host=$1
log "检查 $host 磁盘空间..."
# 获取数据目录
DATA_DIR=$(mysql -h$host -u$MYSQL_USER -p$MYSQL_PASS -e "SHOW VARIABLES LIKE 'datadir'" | grep "datadir" | awk '{print $2}')
# 检查磁盘使用率
DISK_USAGE=$(df -h $DATA_DIR | tail -1 | awk '{print $5}' | sed 's/%//')
if [ "$DISK_USAGE" -lt 80 ]; then
echo "✓ 磁盘使用率: ${DISK_USAGE}%"
elif [ "$DISK_USAGE" -lt 90 ]; then
echo "⚠️ 磁盘使用率: ${DISK_USAGE}%"
else
echo "✗ 磁盘使用率: ${DISK_USAGE}%"
fi
}
# 检查错误日志
check_error_log() {
local host=$1
log "检查 $host 错误日志..."
ERROR_LOG=$(mysql -h$host -u$MYSQL_USER -p$MYSQL_PASS -e "SHOW VARIABLES LIKE 'log_error'" | grep "log_error" | awk '{print $2}')
if [ -f "$ERROR_LOG" ]; then
RECENT_ERRORS=$(tail -20 $ERROR_LOG | grep -i "error" | wc -l)
echo " 最近20行错误: $RECENT_ERRORS 个"
else
echo " 错误日志文件不存在"
fi
}
# 生成健康报告
generate_report() {
local host=$1
local status=$2
log "=== $host 健康报告 ==="
echo "主机: $host"
echo "状态: $status"
echo "检查时间: $(date)"
echo "=============================="
}
# 主函数
main() {
log "=== MySQL健康检查开始 ==="
for host in "${HOSTS[@]}"; do
echo ""
echo "检查主机: $host"
echo "------------------------------"
# 基本连接检查
if check_connection $host; then
generate_report $host "正常"
# 详细检查
check_replication $host
check_performance $host
check_disk_space $host
check_error_log $host
else
generate_report $host "离线"
fi
done
log "=== 健康检查完成 ==="
}
# 执行主函数
main "$@"
4. Binlog分析工具
python
#!/usr/bin/env python3
# binlog_analyzer.py - Binlog分析工具
import mysql.connector
import argparse
import json
from datetime import datetime, timedelta
import sys
class BinlogAnalyzer:
def __init__(self, host, user, password, database=None):
self.config = {
'host': host,
'user': user,
'password': password,
'database': database
}
def get_binlog_files(self):
"""获取所有Binlog文件"""
conn = mysql.connector.connect(**self.config)
cursor = conn.cursor()
cursor.execute("SHOW BINARY LOGS")
logs = cursor.fetchall()
result = []
for log in logs:
result.append({
'file': log[0],
'size': log[1],
'created': log[2]
})
conn.close()
return result
def analyze_binlog_usage(self, days=7):
"""分析Binlog使用情况"""
conn = mysql.connector.connect(**self.config)
cursor = conn.cursor()
# 获取最近7天的查询统计
cursor.execute("""
SELECT
COUNT(*) as query_count,
SUM(CHAR_LENGTH(sql_text)) as total_sql_length
FROM mysql.general_log
WHERE event_time >= DATE_SUB(NOW(), INTERVAL %s DAY)
AND command_type = 'Query'
""", (days,))
stats = cursor.fetchone()
# 获取Binlog总大小
cursor.execute("SHOW BINARY LOGS")
binlogs = cursor.fetchall()
total_size = sum(log[1] for log in binlogs)
conn.close()
return {
'period_days': days,
'query_count': stats[0],
'total_sql_length': stats[1],
'binlog_files': len(binlogs),
'total_size_mb': total_size / (1024*1024),
'avg_query_length': stats[1] / stats[0] if stats[0] > 0 else 0
}
def find_suspicious_queries(self, hours=1):
"""查找可疑查询(DELETE/UPDATE without WHERE)"""
conn = mysql.connector.connect(**self.config)
cursor = conn.cursor()
cursor.execute("""
SELECT
event_time,
user_host,
thread_id,
server_id,
sql_text
FROM mysql.general_log
WHERE event_time >= DATE_SUB(NOW(), INTERVAL %s HOUR)
AND (sql_text LIKE 'DELETE%' OR sql_text LIKE 'UPDATE%')
AND sql_text NOT LIKE '%WHERE%'
AND sql_text NOT LIKE '%LIMIT%'
ORDER BY event_time DESC
LIMIT 50
""", (hours,))
suspicious = cursor.fetchall()
conn.close()
result = []
for row in suspicious:
result.append({
'time': row[0].strftime('%Y-%m-%d %H:%M:%S'),
'user': row[1],
'thread_id': row[2],
'server_id': row[3],
'sql': row[4]
})
return result
def estimate_recovery_time(self, target_time):
"""估算PITR恢复时间"""
# 获取数据大小
conn = mysql.connector.connect(**self.config)
cursor = conn.cursor()
cursor.execute("""
SELECT
SUM(data_length + index_length) as total_size
FROM information_schema.tables
WHERE table_schema NOT IN ('information_schema', 'mysql')
""")
db_size = cursor.fetchone()[0] or 0
# 获取Binlog大小(从指定时间到现在)
cursor.execute("""
SELECT
SUM(LENGTH(argument)) as binlog_size
FROM mysql.general_log
WHERE event_time >= %s
""", (target_time,))
binlog_size = cursor.fetchone()[0] or 0
conn.close()
# 估算恢复时间(基于经验值)
backup_restore_time = db_size / (100 * 1024 * 1024) # 100MB/s
binlog_replay_time = binlog_size / (50 * 1024 * 1024) # 50MB/s
total_time = backup_restore_time + binlog_replay_time
return {
'database_size_mb': db_size / (1024*1024),
'binlog_size_mb': binlog_size / (1024*1024),
'estimated_restore_time_minutes': int(total_time / 60),
'breakdown': {
'backup_restore_minutes': int(backup_restore_time / 60),
'binlog_replay_minutes': int(binlog_replay_time / 60)
}
}
def main():
parser = argparse.ArgumentParser(description='MySQL Binlog分析工具')
parser.add_argument('--host', required=True, help='MySQL主机')
parser.add_argument('--user', required=True, help='MySQL用户')
parser.add_argument('--password', required=True, help='MySQL密码')
parser.add_argument('--database', help='目标数据库')
parser.add_argument('command', choices=['files', 'usage', 'suspicious', 'recovery'],
help='执行的命令')
parser.add_argument('--hours', type=int, default=1, help='查询时间范围(小时)')
parser.add_argument('--target-time', help='目标恢复时间(YYYY-MM-DD HH:MM:SS)')
args = parser.parse_args()
analyzer = BinlogAnalyzer(args.host, args.user, args.password, args.database)
if args.command == 'files':
files = analyzer.get_binlog_files()
print(json.dumps(files, indent=2, default=str))
elif args.command == 'usage':
usage = analyzer.analyze_binlog_usage()
print(json.dumps(usage, indent=2))
elif args.command == 'suspicious':
suspicious = analyzer.find_suspicious_queries(args.hours)
print(json.dumps(suspicious, indent=2, default=str))
elif args.command == 'recovery':
if not args.target_time:
print("错误: --target-time 参数必需")
sys.exit(1)
recovery_info = analyzer.estimate_recovery_time(args.target_time)
print(json.dumps(recovery_info, indent=2))
if __name__ == '__main__':
main()
5. 配置文件模板
bash
#!/bin/bash
# generate_mysql_config.sh - 生成MySQL配置模板
generate_my_cnf() {
local host_type=$1 # master, slave, standalone
local data_size_gb=$2
local memory_gb=$3
cat << EOF
# MySQL配置文件 - $host_type
# 生成时间: $(date)
# 数据大小: ${data_size_gb}GB
# 内存大小: ${memory_gb}GB
[mysqld]
# 基础配置
server-id = 1
port = 3306
basedir = /usr/local/mysql
datadir = /data/mysql
socket = /var/lib/mysql/mysql.sock
pid-file = /var/lib/mysql/mysql.pid
# 字符集配置
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
init_connect = 'SET NAMES utf8mb4'
# InnoDB配置
innodb_buffer_pool_size = $((memory_gb * 1024 * 1024 * 70 / 100))M # 70%内存
innodb_log_file_size = 2G
innodb_log_files_in_group = 2
innodb_flush_log_at_trx_commit = 1
innodb_file_per_table = ON
innodb_open_files = 500
innodb_io_capacity = 1000
innodb_read_io_threads = 8
innodb_write_io_threads = 8
innodb_doublewrite = ON
# 连接配置
max_connections = $((memory_gb * 100))
max_connect_errors = 1000000
connect_timeout = 60
wait_timeout = 28800
interactive_timeout = 28800
# 缓存配置
key_buffer_size = 256M
query_cache_size = 128M
query_cache_type = 1
tmp_table_size = 256M
max_heap_table_size = 256M
# Binlog配置
log-bin = mysql-bin
binlog_format = ROW
sync_binlog = 1
expire_logs_days = 30
max_binlog_size = 1G
# 慢查询日志
slow_query_log = ON
slow_query_log_file = /var/lib/mysql/slow.log
long_query_time = 2
log_queries_not_using_indexes = ON
EOF
# 根据主机类型添加特定配置
if [ "$host_type" = "master" ]; then
cat << EOF
# 主库特定配置
binlog_do_db = app_db
binlog_ignore_db = mysql
binlog_checksum = CRC32
log_slave_updates = ON
EOF
elif [ "$host_type" = "slave" ]; then
cat << EOF
# 从库特定配置
read_only = ON
skip_slave_start = ON
relay_log = relay-bin
relay_log_index = relay-bin.index
log_slave_updates = ON
sync_relay_log = 1
# 复制配置
server-id = 2
master-info-repository = TABLE
relay-log-info-repository = TABLE
EOF
fi
}
# 生成完整配置
if [ $# -eq 3 ]; then
generate_my_cnf "$1" "$2" "$3"
else
echo "用法: $0 <host_type> <data_size_gb> <memory_gb>"
echo "示例: $0 master 100 16"
fi
使用这些自动化脚本可以显著提升MySQL运维效率,减少人为错误,确保在紧急情况下能够快速响应和恢复。
总结与行动指南
崩溃恢复的本质:分层防御体系
MySQL崩溃恢复不是单一技术,而是多层防护、梯度降级的完整体系:
┌─────────────────────────────────────────────────────────────┐
│ 五层防御体系 │
├─────────────────────────────────────────────────────────────┤
│ 第一防线:InnoDB自动恢复 (Redo/Undo) │
│ • 适用场景:实例Crash、OOM、段错误 │
│ • 恢复时间:1-10分钟 │
│ • 数据丢失:0 (理想状态) │
│ • 成功率:90% │
├─────────────────────────────────────────────────────────────┤
│ 第二防线:主从切换 (HA架构) │
│ • 适用场景:主库硬件故障、网络隔离 │
│ • 恢复时间:1-5分钟 │
│ • 数据丢失:<5分钟 (半同步) │
│ • 成功率:7%触发,95%成功 │
├─────────────────────────────────────────────────────────────┤
│ 第三防线:物理备份恢复 (Xtrabackup) │
│ • 适用场景:数据文件损坏、系统盘故障 │
│ • 恢复时间:30-120分钟 │
│ • 数据丢失:<1小时 (备份间隔) │
│ • 成功率:2%触发,85%成功 │
├─────────────────────────────────────────────────────────────┤
│ 第四防线:PITR时间点恢复 (Binlog重放) │
│ • 适用场景:人为误操作、精确时间点恢复 │
│ • 恢复时间:15-180分钟 │
│ • 数据丢失:0 (可精确到秒) │
│ • 成功率:1%触发,80%成功 │
├─────────────────────────────────────────────────────────────┤
│ 第五防线:异地灾备 (跨地域复制) │
│ • 适用场景:机房级故障、地震火灾 │
│ • 恢复时间:60-300分钟 │
│ • 数据丢失:<10分钟 (异步延迟) │
│ • 成功率:0.1%触发,70%成功 │
└─────────────────────────────────────────────────────────────┘
核心关键点速记
| 序号 | 关键点 | 核心价值 | 生产检查项 |
|---|---|---|---|
| 1️⃣ | Redo Log是灵魂 | 保证事务持久性,Crash后自动恢复 | innodb_flush_log_at_trx_commit=1 |
| 2️⃣ | Binlog是保险 | PITR、主从、审计都依赖它 | sync_binlog=1, 保留7天 |
| 3️⃣ | 备份必须验证 | 未验证的备份≈没有备份 | 每周恢复演练 |
| 4️⃣ | RPO/RTO决定架构 | 满足业务需求,不过度设计 | 业务SLA文档化 |
| 5️⃣ | 演练是生命线 | 不演练的预案≈纸上谈兵 | 每月故障演练 |
DBA行动清单
🔴 立即行动 (P0 - 24小时内完成):
- 检查
innodb_flush_log_at_trx_commit=1 - 检查
sync_binlog=1 - 验证备份文件完整性
- 确认监控告警是否生效
🟡 本周行动 (P1 - 7天内完成):
- 做一次完整恢复演练(包含PITR)
- 配置延迟从库(防误删)
- 搭建Prometheus+Grafana监控
- 编写故障处理Runbook
🟢 本月行动 (P2 - 30天内完成):
- 评估当前RPO/RTO是否满足业务需求
- 优化备份策略(全量+增量)
- 搭建MGR或半同步复制
- 进行一次跨团队故障演练
常见错误对照表
| 错误行为 | 后果 | 正确做法 |
|---|---|---|
| ❌ 关闭Double Write | 页半写无法恢复 | ✅ 保持ON,除非SSD+RAID |
| ❌ 不验证备份 | 恢复时发现备份损坏 | ✅ 每周恢复演练 |
| ❌ Binlog保留<3天 | PITR无法回溯 | ✅ 至少保留7天 |
| ❌ 没有延迟从库 | 误删无法恢复 | ✅ 配置1小时延迟从库 |
| ❌ 单实例无HA | RTO>1小时 | ✅ 至少主从架构 |
最后的提醒
记住 :崩溃总有一天会发生,但准备充分的工程师能把灾难变成一次普通的切换。
不要等到故障发生时才想起这篇文章,现在就开始行动。
参考资料与延伸阅读
官方文档
工具资源
- Xtrabackup: https://github.com/percona/percona-xtrabackup
- binlog2sql: https://github.com/danfengcao/binlog2sql
- Mydumper: https://github.com/mydumper/mydumper
- Orchestrator: https://github.com/openark/orchestrator