MGR 的新主選舉演演算法,在節點版本一致的情況下,其實也挺簡單的。
首先比較權重,權重越高,選為新主的優先順序越高。
如果權重一致,則會進一步比較節點的 server_uuid。server_uuid 越小,選為新主的優先順序越高。
所以,在節點版本一致的情況下,會選擇權重最高,server_uuid 最小的節點作為新的主節點。
節點的權重由 group_replication_member_weight 決定,該引數是 MySQL 5.7.20 引入的,可設定 0 到 100 之間的任意整數值,預設是 50。
但如果叢集節點版本不一致,實際的選舉演演算法就沒這麼簡單了。
下面,我們結合原始碼具體分析下。
新主選舉演演算法主要會涉及三個函數:
這三個函數都是在 primary_election_invocation_handler.cc 中定義的。
其中,pick_primary_member 是主函數,它會基於其它兩個函數的結果選擇 Primary 節點。
下面,我們從 pick_primary_member 出發,看看這三個函數的具體實現邏輯。
bool Primary_election_handler::pick_primary_member(
std::string &primary_uuid,
std::vector<Group_member_info *> *all_members_info) {
DBUG_TRACE;
bool am_i_leaving = true;
#ifndef NDEBUG
int n = 0;
#endif
Group_member_info *the_primary = nullptr;
std::vector<Group_member_info *>::iterator it;
std::vector<Group_member_info *>::iterator lowest_version_end;
// 基於 member_version 選擇候選節點。
lowest_version_end =
sort_and_get_lowest_version_member_position(all_members_info);
// 基於節點權重和 server_uuid 對候選節點進行排序。
sort_members_for_election(all_members_info, lowest_version_end);
// 遍歷所有節點,判斷 Primary 節點是否已定義。
for (it = all_members_info->begin(); it != all_members_info->end(); it++) {
#ifndef NDEBUG
assert(n <= 1);
#endif
Group_member_info *member = *it;
// 如果當前節點是單主模式且遍歷的節點中有 Primary 節點,則將該節點賦值給 the_primary
if (local_member_info->in_primary_mode() && the_primary == nullptr &&
member->get_role() == Group_member_info::MEMBER_ROLE_PRIMARY) {
the_primary = member;
#ifndef NDEBUG
n++;
#endif
}
// 檢查當前節點的狀態是否為 OFFLINE。
if (!member->get_uuid().compare(local_member_info->get_uuid())) {
am_i_leaving =
member->get_recovery_status() == Group_member_info::MEMBER_OFFLINE;
}
}
// 如果當前節點的狀態不是 OFFLINE 且 the_primary 還是為空,則選擇一個 Primary 節點
if (!am_i_leaving) {
if (the_primary == nullptr) {
// 因為迴圈的結束條件是 it != lowest_version_end 且 the_primary 為空,所以基本上會將候選節點中的第一個節點作為 Primary 節點。
for (it = all_members_info->begin();
it != lowest_version_end && the_primary == nullptr; it++) {
Group_member_info *member_info = *it;
assert(member_info);
if (member_info && member_info->get_recovery_status() ==
Group_member_info::MEMBER_ONLINE)
the_primary = member_info;
}
}
}
if (the_primary == nullptr) return true;
primary_uuid.assign(the_primary->get_uuid());
return false;
}
這個函數裡面,比較關鍵的地方有三個:
呼叫 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(
std::vector<Group_member_info *> *all_members_info) {
std::vector<Group_member_info *>::iterator it;
// 按照版本對 all_members_info 從小到大排序
std::sort(all_members_info->begin(), all_members_info->end(),
Group_member_info::comparator_group_member_version);
// std::vector::end 會返回一個迭代器,該迭代器參照 vector (向量容器)中的末尾元素。
// 注意,這個元素指向的是 vector 最後一個元素的下一個位置,不是最後一個元素。
std::vector<Group_member_info *>::iterator lowest_version_end =
all_members_info->end();
// 獲取排序後的第一個節點,這個節點版本最低。
it = all_members_info->begin();
Group_member_info *first_member = *it;
// 獲取第一個節點的 major_version
// 對於 MySQL 5.7,major_version 是 5;對於 MySQL 8.0,major_version 是 8
uint32 lowest_major_version =
first_member->get_member_version().get_major_version();
/* to avoid read compatibility issue leader should be picked only from lowest
version members so save position where member version differs.
From 8.0.17 patch version will be considered during version comparison.
set lowest_version_end when major version changes
eg: for a list: 5.7.18, 5.7.18, 5.7.19, 5.7.20, 5.7.21, 8.0.2
the members to be considered for election will be:
5.7.18, 5.7.18, 5.7.19, 5.7.20, 5.7.21
and server_uuid based algorithm will be used to elect primary
eg: for a list: 5.7.20, 5.7.21, 8.0.2, 8.0.2
the members to be considered for election will be:
5.7.20, 5.7.21
and member weight based algorithm will be used to elect primary
eg: for a list: 8.0.17, 8.0.18, 8.0.19
the members to be considered for election will be:
8.0.17
eg: for a list: 8.0.13, 8.0.17, 8.0.18
the members to be considered for election will be:
8.0.13, 8.0.17, 8.0.18
and member weight based algorithm will be used to elect primary
*/
// 遍歷剩下的節點,注意 it 是從 all_members_info->begin() + 1 開始的
for (it = all_members_info->begin() + 1; it != all_members_info->end();
it++) {
// 如果第一個節點的版本號大於 MySQL 8.0.17,且節點的版本號不等於第一個節點的版本號,則將該節點賦值給 lowest_version_end,並退出迴圈。
if (first_member->get_member_version() >=
PRIMARY_ELECTION_PATCH_CONSIDERATION &&
(first_member->get_member_version() != (*it)->get_member_version())) {
lowest_version_end = it;
break;
}
// 如果節點的 major_version 不等於第一個節點的 major_version,則將該節點賦值給 lowest_version_end,並退出迴圈。
if (lowest_major_version !=
(*it)->get_member_version().get_major_version()) {
lowest_version_end = it;
break;
}
}
return lowest_version_end;
}
函數中的 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 的取值邏輯如下:
為了方便大家理解程式碼的邏輯,函數註釋部分還列舉了四個案例,每個案例對應一個典型場景。後面我們會具體分析下。
最後,我們看看 sort_members_for_election 函數的實現邏輯。
void sort_members_for_election(
std::vector<Group_member_info *> *all_members_info,
std::vector<Group_member_info *>::iterator lowest_version_end) {
Group_member_info *first_member = *(all_members_info->begin());
// 獲取第一個節點的版本,這個節點版本最低。
Member_version lowest_version = first_member->get_member_version();
// 如果最小版本大於等於 MySQL 5.7.20,則根據節點的權重來排序。權重越高,在 vector 中的位置越靠前。
// 注意,這裡只會對 [all_members_info->begin(), lowest_version_end) 這個區間內的元素進行排序,不包括 lowest_version_end。
if (lowest_version >= PRIMARY_ELECTION_MEMBER_WEIGHT_VERSION)
std::sort(all_members_info->begin(), lowest_version_end,
Group_member_info::comparator_group_member_weight);
else
// 如果最小版本小於 MySQL 5.7.20,則根據節點的 server_uuid 來排序。server_uuid 越小,在 vector 中的位置越靠前。
std::sort(all_members_info->begin(), lowest_version_end,
Group_member_info::comparator_group_member_uuid);
}
函數中的 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 這個節點不會參與排序。
在基於權重進行排序時,如果兩個節點的權重一致,還會進一步比較這兩個節點的 server_uuid。
這個邏輯是在 comparator_group_member_weight 中定義的。
權重一致,節點的 server_uuid 越小,在 all_members_info 中的位置越靠前。
bool Group_member_info::comparator_group_member_weight(Group_member_info *m1,
Group_member_info *m2) {
return m1->has_greater_weight(m2);
}
bool Group_member_info::has_greater_weight(Group_member_info *other) {
MUTEX_LOCK(lock, &update_lock);
if (member_weight > other->get_member_weight()) return true;
// 如果權重一致,會按照節點的 server_uuid 來排序。
if (member_weight == other->get_member_weight())
return has_lower_uuid_internal(other);
return false;
}
基於上面程式碼的邏輯,接下來我們分析下 sort_and_get_lowest_version_member_position 函數註釋部分列舉的四個案例:
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 節點。
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 節點。
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 節點。
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 開始,我們可以通過以下兩個函數手動選擇新的主節點:
在使用這兩個引數時,注意,指定的 server_uuid 必須屬於候選節點。
另外,這兩個函數是 MySQL 8.0.13 引入的,所以,如果叢集中存在 MySQL 8.0.13 之前的節點,執行時會報錯。
mysql> select group_replication_set_as_primary('5470a304-3bfa-11ed-8bee-83f233272a5d');
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.
結合程式碼和上面四個案例的分析,最後我們總結下 MGR 的新主選舉演演算法:
1. 如果叢集中存在 MySQL 5.7 的節點,則會將 MySQL 5.7 的節點作為候選節點。
2. 如果叢集節點的版本都是 MySQL 8.0,這裡需要區分兩種情況:
3. 在候選節點的基礎上,會進一步根據候選節點的權重和 server_uuid 選擇 Primary 節點。具體來說,