简介
MHA(Master High Availability)目前在MySQL高可用方面是一个相对成熟的解决方案,它由日本DeNA公司youshimaton(现就职于Facebook公司)开发,是一套优秀的作为MySQL高可用性环境下故障切换和主从提升的高可用软件。在MySQL故障切换过程中,MHA能做到在0~30秒之内自动完成数据库的故障切换操作,并且在进行故障切换的过程中,MHA能在最大程度上保证数据的一致性,以达到真正意义上的高可用。
该软件由两部分组成:MHA Manager(管理节点)和MHA Node(数据节点)。MHA Manager可以单独部署在一台独立的机器上管理多个master-slave集群,也可以部署在一台slave节点上。MHA Node运行在每台MySQL服务器上,MHA Manager会定时探测集群中的master节点,当master出现故障时,它可以自动将最新数据的slave提升为新的master,然后将所有其他的slave重新指向新的master。整个故障转移过程对应用程序完全透明。
在MHA自动故障切换过程中,MHA试图从宕机的主服务器上保存二进制日志,最大程度的保证数据的不丢失,但这并不总是可行的。例如,如果主服务器硬件故障或无法通过ssh访问,MHA没法保存二进制日志,只进行故障转移而丢失了最新的数据。使用MySQL 5.5的半同步复制,可以大大降低数据丢失的风险。MHA可以与半同步复制结合起来。如果只有一个slave已经收到了最新的二进制日志,MHA可以将最新的二进制日志应用于其他所有的slave服务器上,因此可以保证所有节点的数据一致性。
目前MHA主要支持一主多从的架构,要搭建MHA,要求一个复制集群中必须最少有三台数据库服务器,一主二从,即一台充当master,一台充当备用master,另外一台充当从库,因为至少需要三台服务器,出于机器成本的考虑,淘宝也在该基础上进行了改造,目前淘宝TMHA已经支持一主一从。
工作原理
MHA工作原理如下:
- 从宕机崩溃的master保存二进制日志事件(binlog events);
- 识别含有最新更新的slave;
- 应用差异的中继日志(relay log)到其他的slave;
- 应用从master保存的二进制日志事件(binlog events);
- 提升一个slave为新的master;
- 使其他的slave连接新的master进行复制;
MHA软件由两部分组成,Manager工具包和Node工具包,具体的说明如下。
Manager工具包主要包括以下几个工具:
masterha_check_ssh 检查MHA的SSH配置状况
masterha_check_repl 检查MySQL复制状况
masterha_manger 启动MHA
masterha_check_status 检测当前MHA运行状态
masterha_master_monitor 检测master是否宕机
masterha_master_switch 控制故障转移(自动或者手动)
masterha_conf_host 添加或删除配置的server信息
Node工具包(这些工具通常由MHA Manager的脚本触发,无需人为操作)主要包括以下几个工具:
save_binary_logs 保存和复制master的二进制日志
apply_diff_relay_logs 识别差异的中继日志事件并将其差异的事件应用于其他的slave
filter_mysqlbinlog 去除不必要的ROLLBACK事件(MHA已不再使用这个工具)
purge_relay_logs 清除中继日志(不会阻塞SQL线程)
安装部署
具体的搭建环境如下:
角色 ip地址 主机名 server_id 类型
Monitor host 192.168.0.20 server01 - 监控复制组
Master 192.168.0.50 server02 1 写入
Candicate master 192.168.0.60 server03 2 读
Slave 192.168.0.70 server04 3 读
创建用户:
chattr -i /etc/group
chattr -i /etc/gshadow
chattr -i /etc/passwd
chattr -i /etc/shadow
useradd -m -d /home/mha mha
passwd mha
chattr +i /etc/passwd
chattr +i /etc/shadow
chattr +i /etc/group
chattr +i /etc/gshadow
修改权限:
#visudo
末尾添加:
mha ALL=(ALL) NOPASSWD: ALL
加入root组:
## Allow root to run any commands anywhere
root ALL=(ALL) ALL
mha ALL=(ALL) ALL
创建、下发公钥,已MHA Server为例:
ssh-keygen -t rsa
ssh-copy-id -i ~/.ssh/id_rsa.pub 192.168.0.50
ssh-copy-id -i ~/.ssh/id_rsa.pub 192.168.0.60
ssh-copy-id -i ~/.ssh/id_rsa.pub 192.168.0.70
修改mha用户环境变量,添加perl、mha、mysqlbinlog:
#vim /home/mha/.bashrc
export PTAH=$PATH:/usr/share/perl5:/opt/soft/mha/bin/:/opt/soft/mysql57/bin/
export LC_ALL=C
修改对应目录权限:
chown -R mha.mha /opt/soft/mha/*
chown -R mha.mha /usr/share/perl5/vendor_perl/MHA/*
安装MHA:
#mha server
rpm -ivh mha4mysql-node-0.56-0.el6.noarch.rpm
rpm -ivh mha4mysql-manager-0.56-0.el6.noarch.rpm
#mha node
rpm -ivh mha4mysql-node-0.56-0.el6.noarch.rpm
rpm包是重新打包的,安装后环境如下:
#mha server
#tree /opt/soft/mha/
/opt/soft/mha/
|-- bin
| |-- apply_diff_relay_logs
| |-- filter_mysqlbinlog
| |-- masterha_check_repl
| |-- masterha_check_ssh
| |-- masterha_check_status
| |-- masterha_conf_host
| |-- masterha_manager
| |-- masterha_master_monitor
| |-- masterha_master_switch
| |-- masterha_secondary_check
| |-- masterha_stop
| |-- purge_relay_logs
| `-- save_binary_logs
|-- binlog
|-- conf
| |-- app1.cnf
| `-- masterha_default.cnf
|-- logs
| `-- log
`-- scripts
#mha node
#tree /opt/soft/mha/
/opt/soft/mha/
└── bin
├── apply_diff_relay_logs
├── filter_mysqlbinlog
├── purge_relay_logs
└── save_binary_logs
添加配置文件:
#masterha_default.cnf
[server default]
user=mha
password=mha
ssh_user=mha
repl_user=repl
repl_password=repl
manager_workdir=/opt/soft/mha
manager_log=/opt/soft/mha/logs/log
master_binlog_dir=/work/mysql6666/var/
remote_workdir=/work/dba/
secondary_check_script=/opt/soft/mha/bin/masterha_secondary_check -s 192.168.0.60 -s 192.168.0.70 --user=mha --master_ip=192.168.0.50 --master_port=6666
# master_ip_failover_script=/opt/soft/mha/scripts/master_ip_failover
# master_ip_online_change_script=/opt/soft/mha/scripts/master_ip_online_change
# report_script=/opt/soft/mha/scripts/send_report
# shutdown_script=/opt/soft/mha/scripts/power_manager
ping_interval=1
#app.conf
[server1]
hostname=192.168.0.50
port=6666
[server2]
hostname=192.168.0.60
port=6666
candidate_master=1
check_repl_delay=0
[server3]
hostname=192.168.0.70
port=6666
no_master
检查SSH配置:
/opt/soft/mha/bin/masterha_check_ssh --global_conf=/opt/soft/mha/conf/masterha_default.cnf --conf=/opt/soft/mha/conf/app.cnf
检查复制环境:
/opt/soft/mha/bin/masterha_check_status --global_conf=/opt/soft/mha/conf/masterha_default.cnf --conf=/opt/soft/mha/conf/app.cnf
开启mha监控
/opt/soft/mha/bin/masterha_manager --global_conf=/opt/soft/mha/conf/masterha_default.cnf --conf=/opt/soft/mha/conf/app.cnf --ignore_last_failover
脚本含义
Manager
- masterha_check_ssh:检查mha的ssh配置情况;
- masterha_check_repl:检查MySQL复制情况;
- masterha_manager:启动mha;
- masterha_check_status:检查当前mha运行状态;
- masterha_master_monitor:检测master是否宕机;
- masterha_check_switch:控制故障转移(自动或手动);
- masteha_conf_host:添加或删除配置的server信息;
Node
- save_binary_logs:保存和复制master的二进制文件;
- apply_diff_relay_logs:识别差异的relay log并将其差异event应用于其他slave;
- filter_mysqlbinlog:去除不必要的rollback event;
- purge_relay_logs:清除relay log;
工作流程
masterha_manager
mha启动脚本为masterha_manager,可选参数为remove_dead_master_conf、manger_log、ignore_last_failover。
masterha_manager主要流程为:
- 调用MasterMonitor,监控MySQL master状态;
- 发现master状态异常后,调用MasterFailover进行切换;
具体细节如下:
my ( $exit_code, $dead_master, $ssh_reachable ) =
MHA::MasterMonitor::main( "--interactive=0", @ARGV );
manager通过monitor监测master状态,一旦获得返回值,则表明monitor状态异常。通过判断exit_code确定是否应切换。
if ( $exit_code && $exit_code != $MHA::ManagerConst::MASTER_DEAD_RC ) {
exit $exit_code;
}
if ( !$dead_master->{hostname}
|| !$dead_master->{ip}
|| !$dead_master->{port}
|| !defined($ssh_reachable) )
{
exit 1;
}
检测通过后,调用MasterFailover进执行切换操作。
$exit_code = MHA::MasterFailover::main(
"--master_state=dead",
"--interactive=0",
"--dead_master_host=$dead_master->{hostname}",
"--dead_master_ip=$dead_master->{ip}",
"--dead_master_port=$dead_master->{port}",
"--ssh_reachable=$ssh_reachable",
@ARGV
);
MasterMonitor
MasterHA_Manager调用MasterMonitor的main方法对MySQL进行监控。流程图如下:
核心方法是一个死循环,不断调用wait_until_master_is_dead方法监测主库状态,部分代码如下:
while (1) {
my ( $exit_code, $dead_master, $ssh_reachable ) =
wait_until_master_is_dead();
my $msg = sprintf( "Got exit code %d (%s).",
$exit_code,
$exit_code == $MHA::ManagerConst::MASTER_DEAD_RC
? "Master dead"
: "Not master dead" );
$log->info($msg) if ($log);
if ($g_check_only) {
finalize();
return $exit_code;
}
if ( $exit_code && $exit_code == $RETRY ) {
prepare_for_retry();
}
else {
if ( $exit_code && $exit_code != $MHA::ManagerConst::MASTER_DEAD_RC ) {
finalize_on_error();
}
elsif ($g_monitor_only) {
finalize();
}
return ( $exit_code, $dead_master, $ssh_reachable );
}
}
wait_until_master_is_dead方法的返回值中,exit_code有的值有四种,分别是0、1、20、retry。其中只有当exit_code=MHA::ManagerConst::MASTER_DEAD_RC,也就是20时,后续才会调用failover方法。
wait_until_master_is_dead方法中,核心方法是调用wait_until_master_is_unreachable方法并处理其返回值。逻辑关系如下:
拿到wait_until_master_is_unreachable的返回值后,会再次根据配置文件探活,确认主库连接失败后,根据配置文件检测slave状态和数量,有合适新主库后,exit_code返回20,否则返回0或者1;
再次检测代码存活的代码如下:
$_server_manager->connect_all_and_read_server_status(
$dead_master->{hostname},
$dead_master->{ip}, $dead_master->{port} );
my @dead_servers = $_server_manager->get_dead_servers();
my @alive_servers = $_server_manager->get_alive_servers();
$log->info("Dead Servers:");
$_server_manager->print_dead_servers();
$log->info("Alive Servers:");
$_server_manager->print_alive_servers();
$log->info("Alive Slaves:");
$_server_manager->print_alive_slaves();
$_server_manager->print_failed_slaves_if();
$_server_manager->print_unmanaged_slaves_if();
wait_until_master_is_unreachable方法的返回值有三个,分别是ret、dead_master和ssh_reachable。该方法的逻辑如下:
wait_until_master_is_unreachable调用MHA::ServerManager对主库进行实时检测,包括deadservers、aliveservers、aliveslaves等。
$_server_manager = new MHA::ServerManager( servers => \@servers_config );
$_server_manager->set_logger($log);$_server_manager->connect_all_and_read_server_status();
@dead_servers = $_server_manager->get_dead_servers();
@alive_servers = $_server_manager->get_alive_servers();
@alive_slaves = $_server_manager->get_alive_slaves();
$log->info("Dead Servers:");
$_server_manager->print_dead_servers();
$log->info("Alive Servers:");
$_server_manager->print_alive_servers();
$log->info("Alive Slaves:");
$_server_manager->print_alive_slaves();
$_server_manager->print_failed_slaves_if();
$_server_manager->print_unmanaged_slaves_if();
$current_master = $_server_manager->get_current_alive_master();
如果启用GTID,则检查binlog server,否则进行ssh和slave版本检测。
if ( !$_server_manager->is_gtid_auto_pos_enabled() ) {
$log->info("GTID (with auto-pos) is not supported");
MHA::SSHCheck::do_ssh_connection_check( \@alive_servers, $log,
$servers_config[0]->{log_level}, $g_workdir )
unless ($g_skip_ssh_check);
$log->info("Checking MHA Node version..");
foreach my $slave (@alive_slaves) {
MHA::ManagerUtil::check_node_version(
$log, $slave->{ssh_user}, $slave->{ssh_host},
$slave->{ssh_ip}, $slave->{ssh_port}
);
}
$log->info(" Version check ok.");
}
else {
$log->info(
"GTID (with auto-pos) is supported. Skipping all SSH and Node package checking."
);
check_binlog_servers( $binlog_server_ref, $log );
}
后续使用MHA::HealthCheck对主库进行ping检查,如果定义了secondary_check_script,则运行该脚本。检查确认主库的确不可达后,返回func_rc, current_master, ssh_reachable。
$master_ping = new MHA::HealthCheck(
user => $current_master->{user},
password => $current_master->{password},
ip => $current_master->{ip},
hostname => $current_master->{hostname},
port => $current_master->{port},
interval => $current_master->{ping_interval},
ssh_user => $current_master->{ssh_user},
ssh_host => $current_master->{ssh_host},
ssh_ip => $current_master->{ssh_ip},
ssh_port => $current_master->{ssh_port},
ssh_connection_timeout => $current_master->{ssh_connection_timeout},
ssh_check_command => $ssh_check_command,
status_handler => $_status_handler,
logger => $log,
logfile => $g_logfile,
workdir => $g_workdir,
ping_type => $current_master->{ping_type},
);
$log->info(
sprintf( "Set master ping interval %d seconds.",
$master_ping->get_ping_interval() )
);
if ( $current_master->{secondary_check_script} ) {
$master_ping->set_secondary_check_script(
$current_master->{secondary_check_script} );
$log->info(
sprintf( "Set secondary check script: %s",
$master_ping->get_secondary_check_script() )
);
}
else {
$log->warning(
"secondary_check_scriptis not defined. It is highly recommended setting it to check master reachability from two or more routes."
);
}
添加vip检测功能
config.pm负责mha的参数处理,如果想添加vip检测参数,则可在my @PARAM_ARRAY 后添加master_vip和master_vip_port。并在parse_server函数中添加解析:
$value{master_vip} = $param_arg->{master_vip};
$value{master_vip_port} = $param_arg->{master_vip_port};
检查vip状态函数可放在MasterMonitor.pm中:
sub check_vip_status {
my @servers_config;
my ( $sc_ref, $binlog_server_ref ) = new MHA::Config(
logger => $log,
globalfile => $g_global_config_file,
file => $g_config_file
)->read_config();
@servers_config = @$sc_ref;
my $master_vip = $servers_config[0]->{master_vip};
my $master_vip_port = $servers_config[0]->{master_vip_port};
my $user = $servers_config[0]->{user};
my $password = $servers_config[0]->{password};
my $logfile = $servers_config[0]->{manager_log};
$log = MHA::ManagerUtil::init_log($logfile);
if ( !defined($master_vip) || !defined($master_vip_port))
{
$log->info("no master_vip or master_vip_port in config file,skip VIP check.") if ($log);
return 2;
}
eval {
my $dbhelper = DBI->connect("DBI:mysql:;host=$master_vip;" . "port=$master_vip_port;",$user,$password,{ PrintError => 0, RaiseError => 1 });
my $sth = $dbhelper->prepare("SELECT 1 As Value");
$sth->execute();
my $href = $sth->fetchrow_hashref;
};
if ($@) {
$log->info($@) if ($log);
$log->info("connect VIP error...") if ($log);
return 0;
}
else {
$log->info("check VIP config success.") if ($log);
return 1;
}
}
对主库进行状态检查时,可以先检测vip状态,若vip存活,则进行下一轮检测。该逻辑可放置在MasterMonitor的main函数中。
while (1) {
my $VIPStatus = check_vip_status();
if ($VIPStatus ne 0)
{
my ( $exit_code, $dead_master, $ssh_reachable ) = wait_until_master_is_dead();
......
}
}
项目地址
未完待续……