故障概述
最近处理了一起生产环境 MySQL 数据库主节点宕机且无法启动的紧急故障。该实例运行的是 MySQL 5.7 版本,采用一主两从的增强半同步复制架构。故障的直接表现是主节点异常宕机,在尝试重启时失败,触发了高可用切换。
故障现象与初步分析
首先,我们检查了 MySQL 的错误日志,这是排查启动失败问题的关键。日志显示了实例启动的完整过程,包括缓冲池初始化、表空间加载等,但在崩溃恢复阶段遇到了致命错误。
启动日志的关键部分如下:
2025-12-04T00:04:19.723007+08:00 [Note] InnoDB: Starting crash recovery.
2025-12-04T00:04:21.006287+08:00 [Note] InnoDB: Starting an apply batch of log records to the database...
InnoDB: Progress in percent: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
2025-12-04 00:04:21 0x400b9511f1c0 InnoDB: Assertion failure in file /data/mysql/5.7/src/storage/innodb/row/row0mysql.cc line 1000
InnoDB: Submit a detailed bug report to http://bugs.mysql.com.
InnoDB: If you get repeated assertion failures or crashes, even
InnoDB: immediately after the mysqld startup, there may be
InnoDB: corruption in the InnoDB tablespace. Please refer to
InnoDB: http://dev.mysql.com/doc/refman/5.7/en/forcing-innodb-recovery.html
InnoDB: Route forcing recovery.
16:04:12 UTC - mysqld got signal 6 :
This could be because you hit a bug. It is also possible that this binary
for one of the libraries it was linked against is corrupt, improperly built,
or misconfigured. This error can also be caused by malfunctioning hardware.
紧接着是一段崩溃的堆栈跟踪信息:
Attempting backtrace... You can use the following information to find out where mysqld died.
stack bottom = 0 thread_stack 0x40000
xxxxxxxxxxxxxxxxxx/bin/mysqld(my_print_stacktrace+0x2c)[0xdff85bc]
xxxxxxxxxxxxxxxxxx/bin/mysqld(handle_fatal_signal+0x40c)[0x74dbdc]
linux-vdso.so.1(__kernel_rt_sigreturn+0x0)[0x4002f48707c0]
/lib64/libc.so.6(gsignal+0xb0)[0x4002f50a64d0]
/lib64/libc.so.6(__abort+0x154)[0x4002f504878c]
xxxxxxxxxxxxxxxxxx/bin/mysqld[0x7245a0]
...
The manual page at `dev.mysql.com/doc/mysql/en/crashing.html` contains information that should help you find out what is causing the crash.
Writing a core file.
日志明确指出了问题:“there may be corruption in the InnoDB tablespace”,即 InnoDB 表空间可能存在损坏。服务器收到了信号 6 (SIGABRT),这是程序异常中止的典型信号。
为了定位损坏的具体位置,我们查看了实例宕机前一刻的日志,发现了更详细的错误信息:
2025-12-03T12:11:41.257928+00:00 [Note] InnoDB: Uncompressed page, stored checksum in field: 79879839, calculated checksums for field: crc32 79879839/2448898256, InnoDB: checksum mismatch, none 3735920559, a stored checksum in field: 79879839, calculated checksum for field: crc32 79879839/2448898256, none 3735920559, page LSN 145 38661252109, low 4 bytes of LSN at page end 3861252109...
2025-12-03T12:11:41.257928+00:00 [Note] InnoDB: Index id is 297 “PRIMARY in table: db1.t1”, index id: 146, page number:63107763[ to be written to data file. We intentionally crash the server to prevent corrupt data from ending up in data files.
这段日志非常关键。它表明在向数据文件写入数据时,InnoDB 检测到表 db1.t1 的 PRIMARY 索引(索引ID 297)在页号 63107763 上发生了校验和(checksum)不匹配。存储的校验和与计算出的校验和不一致,这被判定为页面损坏(Page Corruption)。为了保护数据的一致性,InnoDB 主动触发了服务器崩溃,以防止损坏的数据被写入持久化文件。
原因总结:根本原因是底层数据文件出现了坏块(Bad Block/Page Corruption),导致 InnoDB 存储引擎在崩溃恢复或正常读写时无法通过完整性校验,进而触发保护性宕机,且因损坏持续存在而无法正常启动。
解决方案:基于物理备份恢复
对于此类物理文件损坏,并且有可用备份和从库的场景,最可靠、最快的恢复方式是使用物理备份进行还原。我们的恢复步骤如下:
1. 从健康的从库获取备份
由于主库已损坏,我们选择从一个状态健康的从库上进行物理备份,并直接流式传输到待恢复的主机。
执行备份的命令如下:
xtrabackup --defaults-file=/xxxxx/my.cnf --backup --ftwrl-wait-threshold=10 --ftwrl-wait-query-type=all --ftwrl-wait-timeout=20 --kill-long-queries-timeout=60 --stream=xbsstream --parallel=4 --compress-threads=4 --user='xxxx' --password='xxxxx' --socket=xxxxx/my.sock --target-dir=/tmp --sshpass -p'xxxxx' ssh --oPreferredAuthentication=password -o StrictHostKeyChecking=no -o GSSAPIAuthentication=no teledb@xxxxxx ‘xbstream -x -C /backups’
这条命令的核心是利用 xtrabackup 进行热备份,并通过 --stream=xbsstream 将备份数据流直接通过 SSH 管道传输到目标机器的 /backups 目录。
2. 准备备份(Prepare)
在目标主机上,需要对流式传输过来的原始备份文件应用日志,使其达到一致状态。
innobackupex --apply-log /backup
命令执行输出中会包含关键的状态信息,例如:
251204 01:07:33 innobackupex: Starting the apply-log operation
IMPORTANT: Please check that the apply-log run completes successfully.
At the end of a successful apply-log run innobackupex prints “completed OK”.
...
strdbackup: 5.7.40 started, log sequence number 646252626653
strdbackup: starting shutdown with innodbd_fast_shutdown = 1
...
InnoDB: Shutdown completed; log sequence number 646258296874
看到 “completed OK” 以及日志序列号(LSN)推进,说明 prepare 阶段成功。
3. 复制回数据目录(Copy-Back)
将准备好的备份文件复制回 MySQL 的数据目录。
innobackupex --defaults-file=/xxxxx/my.cnf --user=root --copy-back /backup
此操作会逐个复制数据文件:
201204 01:09:01 [01] Copying undodb001 to /xxxxx/data/undodb001
201204 01:09:02 [01] ...done
...
201204 01:20:19 [01] Copying ./ibtmp1 to /xxxxx/data/ibtmp1
201204 01:20:19 [01] ...done
201204 01:20:19 completed OK!
4. 启动并重建主从关系
恢复文件后,调整文件属主并启动 MySQL 实例。由于是从从库备份恢复的,需要重建主从复制关系。
首先,查看备份中包含的 binlog 位置信息:
cat /smd/teledb_backup/xtrabackup_binlog_info
输出示例:mysql-bin.001471 14853225 e3ee263f-a309-11ee-82d2-8c2a8eed549d:1-80611942, e57fade1-a309-11ee-b5aa-8c2a8eed5475:1-56450773
然后在 MySQL 命令行中操作:
(root@localhost) [(none):mysql_8801.sock]> reset slave;
Query OK, 0 rows affected (0.03 sec)
(root@localhost) [(none):mysql_8801.sock]> show global variables like “%gtid%”;
-- 确认 gtid_mode 为 ON
(root@localhost) [(none):mysql_8801.sock]> set global gtid_purged=”e3ee263f-a309-11ee-82d2-8c2a8eed549d:1-80611942,e57fade1-a309-11ee-b5aa-8c2a8eed5475:1-56450773”;
Query OK, 0 rows affected (0.00 sec)
(root@localhost) [(none):mysql_8801.sock]> start slave;
Query OK, 0 rows affected (0.13 sec)
至此,通过物理备份完成恢复,主从复制重新建立。
应急方案:当没有可用备份时
如果没有可用的物理备份,情况会棘手得多。此时可以尝试让 MySQL 以恢复模式启动,然后尽可能多地逻辑导出数据。
1. 尝试强制恢复模式启动
在 MySQL 配置文件 my.cnf 的 [mysqld] 部分添加以下参数,并尝试按顺序递增级别启动:
innodb_force_recovery = 1
innodb_force_recovery 参数有 6 个级别(1-6),级别越高,InnoDB 在启动时跳过的恢复操作越多,但也意味着数据丢失或不一致的风险越大。
各级别的功能与风险对比如下:
| 级别 |
功能 |
风险 |
| 1 (SRV_FORCE_IGNORE_CORRUPT) |
忽略损坏页面 |
最低 |
| 2 (SRV_FORCE_NO_BACKGROUNDD) |
禁止后台操作 |
低 |
| 3 (SRV_FORCE_NO_TRX_UNDO) |
不执行事务回滚 |
中 |
| 4 (SRV_FORCE_NO_IBUF_MERGE) |
禁止插入缓冲合并 |
中 |
| 5 (SRV_FORCE_NO_UNDO_LOG_SCAN) |
不查看 UNDO 日志 |
高 |
| 6 (SRV_FORCE_NO_LOG_REDO) |
不进行前滚恢复 |
最高 |
操作建议:从级别 1 开始尝试启动实例。如果启动失败,逐渐增加级别值(2, 3, …)直到实例能够启动。务必使用只读模式启动,并尽快导出数据,因为在该模式下数据文件可能处于不一致状态,写操作会导致进一步损坏。
2. 逻辑导出数据
一旦实例以恢复模式启动,立即使用 mysqldump 进行逻辑备份。
导出所有数据库:
mysqldump --all-databases --single-transaction --routines --triggers --events > /tmp/full_backup_$(date +%Y%m%d).sql
或者逐个数据库导出,排除系统库:
mysql -e “SHOW DATABASES;” | grep -v “Database” | grep -v “information_schema” | grep -v “performance_schema” | grep -v “sys” | while read db; do
echo “导出数据库: $db”
mysqldump --single-transaction “$db” > “/tmp/${db}_backup.sql” 2>/tmp/dump${db}.log
done
如果某个表损坏导致导出失败,可以排除该表,导出其他健康表:
mysql -e “SHOW TABLES FROM your_database” | grep -v “损坏的表名” | while read table; do
mysqldump --single-transaction your_database “$table” > “/tmp/your_database_${table}.sql”
done
3. 在新实例中恢复
将成功导出的 SQL 文件导入到一个全新初始化的 MySQL 实例中。完成数据验证后,再将应用流量切换至新实例。
总结与建议
本次故障的核心是 InnoDB 数据页物理损坏。对于生产系统,防范此类风险远胜于补救:
- 定期备份与恢复演练:必须制定并严格执行物理备份策略,并定期进行恢复演练,验证备份的有效性。这是 运维 工作的生命线。
- 构建健壮的高可用架构:一主多从的架构在本案例中起到了关键作用,健康的从库成为了可靠的数据源。
- 监控与预警:需要监控数据库错误日志中的关键告警(如 checksum 错误),以便在问题恶化前提前介入。
- 硬件稳定性:虽然不常见,但存储介质(如磁盘、SSD)故障是导致数据坏块的主要原因之一,需要关注硬件健康状态。
当故障发生时,清晰的处置思路至关重要:首先通过日志精确定位问题;若有可用备份,优先采用物理恢复,速度最快;若无备份,则尝试以恢复模式启动并逻辑导出数据,尽可能减少损失。更多数据库相关的深入讨论和实践经验,欢迎在 云栈社区 交流。
参考资料
[1] MySQL数据文件坏快导致数据库无法启动, 微信公众号:mp.weixin.qq.com/s/1T-Qh0TB1jNv_63qyX0eMg
版权声明:本文由 云栈社区 整理发布,版权归原作者所有。