MySQL 主从故障修复完整技术方案(Bitnami Helm + K8s)

MySQL 主从故障修复完整技术方案(Bitnami Helm + K8s)

本文档记录了针对 Bitnami MySQL Helm 部署在 Kubernetes 集群中,从库(Secondary)因 mysql_upgrade 锁超时导致反复重启,以及后续主从复制因 binlog 过期无法同步的完整修复过程。方案包含两种修复手段:手动介入升级基于物理克隆的重建,并配有自动化脚本,适用于生产环境故障快速恢复。


1. 故障现象与根因分析

1.1 现象


  • 从库 Pod mysql-secondary-0 状态为 CrashLoopBackOff,容器反复重启。


  • 日志关键错误:
    -- javascript typescript shell bash sql json html css c cpp java ruby python go rust markdown

    mysql_upgrade: [ERROR] [MY-013178] Execution of server-side SQL statement
    'ALTER TABLE slave_worker_info STATS_PERSISTENT=0;' failed with error code 1205
    'Lock wait timeout exceeded; try restarting transaction'.


  • 手动修复后复制失败,报错:
    -- javascript typescript shell bash sql json html css c cpp java ruby python go rust markdown

    Last_IO_Error: Got fatal error 1236 from source when reading data from binary log:
    'Cannot replicate because the source purged required binary logs...'

1.2 根因


  • Bitnami 容器启动脚本会执行 mysql_upgrade(即使版本相同),以完成系统表结构升级。

  • 在升级过程中,对 slave_worker_info 表执行 ALTER TABLE 时遇到 InnoDB 锁等待超时,失败导致 mysqld 退出,容器重启后重复尝试,形成死循环。

  • 锁超时原因通常是之前失败升级或复制异常导致元数据表存在未释放的锁或损坏。

  • 修复升级后,由于从库落后主库过多,主库已清理所需的 binlog,导致复制无法继续。

2. 修复方案总览

修复分为两个阶段:

阶段一:解决升级锁超时,让从库容器正常启动。


  • 方法 A:尝试通过环境变量 MYSQL_SKIP_UPGRADE=yes 跳过升级(如未生效,则使用方法 B)。

  • 方法 B:手动覆盖容器启动命令,进入容器后清理元数据表并强制升级。

阶段二:重建从库数据以恢复复制(因为 binlog 已丢失)。


  • 使用 MySQL 物理克隆(CLONE INSTANCE)从主库全量复制数据,并自动配置 GTID 复制。

最终,从库恢复正常,复制状态 Slave_IO_RunningSlave_SQL_Running 均为 Yes


3. 详细修复步骤

3.1 准备工作


  • 确认主库正常运行,mysql-primary-0 状态为 Running

  • 获取主库 IP 和 root 密码(本环境密码均为 Gientech@123,Secret 名称为 mysql)。

  • 确保有足够的存储空间(克隆会占用临时空间)。

3.2 阶段一:修复升级锁超时

方法 A:环境变量跳过升级(尝试,但本案例未生效)

修改 StatefulSet mysql-secondary,在 mysql 容器环境变量中添加:
-- javascript typescript shell bash sql json html css c cpp java ruby python go rust markdown

复制代码
- name: MYSQL_SKIP_UPGRADE
  value: "yes"

删除 Pod 重建,观察日志是否跳过升级。若仍然执行升级,则使用方法 B。

方法 B:手动干预升级(本案例采用)

Step 1:修改 StatefulSet,让容器进入休眠状态

-- javascript typescript shell bash sql json html css c cpp java ruby python go rust markdown

复制代码
kubectl edit statefulset -n mysql-wedevops mysql-secondary

  • spec.template.spec.containers 下找到 name: mysql 的容器。


  • 添加 command 覆盖默认启动:
    -- javascript typescript shell bash sql json html css c cpp java ruby python go rust markdown

    command:

    • bash
    • -c
    • sleep infinity

  • 同时将 spec.replicas0 改为 1


  • 保存退出,等待 Pod 启动(Running)。

Step 2:进入容器并手动启动 MySQL(跳过升级)

-- javascript typescript shell bash sql json html css c cpp java ruby python go rust markdown

复制代码
kubectl exec -it -n mysql-wedevops mysql-secondary-0 -c mysql -- bash

容器内操作:
-- javascript typescript shell bash sql json html css c cpp java ruby python go rust markdown

复制代码
# 启动 mysqld,禁用复制和网络,设置长锁超时
mysqld --skip-slave-start --skip-networking --innodb-lock-wait-timeout=3600 --daemonize
Step 3:清理复制元数据表

-- javascript typescript shell bash sql json html css c cpp java ruby python go rust markdown

复制代码
mysql -uroot -p"$MYSQL_ROOT_PASSWORD"

执行 SQL:
-- javascript typescript shell bash sql json html css c cpp java ruby python go rust markdown

复制代码
TRUNCATE TABLE mysql.slave_worker_info;
TRUNCATE TABLE mysql.slave_relay_log_info;
TRUNCATE TABLE mysql.slave_master_info;

退出 MySQL。

Step 4:关闭当前 mysqld 并以强制升级模式重启

-- javascript typescript shell bash sql json html css c cpp java ruby python go rust markdown

复制代码
mysqladmin -uroot -p"$MYSQL_ROOT_PASSWORD" shutdown
mysqld --skip-slave-start --skip-networking --upgrade=FORCE --daemonize

观察日志:
-- javascript typescript shell bash sql json html css c cpp java ruby python go rust markdown

复制代码
tail -f /opt/bitnami/mysql/logs/mysqld.log

当出现 Server upgrade from '80036' to '80036' completed. 即升级成功。

Step 5:关闭 mysqld,退出容器,恢复 StatefulSet

-- javascript typescript shell bash sql json html css c cpp java ruby python go rust markdown

复制代码
mysqladmin -uroot -p"$MYSQL_ROOT_PASSWORD" shutdown
exit

再次编辑 StatefulSet,删除 之前添加的 command 部分,保留 replicas=1

删除 Pod 重建:
-- javascript typescript shell bash sql json html css c cpp java ruby python go rust markdown

复制代码
kubectl delete pod -n mysql-wedevops mysql-secondary-0

等待新 Pod 正常启动(此时日志不会再运行升级,直接启动 MySQL)。


3.3 阶段二:恢复主从复制(使用物理克隆)

由于主库 binlog 已过期,无法直接通过 CHANGE MASTER 追上,需要使用全量复制重建从库。Bitnami 支持从空数据目录自动通过 xtrabackup 同步,但本案例使用 CLONE 插件,更简单且与 GTID 兼容。

我们利用用户提供的 mysql-pri-rep-repair-v2.0.sh 脚本自动化完成克隆和复制配置。

3.3.1 脚本功能说明

该脚本执行以下操作:

获取主库 Pod IP。
2. 2.
关闭从库只读模式。
3. 3.
安装 clone 插件。
4. 4.
停止并重置原有复制。
5. 5.
执行 CLONE INSTANCE 命令(需人工在另一个终端确认执行,因克隆会重启 Pod)。
6. 6.
监控 Pod 是否因克隆而重启(检测重启次数变化),一旦重启则等待就绪。
7. 7.
检查克隆状态(performance_schema.clone_status)。
8. 8.
配置 GTID 复制(CHANGE MASTER TO MASTER_AUTO_POSITION=1)。
9. 9.
验证复制状态。

注意:克隆过程中 Pod 会重启,数据目录会被完全覆盖。主库数据大于 50G 时需评估磁盘 IO 和网络带宽。

3.3.2 脚本使用方法

将脚本保存为 mysql-pri-rep-repair-v2.0.sh,并赋予执行权限。
2. 2.
根据实际环境修改变量:
-- javascript typescript shell bash sql json html css c cpp java ruby python go rust markdown

复制代码
   NAMESPACE="mysql-wedevops"
   PRIMARY_POD="mysql-primary-0"
   SECONDARY_POD="mysql-secondary-0"
   ROOT_PASSWORD="Gientech@123"
   REPL_PASSWORD="Gientech@123"   # 复制用户密码,需与 Primary 配置一致

运行脚本:
-- javascript typescript shell bash sql json html css c cpp java ruby python go rust markdown

复制代码
   ./mysql-pri-rep-repair-v2.0.sh

3.3.3 脚本执行交互说明


  • 脚本会打印克隆命令,请在另一个终端执行该命令(脚本会暂停等待)。


  • 克隆命令示例:
    -- javascript typescript shell bash sql json html css c cpp java ruby python go rust markdown

    kubectl exec -n mysql-wedevops mysql-secondary-0 --
    mysql -uroot -p'Gientech@123'
    -e "SET GLOBAL clone_valid_donor_list = '主库IP:3306';"
    -e "SET GLOBAL clone_buffer_size = 67108864;"
    -e "SET GLOBAL clone_max_concurrency = 4;"
    -e "CLONE INSTANCE FROM root@'主库IP':3306 IDENTIFIED BY 'Gientech@123';"


  • 执行克隆命令后,Pod 会立即重启(预期行为)。脚本会每 2 秒检测重启次数,一旦增加则判断克隆开始,并等待 Pod 就绪。


  • Pod 就绪后,脚本自动配置复制并验证。

3.3.4 脚本执行结果验证

克隆完成后,脚本会输出复制状态:
-- javascript typescript shell bash sql json html css c cpp java ruby python go rust markdown

复制代码
Slave_IO_Running: Yes
Slave_SQL_Running: Yes
Seconds_Behind_Master: 0

表示从库已成功同步。


4. 故障修复完整流程图

-- javascript typescript shell bash sql json html css c cpp java ruby python go rust markdown

复制代码
[故障] 从库CrashLoopBackOff
        ↓
[诊断] 日志发现升级锁超时
        ↓
[尝试] 环境变量跳过升级 (未生效)
        ↓
[手动干预] 修改sts command=sleep
        ↓
[容器内操作] 
  1. mysqld --skip-slave-start --skip-networking
  2. TRUNCATE 复制元数据表
  3. mysqld --upgrade=FORCE → 升级成功
  4. 关闭,恢复sts,重建Pod
        ↓
[Pod正常启动] 但复制报错 binlog丢失
        ↓
[重建从库] 使用克隆脚本
  1. 执行CLONE INSTANCE
  2. Pod自动重启
  3. 配置GTID复制
  4. 验证Slave状态OK
        ↓
[修复完成]

5. 注意事项与最佳实践

5.1 关于升级锁超时


  • 根本原因可能是之前复制异常导致元数据表损坏,建议在升级前清理复制表。

  • 若环境变量跳过无效,优先使用手动升级方案,避免频繁重启。

5.2 关于物理克隆


  • 克隆前需确保主库 clone 插件已加载(主库默认未加载,但作为 donor 自动支持,无需手动安装)。

  • 克隆会锁表,但使用 clone_buffer_sizemax_concurrency 可优化性能。

  • 若主库数据极大,建议在业务低峰期执行,并监控网络和磁盘。

5.3 复制用户权限


  • 复制用户(replicator)需有 REPLICATION SLAVE, REPLICATION CLIENT 权限,且密码需与脚本一致。

  • 若使用 GTID,需确保主库 gtid_mode=ONenforce_gtid_consistency=ON(Bitnami 默认开启)。

5.4 安全建议


  • 脚本中包含明文密码,可改为从 Secret 动态获取(如 kubectl get secret -n ...)。

  • 生产环境中建议通过 Kubernetes Secret 注入密码,脚本中从环境变量读取。

6. 附录:脚本全量内容

-- javascript typescript shell bash sql json html css c cpp java ruby python go rust markdown

复制代码
#!/bin/bash
# mysql-clone-repair-v2.0.sh
# 基于物理克隆重建MySQL从库,修复复制失败

set -e


############################


# 基础配置



############################

NAMESPACE="mysql-wedevops"

PRIMARY_POD="mysql-primary-0"

SECONDARY_POD="mysql-secondary-0"

ROOT_PASSWORD="Gientech@123"

REPL_PASSWORD="Gientech@123"


############################


# 函数:获取MySQL容器的重启次数



############################

get_pod_restart_count() {

local pod_name="\(1"
    kubectl get pod -n "\)NAMESPACE" "$pod_name" 

-o jsonpath='{.status.containerStatuses[?(@.name=="mysql")].restartCount}' 2>/dev/null || echo "0"

}


############################


# 函数:检查Pod是否就绪



############################

is_pod_ready() {

kubectl get pod -n "\(NAMESPACE" "\)SECONDARY_POD" 

-o jsonpath='{.status.conditions[?(@.type=="Ready")].status}' 2>/dev/null | grep -q "True"

}


############################


# 主流程



############################

echo "=== MySQL克隆修复(快速检测版)==="

echo "开始时间: $(date)"

echo ""


# 1. 获取主库IP



echo "1. 获取主库Pod IP"

PRIMARY_IP=\((kubectl get pod -n "\)NAMESPACE" "$PRIMARY_POD" -o jsonpath='{.status.podIP}')

echo "主库IP: $PRIMARY_IP"

echo ""


# 2. 记录当前重启次数



echo "2. 记录当前Pod状态"

INITIAL_RESTART_COUNT=\((get_pod_restart_count "\)SECONDARY_POD")

echo "当前MySQL容器重启次数: \(INITIAL_RESTART_COUNT"
echo "当前Pod状态:"
kubectl get pod -n "\)NAMESPACE" "$SECONDARY_POD"

echo ""


# 3. 强制关闭从库只读模式



echo "3. 强制关闭只读模式"

kubectl exec -n "\(NAMESPACE" "\)SECONDARY_POD" -- 

mysql -uroot -p"$ROOT_PASSWORD" 

-e "SET GLOBAL read_only = 0; SET GLOBAL super_read_only = 0;" 2>/dev/null || true

echo ""


# 4. 安装克隆插件



echo "4. 安装克隆插件"

kubectl exec -n "\(NAMESPACE" "\)SECONDARY_POD" -- 

mysql -uroot -p"$ROOT_PASSWORD" 

-e "INSTALL PLUGIN clone SONAME 'mysql_clone.so';" 2>/dev/null || echo "插件可能已存在"

echo ""


# 5. 停止复制



echo "5. 停止复制"

kubectl exec -n "\(NAMESPACE" "\)SECONDARY_POD" -- 

mysql -uroot -p"$ROOT_PASSWORD" 

-e "STOP SLAVE; RESET SLAVE ALL;" 2>/dev/null || echo "已停止"

echo ""


# 6. === 关键:执行克隆命令 ===



echo "6. === 执行克隆命令 ="

echo "注意:这会断开连接,请在终端手动执行!"

echo ""

echo "= 克隆命令(复制到终端执行)="

echo "kubectl exec -n $NAMESPACE \(SECONDARY_POD -- \\"
echo "  mysql -uroot -p'\)ROOT_PASSWORD' \"

echo "  -e "SET GLOBAL clone_valid_donor_list = '\(PRIMARY_IP:3306';\" \\"
echo "  -e \"SET GLOBAL clone_buffer_size = 67108864;\" \\"
echo "  -e \"SET GLOBAL clone_max_concurrency = 4;\" \\"
echo "  -e \"CLONE INSTANCE FROM root@'\)PRIMARY_IP':3306 IDENTIFIED BY '$ROOT_PASSWORD';""

echo "= 结束 ==="

echo ""

echo "监控命令:"

echo "1. 监控Pod重启: kubectl get pods -n $NAMESPACE -w"

echo "2. 查看Pod状态: kubectl get pod -n $NAMESPACE $SECONDARY_POD"

echo ""

read -p "是否已在终端执行克隆命令?(按Enter开始监控): "


# 7. 快速检测Pod重启



echo "7. 快速检测Pod重启"

echo "开始监控Pod重启(每2秒检查一次,最多3分钟)..."

echo ""


POD_RESTARTED=false

CHECK_INTERVAL=2

MAX_CHECKS=90


for ((i=1; i<=MAX_CHECKS; i++)); do

sleep $CHECK_INTERVAL


CURRENT_RESTART_COUNT=\((get_pod_restart_count "\)SECONDARY_POD")

ELAPSED_TIME=$((i * CHECK_INTERVAL))


echo "检查 [\(ELAPSED_TIME秒]: 重启次数=\)CURRENT_RESTART_COUNT (初始: $INITIAL_RESTART_COUNT)"


if [ "\(CURRENT_RESTART_COUNT" -gt "\)INITIAL_RESTART_COUNT" ]; then

echo "✅ 检测到Pod重启!重启次数从 $INITIAL_RESTART_COUNT 变为 $CURRENT_RESTART_COUNT"

POD_RESTARTED=true

break

fi


if [ \(((ELAPSED_TIME % 10)) -eq 0 ]; then
        echo "详细状态:"
        kubectl get pod -n "\)NAMESPACE" "$SECONDARY_POD" | tail -1

fi


if [ \(ELAPSED_TIME -eq 30 ] &amp;&amp; [ "\)POD_RESTARTED" = false ]; then

echo "⚠ 已等待30秒,克隆可能还未触发重启或重启较慢..."

fi


if [ \(ELAPSED_TIME -eq 60 ] &amp;&amp; [ "\)POD_RESTARTED" = false ]; then

echo "检查Pod详细状态..."

kubectl describe pod -n "\(NAMESPACE" "\)SECONDARY_POD" | grep -A5 "State:" | head -10

fi

done


echo ""


# 8. 检测结果处理



if [ "\(POD_RESTARTED" = false ]; then
    echo "⚠ 在3分钟内未检测到Pod重启"
    echo "当前最终状态:"
    kubectl get pod -n "\)NAMESPACE" "$SECONDARY_POD"


read -p "是否继续检查克隆结果?(y/N): " -n 1 -r

echo

if [[ ! \(REPLY =~ ^[Yy]\) ]]; then

echo "退出脚本"

exit 1

fi

else

echo "✅ Pod重启检测完成"


echo "等待Pod完全就绪..."

for i in {1..30}; do

if is_pod_ready; then

echo "✅ Pod已就绪 (\(((i*2))秒)"
            break
        fi
        echo "等待Pod就绪... (\)((i*2))秒)"

sleep 2

done

fi


# 9. 检查克隆结果



echo "9. 检查克隆结果"

echo "等待MySQL服务启动..."


MYSQL_READY=false

for i in {1..30}; do

if kubectl exec -n "\(NAMESPACE" "\)SECONDARY_POD" -- 

timeout 5 mysql -uroot -p"\(ROOT_PASSWORD" -e "SELECT 1;" &gt;/dev/null 2&gt;&amp;1; then
        echo "✅ MySQL服务正常 (\)((i2))秒)"  
MYSQL_READY=true  
break  
fi  
echo "等待MySQL连接... ($((i2))秒)"

sleep 2

done


if [ "\(MYSQL_READY" = true ]; then
    echo "检查克隆状态表:"
    CLONE_STATUS=\)(kubectl exec -n "\(NAMESPACE" "\)SECONDARY_POD" -- 

mysql -uroot -p"$ROOT_PASSWORD" 

-e "SELECT STATE, ERROR_NO, ERROR_MESSAGE FROM performance_schema.clone_status ORDER BY END_TIME DESC LIMIT 1;" 2>/dev/null || echo "")


if [ -n "$CLONE_STATUS" ]; then

echo "克隆状态: \(CLONE_STATUS"
        if echo "\)CLONE_STATUS" | grep -q "Completed"; then

echo "✅ 克隆成功完成"

elif echo "$CLONE_STATUS" | grep -q "Failed"; then

echo "❌ 克隆失败"

exit 1

else

echo "⚠ 克隆状态异常: $CLONE_STATUS"

fi

else

echo "⚠ 克隆状态表为空"

fi

else

echo "❌ MySQL服务无法连接,克隆可能失败"

exit 1

fi


# 10. 配置复制



echo "10. 配置复制"

kubectl exec -n "\(NAMESPACE" "\)SECONDARY_POD" -- 

mysql -uroot -p"\(ROOT_PASSWORD" \
  -e "
  STOP SLAVE;
  RESET SLAVE ALL;
  CHANGE MASTER TO
    MASTER_HOST='\)PRIMARY_IP',

MASTER_USER='replicator',

MASTER_PASSWORD='$REPL_PASSWORD',

MASTER_AUTO_POSITION=1;

START SLAVE;

" 2>&1 | grep -v "Using a password" || true


echo "✅ 复制配置完成"


# 11. 验证



echo "11. 验证复制状态"

sleep 5


REPL_STATUS=\((kubectl exec -n "\)NAMESPACE" "\(SECONDARY_POD" -- \
  mysql -uroot -p"\)ROOT_PASSWORD" 

-e "SHOW SLAVE STATUS\G" 2>/dev/null || echo "")


if [ -n "\(REPL_STATUS" ]; then
    echo "\)REPL_STATUS" | grep -E "Slave_IO_Running:|Slave_SQL_Running:|Seconds_Behind_Master:|Last_Error:"


IO_RUNNING=\((echo "\)REPL_STATUS" | grep "Slave_IO_Running:" | awk '{print \(2}')
    SQL_RUNNING=\)(echo "$REPL_STATUS" | grep "Slave_SQL_Running:" | awk '{print $2}')


if [ "\(IO_RUNNING" = "Yes" ] &amp;&amp; [ "\)SQL_RUNNING" = "Yes" ]; then

echo "✅ 复制状态正常"

else

echo "❌ 复制状态异常"

fi

else

echo "⚠ 无法获取复制状态"

fi

`echo ""`
`
echo "=== 完成 ==="`
`
echo "结束时间: `\((date)"
echo ""
echo "最终Pod状态:"
kubectl get pod -n "\)`NAMESPACE" "$SECONDARY_POD"`

7. 总结

通过 手动升级修复 + 物理克隆重建 两步,成功恢复了 MySQL 主从复制。该方案已在本环境验证有效,可作为 Kubernetes 中 Bitnami MySQL 故障的标准处理流程。关键要点:


  • 升级锁超时需清理复制元数据表并强制升级。

  • binlog 丢失后,物理克隆是重建从库最可靠的方式。

  • 自动化脚本可大幅降低人工干预时间,提升运维效率。

如有其他变量(密码、命名空间、存储类等),请根据实际环境调整脚本配置。