宕机物理冷迁移抢救
01 前置理论与核心机制¶
1. 物理备份与逻辑备份的差异¶
- 逻辑恢复受限:当 MySQL 进程彻底崩溃、服务无法正常启动时,传统的 mysqldump 导出 SQL 方案无法使用,因为逻辑导出依赖数据库服务处于正常运行状态。
- 物理冷拷贝机制:只要服务器的底层磁盘未损坏,核心数据文件(表结构、.ibd 数据页、ibdata1 共享表空间、ib_logfile 重做日志)完整,即可绕过 SQL 层,直接将整套物理文件打包迁移至新机器,利用 InnoDB 的崩溃恢复机制重新拉起服务。
2. 物理迁移的两项基本前提¶
- 大版本必须严格一致:新旧两台机器的 MySQL 大版本必须保持一致。例如将 5.7 的数据直接迁移至 8.0,由于底层数据字典(Data Dictionary)格式从 .frm 重构为了 SDI,存储引擎层无法兼容,服务将无法启动。
- 文件权限的一致性:Linux 下通过 root 账号传输和解压文件后,文件属主会发生改变。MySQL 进程默认以 mysql 用户运行,若缺乏对数据目录的读写权限,启动会立即报错退出。
02 故障排查与恢复闭环 SOP¶
当生产机器 A(源机器)故障导致 MySQL 无法启动,需将数据紧急恢复至机器 B(目标机器)时,按以下步骤操作:
步骤一:在源机器全量打包核心数据目录¶
登录源机器,定位 MySQL 的核心数据目录(默认通常为 /var/lib/mysql)。在确认源库 MySQL 进程已完全停止的前提下,将整个目录打包。
- 观测点:必须包含隐藏文件、ibdata1 以及 ib_logfile 系列文件,这些是保障事务一致性的核心文件。
步骤二:跨机传输数据文件¶
将生成的压缩包通过内网传输至备用的目标机器(机器 B)。
步骤三:停止目标机器服务并清空默认数据¶
在覆盖数据文件前,必须先停止目标机器上的 MySQL 服务,防止文件被进程锁定或产生写入冲突。随后清空安装时初始化的默认数据。
步骤四:解压覆盖与权限修复(关键步骤)¶
将传输过来的压缩包解压至标准运行目录中。解压完成后,必须立即重置目录的属主权限。
# 解压到数据目录
tar -xzvf /root/rescue.tar.gz -C /var/lib/mysql/
# 关键操作:将目录及文件的属主和属组修改为 mysql 用户
chown -R mysql:mysql /var/lib/mysql
步骤五:启动服务与业务验证¶
权限修正后,启动 MySQL 服务并检查运行状态。
- 验证点:状态显示为 active (running) 说明数据库已正常加载物理文件并完成启动。随后连入数据库抽查业务表进行验证:
03 真实生产故障现象与案例库¶
案例 1:系统内核崩溃导致 MySQL 无法启动¶
- 故障现象:某宿主机发生 Kernel Panic 频繁重启,导致操作系统环境损坏,MySQL 服务无法正常拉起。
- 排查与处置:通过救援模式挂载磁盘,定位到 /var/lib/mysql 目录。按照上述 SOP 打包迁移至备用机。新实例启动时,InnoDB 自动读取 ib_logfile 执行 Crash Recovery(前滚已提交事务,回滚未提交事务),数据实现完整恢复。
案例 2:权限属主错误导致迁移后启动失败¶
- 故障现象:执行物理迁移并解压数据后,运行 systemctl start mysqld 报错 Job for mysqld.service failed。
- 排查与处置:查看 /var/log/mysqld.log,发现大量 Permission denied 报错,提示无法打开 ./ibdata1。
- 根因与处置:由于全程使用 root 用户执行 scp 和 tar 解压操作,文件属主全部变成了 root。MySQL 进程无权读取核心文件导致启动失败。执行 chown -R mysql:mysql /var/lib/mysql 修正权限后,服务正常启动。
案例 3:跨版本迁移引发存储引擎报错¶
- 故障现象:源机器运行 MySQL 5.7,新申请的备用机默认安装了 MySQL 8.0。替换物理目录后启动直接报错退出。
- 排查与处置:日志提示 [ERROR] [MY-012224] [InnoDB] Tablespace flags are invalid in filepath。
- 根因与处置:MySQL 跨大版本的物理文件结构不兼容。8.0 废弃了 .frm 文件并将元数据整合进了 InnoDB 系统表空间。物理冷拷贝必须保证大版本一致。处置方案为卸载目标机器的 8.0 版本,重装同版本的 MySQL 5.7 后重新拉起。
04 生产排障速查表(物理冷迁移链路)¶
顺序排查/操作层级执行命令 / 观测点动作定性处置动作01源端打包tar -czvf rescue.tar.gz -C /var/lib/mysql .物理文件全量归档确保源端 MySQL 已彻底停止,防止打包过程中存在文件写入02目标端清理systemctl stop mysql 及 rm -rf清理初始化残留文件腾出目录空间,避免新旧数据文件混杂引发元数据冲突03目标端解压tar -xzvf rescue.tar.gz -C /var/lib/mysql/数据文件还原保持原有目录层级与相对路径一致04权限修正chown -R mysql:mysql /var/lib/mysql修正文件属主权限必须执行,否则 MySQL 进程会因缺乏读写权限而启动失败05服务拉起systemctl start mysql触发崩溃恢复机制观察 error 日志,等待 InnoDB 完成自动恢复流程06业务验证mysql -e "SELECT ..."数据可用性确认确认核心业务表数据完整,通知上游应用切换数据库连接
05 面试应答参考话术¶
面试官问:“如果一台生产 MySQL 发生故障,系统或服务无法启动,但底层磁盘未损坏。此时无法使用 mysqldump,该如何将数据恢复到新机器上?”
可以这样回答:只要底层磁盘和核心数据文件没有损坏,就不需要依赖 SQL 层的逻辑导出。可以直接采用底层物理文件冷拷贝的方案,将数据目录迁移到新机器上重新拉起服务。第一步:全量打包源数据
在故障机器上,确认 MySQL 进程彻底停止后,将核心数据目录(通常为 /var/lib/mysql)下的所有文件,包括 ibdata1、.ibd 数据页以及重做日志,打包压缩并传输到已安装好 MySQL 的备用机器上。第二步:清理与解压覆盖
在备用机器操作前,先执行 systemctl stop mysql 停止服务。随后将该机器默认初始化生成的空数据目录清空,再将传输过来的压缩包解压至该目录下。第三步:修正属主权限(关键细节)
解压完成后不能直接启动服务。因为传输和解压通常使用 root 账号操作,解压后的文件属主会变成 root;而 MySQL 默认以 mysql 用户身份运行,直接启动会因权限不足报错。因此必须执行 chown -R mysql:mysql /var/lib/mysql,将目录所有权交还给 mysql 用户。第四步:版本核对与启动验证
修改权限后即可启动 MySQL 服务,InnoDB 会自动读取物理文件并执行崩溃恢复。此外,执行物理冷迁移有一个硬性前提:新旧两台机器的 MySQL 大版本必须保持一致(例如同为 5.7),跨大版本直接拷贝物理文件会因底层数据字典格式不兼容而无法启动。
06 面试高频追问与应答预案¶
追问 1:只把旧机器的 .ibd 数据文件拷贝过去,其他文件不要行不行?¶
- 回答要点:
- 不行。仅拷贝 .ibd 文件无法直接启动。必须连同共享表空间(ibdata1)以及重做日志(ib_logfile0、ib_logfile1 等)一并拷贝。
- ibdata1 中存储了 InnoDB 的数据字典、表空间 ID 映射、Undo 段和双写缓冲(Doublewrite buffer)。若缺失这些信息或文件间的日志序列号(LSN)不一致,MySQL 启动时会判定数据损坏并拒绝加载。
追问 2:如果在打包时,旧机器的 MySQL 进程实际上并未完全退出,仍在向磁盘写数据,会导致什么后果?¶
- 回答要点:
- 会导致拷贝出的数据处于不一致状态(出现页面断裂或半写情况)。迁移到新机器启动时,通常会触发页校验和错误(Page Checksum Error),导致服务无法拉起。
- 物理冷拷贝的前提是源端 MySQL 服务已完全停止。若必须在运行状态下进行物理备份,则需要使用 Percona XtraBackup 等支持热备的工具,通过拷贝过程同步记录 Redo Log 来保障数据一致性。
07 自动化迁移环境校验脚本(check_mysql_migration.sh)¶
该脚本用于在新机器解压数据后、启动服务前,自动检测数据目录权限、残留进程及核心文件完整性,规避启动故障。
#!/usr/bin/env bash
# ==============================================================================
# 脚本名称: check_mysql_migration.sh
# 功能描述: MySQL 物理迁移目标机启动前置安全校验 (纯只读)
# 使用方式: 在目标服务器解压数据后、准备启动 MySQL 前执行
# ==============================================================================
set -o pipefail
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
PLAIN='\033[0m'
DATA_DIR="/var/lib/mysql"
echo -e "${BLUE}================================================================${PLAIN}"
echo -e "${BLUE} MySQL 物理迁移启动前置环境安全校验 ${PLAIN}"
echo -e "${BLUE}================================================================${PLAIN}"
# 1. 检查 MySQL 进程是否已停止
echo -e "\n${YELLOW}[1/3] 检查目标机 MySQL 服务运行状态...${PLAIN}"
if pgrep -x "mysqld" > /dev/null; then
echo -e " -> ${RED}[FATAL] 目标机 mysqld 进程仍在运行!请先执行 systemctl stop mysql,避免覆盖运行时文件。${PLAIN}"
exit 1
else
echo -e " -> ${GREEN}[OK] 目标机 mysqld 服务处于停止状态。${PLAIN}"
fi
# 2. 检查数据目录属主权限
echo -e "\n${YELLOW}[2/3] 检查数据目录 (${DATA_DIR}) 属主权限...${PLAIN}"
if [ ! -d "$DATA_DIR" ]; then
echo -e " -> ${RED}[FATAL] 数据目录 $DATA_DIR 不存在,请确认解压路径。${PLAIN}"
exit 1
fi
ROOT_OWNED_FILES=$(find "$DATA_DIR" -user root | wc -l)
if [ "$ROOT_OWNED_FILES" -gt 0 ]; then
echo -e " -> ${RED}[WARN] 检测到 $ROOT_OWNED_FILES 个属主为 root 的文件或目录。${PLAIN}"
echo -e " -> ${YELLOW}[风险] 此时启动 MySQL 会因 Permission denied 权限不足而失败。${PLAIN}"
echo -e " -> ${GREEN}[修复建议] 请执行: chown -R mysql:mysql $DATA_DIR${PLAIN}"
else
# 修复了这里的 \%U:\%G 为 %U:%G
MYSQL_OWNED=$(stat -c "%U:%G" "$DATA_DIR")
if [ "$MYSQL_OWNED" == "mysql:mysql" ]; then
echo -e " -> ${GREEN}[OK] 数据目录权限正常 ($MYSQL_OWNED)。${PLAIN}"
else
echo -e " -> ${YELLOW}[INFO] 数据目录当前属主为: $MYSQL_OWNED,请确认是否与当前环境的 MySQL 运行用户一致。${PLAIN}"
fi
fi
# 3. 检查核心文件完整性
echo -e "\n${YELLOW}[3/3] 检查底层事务与系统文件完整性...${PLAIN}"
MISSING_CORE=0
for core_file in ibdata1 ib_logfile0 mysql; do
if [ ! -e "${DATA_DIR}/${core_file}" ]; then
echo -e " -> ${RED}[WARN] 缺失关键文件或目录: ${core_file}${PLAIN}"
MISSING_CORE=1
fi
done
if [ "$MISSING_CORE" -eq 1 ]; then
echo -e " -> ${RED}[FATAL] 核心引擎文件缺失,InnoDB 将无法完成初始化或恢复,请检查源端打包是否完整。${PLAIN}"
else
echo -e " -> ${GREEN}[OK] 基础核心文件 (ibdata1, ib_logfile0, mysql) 存在。${PLAIN}"
fi
echo -e "\n${BLUE}================================================================${PLAIN}"
echo -e "${BLUE}校验完毕。请同时确保新旧实例大版本一致,确认无误后即可启动服务。${PLAIN}"
echo -e "${BLUE}================================================================${PLAIN}"