一文讲透 MySQL 崩溃恢复方案设计

TL;DR: MySQL崩溃恢复不是简单的"重启服务",而是涉及InnoDB引擎、Binlog、备份、HA架构的完整防御体系。本文从内核原理到生产实践,系统拆解崩溃恢复的完整方案设计。

适合人群 : DBA、运维工程师、后端架构师
阅读时间 : 30-45分钟
难度等级: ⭐⭐⭐⭐ (需要MySQL基础知识)

目录

  1. MySQL崩溃场景全景图
  2. InnoDB崩溃恢复机制:从Redo到Undo
  3. Binlog与数据一致性:XID的奥秘
  4. 物理备份恢复:Xtrabackup实战
  5. 逻辑备份恢复:mysqldump与Mydumper
  6. 时间点恢复(PITR):从Binlog重放
  7. 高可用架构下的恢复策略
  8. 实战:一次生产Crash恢复全记录
  9. 方案设计:RPO/RTO与成本权衡
  10. 最佳实践与避坑指南

🚀 快速导航索引

按故障类型快速定位

故障类型 定位章节 核心技术 预期恢复时间
实例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时间点恢复流程

timeline title PITR恢复时间线 section 15:30 误删表 15:30 : 误操作执行 15:35 : 发现问题 15:40 : 开始恢复流程 section 15:40 恢复执行 15:40 : 确定全量备份位置 15:45 : 恢复全量到凌晨2点 15:50 : 定位Binlog重放起点 16:00 : 开始重放Binlog 16:10 : 重放到15:29 section 16:10 验证完成 16:10 : 数据验证通过 16:15 : 应用恢复写入 16:20 : 业务恢复正常

异地灾备切换架构图

应用层 代理层 上海可用区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小时 ✅ 至少主从架构

最后的提醒

记住 :崩溃总有一天会发生,但准备充分的工程师能把灾难变成一次普通的切换

不要等到故障发生时才想起这篇文章,现在就开始行动


参考资料与延伸阅读

官方文档

工具资源

社区资源


相关推荐
山峰哥2 小时前
现代 C++ 的最佳实践:从语法糖到工程化思维的全维度探索
java·大数据·开发语言·数据结构·c++
一水鉴天2 小时前
整体设计 定稿 备忘录仪表盘方案 之1 初稿之8 V5版本的主程序 之2: 自动化导航 + 定制化服务 + 个性化智能体(豆包助手)
前端·人工智能·架构
苏 凉2 小时前
openEuler云原生AI性能测试:Qwen3模型KServe部署实战
人工智能·云原生
weixin_457760002 小时前
深度学习的链式法则
人工智能·深度学习
狂奔solar2 小时前
agent 自反馈实现用户triage feedback 自动化分析
运维·人工智能·自动化
微学AI2 小时前
生成式AI应用平台架构设计:ModelEngine核心能力与工程化实践路径
android·人工智能·rxjava
老蒋新思维2 小时前
创客匠人启示录:AI 时代知识变现的效率革命 —— 从人力驱动到智能体自动化的跃迁
网络·人工智能·网络协议·tcp/ip·数据挖掘·创始人ip·创客匠人
努力搬砖的咸鱼2 小时前
API 网关:微服务的大门卫
java·大数据·微服务·云原生