天天看點

從源碼分析 MySQL Group Replication 的新主選舉算法代碼實作邏輯案例分析手動選主總結

MGR 的新主選舉算法,在節點版本一緻的情況下,其實也挺簡單的。

首先比較權重,權重越高,選為新主的優先級越高。

如果權重一緻,則會進一步比較節點的 server_uuid。server_uuid 越小,選為新主的優先級越高。

是以,在節點版本一緻的情況下,會選擇權重最高,server_uuid 最小的節點作為新的主節點。

節點的權重由 group_replication_member_weight 決定,該參數是 MySQL 5.7.20 引入的,可設定 0 到 100 之間的任意整數值,預設是 50。

但如果叢集節點版本不一緻,實際的選舉算法就沒這麼簡單了。

下面,我們結合源碼具體分析下。

代碼實作邏輯

新主選舉算法主要會涉及三個函數:

  1. pick_primary_member
  2. sort_and_get_lowest_version_member_position
  3. 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(
    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;
}
           

這個函數裡面,比較關鍵的地方有三個:

  1. 調用 sort_and_get_lowest_version_member_position。

    這個函數會基于 member_version (節點版本)選擇候選節點。

    隻有候選節點才有資格被選為主節點 。

  2. 調用 sort_members_for_election。

    這個函數會基于節點權重和 server_uuid,對候選節點進行排序。

  3. 基于排序後的候選節點選擇 Primary 節點。

    因為候選節點是從頭開始周遊,是以基本上,隻要第一個節點是 ONLINE 狀态,就會把這個節點作為 Primary 節點。

sort_and_get_lowest_version_member_position

接下來我們看看 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 的取值邏輯如下:

  1. 如果最小版本大于等于 MySQL 8.0.17,則會将最小版本之後的第一個節點設定為 lowest_version_end。
  2. 如果叢集中既有 5.7,又有 8.0,則會将 8.0 的第一個節點設定為 lowest_version_end。
  3. 如果最小版本小于 MySQL 8.0.17,且隻有一個大版本(major_version),則會取 all_members_info->end()。此時,所有節點都是候選節點。

為了友善大家了解代碼的邏輯,函數注釋部分還列舉了四個案例,每個案例對應一個典型場景。後面我們會具體分析下。

sort_members_for_election

最後,我們看看 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 這個節點不會參與排序。

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,
                                                       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, 5.7.18, 5.7.19, 5.7.20, 5.7.21, 8.0.2

這幾個節點中,最小版本号是 5.7.18,小于 MySQL 8.0.17。是以會比較各個節點的 major_version,因為最後一個節點(8.0.2)的 major_version 和第一個節點不一緻,是以會将 8.0.2 作為 lowest_version_end。此時,除了 8.0.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 的節點不會參與排序。

選擇 server_uuid 最小的節點作為 Primary 節點。

案例 2:5.7.20, 5.7.21, 8.0.2, 8.0.2

同案例 1 一樣,會将 8.0.2 作為 lowest_version_end。此時,候選節點隻有 5.7.20 和 5.7.21。

最小版本号 5.7.20 等于 MySQL 5.7.20,是以,5.7.20, 5.7.21 這兩個節點會根據節點的權重進行排序。如果權重一緻,則會基于 server_uuid 進行進一步的排序。

選擇權重最高,server_uuid 最小的節點作為 Primary 節點。

案例 3:8.0.17, 8.0.18, 8.0.19

最小版本号是 MySQL 8.0.17,等于 MySQL 8.0.17,是以會判斷其它節點的版本号是否與第一個節點相同。不相同,則會将該節點的版本号指派給 lowest_version_end。是以,會将 8.0.18 作為 lowest_version_end。此時,候選節點隻有 8.0.17。

選擇 8.0.17 這個節點作為 Primary 節點。

案例 4:8.0.13, 8.0.17, 8.0.18

最小版本号是 MySQL 8.0.13,小于 MySQL 8.0.17,而且各個節點的 major_version 一緻,是以最後傳回的 lowest_version_end 實際上是 all_members_info->end()。此時,這三個節點都是候選節點。

MySQL 8.0.13 大于 MySQL 5.7.20,是以這三個節點會根據權重進行排序。如果權重一緻,則會基于 server_uuid 進行進一步的排序。

選擇權重最高,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');
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 的新主選舉算法:

  • 如果最小版本小于 MySQL 8.0.17,則所有的節點都可作為候選節點。
  • 如果最小版本大于等于 MySQL 8.0.17,則隻有最小版本的節點會作為候選節點。
  • 如果候選節點中存在 MySQL 5.7.20 之前版本的節點,則會選擇 server_uuid 最小的節點作為 Primary 節點。
  • 如果候選節點都大于等于 MySQL 5.7.20,則會選擇權重最高,server_uuid 最小的節點作為 Primary 節點。

繼續閱讀