MySQL索引页损坏故障排查与恢复实战:一次集群Crash的完整复盘

前言

在数据库运维中,数据页损坏是最令人头疼的问题之一。它不像慢查询那样可以逐步优化,也不像连接数爆满那样可以快速扩容——页损坏往往会导致MySQL进程直接Crash,甚至引发主从切换失败、集群不可用等严重故障。

本文将完整复盘一次生产环境中因索引页损坏导致的MySQL故障,从故障现象、排查过程、根因分析到最终修复和预防措施,帮助你在遇到类似问题时能够快速定位和解决。

一、故障概述

项目 内容
涉及系统 业务系统XX集群
事件级别 P1(严重)
事件分类 MySQL故障
影响范围 核心业务
是否重复
是否彻底解决

故障现象

MySQL主库发生Crash并自动重启,触发主从切换。主从切换完成后,新主库再次Crash重启,导致应用程序持续访问数据库失败。

经排查,MySQL重启的根本原因是某业务表的索引存在坏页(损坏的数据页)。通过重建该索引,问题得到彻底解决。

二、环境信息

2.1 故障环境配置

配置项 信息
环境 某云环境
数据库架构 DRDS集群
MySQL版本 5.7.x
老主库IP 10.x.x.x1
新主库IP 10.x.x.x2

2.2 集群拓扑关系

┌─────────────────────────────────────────────────────────┐
│                     DRDS集群                             │
├─────────────────────────────────────────────────────────┤
│                                                         │
│   老主库: 10.x.x.x1 ──┬── 从库(基于备库创建)             │
│                       │                                 │
│   新主库: 10.x.x.x2 ──┼── 备库(2021-12创建)             │
│                       │                                 │
│                       └── 线下库(2020-12创建)            │
│                                                         │
└─────────────────────────────────────────────────────────┘

关键时间线:

  • 线下库创建时间:2020-12 ✅ 正常
  • 备库创建时间:2021-12 ⚠️ 可能已损坏
  • 主库创建时间:2022-04 ⚠️ 基于备库创建
  • 从库创建时间:2023-11 ⚠️ 基于备库创建

三、故障处理全过程

3.1 故障发现与初步定位

从MySQL错误日志中可以看到明显的数据页损坏信息:

tail -f /var/log/mysql/mysql.err.log

日志显示:

[ERROR] InnoDB: Page [page id: space=xxx, page number=xxx] 
         log sequence number is in the future!
[ERROR] InnoDB: Your database may be corrupt...

触发Crash的SQL(脱敏后):

SELECT MAX(id) 
FROM T_XXX_XXX 
WHERE PRODUCTID = 'xxxxxxxxxxxxx' 
  AND STATUS IN (2, 3);

3.2 主从切换分析

MySQL Crash后,系统自动触发主从切换。查看切换日志:

tail -f /var/log/cluster/agent.log

日志显示:

[INFO] Master 10.x.x.x1 is down, initiating failover...
[INFO] Promoting slave 10.x.x.61 to new master...
[INFO] Failover completed

关键问题:切换成功后,新主库10.x.x.x2同样出现Crash,说明损坏的数据页已经同步到了多个节点。

3.3 排查MySQL Bug可能性

检查是否存在Core文件:

find / -name "core.*" 2>/dev/null
ls -la /var/log/mysql/core*

结果:未发现Core文件,排除了典型的MySQL Bug导致的Crash。

3.4 故障止损方案对比

方案一:基于线下库重建主从(耗时长)

步骤 操作 预估耗时
1 将线下库切换为备库 5分钟
2 执行全量备份 1-2小时
3 基于备份创建新主从 30分钟

总耗时:约2-3小时

方案二:原地重建损坏索引(推荐✅)

步骤 操作 预估耗时
1 在从库执行CHECK TABLE定位损坏 2分钟
2 DROP损坏索引 1分钟
3 重建索引 5分钟
4 主从切换 2分钟
5 对新主库执行索引重建 5分钟

总耗时:约15分钟

最终选择方案二,因为:

  1. 执行效率高,业务影响小
  2. 只涉及单表索引重建,风险可控
  3. 不需要全量备份恢复

3.5 具体修复步骤

Step 1: 定位损坏的表和索引

-- 在从库执行CHECK TABLE
CHECK TABLE t_xxx_xxx;

-- 查看错误日志定位具体损坏的索引
SHOW ENGINE INNODB STATUS\G

发现:损坏发生在IDX_XXX_XXX索引上

Step 2: 在从库重建索引

-- 1. 删除损坏的索引
ALTER TABLE t_xxx_xxx DROP INDEX IDX_XXX_XXX;

-- 2. 重建索引
ALTER TABLE t_xxx_xxx ADD INDEX IDX_XXX_XXX (column1, column2);

Step 3: 验证从库恢复正常

-- 执行触发Crash的SQL,验证不再报错
SELECT MAX(id) 
FROM T_XXX_XXX 
WHERE PRODUCTID = 'xxxxxxxxxxxxx' 
  AND STATUS IN (2, 3);

Step 4: 主从切换

# 执行主从切换,将从库提升为主库
# (具体命令根据集群管理工具而定)

Step 5: 对新主库执行索引重建

-- 对新主库执行同样的索引重建操作
ALTER TABLE t_xxx_xxx DROP INDEX IDX_XXX_XXX;
ALTER TABLE t_xxx_xxx ADD INDEX IDX_XXX_XXX (column1, column2);

3.6 修复验证

-- 验证主从同步状态
SHOW SLAVE STATUS\G
-- 确认 Slave_IO_Running: Yes, Slave_SQL_Running: Yes

-- 验证业务SQL正常执行
SELECT COUNT(*) FROM t_xxx_xxx WHERE PRODUCTID = 'xxxxxxxxxxxxx';

四、根本原因分析

4.1 为什么多次重启?

备库索引页损坏(2021-12)
        ↓
基于备库创建主库(2022-04) ←── 基于备库创建从库(2023-11)
        ↓                              ↓
主库包含损坏索引页              从库包含损坏索引页
        ↓                              ↓
        └──────────┬───────────────────┘
                   ↓
          访问到损坏page触发Crash
                   ↓
              主从切换
                   ↓
        新主库同样存在损坏page
                   ↓
              再次Crash

结论

  • 备库在创建时就已经存在索引页损坏
  • 主库和从库都是基于这个已损坏的备库创建的
  • 线下库创建时间更早,数据正常
  • 由于该索引是高效索引,平时查询只扫描少量索引页,一直未访问到损坏的page,直到某个特定查询触发了问题

4.2 MySQL页损坏的可能原因

可能原因 本次排查结果
硬件故障(磁盘坏道) ❌ 操作系统日志无硬件错误
文件系统损坏 ⚠️ 可能性较大
磁盘空间不足 ❌ 监控显示空间充足
MySQL软件Bug ❌ 无Core文件,未命中已知Bug
内存损坏 ⚠️ 可能性存在

4.3 源码层面分析:为什么页损坏会导致Crash?

MySQL对访问到的每个索引页都会做数据校验,校验机制如下:

// btr0pcur.cc line 430
ut_a(page_is_comp(next_page) == page_is_comp(page));

代码解释:

  • ut_a():断言宏,条件为false时触发SIGABRT终止进程
  • page_is_comp():检查page是否为压缩格式
  • 该断言验证当前page与下一个page的header是否一致

MySQL错误日志示例:

InnoDB: Assertion failure in file btr0pcur.cc line 430
InnoDB: Failing assertion: page_is_comp(next_page) == page_is_comp(page)
InnoDB: We intentionally generate a memory trap.

当索引页损坏导致相邻page的压缩格式不一致时,断言失败,MySQL主动Crash。

索引页校验流程:

开始访问索引页 → 读取page A → 读取page B
                    ↓
          校验page A和page B的header
                    ↓
        ┌───────────┴───────────┐
        ↓                       ↓
   header一致               header不一致
        ↓                       ↓
   正常返回数据            断言失败(ut_a)
                                ↓
                          MySQL Crash

五、预防措施与止损预案

5.1 预防措施

措施 频率 说明
备份恢复测试 每季度 对核心业务库的备份进行恢复测试
CHECK TABLE校验 每季度 对恢复出来的表执行CHECK TABLE
备份工具增强 持续 物理备份工具无法校验页损坏,需补充验证手段

具体操作:

-- 定期对核心表执行CHECK TABLE
CHECK TABLE t_xxx_xxx;

-- 检查所有表的完整性
SELECT 
    TABLE_SCHEMA,
    TABLE_NAME,
    CHECK_TABLE(TABLE_SCHEMA, TABLE_NAME) AS check_result
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_SCHEMA NOT IN ('mysql', 'information_schema', 'performance_schema');

5.2 止损预案(SOP)

当遇到MySQL页损坏故障时,按以下优先级执行:

优先级1:原地重建损坏索引(最快)

-- 1. 定位损坏的表和索引
CHECK TABLE table_name;

-- 2. 删除损坏索引
ALTER TABLE table_name DROP INDEX index_name;

-- 3. 重建索引
ALTER TABLE table_name ADD INDEX index_name (column1, column2);

优先级2:基于正常节点重建

# 1. 找到数据正常的节点(如下线库)
# 2. 从正常节点导出数据
mysqldump --single-transaction --master-data=2 db_name table_name > table.sql

# 3. 导入到问题节点
mysql db_name < table.sql

优先级3:全量备份恢复(最后手段)

# 1. 从最近的正常备份恢复
xtrabackup --prepare --target-dir=/backup/full
xtrabackup --copy-back --target-dir=/backup/full

# 2. 重建主从关系
CHANGE MASTER TO ...
START SLAVE;

5.3 监控改进

监控项 当前状态 改进方案
MySQL Crash告警 ✅ 已有 保持
主从切换告警 ✅ 已有 保持
CHECK TABLE定期执行 ❌ 缺失 新增:每月执行
备份恢复测试 ❌ 缺失 新增:每季度执行

新增监控脚本:

#!/bin/bash
# check_table_integrity.sh - 定期检查表完整性

DB_HOST="localhost"
DB_USER="monitor"
DB_PASS="xxxxx"
LOG_FILE="/var/log/table_check.log"

echo "=== Table Integrity Check at $(date) ===" >> $LOG_FILE

mysql -h $DB_HOST -u $DB_USER -p$DB_PASS -e "
SELECT 
    TABLE_SCHEMA,
    TABLE_NAME,
    CHECK_TABLE(TABLE_SCHEMA, TABLE_NAME) AS status
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_SCHEMA IN ('your_core_db')
" >> $LOG_FILE

# 检查是否有错误
if grep -q "error\|corrupt" $LOG_FILE; then
    # 发送告警
    curl -X POST "https://your-alert-webhook" -d "MySQL表完整性检查发现异常"
fi

六、经验总结

6.1 本次故障的关键教训

  1. 备份不等于安全:物理备份工具不校验数据页完整性,需要额外补充验证手段
  2. 历史数据会传染:基于损坏数据创建的节点会继承损坏,需要在创建新节点前做完整性校验
  3. 高效索引隐藏问题:损坏的索引页如果未被访问,问题可能潜伏数月甚至数年
  4. 主从切换不是万能药:如果损坏数据已同步,切换后问题依然存在

6.2 改进措施清单

序号 改进措施 责任方 状态
1 核心业务库增加季度备份恢复测试 DBA团队 已纳入SOP
2 新增CHECK TABLE定期执行监控 监控团队 已完成
3 编写《MySQL页损坏修复SOP》文档 DBA团队 已完成
4 新节点创建前执行表完整性校验 DBA团队 已纳入流程
5 评估开启页校验相关参数 DBA团队 待评估

七、附录

7.1 相关命令速查

-- 检查表完整性
CHECK TABLE table_name;

-- 查看InnoDB状态
SHOW ENGINE INNODB STATUS\G

-- 查看主从状态
SHOW SLAVE STATUS\G
# 查看MySQL错误日志
tail -f /var/log/mysql/error.log

本文为原创内容,欢迎转载,请注明出处。

posted @ 2026-04-22 17:29  不断精进,终生成长  阅读(44)  评论(0)    收藏  举报