MGR 的新主选举算法,在节点版本一致的情况下,其实也挺简单的。
首先比较权重,权重越高,选为新主的优先级越高。
如果权重一致,则会进一步比较节点的 server_uuid。server_uuid 越小,选为新主的优先级越高。
所以,在节点版本一致的情况下,会选择权重最高,server_uuid 最小的节点作为新的主节点。
节点的权重由 group_replication_member_weight 决定,该参数是 MySQL 5.7.20 引入的,可设置 0 到 100 之间的任意整数值,默认是 50。
但如果集群节点版本不一致,实际的选举算法就没这么简单了。
下面,我们结合源码具体分析下。
代码实现逻辑
新主选举算法主要会涉及三个函数:
- pick_primary_member
- sort_and_get_lowest_version_member_position
- sort_members_for_election
这三个函数都是在 primary_election_invocation_handler.cc 中定义的。
其中,pick_primary_member 是主函数,它会基于其它两个函数的结果选择 Primary 节点。
下面,我们从 pick_primary_member 出发,看看这三个函数的具体实现逻辑。
pick_primary_member
bool Primary_election_handler::pick_primary_member(<br/> std::string &primary_uuid,<br/> std::vector<Group_member_info *> *all_members_info) {<br/> DBUG_TRACE;<br/><br/> bool am_i_leaving = true;<br/>#ifndef NDEBUG<br/> int n = 0;<br/>#endif<br/> Group_member_info *the_primary = nullptr;<br/><br/> std::vector<Group_member_info *>::iterator it;<br/> std::vector<Group_member_info *>::iterator lowest_version_end;<br/><br/> // 基于 member_version 选择候选节点。<br/> lowest_version_end =<br/> sort_and_get_lowest_version_member_position(all_members_info);<br/><br/> // 基于节点权重和 server_uuid 对候选节点进行排序。<br/> sort_members_for_election(all_members_info, lowest_version_end);<br/><br/><br/> // 遍历所有节点,判断 Primary 节点是否已定义。<br/> for (it = all_members_info->begin(); it != all_members_info->end(); it++) {<br/>#ifndef NDEBUG<br/> assert(n <= 1);<br/>#endif<br/><br/> Group_member_info *member = *it;<br/> // 如果当前节点是单主模式且遍历的节点中有 Primary 节点,则将该节点赋值给 the_primary<br/> if (local_member_info->in_primary_mode() && the_primary == nullptr &&<br/> member->get_role() == Group_member_info::MEMBER_ROLE_PRIMARY) {<br/> the_primary = member;<br/>#ifndef NDEBUG<br/> n++;<br/>#endif<br/> }<br/><br/> // 检查当前节点的状态是否为 OFFLINE。<br/> if (!member->get_uuid().compare(local_member_info->get_uuid())) {<br/> am_i_leaving =<br/> member->get_recovery_status() == Group_member_info::MEMBER_OFFLINE;<br/> }<br/> }<br/><br/> // 如果当前节点的状态不是 OFFLINE 且 the_primary 还是为空,则选择一个 Primary 节点<br/> if (!am_i_leaving) {<br/> if (the_primary == nullptr) {<br/> // 因为循环的结束条件是 it != lowest_version_end 且 the_primary 为空,所以基本上会将候选节点中的第一个节点作为 Primary 节点。<br/> for (it = all_members_info->begin();<br/> it != lowest_version_end && the_primary == nullptr; it++) {<br/> Group_member_info *member_info = *it;<br/><br/> assert(member_info);<br/> if (member_info && member_info->get_recovery_status() ==<br/> Group_member_info::MEMBER_ONLINE)<br/> the_primary = member_info;<br/> }<br/> }<br/> }<br/><br/> if (the_primary == nullptr) return true;<br/><br/> primary_uuid.assign(the_primary->get_uuid());<br/> return false;<br/>}<br/>
这个函数里面,比较关键的地方有三个:
-
调用 sort_and_get_lowest_version_member_position。
这个函数会基于 member_version (节点版本)选择候选节点。
只有候选节点才有资格被选为主节点 。
-
调用 sort_members_for_election。
这个函数会基于节点权重和 server_uuid,对候选节点进行排序。
-
基于排序后的候选节点选择 Primary 节点。
因为候选节点是从头开始遍历,所以基本上,只要第一个节点是 ONLINE 状态,就会把这个节点作为 Primary 节点。
sort_and_get_lowest_version_member_position
接下来我们看看 sort_and_get_lowest_version_member_position 函数的实现逻辑。
sort_and_get_lowest_version_member_position(<br/> std::vector<Group_member_info *> *all_members_info) {<br/> std::vector<Group_member_info *>::iterator it;<br/><br/> // 按照版本对 all_members_info 从小到大排序<br/> std::sort(all_members_info->begin(), all_members_info->end(),<br/> Group_member_info::comparator_group_member_version);<br/><br/> // std::vector::end 会返回一个迭代器,该迭代器引用 vector (向量容器)中的末尾元素。<br/> // 注意,这个元素指向的是 vector 最后一个元素的下一个位置,不是最后一个元素。<br/> std::vector<Group_member_info *>::iterator lowest_version_end =<br/> all_members_info->end();<br/><br/> // 获取排序后的第一个节点,这个节点版本最低。<br/> it = all_members_info->begin();<br/> Group_member_info *first_member = *it;<br/> // 获取第一个节点的 major_version<br/> // 对于 MySQL 5.7,major_version 是 5;对于 MySQL 8.0,major_version 是 8<br/> uint32 lowest_major_version =<br/> first_member->get_member_version().get_major_version();<br/> <br/> /* to avoid read compatibility issue leader should be picked only from lowest<br/> version members so save position where member version differs.<br/> From 8.0.17 patch version will be considered during version comparison.<br/><br/> set lowest_version_end when major version changes<br/><br/> eg: for a list: 5.7.18, 5.7.18, 5.7.19, 5.7.20, 5.7.21, 8.0.2<br/> the members to be considered for election will be:<br/> 5.7.18, 5.7.18, 5.7.19, 5.7.20, 5.7.21<br/> and server_uuid based algorithm will be used to elect primary<br/><br/> eg: for a list: 5.7.20, 5.7.21, 8.0.2, 8.0.2<br/> the members to be considered for election will be:<br/> 5.7.20, 5.7.21<br/> and member weight based algorithm will be used to elect primary<br/><br/> eg: for a list: 8.0.17, 8.0.18, 8.0.19<br/> the members to be considered for election will be:<br/> 8.0.17<br/><br/> eg: for a list: 8.0.13, 8.0.17, 8.0.18<br/> the members to be considered for election will be:<br/> 8.0.13, 8.0.17, 8.0.18<br/> and member weight based algorithm will be used to elect primary<br/> */<br/> <br/><br/> // 遍历剩下的节点,注意 it 是从 all_members_info->begin() + 1 开始的<br/> for (it = all_members_info->begin() + 1; it != all_members_info->end();<br/> it++) {<br/> // 如果第一个节点的版本号大于 MySQL 8.0.17,且节点的版本号不等于第一个节点的版本号,则将该节点赋值给 lowest_version_end,并退出循环。<br/> if (first_member->get_member_version() >=<br/> PRIMARY_ELECTION_PATCH_CONSIDERATION &&<br/> (first_member->get_member_version() != (*it)->get_member_version())) {<br/> lowest_version_end = it;<br/> break;<br/> }<br/> // 如果节点的 major_version 不等于第一个节点的 major_version,则将该节点赋值给 lowest_version_end,并退出循环。<br/> if (lowest_major_version !=<br/> (*it)->get_member_version().get_major_version()) {<br/> lowest_version_end = it;<br/> break;<br/> }<br/> }<br/> return lowest_version_end;<br/>}<br/>
函数中的 PRIMARY_ELECTION_PATCH_CONSIDERATION 是 0x080017,即 MySQL 8.0.17。
在 MySQL 8.0.17 中,Group Replication 引入了兼容性策略。引入兼容性策略的初衷是为了避免集群中出现节点不兼容的情况。
该函数首先会对 all_members_info 按照版本从小到大排序。
接着会基于第一个节点的版本(最小版本)确定 lowest_version_end。
MGR 用 lowest_version_end 标记最低版本的结束点。只有 lowest_version_end 之前的节点才是候选节点。
lowest_version_end 的取值逻辑如下:
- 如果最小版本大于等于 MySQL 8.0.17,则会将最小版本之后的第一个节点设置为 lowest_version_end。
- 如果集群中既有 5.7,又有 8.0,则会将 8.0 的第一个节点设置为 lowest_version_end。
- 如果最小版本小于 MySQL 8.0.17,且只有一个大版本(major_version),则会取 all_members_info->end()。此时,所有节点都是候选节点。
为了方便大家理解代码的逻辑,函数注释部分还列举了四个案例,每个案例对应一个典型场景。后面我们会具体分析下。
sort_members_for_election
最后,我们看看 sort_members_for_election 函数的实现逻辑。
void sort_members_for_election(<br/> std::vector<Group_member_info *> *all_members_info,<br/> std::vector<Group_member_info *>::iterator lowest_version_end) {<br/> Group_member_info *first_member = *(all_members_info->begin());<br/> // 获取第一个节点的版本,这个节点版本最低。<br/> Member_version lowest_version = first_member->get_member_version();<br/><br/> // 如果最小版本大于等于 MySQL 5.7.20,则根据节点的权重来排序。权重越高,在 vector 中的位置越靠前。<br/> // 注意,这里只会对 [all_members_info->begin(), lowest_version_end) 这个区间内的元素进行排序,不包括 lowest_version_end。<br/> if (lowest_version >= PRIMARY_ELECTION_MEMBER_WEIGHT_VERSION)<br/> std::sort(all_members_info->begin(), lowest_version_end,<br/> Group_member_info::comparator_group_member_weight);<br/> else<br/> // 如果最小版本小于 MySQL 5.7.20,则根据节点的 server_uuid 来排序。server_uuid 越小,在 vector 中的位置越靠前。<br/> std::sort(all_members_info->begin(), lowest_version_end,<br/> Group_member_info::comparator_group_member_uuid);<br/>}<br/>
函数中的 PRIMARY_ELECTION_MEMBER_WEIGHT_VERSION 是 0x050720,即 MySQL 5.7.20。
如果最小节点的版本大于等于 MySQL 5.7.20,则会基于权重来排序。权重越高,在 all_members_info 中的位置越靠前。
如果最小节点的版本小于 MySQL 5.7.20,则会基于节点的 server_uuid 来排序。server_uuid 越小,在 all_members_info 中的位置越靠前。
注意,std::sort 中的结束位置是 lowest_version_end,所以 lowest_version_end 这个节点不会参与排序。
comparator_group_member_weight
在基于权重进行排序时,如果两个节点的权重一致,还会进一步比较这两个节点的 server_uuid。
这个逻辑是在 comparator_group_member_weight 中定义的。
权重一致,节点的 server_uuid 越小,在 all_members_info 中的位置越靠前。
bool Group_member_info::comparator_group_member_weight(Group_member_info *m1,<br/> Group_member_info *m2) {<br/> return m1->has_greater_weight(m2);<br/>}<br/><br/>bool Group_member_info::has_greater_weight(Group_member_info *other) {<br/> MUTEX_LOCK(lock, &update_lock);<br/> if (member_weight > other->get_member_weight()) return true;<br/> // 如果权重一致,会按照节点的 server_uuid 来排序。<br/> if (member_weight == other->get_member_weight())<br/> return has_lower_uuid_internal(other);<br/><br/> return false;<br/>}<br/>
案例分析
基于上面代码的逻辑,接下来我们分析下 sort_and_get_lowest_version_member_position 函数注释部分列举的四个案例:
案例 1:5.7.18, 5.7.18, 5.7.19, 5.7.20, 5.7.21, 8.0.2
1. 这几个节点中,最小版本号是 5.7.18,小于 MySQL 8.0.17。所以会比较各个节点的 major_version,因为最后一个节点(8.0.2)的 major_version 和第一个节点不一致,所以会将 8.0.2 作为 lowest_version_end。此时,除了 8.0.2,其它都是候选节点。
2. 最小版本号 5.7.18 小于 MySQL 5.7.20,所以 5.7.18, 5.7.18, 5.7.19, 5.7.20, 5.7.21 这几个节点会根据 server_uuid 进行排序。注意,lowest_version_end 的节点不会参与排序。
3. 选择 server_uuid 最小的节点作为 Primary 节点。
案例 2:5.7.20, 5.7.21, 8.0.2, 8.0.2
1. 同案例 1 一样,会将 8.0.2 作为 lowest_version_end。此时,候选节点只有 5.7.20 和 5.7.21。
2. 最小版本号 5.7.20 等于 MySQL 5.7.20,所以,5.7.20, 5.7.21 这两个节点会根据节点的权重进行排序。如果权重一致,则会基于 server_uuid 进行进一步的排序。
3. 选择权重最高,server_uuid 最小的节点作为 Primary 节点。
案例 3:8.0.17, 8.0.18, 8.0.19
1. 最小版本号是 MySQL 8.0.17,等于 MySQL 8.0.17,所以会判断其它节点的版本号是否与第一个节点相同。不相同,则会将该节点的版本号赋值给 lowest_version_end。所以,会将 8.0.18 作为 lowest_version_end。此时,候选节点只有 8.0.17。
2. 选择 8.0.17 这个节点作为 Primary 节点。
案例 4:8.0.13, 8.0.17, 8.0.18
1. 最小版本号是 MySQL 8.0.13,小于 MySQL 8.0.17,而且各个节点的 major_version 一致,所以最后返回的 lowest_version_end 实际上是 all_members_info->end()。此时,这三个节点都是候选节点。
2. MySQL 8.0.13 大于 MySQL 5.7.20,所以这三个节点会根据权重进行排序。如果权重一致,则会基于 server_uuid 进行进一步的排序。
3. 选择权重最高,server_uuid 最小的节点作为 Primary 节点。
手动选主
从 MySQL 8.0.13 开始,我们可以通过以下两个函数手动选择新的主节点:
- group_replication_set_as_primary(server_uuid) :切换单主模式下的 Primary 节点。
- group_replication_switch_to_single_primary_mode([server_uuid]) :将多主模式切换为单主模式。可通过 server_uuid 指定单主模式下的 Primary 节点。
在使用这两个参数时,注意,指定的 server_uuid 必须属于候选节点。
另外,这两个函数是 MySQL 8.0.13 引入的,所以,如果集群中存在 MySQL 8.0.13 之前的节点,执行时会报错。
mysql> select group_replication_set_as_primary('5470a304-3bfa-11ed-8bee-83f233272a5d');<br/>ERROR 3910 (HY000): The function 'group_replication_set_as_primary' failed. The group has a member with a version that does not support group coordinated operations.<br/>
总结
结合代码和上面四个案例的分析,最后我们总结下 MGR 的新主选举算法:
1. 如果集群中存在 MySQL 5.7 的节点,则会将 MySQL 5.7 的节点作为候选节点。
2. 如果集群节点的版本都是 MySQL 8.0,这里需要区分两种情况:
- 如果最小版本小于 MySQL 8.0.17,则所有的节点都可作为候选节点。
- 如果最小版本大于等于 MySQL 8.0.17,则只有最小版本的节点会作为候选节点。
3. 在候选节点的基础上,会进一步根据候选节点的权重和 server_uuid 选择 Primary 节点。具体来说,
- 如果候选节点中存在 MySQL 5.7.20 之前版本的节点,则会选择 server_uuid 最小的节点作为 Primary 节点。
- 如果候选节点都大于等于 MySQL 5.7.20,则会选择权重最高,server_uuid 最小的节点作为 Primary 节点。
文章评论