本文目录:背景测试场景问题分析小结
背景
在一次变更中使用 MHA 进行主从切换,命令如下:
```shell
masterha_master_switch--master_state=alive--conf=/etc/mha/mha3306.cnf--new_master_host=xx.xx.xx.xx--new_master_port=3306--interactive=0--orig_master_is_new_slave
```
然而却遇到了报错,如下:
[error][/usr/share/perl5/vendor_perl/MHA/ServerManager.pm,ln1213]XX.XX.XX.XXis badasanewmaster![error][/usr/share/perl5/vendor_perl/MHA/MasterRotate.pm,ln232]Failed togetnewmaster![error][/usr/share/perl5/vendor_perl/MHA/ManagerUtil.pm,ln177]GotERROR:at/bin/masterha_master_switch line53.
看报错是认为指定的新主是一个bad new master。
遇到这个报错内心是懵的,明明切换前检查集群状态、masterha_check_repl都是正常的。嗯……还是对 MHA 的原理了解不够深入。
当时也没时间去研究为什么报错了,于是就手工切换了,接下来就让我们一起去探索为什么会出现这个报错吧!
说明一下,线上主从集群的环境是这样的:
角色MySQL版本
MMySQL 5.6.40
S1MySQL 5.6.40
S2MySQL 5.7.29
S3MySQL 5.7.29
PS:为什么主从版本会不一致呢?是因为正在做升级,本次切换就是为了将 S2 切换为主,然后将低版本的两个实例升级上去。
测试场景
线上通过手工切换绕过了 MHA 的报错,后面要进行分析具体原因。因为现场环境新主的版本和老主库版本是不一样的,猜想是否 MHA 不支持跨版本切换,之前也没有留意这个问题。于是在测试环境中进行了一波测试,下面列出测试场景和测试结论,有兴趣的可以自己测试一下:
测试场景原master版本新master版本其他slaves 版本切换结果
场景15.6.405.7.29无切换成功
场景25.7.295.6.40无切换成功
场景35.6.405.7.295.6.38切换失败
场景45.6.385.7.295.6.40切换失败
场景55.6.385.7.295.7.29切换成功
现象是这么个现象,是不是很好奇,为什么只有一个从库的时候,跨版本可以切换成功,当还有其他从库的时候某些情况可以切换成功,某些情况又切换失败,往下看吧!
问题分析
先去google一下,搜索关键词:mha .. is bad as a new master,
然后搜出来的并没有我想要的结果,有些参考价值的文章如下:
https://blog.51cto.com/u_860143/2431044 【和我的场景相似,但什么解释也没说】
https://www.modb.pro/db/50655【不太明确,当时没理解】
穷途末路,只能去源码中翻翻了,毕竟 MHA 一款开源的工具【不逼自己一把就不知道自己英文还是不错的】 找到 MHA 选主的相关代码,首先定义了几个数组:
slaves 数组:选取 alive 的 slaves
latest 数组:从 alive slave 中选取复制位点最新的 slaves
pref 数组:配置文件中配置了 candidate_master 的 slaves
bad 数组:后面解释
接着在进行选主的时候按照以下的顺序进行选举:
选举优先级最高的 slave 作为新主(通常是手工切换指定的 new master),如果该 slave 不能作为新主,则报错退出,否则如果是故障切换,则进行下面的步骤
选择复制位点最新并且在 pref 数组里的 slave 作为新主,如果复制位点最新的 slave 不在 pref 数组中,则继续下面步骤
从 pref 中选择一个 slave 作为新主,如果没有选出则继续
选择复制位点最新的 slave 作为新主,如果没有选出则继续
从所有的 slave 中进行选择
经过以上步骤仍然选择不出主则选举失败
注意:前面的6个选举步骤,都需要保证新主不在 bad 数组中
# Picking upnewmaster# If preferred node is specified,oneofactive preferred nodes will benewmaster.# If the latest server behinds toomuch(i.e.stopping sql threadforonline backups),we should not use itasanewmaster,but we should fetch relay log there.Even though preferred master is configured,it does not become a masterifit's far behind.sub select_new_master{my $self=shift;my $prio_new_master_host=shift;my $prio_new_master_port=shift;my $check_replication_delay=shift;$check_replication_delay=1if(!defined($check_replication_delay));my $log=$self->{logger};my @latest=$self->get_latest_slaves();my @slaves=$self->get_alive_slaves();my @pref=$self->get_candidate_masters();my @bad=$self->get_bad_candidate_masters($latest[0],$check_replication_delay);if($prio_new_master_host&&$prio_new_master_port){my $new_master=$self->get_alive_server_by_hostport($prio_new_master_host,$prio_new_master_port);if($new_master){my $a=$self->get_server_from_by_id(\@bad,$new_master->{id});unless($a){$log->info("$prio_new_master_host can be new master.");return$new_master;}else{$log->error("$prio_new_master_host is bad as a new master!");return;}}else{$log->error("$prio_new_master_host is not alive!");return;}}$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$hif(!$self->get_server_from_by_id(\@bad,$p->{id}));}}}$log->info(" Not found.")if($#pref>=0);}#newmasteris 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$sunless($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$hunless($a);}$log->info(" Not found.");}# noneoflatest 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$sunless($a);}$log->info(" Not found.");return;}
因为报错是说新主是 bad ,那我们重点看下新主为什么会被判定为 bad ,如何判定的。获取 bad 列表的函数是get_bad_candidate_masters,如下,可以看出具有以下五种情况的 slave 会被判定为 bad :
dead servers
{no_master} >= 1【在配置文件中设置了no_master】
log_bin is disabled【未开启binlog】
{oldest_major_version} eq '0'【MySQL major 版本不是最旧的】
too much replication delay【延迟大,与 master 的 binlog position 差距大于 100000000】
# The following servers can not be master:#-dead servers#-Set no_masterinconffiles(i.e.DRservers)#-log_bin is disabled#-Major version is not the oldest#-too much replication delaysubget_bad_candidate_masters($$$){my $self=shift;my $latest_slave=shift;my $check_replication_delay=shift;my $log=$self->{logger};my @servers=$self->get_alive_slaves();my @ret_servers=();foreach(@servers){if($_->{no_master}>=1||$_->{log_bin}eq'0'||$_->{oldest_major_version}eq'0'||($latest_slave&&($check_replication_delay&&$self->check_slave_delay($_,$latest_slave)>=1))){push(@ret_servers,$_);}}return@ret_servers;}
对于1-3,5很好理解,而且线上后来通过监控进行了排查,并不存在这些问题,于是重点看下4是如何来进行定义的。
找到相关的函数:
subcompare_slave_version($){my $self=shift;my @servers=$self->get_alive_servers();my $log=$self->{logger};$log->debug(" Comparing MySQL versions..");my $min_major_version;foreach(@servers){my $dbhelper=$_->{dbhelper};--如果dead或不为从库,则跳过判断 nextif($_->{dead}||$_->{not_slave});my $parsed_major_version=MHA::NodeUtil::parse_mysql_major_version($_->{mysql_version});if(!$min_major_version||$parsed_major_version<$min_major_version){$min_major_version=$parsed_major_version;}}foreach(@servers){my $dbhelper=$_->{dbhelper};nextif($_->{dead}||$_->{not_slave});my $parsed_major_version=MHA::NodeUtil::parse_mysql_major_version($_->{mysql_version});if($min_major_version==$parsed_major_version){$_->{oldest_major_version}=1;}else{$_->{oldest_major_version}=0;}}$log->debug(" Comparing MySQL versions done.");}
可以看到,这里首先会从 alive_servers 中获取最小的版本,也就是min_major_version:
如果实例是 dead 或非从库,则不比较该实例,否则进行比较,关键代码next if (−>dead||
接下来,根据传入的 server 的parsed_major_version【MySQL 的主版本,例如,5.6,5.7】和min_major_version进行对比:
如果parsed_major_version==min_major_version,则oldest_major_version=1;否则oldest_major_version=0
综上可以看出,新主的版本号,需要是所有从库中版本最低的才能作为新的主库,否则将不能作为新的主库。
到这里,问题就水落石出了,回到我们前面测试的场景中,就弄明白了:
场景1和场景2只有一个从库的时候,跨版本切换可以切换成功,是因为这个从库的主版本就是 min_major_version
场景3和场景4中切换失败的原因是,新主的主版本为5.7,而所有从库中最小的主版本号为5.6,因此不能切换
但是,MHA 为什么会这样设计呢?
MySQL 源端(master)低版本到目标端(slave)高版本数据复制是没有问题,源端(master)高版本到目标端(slave)数据复制可能会出现问题。即:5.7可以作为8.0版本的从库,5.6可以作为5.7的从库;但是8.0作为5.7或者5.7作为5.6的从库就会有问题。这个在官方有介绍:https://dev.mysql.com/doc/refman/5.7/en/replication-compatibility.html
不过 MHA 在比较最小版本的时候没有比较原主库的版本,这在切换的时候还是可能会出现低版本向高版本复制的情况,比如测试场景1,不知道是基于什么考虑,欢迎大家留言讨论。
小结
MHA 选主逻辑:
选举优先级最高的 slave 作为新主(通常是手工切换指定的 new master),如果该 slave 不能作为新主,则报错退出,否则如果是故障切换,则进行下面的步骤
选择复制位点最新并且在设置了 candidate_master 的 slave 作为新主,如果复制位点最新的 slave 没有设置 candidate_master ,则继续下面步骤
从设置了 candidate_master 中选择一个 slave 作为新主,如果没有选出则继续
选择复制位点最新的 slave 作为新主,如果没有选出则继续
从所有的 slave 中进行选择
经过以上步骤仍然选择不出主则选举失败
注意:前面的6个选举步骤,都需要保证新主不在bad数组中
bad 数组定义如下:
dead servers
{no_master} >= 1【在配置文件中设置了no_master】
log_bin is disabled【未开启binlog】
{oldest_major_version} eq '0'【MySQL major 版本不是最旧的】
too much replication delay【延迟大,与 master 的 binlog position 差距大于 100000000】
其中4这个是比较容易忽视的一点,需要注意!
转载于https://cloud.tencent.com/developer/article/1875033