MHA已经是一个比较成熟的MySQL Master切换软件,很多公司都在使用,这里主要介绍一下MHA关于Failover的具体实现原理。主要从代码出发,如有错误,还望指正!
关于MHA的Failover主要代码都在MasterFailover.pm中,具体的Failover流程步骤如下:
- a.停止所有slave从master接收数据(stop io_thread),并摘除Master的vip.
- b.通过所有slave的信息,获取latest slaves和oldest slaves
- c.取出一个latest_slave,并通过该slave的和待切换的master(dead master)做比较,将之间相差的binlog保存到binlog server上(通常为mha monitor server).
- d.从所有的slave中,选择一个合适的slave做为new master(通常为latest slave).
- e.应用相应的binlog,将该new master恢复到和dead master一致的位置,并保存此时new master的binlog 信息。
- f.将vip添加到new master上.
- g.并发的将所有的slave恢复到latest slave的位置,并应用所有的binlog server上的binlog,将所有的slave并发的恢复到和dead master一致的位置.
- h.将所有的slave指向新的new master(根据第e步获取的信息).
- i.所有slave执行start slave,并且在new master上执行reset slave all.
- j.切换完成.
上述所有切换是正常情况下,MHA Failover的操作流程。下面会讲讲具体的实现细节:
- 1.停止所有slave从master接收数据(stop io_thread),并摘除Master的vip.
该步骤,主要通过函数force_shutdown实现,该函数主要部分如下:
点击(此处)折叠或打开
-
my $slave_io_stopper = new Parallel::ForkManager( $#alive_slaves + 1 );
-
my $stop_io_failed = 0;
-
$slave_io_stopper->run_on_start(
-
sub {
-
my ( $pid, $target ) = @_;
-
}
-
);
-
$slave_io_stopper->run_on_finish(
-
sub {
-
my ( $pid, $exit_code, $target ) = @_;
-
return if ( $target->{ignore_fail} );
-
$stop_io_failed = 1 if ($exit_code);
-
}
-
);
-
-
foreach my $target (@alive_slaves) {
-
$slave_io_stopper->start($target) and next;
-
eval {
-
$SIG{INT} = $SIG{HUP} = $SIG{QUIT} = $SIG{TERM} = "DEFAULT";
-
my $rc = $target->stop_io_thread();
-
$slave_io_stopper->finish($rc);
-
};
-
if ($@) {
-
$log->error($@);
-
undef $@;
-
$slave_io_stopper->finish(1);
-
}
-
$slave_io_stopper->finish(0);
-
}
-
…...
- force_shutdown_internal($dead_master); //摘除vip
该函数很明显,并发的去所有alive的slave上执行stop io_thread,之后调用force_shutdown_internal 摘除主库的vip。
总结:
从第一步我们看出,这一步所做的事情就是所有从库停止接收主库的数据,并且摘除主库的vip,此时主库无法在进行写入,为后面的补数据,并重新构建主从结构做好准备工作!
- 2.通过所有slave的信息,获取latest slaves和oldest slaves
第二步获取latest slaves和oldest slaves的实现代码如下:
点击(此处)折叠或打开
-
sub check_set_latest_slaves {
-
$_server_manager->read_slave_status(); //读取所有alive slave的show slave status信息
-
$_server_manager->identify_latest_slaves(); //根据这些信息将latest slaves信息保存起来
-
….
-
$_server_manager->identify_oldest_slaves(); //同样根据alive slave的信息,将oldest slaves信息保存起来。
-
….
- }
关于如何获取最新的slave和最老的slave,实现代码也比较简单:
点击(此处)折叠或打开
-
if (
-
!$find_oldest
-
&& (
-
( !$a && !defined($b) )
-
|| ( $_->{Master_Log_File} gt $latest[0]{Master_Log_File} )
-
|| ( ( $_->{Master_Log_File} ge $latest[0]{Master_Log_File} )
-
&& $_->{Read_Master_Log_Pos} > $latest[0]{Read_Master_Log_Pos} )
-
)
-
)
-
{
-
@latest = ();
-
push( @latest, $_ );
-
}
-
elsif (
-
$find_oldest
-
&& (
-
( !$a && !defined($b) )
-
|| ( $_->{Master_Log_File} lt $latest[0]{Master_Log_File} )
-
|| ( ( $_->{Master_Log_File} le $latest[0]{Master_Log_File} )
-
&& $_->{Read_Master_Log_Pos} < $latest[0]{Read_Master_Log_Pos} )
-
)
-
)
-
{
-
@latest = ();
-
push( @latest, $_ );
-
}
-
elsif ( ( $_->{Master_Log_File} eq $latest[0]{Master_Log_File} )
-
&& ( $_->{Read_Master_Log_Pos} == $latest[0]{Read_Master_Log_Pos} ) )
-
{
-
push( @latest, $_ );
-
}
-
}
-
foreach (@latest) {
-
$_->{latest} = 1 if ( !$find_oldest );
-
$_->{oldest} = 1 if ($find_oldest);
- }
从上述代码可以看到,主要通过slave的io thread读取主库的信息来判断。根据判断,将相关的slave进行标记,是否为latest或着oldest。
总结:
这一步所做的事情,就是根据所有alive slaves的信息,来确定谁是最新的slave,谁是最老的slave。这样做的目的是为了之后保存和dead master之间相差binlog 做准备。有了latest slave,就可以知道从dead master哪个位置copy binlog events,从而为恢复做准备。
- 3.取出一个latest_slave,并通过该slave的和待切换的master(dead master)做比较,将之间相差的binlog保存到binlog server上(通常为mha monitor server).
这一步的主要功能,是保存latest slave和dead master之间相差的binlog events,具体实现如下:
点击(此处)折叠或打开
-
sub save_master_binlog {
-
my $dead_master = shift;
-
if ( $_real_ssh_reachable && !$g_skip_save_master_binlog ) { // 如果dead master ssh 可达,且不允许跳过补齐binlog
-
MHA::ManagerUtil::check_node_version( //检测apply_diff_relay_logs的版本
-
$log,
-
$dead_master->{ssh_user},
-
$dead_master->{ssh_host},
-
$dead_master->{ssh_ip},
-
$dead_master->{ssh_port}
-
);
-
-
//获取latest 读取到的dead master binlog file信息
-
my $latest_file =( $_server_manager->get_latest_slaves() )[0]->{Master_Log_File};
-
//获取latest 读取到的dead master binlog position信息
-
my $latest_pos =( $_server_manager->get_latest_slaves() )[0]->{Read_Master_Log_Pos};
-
//保存dead master和latest slave之间相差的binlog events到binlog server上。
-
save_master_binlog_internal( $latest_file, $latest_pos, $dead_master, );
-
}
-
else {
-
if ($g_skip_save_master_binlog) {
-
$log->info("Skipping trying to save dead master's binary log.");
-
}
-
elsif ( !$_real_ssh_reachable ) {
-
$log->warning(
-
"Dead Master is not SSH reachable. Could not save it's binlogs. Transactions that were not sent to the latest slave (Read_Master_Log_Pos to the tail of the dead master's binlog) were lost."
-
);
-
}
-
}
- }
从上述代码得知,该步做了下面几件事情:
- 3.1 在ssh可达并且不允许跳过save binlog步骤的情况下,检测apply_diff_relay_logs版本(这一步应该是个bug,应该为检测 save_binary_logs)
- 3.2 获取latest slave的(master_log_file,master_log_pos)信息,并根据该信息调用save_master_binlog_internal保存latest slave和dead master之间相差的binlog events到binlog server。
这里提到了copy这段dead master和latest slave之间相差的binlog events是通过save_master_binlog_internal函数实现的,这段代码主要是通过slave_binary_logs脚本实现:
点击(此处)折叠或打开
- save_binary_logs --command=save --start_file=$master_log_file --start_pos=$read_master_log_pos --binlog_dir=$dead_master->{master_binlog_dir} --output_file=$_diff_binary_log_remote --handle_raw_binlog=$dead_master->{handle_raw_binlog} --disable_log_bin=$dead_master->{disable_log_bin} --manager_version=$MHA::ManagerConst::VERSION
- 4.从所有的slave中,选择一个合适的slave做为new master(通常为latest slave).
这一步所做的是整个切换最关键的一环,选择一个slave做为master,这一步的实现如下:
点击(此处)折叠或打开
-
sub select_new_master($$) {
-
my $dead_master = shift;
-
my $latest_base_slave = shift;
-
-
my $new_master =
-
$_server_manager->select_new_master( $g_new_master_host, $g_new_master_port,
-
$latest_base_slave->{check_repl_delay} );
-
unless ($new_master) {
-
my $msg =
-
"None of existing slaves matches as a new master. Maybe preferred node is misconfigured or all slaves are too far behind.";
-
$log->error($msg);
-
$mail_body .= $msg . "\n";
-
croak;
-
}
-
$log->info( "New master is " . $new_master->get_hostinfo() );
-
$mail_body .=
-
"Selected " . $new_master->get_hostinfo() . " as a new master.\n";
-
$log->info("Starting master failover..");
-
$_server_manager->print_servers_migration_ascii( $dead_master, $new_master );
-
if ($g_interactive) {
-
$new_master =
-
$_server_manager->manually_decide_new_master( $dead_master, $new_master );
-
$log->info(
-
"New master decided manually is " . $new_master->get_hostinfo() );
-
}
-
return $new_master;
- }
这里我们只讨论正常的,自动的切换。从上述代码我们可以看到,切换中new master的选择调用了
点击(此处)折叠或打开
- $_server_manager->select_new_master( $g_new_master_host, $g_new_master_port,$latest_base_slave->{check_repl_delay} );
这里我们看看这部分的实现细节:
点击(此处)折叠或打开
-
my @pref = $self->get_candidate_masters(); //配置为候选主库
-
-
# The following servers can not be master:
-
# - dead servers
-
# - Set no_master in conf files (i.e. DR servers)
-
# - log_bin is disabled
-
# - Major version is not the oldest
-
# - too much replication delay
-
my @bad =
-
$self->get_bad_candidate_masters( $latest[0], $check_replication_delay );
-
-
.....
-
-
$log->info("Searching new master from slaves..");
-
$log->info(" Candidate masters from the configuration file:");
-
$self->print_servers( \@pref );
-
$log->info(" Non-candidate masters:");
-
$self->print_servers( \@bad );
-
-
return $latest[0]
-
if ( $#pref < 0 && $#bad < 0 && $latest[0]->{latest_priority} );
-
-
if ( $latest[0]->{latest_priority} ) {
-
$log->info(
-
" Searching from candidate_master slaves which have received the latest relay log events.."
-
) if ( $#pref >= 0 );
-
foreach my $h (@latest) {
-
foreach my $p (@pref) {
-
if ( $h->{id} eq $p->{id} ) {
-
return $h
-
if ( !$self->get_server_from_by_id( \@bad, $p->{id} ) );
-
}
-
}
-
}
-
$log->info(" Not found.") if ( $#pref >= 0 );
-
}
-
-
-
#new master is not latest
-
$log->info(" Searching from all candidate_master slaves..")
-
if ( $#pref >= 0 );
-
foreach my $s (@slaves) {
-
foreach my $p (@pref) {
-
if ( $s->{id} eq $p->{id} ) {
-
my $a = $self->get_server_from_by_id( \@bad, $p->{id} );
-
return $s unless ($a);
-
}
-
}
-
}
-
$log->info(" Not found.") if ( $#pref >= 0 );
-
-
if ( $latest[0]->{latest_priority} ) {
-
$log->info(
-
" Searching from all slaves which have received the latest relay log events.."
-
);
-
foreach my $h (@latest) {
-
my $a = $self->get_server_from_by_id( \@bad, $h->{id} );
-
return $h unless ($a);
-
}
-
$log->info(" Not found.");
-
}
-
-
# none of latest servers can not be a master
-
$log->info(" Searching from all slaves..");
-
foreach my $s (@slaves) {
-
my $a = $self->get_server_from_by_id( \@bad, $s->{id} );
-
return $s unless ($a);
-
}
-
$log->info(" Not found.");
-
-
return;
- }
从上述代码,我们可以得出如下选择new master结论:
- 4.1 如果该slave 配置为候选主库,且该slave 是latest slave,并且该slave为非bad slave,则选择该slave 为new master。否则继续选择。
- 4.2 如果该slave配置为候选主库。且该slave为非bad slave,则选择改slave为new master。否则继续选择。
- 4.3 从所有latest slave中,选择第一个非bad 的slave做为 new master。
- 4.4 从所有的slave中,选择第一个非bad的slave作为new master 。
何为bad slave呢 ?
# The following servers can not be master:
# - dead servers
# - Set no_master in conf files (i.e. DR servers)
# - log_bin is disabled
# - Major version is not the oldest
# - too much replication delay
- 5.应用相应的binlog,将该new master恢复到和dead master一致的位置,并保存此时new master的binlog 信息。
该部分要做的事情,是根据上一步获取到的new master,将该new master恢复到和dead master一致的位置,为后续主库重新激活(接受应用写)和构建ms架构做准备.
该部分的实现步骤如下:
- 5.1 将new master和latest slave之间相差的relay log events拷贝到new master 上。
- 5.2 将new master上,SQL Thread 领先IO Thread的relay log events apply掉。
- 5.3 将5.1拷贝过来的relay log events应用到new master,此是new master到达了和latest slave一致的位置。
- 5.4 将latest 和dead master之间相差的binlog events应用到该new master上,使该new master和dead master达到一致的状态。
- 5.5 保存该new master的binlog 信息,用以构建master-slave集群。
关于上面的分析,具体实现如下:
点击(此处)折叠或打开
-
# apply diffs to master and get master status
-
# We do not reset slave here
-
sub recover_slave {
-
my ( $target, $logger ) = @_;
-
$logger = $log unless ($logger);
-
-
$logger->info(
-
sprintf(
-
"Starting recovery on %s(%s:%d)..",
-
$target->{hostname}, $target->{ip}, $target->{port}
-
)
-
);
-
-
if ( $target->{latest} eq '0' || $_has_saved_binlog ) {
-
$logger->info(" Generating diffs succeeded.");
-
my ( $high, $low ) = apply_diff( $target, $logger );
-
if ( $high ne '0' || $low ne '0' ) {
-
$logger->error(" Applying diffs failed with return code $high:$low.");
-
return -1;
-
}
-
}
-
else {
-
$logger->info(
-
" This server has all relay logs. Waiting all logs to be applied.. ");
-
my $ret = $target->wait_until_relay_log_applied($logger);
-
if ($ret) {
-
$logger->error(" Failed with return code $ret");
-
return -1;
-
}
-
$logger->info(" done.");
-
$target->stop_sql_thread($logger);
-
}
-
$logger->info(" All relay logs were successfully applied.");
-
return 0;
- }
- 6.将vip添加到new master上.
由于new master和dead master达到了一致的状态,为了使业务尽快恢复。所以先让new master提供服务。
具体添加vip的实现是通过调用脚本master_ip_failover_script来完成。具体实现如下:
具体添加vip的实现是通过调用脚本master_ip_failover_script来完成。具体实现如下:
点击(此处)折叠或打开
- $new_master->{master_ip_failover_script} --command=start --ssh_user=$new_master->{ssh_user} --orig_master_host=$dead_master->{hostname} --orig_master_ip=$dead_master->{ip} --orig_master_port=$dead_master->{port} --new_master_host=$new_master->{hostname} --new_master_ip=$new_master->{ip} --new_master_port=$new_master->{port} --new_master_user=$new_master->{escaped_user} --new_master_password=$new_master->{escaped_password}
- 7.并发的将所有的slave恢复到latest slave的位置,并应用所有的binlog server上的binlog,将所有的slave并发的恢复到和dead master一致的位置.
该过程主要是为了构建master-slave架构,通过前面可知new master保存了恢复到dead master时,自身的binlog位置信息。这里我们只要将所有slave也恢复到这个位置,并通过修改slave指向,master-slave架构即完成。
该部分的执行逻辑如下:
- 7.1 将所有alive slaves和latest slave之间相差的relay log events 复制到每个slave的机器上。
- 7.2 将binlog server上产生的binlog events 复制到每个slave机器上。
- 7.3 将每个slave上的io thread和sql thread之间相差的部分relay log events应用掉。
- 7.4 将latest slave和每个slave之间的相差的部分relay log events应用掉,此时所有的slave都成为了latest slave。
- 7.5 每个slave应用从binlog server上拷贝过来的binlog events,所有的slave恢复到dead server的状态。
代码部分由于太长,这里不进行分析,有兴趣可以查看Failover.pm中的recover_slaves函数的实现。
- 8.将所有的slave指向新的new master(根据第5步获取的信息).
- 9.所有slave执行start slave,并且在new master上执行reset slave all.
- 10.切换完成.
整个过程就分析完了,这里只分析了正常Failover的部分,由于分支太多,为避免文章太过复杂,所以没有对异常进行分析。有兴趣的同学可以自行阅读代码。
well done...