Master High Availability, MHA 在 MySQL 高可用方面是一个相对成熟的解决方案,能做到在 0~30 秒之内自动完成数据库的故障切换操作,而且能最大程度上保证数据的一致性。
这里介绍使用配置方法,以及其原理。
简介
该软件由管理节点 (Manager) 和数据节点 (Node) 组成,其中 Manager 可以单独部署在一台机器上,并管理多个 Master-Slave 集群,当然,也可以部署在一台备节点上。而 Node 节点需要在每个服务器上都部署。
Node 运行在每台 MySQL 服务器上,Manager 会定时探测集群中的主节点,当主出现故障时,可以自动将最新的备提升为新的主,然后将所有其它的备重新指向新的主。
使用条件
- MHA 是为了解决数据一致性,主要支持一主多从,一般一个集群中最少有三台数据库服务器,也就是一主二从;从 0.52 开始,就开始支持多主复制架构了,但是多主只允许单点写入,默认只支持 2 层复制架构。
- 候选 Master 必须开启 log-bin ,如果备库没有开启 log-bin 则不会被提升为主,如果所有备库未开启则报错。
- 主备环境的 binlog、relay-log 必须一致,包括复制过滤规则 binlog-do-db、replicate-ignore-db 等等必须全部一致。
- 复制用户必须在候选主上要存在,而且切换完后,所有备库都需要执行
CHANGE MASTER
命令,因此在 new master 上复制用户必须有REPLICATEION SLAVE
权限。简单来说,所有的节点都会创建复制用户及其权限。 - SQL 线程默认在执行完 relay-log 后自动删除,不过这些 relaylog 可能还会使用,所以需要关闭自动删除,然后自己来删除,当然必须考虑 repl 延迟问题。
- 不要使用
LOAD DATA INFILE
,尤其是 SBR 环境中。
安装
该软件实际上是使用 Perl 编写的,可以直接从 Github Manager 和 Github Node 下载即可。如下是关于这两个包中各个文件的介绍。
Manager:
masterha_check_repl 检查MySQL复制状况
masterha_check_ssh 检查MHA的SSH配置状况
masterha_check_status 检测当前MHA运行状态
masterha_manager 启动MHA监控进程,执行一次切换之后会退出
masterha_master_monitor 检测master是否宕机
masterha_master_switch 控制故障转移,可以自动或者手动
masterha_conf_host 添加或删除配置的server信息
masterha_stop 停止manager服务
masterha_secondary_check 第二次检查服务器,防止出现脑裂现象
Node: 一般由Manager脚本触发,无需手动操作
save_binary_logs 保存和复制宕掉主库的bilog
apply_diff_relay_logs 识别差异的中继日志事件并将其差异的事件应用于其他的slave
filter_mysqlbinlog 去除不必要的ROLLBACK事件(MHA已不再使用这个工具)
purge_relay_logs 清除中继日志(不会阻塞SQL线程)
源码安装时,需要安装如下依赖包;接下来看看如何源码安装。
----- 1. 安装依赖库
# yum install perl-Module-Install perl-Module-Build # 源码安装编译
# yum install perl-DBD-MySQL # MySQL驱动
# yum install perl-Config-Tiny
# yum install --enablerepo=epel perl-Log-Dispatch perl-Parallel-ForkManager
----- 2. 源码安装Node
$ perl Makefile.pl
$ make
# make install
----- 3. 源码安装Manager
$ perl Makefile.pl
$ make
# make install
# cp samples/conf/masterha_default.cnf /etc/
配置文件
在 samples/conf
目录下存在示例配置文件,各个配置项简单列举如下。
$ cat << EOF > /tmp/mysql-mha.cnf
[server default]
manager_workdir=/tmp/mha-master/foobar # Manager工作目录
manager_log=/tmp/mha-master/foobar/manager.log # Manager日志
user=root # 设置监控时使用的用户名
password=new-password # 及其密码
repl_user=mysync # 复制时使用的用户名
repl_password=kidding # 复制用户的密码
ssh_user=root # ssh的登录用户名
master_binlog_dir=/tmp/mysql-master # 主库binlog的保存目录
log_level=debug # 设置日志打印级别,包括debug/info/warning/error
ping_interval=1 # 向主库发送ping的时间间隔,默认3秒,三次失败则自动failover
remote_workdir=/tmp/mha # 远端mysql在发生切换时binlog的保存位置
# Failover时执行的脚本,如果使用了keepalived类似工具,可以配置切换VIP,详见force_shutdown_internal()函数的实现
#master_ip_failover_script= /usr/local/bin/master_ip_failover
# Failover时执行的脚本,可用来关闭MySQL服务或者服务器,防止发生脑裂,详见force_shutdown_internal()函数的实现
shutdown_script = ""
master_ip_online_change_script= /usr/local/bin/master_ip_online_change //设置手动切换时候的切换脚本
report_script=/usr/local/send_report //设置发生切换后发送的报警的脚本
secondary_check_script= /usr/local/bin/masterha_secondary_check -s server03 -s server02
[server1]
hostname=127.0.0.1
port=3307
[server2]
hostname=127.0.0.1
port=3308
candidate_master=1 # 候选主库,即使不是最新的,仍会将该库提升为主库
check_repl_delay=0 # 默认会检查relaylog的延迟,超过100M时不会将其提升为主,在此设置为忽略
[server3]
hostname=127.0.0.1
port=3309
EOF
简单测试
如果主库宕了之后,MHA 在完成切换之后会自动停止,所以最好配合 deamontools 工具,在进程停止之后可以自动拉起,切换过程可以查看日志 mha-manager.log 。
接下来,一步步测试。
1. 设置免登陆
可以通过如下命令查看配置是否正常。
$ masterha_check_ssh --conf=/tmp/mysql-mha.cnf
All SSH connection tests passed successfully.
2. 检查复制配置
$ masterha_check_repl --conf=/tmp/mysql-mha.cnf
3. 开启Manager
$ nohup masterha_manager --conf=/tmp/mysql-mha.cnf < /dev/null > /tmp/mha-manager.log 2>&1 &
常用参数:
--check_only
只检查状态是否正常。
4. 检查Manager的状态
$ masterha_check_status --conf=/tmp/mysql-mha.cnf
mysql-mha (pid:17446) is running(0:PING_OK), master:127.0.0.1
5. 测试关闭Manager
$ masterha_stop --conf=/tmp/mysql-mha.cnf
Stopped mysql-mha successfully.
注意:binlog-do-db 和 replicate-ignore-db 设置必须相同。 MHA 在启动时候会检测过滤规则,如果过滤规则不同,MHA 不启动监控和故障转移。
主备切换
当主库故障时,如果没有自动切换,那么就只能等待手动切换,当然,时间就不确定了。自动切换就需要找到一个合适的备库作为主库,并将主库指向新主库,并重新启动复制。
故障场景
当主库故障时,会有如下的几种情况,简单介绍下。
全部同步
所有备库都从主库收到了 binlog 事件,那么此时,任何一个备库都可以选为新的主库,只需要执行如下命令即可。
mysql> CHANGE MASTER TO MASTER_HOST='slave1', MASTER_LOG_FILE='file1', MASTER_LOG_POS=pos1;
mysql> START SLAVE;
当然,这是最简单的一种方式,大多数情况下不会这么幸运。
备库相同,部分事件丢失
也就是所有的备库都从主库收到了相同的 binlog 事件,但主库部分事件没有发送成功,如果直接切换过去,那么上述的 id=17
事件就会丢失了。
如果崩溃的主库可以通过 SSH 访问,而且能够读取 binlog 文件,那么在提升一个主库前,应该将 17 对应的事件保存到 binlog 文件中。
当然,可以通过 semi-sync 减小这种情况发生的概率。
某个备库事件完整
某个备库收到了其它备库未收到的事件,需要从中选出一个备库,并将事件同步到其它备库,从而使所有的备库保持一致。
现在的问题是:1) 如何确认那些 binlog 事件未发送? 2) 如何使所有的备库保持最终的一致?
备库事件不一致,最新备库有事件丢失
如果采用异步复制,那么估计这是最常见的故障场景,也就是备库收到的事件数不同,其中的某个库收到的事件比较多,但是仍有部分主库的事件丢失。
如上图所示,备库收到的事件有所不同,Slave2 是最新的备库,Slave1 和 Slave3 备库丢失更多事件,而且 id=17
对应的事件所有备库都没有收到。
接下来需要:1) 如果需要将 id=17
对应的事件从主库复制到备库;2) 将所有备库的事件补齐,防止备库发生数据不一致性。
主备复制状态
可以通过 SHOW SLAVE STATUS
命令查看当前主备的复制状态,如下简单列举常见状态。
mysql> SHOW SLAVE STATUS\G
... ...
Master_Log_File: mysql-bin.000001 # IO线程正在读取的主库binlog文件名称
Read_Master_Log_Pos: 107 # IO线程已读取主库binlog位置
Relay_Log_File: relay-bin.000002 # SQL线程正在读取执行的relaylog名称
Relay_Log_Pos: 253 # SQL线程已读取执行的relaylog位置
Relay_Master_Log_File: mysql-bin.000001 # SQL线程执行的主库binlog文件名称
Exec_Master_Log_Pos: 107 # SQL线程执行来自主库的binlog最后一个事件位置
... ...
其中一些常见状态查看方式如下:
- 备库同步是否正常。查看
Slave_IO_Running
、Slave_SQL_Running
是否正常(Yes),不正常则可以查看报错; - 从库是否有事件在 relaylog 中等待执行。查看 IO 线程从主库的读取位置
Read_Master_Log_Pos
和从库的执行位置Exec_Master_Log_Pos
是否一致; - 判断备库落后主库多长时间,可以查看
Seconds_Behind_Master
的值。
其中,SHOW SLAVE STATUS
命令的源码执行过程如下。
show_slave_status_cmd()
|-show_slave_status()
|-show_slave_status_metadata() 发送元数据
|-show_slave_status_send_data() 真正的监控数据
在 show_slave_status_send_data()
函数中的相关内容如下。
bool show_slave_status_send_data(THD *thd, Master_info *mi,
char* io_gtid_set_buffer,
char* sql_gtid_set_buffer)
{
// ... ...
protocol->store(mi->get_master_log_name(), &my_charset_bin); // Master_Log_File
protocol->store((ulonglong) mi->get_master_log_pos()); // Read_Master_Log_Pos
protocol->store(mi->rli->get_group_relay_log_name() +
dirname_length(mi->rli->get_group_relay_log_name()),
&my_charset_bin); // Relay_Log_File
protocol->store((ulonglong) mi->rli->get_group_relay_log_pos()); // Relay_Log_Pos
protocol->store(mi->rli->get_group_master_log_name(), &my_charset_bin); // Relay_Master_Log_File
// ... ...
protocol->store((ulonglong) mi->rli->get_group_master_log_pos()); // Exec_Master_Log_Pos
// ... ...
}
主备延迟
一般都是通过 SHOW SLAVE STATUS
命令查看如下的状态。
mysql> SHOW SLAVE STATUS\G
... ...
Seconds_Behind_Master: 0 # 备库延迟时间,单位为秒
源码解析
masterha_manger
|-MHA::MasterMonitor::main() 监控是否有异常
| |-wait_until_master_is_dead()
| |-wait_until_master_is_unreachable()
| | ###<>###Phase 1: Configuration Check Phase
| |-MHA::ManagerUtil::check_node_version() 检查Manager+Node的版本,要求Node版本>=Manager版本
| | |-get_node_version() 通过SSH命令获取Node版本,可以直接查看该函数
| |-MHA::NodeUtil::check_manager_version()
| |-MHA::ServerManager::connect_all_and_read_server_status() 确认各个Node节点是否可以连接MySQL服务
| |-MHA::ServerManager::get_dead_servers() 再次检测node节点的状态
| |-MHA::ServerManager::get_alive_servers()
| |-MHA::ServerManager::get_alive_slaves()
| |-MHA::ServerManager::print_dead_servers()
| |
| |-MHA::HealthCheck::wait_until_unreachable()
|
|-MHA::MasterFailover::main() 开始执行切换
|-init_config() 初始化配置
|-do_master_failover() 真正执行Failover切换操作
| |
| | ###<>###Phase 1: Configuration Check Phase
| |-MHA::ServerManager::init_binlog_server() 检查每个MySQL服务器,保证SSH+Node版本号
| | |-MHA::HealthCheck::ssh_check_simple() 检查SSH是否正常
| | | |-ssh_check()
| | |-MHA::ManagerUtil::get_node_version() 同时检查Node的版本号
| |-check_settings()
| | |-MHA::ManagerUtil::check_node_version()
| | |-connect_all_and_read_server_status()
| | |-run_on_start()
| | |-run_on_finish()
| |
| | ###<>###Phase 2: Dead Master Shutdown Phase
| |-force_shutdown()
| | |-stop_io_thread() 停止所有备库的IO-thread
| | |-force_shutdown_internal() 执行master_ip_failover_script(例如切换VIP)以及
| | | shutdown_script(如关闭mysqld进程)指定的脚本
| |
| | ###<>###Phase 3: Master Recovery Phase
| | ###<>###Phase 3.1: Getting Latest Slaves Phase
| |-check_set_latest_slaves()
| | |-read_slave_status()
| | |-identify_latest_slaves()
| | |-print_latest_slaves()
| | |-identify_oldest_slaves()
| | |-print_oldest_slaves()
| |
| | ###<>###Phase 3.2: Saving Dead Master's Binlog Phase
| |-is_gtid_auto_pos_enabled()
| |-save_master_binlog() 如果非GTID,会判断是否有数据丢失
| | |-MHA::ManagerUtil::check_node_version() 检查宕机主库的版本信息
| | |-save_master_binlog_internal() 保存binlog
| | |-MHA::ManagerUtil::exec_ssh_cmd() 调用Node的save_binary_logs脚本
| | |-MHA::NodeUtil::file_copy() 从宕掉的库复制差异数据
| |
| | ###<>###Phase 3.3: Determining New Master Phase
| |-is_gtid_auto_pos_enabled()
| |-get_most_advanced_latest_slave() 是GTID
| |-find_latest_base_slave() 非GTID
| |-select_new_master()
| |-recover_master()
| | |-recover_master_gtid_internal() GTID
| | |-recover_master_internal() NOT GTID
| | | | ###<>###Phase 3.3: New Master Diff Log Generation Phase
| | | |-recover_relay_logs()
| | | | |-generate_diff_from_readpos() 执行Node节点上的apply_diff_relay_logs脚本
| | | |-send_binlog()
| | | | ###<>###Phase 3.4: Master Log Apply Phase
| | | |-recover_slave() 将获取的binlog差值恢复到新主库
| | | | |-apply_diff() 执行Node节点上的apply_diff_relay_logs脚本
| | | | |-wait_until_relay_log_applied()
| | | | |-stop_sql_thread()
| | | |-get_new_master_binlog_position()
| | |
| | |-MHA::ManagerUtil::exec_system() 执行master_ip_failover_script脚本
| |
| | ###<>###Phase 4: Slaves Recovery Phase
| |-recover_slaves()
| | |-is_gtid_auto_pos_enabled() 判断是否开启GTID
| | |-recover_slaves_gtid_internal() GTID
| | |
| | | ###NOT GTID###BEGIN
| | |-recover_all_slaves_relay_logs()
| | |-recover_slaves_internal()
| | | ###<>###Phase 5: New master cleanup phase
| | |-reset_slave_on_new_master()
| | | |-reset_slave_info()
| | | ###NOT GTID###END
| |
| |-cleanup()
|
|-finalize_on_error()
参考
关于 MHA 的介绍,可以参考 Github MHA-Wiki。