MySQL MHA Failover 实现

5630阅读 0评论2015-12-03 zxszcaijin
分类:Mysql/postgreSQL

     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实现,该函数主要部分如下:

点击(此处)折叠或打开

  1. my $slave_io_stopper = new Parallel::ForkManager( $#alive_slaves + 1 );
  2. my $stop_io_failed = 0;
  3. $slave_io_stopper->run_on_start(
  4. sub {
  5. my ( $pid, $target ) = @_;
  6. }
  7. );
  8. $slave_io_stopper->run_on_finish(
  9. sub {
  10. my ( $pid, $exit_code, $target ) = @_;
  11. return if ( $target->{ignore_fail} );
  12. $stop_io_failed = 1 if ($exit_code);
  13. }
  14. );

  15. foreach my $target (@alive_slaves) {
  16. $slave_io_stopper->start($target) and next;
  17. eval {
  18. $SIG{INT} = $SIG{HUP} = $SIG{QUIT} = $SIG{TERM} = "DEFAULT";
  19. my $rc = $target->stop_io_thread();
  20. $slave_io_stopper->finish($rc);
  21. };
  22. if ($@) {
  23. $log->error($@);
  24. undef $@;
  25. $slave_io_stopper->finish(1);
  26. }
  27. $slave_io_stopper->finish(0);
  28. }
  29. ...
  30. 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的实现代码如下:

点击(此处)折叠或打开

  1. sub check_set_latest_slaves {
  2. $_server_manager->read_slave_status(); //读取所有alive slave的show slave status信息
  3. $_server_manager->identify_latest_slaves(); //根据这些信息将latest slaves信息保存起来
  4. .
  5. $_server_manager->identify_oldest_slaves(); //同样根据alive slave的信息,将oldest slaves信息保存起来。
  6. .
  7. }
关于如何获取最新的slave和最老的slave,实现代码也比较简单:

点击(此处)折叠或打开

  1. if (
  2. !$find_oldest
  3. && (
  4. ( !$a && !defined($b) )
  5. || ( $_->{Master_Log_File} gt $latest[0]{Master_Log_File} )
  6. || ( ( $_->{Master_Log_File} ge $latest[0]{Master_Log_File} )
  7. && $_->{Read_Master_Log_Pos} > $latest[0]{Read_Master_Log_Pos} )
  8. )
  9. )
  10. {
  11. @latest = ();
  12. push( @latest, $_ );
  13. }
  14. elsif (
  15. $find_oldest
  16. && (
  17. ( !$a && !defined($b) )
  18. || ( $_->{Master_Log_File} lt $latest[0]{Master_Log_File} )
  19. || ( ( $_->{Master_Log_File} le $latest[0]{Master_Log_File} )
  20. && $_->{Read_Master_Log_Pos} < $latest[0]{Read_Master_Log_Pos} )
  21. )
  22. )
  23. {
  24. @latest = ();
  25. push( @latest, $_ );
  26. }
  27. elsif ( ( $_->{Master_Log_File} eq $latest[0]{Master_Log_File} )
  28. && ( $_->{Read_Master_Log_Pos} == $latest[0]{Read_Master_Log_Pos} ) )
  29. {
  30. push( @latest, $_ );
  31. }
  32. }
  33. foreach (@latest) {
  34. $_->{latest} = 1 if ( !$find_oldest );
  35. $_->{oldest} = 1 if ($find_oldest);
  36. }
从上述代码可以看到,主要通过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,具体实现如下:

点击(此处)折叠或打开

  1. sub save_master_binlog {
  2. my $dead_master = shift;
  3. if ( $_real_ssh_reachable && !$g_skip_save_master_binlog ) { // 如果dead master ssh 可达,且不允许跳过补齐binlog
  4. MHA::ManagerUtil::check_node_version( //检测apply_diff_relay_logs的版本
  5. $log,
  6. $dead_master->{ssh_user},
  7. $dead_master->{ssh_host},
  8. $dead_master->{ssh_ip},
  9. $dead_master->{ssh_port}
  10. );

  11. //获取latest 读取到的dead master binlog file信息
  12. my $latest_file =( $_server_manager->get_latest_slaves() )[0]->{Master_Log_File};
  13. //获取latest 读取到的dead master binlog position信息
  14. my $latest_pos =( $_server_manager->get_latest_slaves() )[0]->{Read_Master_Log_Pos};
  15. //保存dead master和latest slave之间相差的binlog events到binlog server上。
  16. save_master_binlog_internal( $latest_file, $latest_pos, $dead_master, );
  17. }
  18. else {
  19. if ($g_skip_save_master_binlog) {
  20. $log->info("Skipping trying to save dead master's binary log.");
  21. }
  22. elsif ( !$_real_ssh_reachable ) {
  23. $log->warning(
  24. "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."
  25. );
  26. }
  27. }
  28. }

从上述代码得知,该步做了下面几件事情:
  • 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脚本实现:

点击(此处)折叠或打开

  1. 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,这一步的实现如下:

点击(此处)折叠或打开

  1. sub select_new_master($$) {
  2. my $dead_master = shift;
  3. my $latest_base_slave = shift;

  4. my $new_master =
  5. $_server_manager->select_new_master( $g_new_master_host, $g_new_master_port,
  6. $latest_base_slave->{check_repl_delay} );
  7. unless ($new_master) {
  8. my $msg =
  9. "None of existing slaves matches as a new master. Maybe preferred node is misconfigured or all slaves are too far behind.";
  10. $log->error($msg);
  11. $mail_body .= $msg . "\n";
  12. croak;
  13. }
  14. $log->info( "New master is " . $new_master->get_hostinfo() );
  15. $mail_body .=
  16. "Selected " . $new_master->get_hostinfo() . " as a new master.\n";
  17. $log->info("Starting master failover..");
  18. $_server_manager->print_servers_migration_ascii( $dead_master, $new_master );
  19. if ($g_interactive) {
  20. $new_master =
  21. $_server_manager->manually_decide_new_master( $dead_master, $new_master );
  22. $log->info(
  23. "New master decided manually is " . $new_master->get_hostinfo() );
  24. }
  25. return $new_master;
  26. }

这里我们只讨论正常的,自动的切换。从上述代码我们可以看到,切换中new master的选择调用了

点击(此处)折叠或打开

  1. $_server_manager->select_new_master( $g_new_master_host, $g_new_master_port,$latest_base_slave->{check_repl_delay} );
这里我们看看这部分的实现细节:

点击(此处)折叠或打开

  1. my @pref = $self->get_candidate_masters(); //配置为候选主库

  2. # The following servers can not be master:
  3. # - dead servers
  4. # - Set no_master in conf files (i.e. DR servers)
  5. # - log_bin is disabled
  6. # - Major version is not the oldest
  7. # - too much replication delay
  8. my @bad =
  9. $self->get_bad_candidate_masters( $latest[0], $check_replication_delay );

  10. .....

  11. $log->info("Searching new master from slaves..");
  12. $log->info(" Candidate masters from the configuration file:");
  13. $self->print_servers( \@pref );
  14. $log->info(" Non-candidate masters:");
  15. $self->print_servers( \@bad );

  16. return $latest[0]
  17. if ( $#pref < 0 && $#bad < 0 && $latest[0]->{latest_priority} );

  18. if ( $latest[0]->{latest_priority} ) {
  19. $log->info(
  20. " Searching from candidate_master slaves which have received the latest relay log events.."
  21. ) if ( $#pref >= 0 );
  22. foreach my $h (@latest) {
  23. foreach my $p (@pref) {
  24. if ( $h->{id} eq $p->{id} ) {
  25. return $h
  26. if ( !$self->get_server_from_by_id( \@bad, $p->{id} ) );
  27. }
  28. }
  29. }
  30. $log->info(" Not found.") if ( $#pref >= 0 );
  31. }


  32. #new master is not latest
  33. $log->info(" Searching from all candidate_master slaves..")
  34. if ( $#pref >= 0 );
  35. foreach my $s (@slaves) {
  36. foreach my $p (@pref) {
  37. if ( $s->{id} eq $p->{id} ) {
  38. my $a = $self->get_server_from_by_id( \@bad, $p->{id} );
  39. return $s unless ($a);
  40. }
  41. }
  42. }
  43. $log->info(" Not found.") if ( $#pref >= 0 );

  44. if ( $latest[0]->{latest_priority} ) {
  45. $log->info(
  46. " Searching from all slaves which have received the latest relay log events.."
  47. );
  48. foreach my $h (@latest) {
  49. my $a = $self->get_server_from_by_id( \@bad, $h->{id} );
  50. return $h unless ($a);
  51. }
  52. $log->info(" Not found.");
  53. }

  54. # none of latest servers can not be a master
  55. $log->info(" Searching from all slaves..");
  56. foreach my $s (@slaves) {
  57. my $a = $self->get_server_from_by_id( \@bad, $s->{id} );
  58. return $s unless ($a);
  59. }
  60. $log->info(" Not found.");

  61. return;
  62. }
从上述代码,我们可以得出如下选择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集群。

关于上面的分析,具体实现如下:

点击(此处)折叠或打开

  1. # apply diffs to master and get master status
  2. # We do not reset slave here
  3. sub recover_slave {
  4. my ( $target, $logger ) = @_;
  5. $logger = $log unless ($logger);

  6. $logger->info(
  7. sprintf(
  8. "Starting recovery on %s(%s:%d)..",
  9. $target->{hostname}, $target->{ip}, $target->{port}
  10. )
  11. );

  12. if ( $target->{latest} eq '0' || $_has_saved_binlog ) {
  13. $logger->info(" Generating diffs succeeded.");
  14. my ( $high, $low ) = apply_diff( $target, $logger );
  15. if ( $high ne '0' || $low ne '0' ) {
  16. $logger->error(" Applying diffs failed with return code $high:$low.");
  17. return -1;
  18. }
  19. }
  20. else {
  21. $logger->info(
  22. " This server has all relay logs. Waiting all logs to be applied.. ");
  23. my $ret = $target->wait_until_relay_log_applied($logger);
  24. if ($ret) {
  25. $logger->error(" Failed with return code $ret");
  26. return -1;
  27. }
  28. $logger->info(" done.");
  29. $target->stop_sql_thread($logger);
  30. }
  31. $logger->info(" All relay logs were successfully applied.");
  32. return 0;
  33. }
  • 6.将vip添加到new master上.
由于new master和dead master达到了一致的状态,为了使业务尽快恢复。所以先让new master提供服务。
具体添加vip的实现是通过调用脚本master_ip_failover_script来完成。具体实现如下:

点击(此处)折叠或打开

  1. $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...



上一篇:由mysql timestamp字段引发的一个系统bug
下一篇:mysqldump注意问题一则