MySQL 8.0+MHA+Keepalived 高可用

MySQL 8.0 + MHA + Keepalived 高可用架构

本此故障切换不到5秒钟就ok了。

文章大纲
一、架构总览与角色分配
二、环境初始化
三、MySQL 8.0 配置
四、主从复制与半同步复制配置
五、Keepalived 安装与配置
六、MHA 安装
七、MHA 核心配置
八、MHA + Keepalived 联动脚本
九、MHA 检查与启动
十、实战演练:自动故障切换
十一、故障恢复:旧主库重新加入集群
十二、实战演练 手动在线切换(计划性维护场景
十三、项目实战注意要点
十四、Q&A

一、架构总览与角色分配
1.1服务器角色
192.168.1.6 master1 主库
192.168.1.7 master2 延迟从库
192.168.1.8 slave1 半同步复制从库1(候选主库)
192.168.1.9 slave2 半同步复制从库2(候选主库)
192.168.1.10 mycat MHA manager

需要在4个数据库节点上安装好同一版本的mysql

1.2VIP规划
虚拟IP(VIP):192.168.1.100
漂移规则:始终由当前主库持有,应用统一通过 VIP 连接
漂移控制:MHA master_ip_failover 脚本通过 SSH 控制各节点 Keepalived 的启停

1.3 架构设计理由
MHA 负责数据库层切换:检测主库故障、选择最新从库、执行主从切换、恢复其他从库指向新主。
Keepalived 负责网络层 VIP:所有数据库节点预装相同 Keepalived 配置,但仅当前主库运行 Keepalived 并持有 VIP。切换时 MHA 停止旧主 Keepalived、启动新主 Keepalived,实现 VIP 秒级漂移。
半同步复制:确保主库提交的事务至少同步到一个从库,故障切换时 RPO(数据丢失量)接近 0。
延迟从库 master2:延迟 1 小时,用于误操作闪回;标记 no_master=1,永不参与竞选,避免延迟数据被提升为主库。

二、环境初始化
2.1配置hosts解析

#五台服务器都要配置
cat >> /etc/hosts << 'EOF'
# MySQL MHA 集群节点
192.168.1.6  master1
192.168.1.7  master2
192.168.1.8  slave1
192.168.1.9  slave2
192.168.1.10 mycat
EOF

2.2 关闭防火墙与 SELinux

# 停止防火墙服务,避免阻断 VRRP 组播(Keepalived)和 SSH/MYSQL 端口
systemctl stop firewalld
# 禁用防火墙开机自启
systemctl disable firewalld
# 临时关闭 SELinux
setenforce 0
# 永久关闭 SELinux,重启后生效
sed -i 's/^SELINUX=.*/SELINUX=disabled/' /etc/selinux/config

2.3 校时(所有节点时间必须一致)

# 安装 chrony 时间同步服务
yum install -y chrony
# 启动并设为开机自启
systemctl start chronyd
systemctl enable chronyd
# 立即强制同步一次时间
chronyc -a makestep

2.4 创建 MySQL 命令软链接(所有数据库节点)

MHA Node 工具会调用 mysql 和 mysqlbinlog 命令,需确保在默认 PATH 中。
# 查找 MySQL 实际安装路径(根据你的安装方式可能是 /usr/local/mysql/bin/ 或 /usr/bin/)
# 以下以源码安装路径 /usr/local/mysql/bin/ 为例,如为 yum 安装可跳过
ln -sf /usr/local/mysql/bin/mysql /usr/bin/mysql
ln -sf /usr/local/mysql/bin/mysqlbinlog /usr/bin/mysqlbinlog
# 验证命令是否可用
which mysql
which mysqlbinlog

2.5配置所有服务器节点root ssh密码登录

1. 每台机器都执行(生成密钥):
ssh-keygen -t ed25519 -N "" -f ~/.ssh/id_ed25519

2. 在 mycat (192.168.1.10) 上执行(收集公钥):
for ip in 6 7 8 9 10; do
  ssh 192.168.1.$ip "cat ~/.ssh/id_ed25519.pub" >> /tmp/all_keys.txt
done

3. 在 mycat 上执行(分发公钥到所有机器):
for ip in 6 7 8 9 10; do
  cat /tmp/all_keys.txt | ssh 192.168.1.$ip "cat >> ~/.ssh/authorized_keys"
  ssh 192.168.1.$ip "chmod 700 ~/.ssh && chmod 600 ~/.ssh/authorized_keys"
done

4. 验证:
# 在 mycat 上测试
ssh 192.168.1.6或者ssh master1
ssh 192.168.1.8

这样5台机器就能互相免密登录了

# 在 mycat (192.168.1.10) 上执行,验证能免密登录所有数据库节点(一定要确保他们之间相互root ssh免费登录的)
for host in master1 master2 slave1 slave2; do
    ssh root@$host "hostname && echo 'SSH OK'"
done
# 如果任何一台提示输入密码,请重新配置 ssh-copy-id

三、MySQL 8.0 配置(4 台数据库节点)

注意:以下配置中 eth0 为网卡名示例,请根据实际服务器网卡名(如 ens33、ens160)修改。

3.1 master1 (192.168.1.6) 的 my.cnf

============================================================

datadir=/var/lib/mysql
socket=/var/lib/mysql/mysql.sock

log-error=/var/log/mysqld.log
pid-file=/var/run/mysqld/mysqld.pid

#开启binlog并指定文件前缀
log_bin=mysql-bin
binlog_format=ROW
log_slave_updates=ON

server-id=6
port=3306

gtid_mode=ON
enforce_gtid_consistency=1

#半同步复制
plugin-load-add=rpl_semi_sync_master.so
rpl_semi_sync_master_enabled=1
rpl_semi_sync_master_timeout=5000
rpl_semi_sync_master_wait_point=AFTER_SYNC

read_only=1

#从库侧
plugin_load_add=semisync_slave.so
rpl_semi_sync_slave_enabled=1

#指定密码插件,避免兼容性问题
default_authentication_plugin=mysql_native_password

#MHA要求:binlog必须完整记录每一行的所有列(用于解析和补全差异数据)
binlog_row_image=FULL
binlog_expire_logs_seconds=604800

#Innodb持久化配置(确保失误提交后不丢失)
innodb_flush_log_at_trx_commit=1
sync_binlog=1


#中继日志
relay_log=mysql-relay-bin
relay_log_purge=0
relay_log_recovery=ON

============================================================
使用mysqld --validate-config检测/etc/my.cnf是否有语法错误
如果是关于半同步插件的错误,只要确保已安装且插件状态为ACTIVE,半同步复制状态为ON即可,不影响。

3.2 master2 (192.168.1.7) 延迟从库的 my.cnf

datadir=/var/lib/mysql
socket=/var/lib/mysql/mysql.sock

log-error=/var/log/mysqld.log
pid-file=/var/run/mysqld/mysqld.pid

server-id=7
port=3306

#开启binlog并指定文件前缀
log_bin=mysql-bin
binlog_format=ROW
log_slave_updates=ON

binlog_row_image=FULL
binlog_expire_logs_seconds=604800
#从库只读
read_only=ON

#延迟从库不参与半同步,避免阻塞主库提交
#plugin_load_add=semisync_slave.so
#rpl_semi_sync_slave_enabled=0

#中继日志,MHA强制要求关闭relay_log_purge,由MHA自己管理relay log的清理
relay-log-recovery=ON
#relay-log=relay-log-201
#relay-log-index=relay-log-201.index
relay_log_purge=0
relay_log=mysql-relay-bin

#指定密码插件类型,避免兼容性问题
default_authentication_plugin=mysql_native_password

#gtid模式开启
gtid_mode=ON
enforce_gtid_consistency=ON

#innodb持久化
innodb_flush_log_at_trx_commit=1
sync_binlog=1

=============================================================

3.3 slave1 (192.168.1.8) 半同步从库的 my.cnf

datadir=/var/lib/mysql
socket=/var/lib/mysql/mysql.sock

log-error=/var/log/mysqld.log
pid-file=/var/run/mysqld/mysqld.pid

server-id=8
port=3306

#gtid模式开启
gtid_mode=ON
enforce_gtid_consistency=1

#binlog配置
log_bin=mysql-bin
binlog_format=ROW
log_slave_updates=ON

binlog_row_image=FULL
binlog_expire_logs_seconds=604800

#配置只读
read_only=ON

#MHA强制要求由MHA管理relay log
#relay-log=relay-301
#relay-log-index=relay-301.index
relay_log=mysql-relay-bin
relay_log_purge=0
relay_log_recovery=ON

#指定密码插件
default_authentication_plugin=mysql_native_password

#半同步复制
plugin-load-add=rpl_semi_sync_slave.so
rpl_semi_sync_slave_enabled=1

#主库侧配置
plugin-load-add=semisync_master.so
rpl_semi_sync_master_enabled=1
rpl_semi_sync_master_timeout=10000

#innodb 持久化
innodb_flush_log_at_trx_commit=1
sync_binlog=1

=============================================================

3.4 slave2 (192.168.1.9) 半同步从库的 my.cnf

datadir=/var/lib/mysql
socket=/var/lib/mysql/mysql.sock

log-error=/var/log/mysqld.log
pid-file=/var/run/mysqld/mysqld.pid

server-id=9
port=3306

gtid_mode=ON
enforce_gtid_consistency=1

log_bin=mysql-bin
binlog_format=ROW
log_slave_updates=ON

binlog_row_image=FULL
binlog_expire_logs_seconds=604800

read_only=ON

MHA强制要求

relay-log=relay-302

relay-log-index=relay-302.index

relay_log=mysql-relay-bin
relay_log_purge=0
relay_log_recovery=ON

指定密码插件

default_authentication_plugin=mysql_native_password

半同步复制

plugin-load-add=rpl_semi_sync_slave.so
rpl_semi_sync_slave_enabled = 1

主库侧配置

plugin-load-add=semisync_master.so
rpl_semi_sync_master_enabled=1
rpl_semi_sync_master_timeout=10000

innodb持久化

innodb_flush_log_at_trx_commit=1
sync_binlog=1

=============================================================

配置完成后,在 4 台数据库节点上重启 MySQL:

重启 MySQL 服务使配置生效

systemctl restart mysqld

四、主从复制与半同步复制配置
4.1 在主库 master1 创建专用账户

-- 登录 MySQL(使用 root 或管理员账户)
mysql -uroot -p

-- ============================================
-- 1. 创建复制用户:供所有从库连接主库拉取 binlog
-- ============================================
CREATE USER 'repl'@'192.168.1.%'
    IDENTIFIED WITH mysql_native_password        -- 显式指定认证插件,兼容 MHA
    BY 'Repl_Pass_123';                           -- 复制用户密码,生产环境请使用强密码

-- 授予复制权限
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'192.168.1.%';

-- ============================================
-- 2. 创建 MHA 管理用户:供 MHA Manager 连接所有节点进行检测和切换
-- ============================================
CREATE USER 'mha'@'192.168.1.%'
    IDENTIFIED WITH mysql_native_password
    BY 'Mha_Pass_456';

-- 授予所有权限(MHA 需要执行 STOP SLAVE、CHANGE MASTER、RESET SLAVE、SET GLOBAL 等)
GRANT ALL PRIVILEGES ON *.* TO 'mha'@'192.168.1.%';

-- ============================================
-- 3. 创建应用用户(示例,后续业务通过 VIP 连接时使用)
-- ============================================
CREATE USER 'app_user'@'192.168.1.%'
    IDENTIFIED WITH mysql_native_password
    BY 'App_Pass_999';

-- 授予业务所需的 DML 权限
GRANT SELECT, INSERT, UPDATE, DELETE ON *.* TO 'app_user'@'192.168.1.%';

-- 刷新权限表使上述授权立即生效
FLUSH PRIVILEGES;

4.2 从库配置复制
在 master2 (延迟从库) 上执行:

-- 停止已有的复制线程(如之前配置过)
STOP SLAVE;
-- 清除之前的复制配置
RESET SLAVE ALL;

-- 配置指向主库 master1 的复制通道
CHANGE MASTER TO
    MASTER_HOST = '192.168.1.6',           -- 主库 IP
    MASTER_PORT = 3306,                    -- 主库端口
    MASTER_USER = 'repl',                  -- 复制用户名
    MASTER_PASSWORD = 'Repl_Pass_123',     -- 复制用户密码
    MASTER_AUTO_POSITION = 1,              -- GTID 模式,自动定位同步位置
    MASTER_DELAY = 3600;                   -- 延迟 3600 秒(1 小时),用于误操作恢复

-- 启动从库 IO 线程和 SQL 线程
START SLAVE;

在 slave1 (半同步从库) 上执行:

STOP SLAVE;
RESET SLAVE ALL;

CHANGE MASTER TO
    MASTER_HOST = '192.168.1.6',
    MASTER_PORT = 3306,
    MASTER_USER = 'repl',
    MASTER_PASSWORD = 'Repl_Pass_123',
    MASTER_AUTO_POSITION = 1;              -- 半同步从库不设置延迟

START SLAVE;

在 slave2 (半同步从库) 上执行:

STOP SLAVE;
RESET SLAVE ALL;

CHANGE MASTER TO
    MASTER_HOST = '192.168.1.6',
    MASTER_PORT = 3306,
    MASTER_USER = 'repl',
    MASTER_PASSWORD = 'Repl_Pass_123',
    MASTER_AUTO_POSITION = 1;

START SLAVE;

4.3 验证主从状态
-- 在任意从库上执行以下命令查看复制状态
SHOW SLAVE STATUS\G

-- 关键检查项:
-- Slave_IO_Running: Yes -- IO 线程正常运行,正在接收主库 binlog
-- Slave_SQL_Running: Yes -- SQL 线程正常运行,正在重放事件
-- Seconds_Behind_Master: 0 -- 半同步从库延迟应为 0 或接近 0
-- Slave_SQL_Running_State: Slave has read all relay log; waiting for more updates

4.4 验证半同步复制状态

-- 在主库 master1 上查看半同步状态
SHOW STATUS LIKE 'Rpl_semi_sync_master_status';
-- 应显示:Value = ON

SHOW STATUS LIKE 'Rpl_semi_sync_master_clients';
-- 应显示:Value = 2 (slave1 和 slave2 两个半同步从库)

-- 在 slave1 和 slave2 上查看

SHOW STATUS LIKE 'Rpl_semi_sync_slave_status';
-- 应显示:Value = ON

五、Keepalived 安装与配置(4 台数据库节点)
5.1 安装 Keepalived

在所有 4 台数据库节点上执行安装

yum install -y keepalived

5.2 配置 Keepalived(所有数据库节点使用相同配置)
/etc/keepalived/keepalived.conf(4 台数据库节点完全一致):

! Configuration File for keepalived

global_defs {
   router_id MYSQL_HA
# 路由器标识,同一集群内相同
   script_user root
# 执行脚本的用户
   enable_script_security
# 启用脚本安全执行
}

vrrp_instance VI_1 {
    state BACKUP
# 【所有节点统一为 BACKUP】由 MHA 脚本控制谁成为 MASTER
    interface enp0s3
# 【务必按实际网卡修改】如 ens33、ens160 等
    virtual_router_id 51
# 虚拟路由 ID,同一局域网内唯一
    priority 100
# 优先级,统一设相同值
    advert_int 1
# VRRP 广播间隔 1 秒
    
    authentication {
        auth_type PASS
# 认证类型
        auth_pass 1111
# 认证密码,主备必须一致
    }
    
    virtual_ipaddress {
        192.168.1.100/24
# VIP 地址及子网掩码
    }
    
    nopreempt
# 不抢占模式,避免网络抖动导致频繁切换
}

=====================================================================
keepalived -f /etc/keepalived/keepalived.conf检测一下语法是否有误

5.3 启动初始主库的 Keepalived

# 仅在 master1 (192.168.1.6) 上启动 Keepalived,初始持有 VIP
systemctl start keepalived
# 设为开机自启(即使当前由 MHA 控制,也建议设置)
systemctl enable keepalived

# 验证 VIP 是否绑定成功
ip addr show eth0
# 应能看到 eth0 接口上绑定了 192.168.1.100

5.4 其余节点保持 Keepalived 停止状态

# 在 master2、slave1、slave2 上执行,确保 Keepalived 未运行
systemctl stop keepalived
# 禁用开机自启(由 MHA 在切换时按需启动)
systemctl disable keepalived

为什么搭建好了keepalived但是主机ip a并看不到虚拟vip192.168.1.100????
答:需要开启并设置开机自启主库的keepalived,将其他从库节点都执行停止、停止开机自启之后,再去主机查看ip a才能看得到192.168.1.100虚拟vip地址。

六、MHA 安装
6.1 所有数据库节点安装 MHA Node

yum install -y perl-DBD-MySQL perl-DBI perl-Config-Tiny perl-Log-Dispatch perl-Parallel-ForkManager perl-Time-HiRes

wget https://github.com/yoshinorim/mha4mysql-node/releases/download/v0.58/mha4mysql-node-0.58-0.el7.centos.noarch.rpm


# 常规安装
rpm -ivh mha4mysql-node-0.58-0.el7.centos.noarch.rpm


# 若提示依赖缺失,用yum本地安装自动补全依赖(推荐)
yum install -y ./mha4mysql-node-0.58-0.el7.centos.noarch.rpm

完成!

6.2 mycat (Manager) 安装 MHA Manager + Node

步骤 1:安装 MySQL5.7 兼容库(解决 libmysqlclient.so.18 缺失,MySQL8 必装)
# 下载5.7兼容库包
wget https://repo.mysql.com/yum/mysql-5.7-community/el/7/x86_64/mysql-community-libs-compat-5.7.44-1.el7.x86_64.rpm
# 安装
rpm -ivh mysql-community-libs-compat-5.7.44-1.el7.x86_64.rpm
# 更新系统动态链接库缓存
ldconfig

步骤 2:安装全套 Perl 依赖(Node 基础 + Manager 邮件专属依赖)
yum install -y perl-DBD-MySQL perl-DBI perl-Config-Tiny perl-Log-Dispatch \
perl-Parallel-ForkManager perl-Time-HiRes perl-Mail-Sendmail perl-MIME-Lite \
perl-MIME-Types perl-Email-Date-Format

步骤 3:下载两个 MHA rpm 包
# node包(manager强制依赖node)
wget https://github.com/yoshinorim/mha4mysql-node/releases/download/v0.58/mha4mysql-node-0.58-0.el7.centos.noarch.rpm
# manager包
wget https://github.com/yoshinorim/mha4mysql-manager/releases/download/v0.58/mha4mysql-manager-0.58-0.el7.centos.noarch.rpm

步骤 4:最优安装命令(yum 本地安装,自动处理依赖顺序)
yum install -y ./mha4mysql-node-0.58-0.el7.centos.noarch.rpm ./mha4mysql-manager-0.58-0.el7.centos.noarch.rpm

# 查看是否安装两个包
rpm -qa | grep mha4mysql

# 校验manager核心命令存在
which masterha_manager masterha_check_repl masterha_check_ssh

完成!

如何检查是否已经安装完成mha node 和keepalived?

which save_binary_logs#node
keepalived -v

七、MHA 核心配置(在 mycat 上)
6.1 创建目录

mkdir -p /etc/mha
mkdir -p /var/log/mha/app1

6.2 编辑 /etc/mha/app1.cnf
关键教训 1:[server default] 必须是空格分隔,不能写成 [server_default],否则 MHA 会报 Server server_default does not have hostname!
关键教训 2:延迟从库必须标记 no_master=1,防止被提升为主库导致数据回滚。
关键教训 3:延迟从库会导致 masterha_check_repl 报错(Slave is currently behind xxx seconds),这是预期行为,可直接启动 Manager。

/etc/mha/app1.cnf文件配置

[server default]
manager_log=/var/log/mha/app1/manager.log
manager_workdir=/var/log/mha/app1
master_ip_failover_script=/usr/local/bin/master_ip_failover
master_ip_online_change_script=/usr/local/bin/master_ip_online_change
password=Mha_Pass_456
ping_interval=3
ping_type=SELECT
remote_workdir=/var/log/mha/app1
repl_password=Rep_password123
repl_user=rep
#如果两个节点同时ping都ping不通,判定主库死亡,触发故障切换,如果至少一台ping得通,则表明mha误判,不做切换
secondary_check_script=masterha_secondary_check -s 192.168.1.8 -s 192.168.1.9

ssh_user=root
user=mha

[server1]
hostname=192.168.1.6
port=3306
master_binlog_dir=/var/lib/mysql
candidate_master=1


[server2]
hostname=192.168.1.7
ignore_fail=1
no_master=1
port=3306

[server3]
candidate_master=1
check_repl_delay=0
hostname=192.168.1.8
port=3306

[server4]
candidate_master=1
check_repl_delay=0
hostname=192.168.1.9
port=3306

=============================================================

八、MHA + Keepalived 联动脚本(在 mycat 上)
8.1 创建 master_ip_failover
关键教训:MHA 0.58 调用脚本时会传入 --new_master_user 和 --new_master_password 参数,如果脚本里没定义这两个选项,会报 Unknown option。必须在 GetOptions 中声明。

/usr/local/bin/master_ip_failover

#!/usr/bin/env perl
use strict;
use warnings FATAL => 'all';
use Getopt::Long;

# ============================================
# 定义变量:接收 MHA 传入的参数
# ============================================
my (
    $command,
    $ssh_user,
    $orig_master_host,
    $orig_master_ip,
    $orig_master_port,
    $new_master_host,
    $new_master_ip,
    $new_master_port,
    $new_master_user,
# 【新增】MHA 0.58 传入的新主库管理用户
    $new_master_password
# 【新增】MHA 0.58 传入的新主库管理密码
);

# ============================================
# Keepalived 控制命令(通过 SSH 在远端执行 systemctl)
# ============================================
my $ssh_start_vip = "systemctl start keepalived";
my $ssh_stop_vip = "systemctl stop keepalived";

# ============================================
# 解析 MHA 传入的所有命令行参数
# ============================================
GetOptions(
    'command=s'               => \$command,
    'ssh_user=s'              => \$ssh_user,
    'orig_master_host=s'      => \$orig_master_host,
    'orig_master_ip=s'        => \$orig_master_ip,
    'orig_master_port=i'      => \$orig_master_port,
    'new_master_host=s'       => \$new_master_host,
    'new_master_ip=s'         => \$new_master_ip,
    'new_master_port=i'       => \$new_master_port,
    'new_master_user=s'       => \$new_master_user,
# 【必须声明】
    'new_master_password=s'   => \$new_master_password
# 【必须声明】
);

exit &main();

sub main {
    print "\n\nIN SCRIPT TEST====$ssh_stop_vip==$ssh_start_vip===\n\n";

    # ----------------------------------------
    # stop / stopssh:旧主库故障时,停止其 Keepalived 释放 VIP
    # ----------------------------------------
    if ( $command eq "stop" || $command eq "stopssh" ) {
        my $exit_code = 1;
        eval {
            print "Disabling the VIP on old master: $orig_master_host \n";
            &stop_vip();
            $exit_code = 0;
        };
        if ($@) {
            warn "Got Error: $@\n";
            exit $exit_code;
        }
        exit $exit_code;
    }
    # ----------------------------------------
    # start:新主库提升后,启动其 Keepalived 绑定 VIP
    # ----------------------------------------
    elsif ( $command eq "start" ) {
        my $exit_code = 10;
        eval {
            print "Enabling the VIP on the new master - $new_master_host \n";
            &start_vip();
            $exit_code = 0;
        };
        if ($@) {
            warn $@;
            exit $exit_code;
        }
        exit $exit_code;
    }
    # ----------------------------------------
    # status:MHA 启动时检查脚本状态
    # ----------------------------------------
    elsif ( $command eq "status" ) {
        print "Checking the Status of the script.. OK \n";
        exit 0;
    }
    else {
        &usage();
        exit 1;
    }
}

sub start_vip {
    `ssh $ssh_user\@$new_master_host \" $ssh_start_vip \"`;
}

sub stop_vip {
    return 0 unless ($ssh_user);
    `ssh $ssh_user\@$orig_master_host \" $ssh_stop_vip \"`;
}

sub usage {
    print
    "Usage: master_ip_failover --command=start|stop|stopssh|status --orig_master_host=host --orig_master_ip=ip --orig_master_port=port --new_master_host=host --new_master_ip=ip --new_master_port=port\n";
}

=====================================================================

检查有没有语法错误

perl -c /usr/local/bin/master_ip_failover

为脚本添加执行权限

chmod +x /usr/local/bin/master_ip_failover

8.2 创建 master_ip_online_change(手动切换时使用)
/usr/local/bin/master_ip_online_change配置文件

#!/usr/bin/env perl
use strict;
use warnings;   # 不设为 FATAL
use Getopt::Long;

my (
    $command,
    $ssh_user,
    $orig_master_host,
    $orig_master_ip,
    $orig_master_port,
    $new_master_host,
    $new_master_ip,
    $new_master_port,
    $new_master_user,
    $new_master_password
);

my $ssh_start_vip = "systemctl start keepalived";
my $ssh_stop_vip  = "systemctl stop keepalived";

GetOptions(
    'command=s'               => \$command,
    'ssh_user=s'              => \$ssh_user,
    'orig_master_ssh_user=s'  => \$ssh_user,
    'new_master_ssh_user=s'   => \$ssh_user,
    'orig_master_host=s'      => \$orig_master_host,
    'orig_master_ip=s'        => \$orig_master_ip,
    'orig_master_port=i'      => \$orig_master_port,
    'new_master_host=s'       => \$new_master_host,
    'new_master_ip=s'         => \$new_master_ip,
    'new_master_port=i'       => \$new_master_port,
    'new_master_user=s'       => \$new_master_user,
    'new_master_password=s'   => \$new_master_password,
    'orig_master_user=s'      => sub {},
    'orig_master_password=s'  => sub {},
    'orig_master_is_new_slave' => sub {},
) or die "Error in command line arguments\n";

if (!$ssh_user) {
    die "ERROR: ssh_user not defined.\n";
}

exit &main();

sub main {
    if ($command eq "stop" || $command eq "stopssh") {
        print "Disabling the VIP on old master: $orig_master_host\n";
        stop_vip();
        return 0;
    }
    elsif ($command eq "start") {
        print "Enabling the VIP on the new master - $new_master_host\n";
        start_vip();
        return 0;
    }
    elsif ($command eq "status") {
        print "Checking the Status of the script.. OK\n";
        return 0;
    }
    else {
        print "Usage: master_ip_online_change --command=start|stop|stopssh|status ...\n";
        return 1;
    }
}

sub start_vip {
    my $cmd = "ssh $ssh_user\@$new_master_host \" $ssh_start_vip \"";
    system($cmd);
    if ($? != 0) {
        die "ERROR: Failed to start VIP on $new_master_host (exit code: " . ($? >> 8) . ")\n";
    }
}

sub stop_vip {
    my $cmd = "ssh $ssh_user\@$orig_master_host \" $ssh_stop_vip \"";
    system($cmd);
    if ($? != 0) {
        die "ERROR: Failed to stop VIP on $orig_master_host (exit code: " . ($? >> 8) . ")\n";
    }
}

=====================================================================

检查有没有语法错误

perl -c /usr/local/bin/master_ip_online_change

为脚本添加执行权限

chmod +x /usr/local/bin/master_ip_online_change

九、MHA 检查与启动(在 mycat 上)
9.1 检查 SSH 连通性

# 验证 MHA Manager 能通过 SSH 免密连接到所有数据库节点
sudo masterha_check_ssh --conf=/etc/mha/app1.cnf
# 预期:所有节点显示 HealthCheck: SSH to 192.168.1.xx is reachable.

9.2 检查复制状态

# 检查 MySQL 复制拓扑和健康状态
sudo masterha_check_repl --conf=/etc/mha/app1.cnf

因为 master2 故意延迟 1 小时,masterha_check_repl 这个预检工具会严格检查所有从库延迟。会提示出错误。
解决方案:跳过即可 masterha_check_repl,直接启动 MHA Manager。Manager 运行时不会因为 no_master=1 的延迟从库而退出。

如果提示这两个参数错误,注释掉即可
#ignore_last_failover=1
#remove_dead_master_conf=1

# 重新检查 SSH
sudo masterha_check_ssh --conf=/etc/mha/app1.cnf
如果检查失败,大概率是权限问题,将/etc/mha/app1.cnf文件权限设置为777即可。

9.3 启动 MHA Manager

# 后台启动 MHA Manager
# --remove_dead_master_conf:故障切换后自动移除死主配置
# --ignore_last_failover:忽略上次故障时间戳限制
sudo nohup masterha_manager \
    --conf=/etc/mha/app1.cnf \
    --remove_dead_master_conf \
    --ignore_last_failover \
    < /dev/null \
    > /var/log/mha/app1/manager.log \
    2>&1 &

# 验证启动成功
sudo masterha_check_status --conf=/etc/mha/app1.cnf
# 预期输出:app1 (pid:xxxx) is running(0:PING_OK)

十、实战演练:自动故障切换
10.1 演练前状态确认

# 1. 确认 MHA Manager 正在运行
sudo masterha_check_status --conf=/etc/mha/app1.cnf

# 2. 确认 VIP 当前在 master1 上
ssh root@192.168.1.6 "ip addr | grep 192.168.1.100"

# 3. 确认 Keepalived 仅在 master1 运行,其他节点停止
ssh root@192.168.1.6 "systemctl is-active keepalived"   # 预期:active
ssh root@192.168.1.8 "systemctl is-active keepalived"   # 预期:inactive 或 unknown

# 4. 实时观察日志(开两个窗口)
sudo tail -f /var/log/mha/app1/manager.log

10.2 模拟主库宕机

# 在 master1 (192.168.1.6) 上执行,模拟硬件故障或 MySQL 进程崩溃
sudo systemctl stop mysqld
# 或者更极端:sudo killall -9 mysqld

10.3 观察切换过程
在 Manager 日志窗口,你会看到:
[info] Master 192.168.1.6 is down!
[info] Executing master IP deactivation script:
Disabling the VIP on old master: 192.168.1.6
[info] New master is 192.168.1.8(192.168.1.8:3306)
[info] Executing master IP activate script:
Enabling the VIP on the new master - 192.168.1.8
[info] Master failover to 192.168.1.8 completed successfully.

10.4 验证切换结果

# 1. 确认 VIP 已漂移到新主库(如 slave1 192.168.1.8)
ssh root@192.168.1.8 "ip addr | grep 192.168.1.100"

# 2. 确认新主库已关闭只读,可接受写流量
# 由于 mycat 没装 MySQL 客户端,通过 SSH 到数据库节点执行
ssh root@192.168.1.8 "mysql -umha -pMha_Pass_456 -e 'SELECT @@server_id, @@read_only;'"
# 预期:server_id=8, read_only=0

# 3. 确认 slave2 已自动指向新主库
ssh root@192.168.1.9 "mysql -umha -pMha_Pass_456 -e 'SHOW SLAVE STATUS\G'" | grep -E "Master_Host|Slave_IO_Running|Slave_SQL_Running"
# 预期:Master_Host: 192.168.1.8, Slave_IO_Running: Yes, Slave_SQL_Running: Yes

# 4. 确认新主库的半同步复制已生效(作为主库侧)
ssh root@192.168.1.8 "mysql -umha -pMha_Pass_456 -e 'SHOW STATUS LIKE \"Rpl_semi_sync_master_status\";'"

# 5. 确认 slave2 的半同步复制已生效(作为从库侧)
ssh root@192.168.1.9 "mysql -umha -pMha_Pass_456 -e 'SHOW STATUS LIKE \"Rpl_semi_sync_slave_status\";'"

# 6. 验证 GTID 一致性(确保没有数据丢失)
ssh root@192.168.1.8 "mysql -umha -pMha_Pass_456 -e 'SHOW MASTER STATUS;'"
# 记录 Executed_Gtid_Set

# 预期:slave2 的 Executed_Gtid_Set 与新主库一致
ssh root@192.168.1.8 "mysql -umha -pMha_Pass_456 -h192.168.1.8 -N -e 'SELECT @@global.gtid_executed;'"
最好是直接在各个数据库节点进行show slave status\G去看Executed_Gtid_Set选项。

10.5 故障切换后 MHA 状态
关键教训:MHA Manager 完成一次自动故障切换后会自动退出,这是安全设计(避免脑裂反复切换)。

sudo masterha_check_status --conf=/etc/mha/app1.cnf
# 预期输出:app1 is stopped(2:NOT_RUNNING).

十一、故障恢复:旧主库重新加入集群
当 master1 (192.168.1.6) 修复完成后,按以下流程恢复:
11.1 修复并启动 MySQL

# 在 master1 上
sudo systemctl start mysqld

11.2 将旧主库配置为新主库的从库(故障切换,需要重新搭建旧主与新主之间的主从复制关系,在线手动切换的不用,会自动跟随并自动配置和主从复制关系)

记住,一定不能执行RESET MASTER ALL,而是执行RESET SLAVE ALL!!!!!!

# 在 master1 上执行
ssh root@192.168.1.6 << 'REMOTECMD'
mysql -uroot -p'你的root密码' -e "
  STOP SLAVE;
  RESET SLAVE ALL;
  CHANGE MASTER TO
    MASTER_HOST = '192.168.1.8',
    MASTER_PORT = 3306,
    MASTER_USER = 'rep',
    MASTER_PASSWORD = 'Rep_password123',
    MASTER_AUTO_POSITION = 1;
  START SLAVE;
  SHOW SLAVE STATUS\G;
"
REMOTECMD

11.3 将旧主库加回 MHA 配置
如果配置了 remove_dead_master_conf=1,旧主库配置已被自动删除,需要手动加回:

# 在 mycat 上编辑 MHA 配置文件
sudo tee -a /etc/mha/app1.cnf << 'EOF'

[server1]
hostname=192.168.1.6
port=3306
master_binlog_dir=/var/lib/mysql
candidate_master=1
EOF


把旧主库设置为只读
SET GLOBAL read_only = ON;

/etc/my.cnf
read_only=ON

11.4 重新启动 MHA Manager

# 在 mycat 上执行

# 1. 删除故障标记文件(如果存在),不删的话无法再次启动MHA
sudo rm -f /var/log/mha/app1/app1.failover.complete

# 2. 重新启动 Manager
sudo nohup masterha_manager \
    --conf=/etc/mha/app1.cnf \
    --remove_dead_master_conf \
    --ignore_last_failover \
    < /dev/null \
    > /var/log/mha/app1/manager.log \
    2>&1 &

# 3. 验证
sudo masterha_check_status --conf=/etc/mha/app1.cnf
# 预期:app1 (pid:xxxx) is running(0:PING_OK)

十二、实战演练 手动在线切换(计划性维护场景)
用于主库需要停机维护(如操作系统升级、硬件更换)时的平滑切换,主库还活着。
12.1 停止 MHA Manager

# 在 mycat 上
sudo masterha_stop --conf=/etc/mha/app1.cnf
masterha_check_status --conf=/etc/mha/app1.cnf

防止冲突:MHA Manager 的设计是一次只允许运行一个实例。它本身是一个持续监控的守护进程,如果在其运行时强行执行手动切换,两个管理进程会争夺集群的控制权,导致状态混乱。

避免干扰:手动切换时,masterha_master_switch 需要执行 FLUSH TABLES WITH READ LOCK 等操作来短暂锁定主库以保证数据一致性。如果 Manager 的监控进程还在运行,它可能会误判集群状态(如认为主库无响应)并触发意外的自动故障转移(Failover),造成严重后果。

12.2 执行在线切换

# 将主库从 master1 平滑切换到 slave1
sudo masterha_master_switch \
    --conf=/etc/mha/app1.cnf \
    --master_state=alive \
    --new_master_host=192.168.1.8 \
    --new_master_port=3306 \
    --orig_master_is_new_slave \
    --running_updates_limit=10000 \
    --interactive=0

参数说明:
--master_state=alive 主库还活着,执行在线切换
--new_master_host 指定新主库
--orig_master_is_new_slave 切换后,原主库变为新主库的从库
--running_updates_limit=10000 允许从库延迟在 10000 秒内仍可切换
--interactive=0 非交互模式,自动执行

12.3 验证并重启 MHA

# 验证 VIP 已漂移到 192.168.1.8

ssh root@192.168.1.8 "ip addr | grep 192.168.1.100"


# 验证原主库已变为从库
ssh root@192.168.1.6 "mysql -umha -pMha_Pass_456 -e 'SHOW SLAVE STATUS\G'" | grep Master_Host

# 重新启动 Manager 监控新拓扑
sudo nohup masterha_manager \
    --conf=/etc/mha/app1.cnf \
    --remove_dead_master_conf \
    --ignore_last_failover \
    < /dev/null \
    > /var/log/mha/app1/manager.log \
    2>&1 &

十三、项目实战注意要点
在mha+keepalived高可用架构中,如果旧主库宕机,mha故障切换新主库之后,需要检查以下状态:
1.其他的从库以及新主库之间是否存在正常的主从关系,查看io、sql线程是否都为yes,仅表明主从关系正常运行,不代表主从之间数据一致。
2.查看新主库与其从库之间的已执行GTID式事务集即Executed_Gtid_Set是否一致,如果不一致,表明主库从库之间数据不一致,则需要重新进行主库全量备份,从库删除现有数据,重新搭建新的主从结构。
3.延迟从库变成了异步复制从库是正常的,如果还需继续延迟,手动配置延迟时间即可。
4.查看新主库的读写权限,是否是可读可写,read_only=0表示新主库可正常读写。
5.修复旧主库之后,在旧主库上重新搭建主从复制关系指向新主库,并在旧主库/etc/my.cnf配置只读,及read_only=1 。在修复旧主库主从复制时,绝对不能执行reset master,因为该语句会清空binlog,旧主库的 GTID 历史被彻底抹除,如果它将来再次被提升为主库,会导致整个集群的 GTID 出现“孤儿事务”。所以只需要在旧主库上执行reset slave all;重置从库状态即可。不要在从库执行RESET MASTER!!从库执行 RESET MASTER 会清空本机 binlog,会破坏备份数据,严禁随便执行。禁止在从库执行 RESET MASTER
RESET MASTER是清空本机 binlog,只有全新初始化、测试环境才用,生产从库绝对不要。
6.删除锁定文件/var/log/mha/app1/app1.failover.complete,否则mha manager无法再次启动,会提示Last failover is not completed。

7.搭建好旧主库与新主库直接的主从复制结构之后,需要在/etc/mha/app1.cnf中添加故障切换后系统自动移除的原本[server1]192.168.1.6的配置。
[server1]
hostname=192.168.1.6
port=3306
master_binlog_dir=/var/lib/mysql
candidate_master=1
即可。

8.如果是mha故障引起的切换,则需要在切换之后,重新搭建好旧主库与新主库之间的主从复制关系,因为是故障引起的切换,可能在切换时旧主库mysql服务是关闭的,无法跟进复制结构,所以需要手动配置,且删除mha故障切换产生的锁定文件/var/log/mha/app1/app1.failover.complete,否则mha manager无法再次启动,会提示Last failover is not completed。

如果是手动切换新主库的话,在切换之前,需要先把mha manager停掉,避免冲突出错,然后使用masterha_master_switch手动切换即可,切换之后其他从库是会自动搭建与新主库之间的主从关系的,不用重新搭建,当然,可以检查以下延迟从库的主从是否正确。

9.ssh root@192.168.1.6 "mysql -umha -pMha_Pass_456 -h192.168.1.6 -e 'SHOW SLAVE STATUS\G'" | grep -E "Retrieved_Gtid_Set|Executed_Gtid_Set"有缺陷,grep命令只能看到关键字所在行,看不到剩下的内容。
替换成
ssh root@192.168.1.8 "mysql -umha -pMha_Pass_456 -h192.168.1.8 -N -e 'SELECT @@global.gtid_executed;'"
最好是直接在各个数据库节点进行show slave status\G去看Executed_Gtid_Set选项。

10.如何在停止有的虚拟vip偏移?
答:在现有的vip主机上,在/etc/keepalived/keepalived.conf注释掉虚拟vip,systemctl reload keepalived重载只有就没有192.168.1.100,不需要手动删除。
11.实际上需要在各个数据库节点上都安装上xtrabackup备份工具,以便复制备份到其他节点。
12.所有在数据库主从关系正常运行时要去检查好每个数据库是否都有该业务连接数据库账号,且登录网段属于192.168.1.%。业务只认 VIP,不认物理机。在已搭建好主从关系集群主库上,业务连接账号只需要在主库创建一次,通过主从复制自动同步到所有从库。
13.所有节点统一配置 semisync_master + semisync_slave,无论角色如何都能立即启用半同步。

十四、Q&A
MHA的作用是什么?Keepalived的作用是什么? MHA 与 Keepalived 是如何配合的?
MHA 是“数据库大脑”:负责切换决定“谁是主库”。
Keepalived 是“网络引流员”:负责把客户端的流量导向“当前的主库”。

  1. MHA(MySQL Master High Availability)的主要功能
    MHA 的核心使命是维护 MySQL 集群的主从拓扑关系,并确保数据的一致性。
    1)自动/手动故障切换:当主库宕机时,MHA 会从众多从库中选出一个数据最全的(与旧主库 GTID 或 Binlog Position 最接近),将其提升为新主库。
    2)自动重建复制链:切换完成后,MHA 会自动登录到其他存活的从库上,执行 CHANGE MASTER TO,让它们全部指向新主库,从而重新形成一主多从的复制拓扑。
    3)数据补全(数据拯救):在故障切换时,如果旧主库还能 SSH 连接(但 MySQL 挂了),MHA 会尝试从旧主库的 Binlog 中拉取最后几秒没来得及同步的事务,通过 applybinlog 脚本补全到新主库上,最大程度避免数据丢失。
    4)健康监控:通过 ping_interval 参数定时检测主库心跳,一旦发现主库不可达,立即触发切换流程。

核心职责 维护 MySQL 主从复制拓扑,保证数据一致性 主要对象 MySQL 数据库实例(Binlog、GTID、复制线程) 切换方式 通过执行 SQL 指令、SSH 拉取 Binlog 来实现 故障判断依据 MySQL 端口存活、复制延迟、GTID 差异 生产依赖 依赖集群间 SSH 免密


  1. Keepalived 的主要功能
    Keepalived 的核心使命是提供虚拟 IP(VIP),实现客户端流量的无缝漂移.
    1)虚拟 IP(VIP)管理:在集群中提供一个固定的浮动 IP(如 192.168.1.100)。客户端只连接这个 IP,不关心后端物理机是谁。
    2)VRRP 协议(心跳选举):集群中运行 Keepalived 的节点通过 VRRP 协议发送组播心跳。拥有 VIP 的节点是 MASTER,其他是 BACKUP。一旦 MASTER 宕机(或 Keepalived 进程停止),BACKUP 会立刻接管该 VIP。
    3)健康检查联动:Keepalived 可以配置脚本检查 MySQL 的存活状态(如检查 mysqladmin ping)。如果 MySQL 挂了,即便机器没宕机,Keepalived 也会主动释放 VIP,将流量切走。

核心职责 维护虚拟 IP(VIP),保证网络接入高可用 主要对象 Linux 网卡、路由表、ARP 缓存 切换方式 通过 VRRP 组播心跳选举来决定谁来持有 VIP 故障判断依据 进程存活(keepalived 自身)或自定义脚本返回值 生产依赖 依赖网卡配置和组播通信

  1. MHA 与 Keepalived 是如何配合的?(必看重点)
    很多初学者会混淆这两者的边界。记住一句话:MHA 负责切主(数据库层),Keepalived 负责切 IP(网络层)。它们必须协同工作,缺一不可。
    Ⅰ.如果只用 MHA 不用 Keepalived:
    切换后,应用需要手动修改代码里的数据库 IP 地址,或者依赖 DNS 解析(但 DNS 有缓存,切换太慢)。
    Ⅱ.如果只用 Keepalived 不用 MHA:
    Keepalived 只能把 VIP 漂移到另一台机器上,但如果那台机器的 MySQL 还没配置成主库,数据是旧的,业务依然会报错(即使 IP 过去了,查到的数据也不完整)。

典型的协同工作流如下:
1)正常状态:Keepalived 在 192.168.1.6 上持有 VIP。MHA Manager 安静地监视着 1.6 的主库状态。
2)故障发生:主库 1.6 宕机。
3)MHA 动作:MHA 介入,将 1.8 提升为新主库,并让 1.9 指向 1.8(主从拓扑切换完成)。
4)触发 VIP 漂移:MHA 切换完成后,调用 master_ip_failover 脚本,通知 Keepalived 停止旧主(1.6)的 VIP,并在新主(1.8)上启动 Keepalived。
5)业务无感:由于 VIP 从 1.6 漂移到了 1.8,客户端连接 VIP 依然畅通,完全没感知到后端 MySQL 已经换了一台物理机。

调用逻辑:
MHA 读取 app1.cnf → 检测到主库故障 → 选举新主库 → 自动在存活从库上执行 CHANGE MASTER TO 重建复制拓扑 → 调用 master_ip_failover 脚本 → 脚本通过 SSH 远程操控 Keepalived 启动/停止 → VIP 漂移到新主库 → 业务恢复。

posted @ 2026-10-16 22:13  解码青年  阅读(3)  评论(0)    收藏  举报