如何讓 MGR 不從 Primary 節點克隆資料?

iVictor發表於2024-07-22

問題

MGR 中,新節點在加入時,為了與組內其它節點的資料保持一致,它會首先經歷一個分散式恢復階段。在這個階段,新節點會隨機選擇組內一個節點(Donor)來同步差異資料。

在 MySQL 8.0.17 之前,同步的方式只有一種,即基於 Binlog 的非同步複製,這種方式適用於差異資料較少或需要的 Binlog 都存在的場景。

從 MySQL 8.0.17 開始,新增了一種同步方式-克隆外掛,克隆外掛可用來進行物理備份恢復,這種方式適用於差異資料較多或需要的 Binlog 已被 purge 的場景。

克隆外掛雖然極大提升了恢復的效率,但備份畢竟是一個 IO 密集型的操作,很容易影響備份例項的效能,所以,我們一般不希望克隆操作在 Primary 節點上執行。

但 Donor 的選擇是隨機的(後面會證明這一點),有沒有辦法讓 MGR 不從 Primary 節點克隆資料呢?

本文主要包括以下幾部分:

  1. MGR 是如何執行克隆操作的?
  2. 可以透過 clone_valid_donor_list 設定 Donor 麼?
  3. MGR 是如何選擇 Donor 的?
  4. MGR 克隆操作的實現邏輯。
  5. group_replication_advertise_recovery_endpoints 的生效時機。

MGR 是如何執行克隆操作的?

起初還以為 MGR 執行克隆操作是呼叫克隆外掛的一些內部介面。但實際上,MGR 呼叫的就是CLONE INSTANCE命令。

// plugin/group_replication/src/sql_service/sql_service_command.cc
long Sql_service_commands::internal_clone_server(
Sql_service_interface *sql_interface, void *var_args) {
...
std::string query = "CLONE INSTANCE FROM \'";
query.append(q_user);
query.append("\'@\'");
query.append(q_hostname);
query.append("\':");
query.append(std::get<1>(*variable_args));
query.append(" IDENTIFIED BY \'");
query.append(q_password);
bool use_ssl = std::get<4>(*variable_args);
if (use_ssl)
query.append("\' REQUIRE SSL;");
else
query.append("\' REQUIRE NO SSL;");

Sql_resultset rset;
long srv_err = sql_interface->execute_query(query, &rset);
...
}

既然呼叫的是 CLONE INSTANCE 命令,那是不是就可以透過 clone_valid_donor_list 引數來設定 Donor(被克隆例項)呢?

可以透過 clone_valid_donor_list 設定Donor麼

不能。

在獲取到 Donor 的 endpoint(端點,由 hostname 和 port 組成)後,MGR 會透過update_donor_list函式設定 clone_valid_donor_list。

clone_valid_donor_list 的值即為 Donor 的 endpoint。

所以,在啟動組複製之前,在 mysql 客戶端中顯式設定 clone_valid_donor_list 是沒有效果的。

// plugin/group_replication/src/plugin_handlers/remote_clone_handler.cc
int Remote_clone_handler::update_donor_list(
Sql_service_command_interface *sql_command_interface, std::string &hostname,
std::string &port) {
std::string donor_list_query = " SET GLOBAL clone_valid_donor_list = \'";
plugin_escape_string(hostname);
donor_list_query.append(hostname);
donor_list_query.append(":");
donor_list_query.append(port);
donor_list_query.append("\'");
std::string error_msg;
if (sql_command_interface->execute_query(donor_list_query, error_msg)) {
...
}
return 0;
}

既然是先有 Donor,然後才會設定 clone_valid_donor_list,接下來我們看看 MGR 是如何選擇 Donor 的?

MGR 是如何選擇 Donor 的?

MGR 選擇 Donor 可分為以下兩步:

  1. 首先,判斷哪些節點適合當 Donor。滿足條件的節點會放到一個動態陣列(m_suitable_donors)中, 這個操作是在Remote_clone_handler::get_clone_donors函式中實現的。
  2. 其次,迴圈遍歷 m_suitable_donors 中的節點作為 Donor。如果第一個節點執行克隆操作失敗,則會選擇第二個節點,依次類推。

下面,我們看看Remote_clone_handler::get_clone_donors的實現細節。

void Remote_clone_handler::get_clone_donors(
std::list<Group_member_info *> &suitable_donors) {
// 獲取叢集所有節點的資訊
Group_member_info_list *all_members_info =
group_member_mgr->get_all_members();
if (all_members_info->size() > 1) {
// 這裡將原來的 all_members_info 打亂了,從這裡可以看到 donor 是隨機選擇的。
vector_random_shuffle(all_members_info);
}

for (Group_member_info *member : *all_members_info) {
std::string m_uuid = member->get_uuid();
bool is_online =
member->get_recovery_status() == Group_member_info::MEMBER_ONLINE;
bool not_self = m_uuid.compare(local_member_info->get_uuid());
// 注意,這裡只是比較了版本
bool supports_clone =
member->get_member_version().get_version() >=
CLONE_GR_SUPPORT_VERSION &&
member->get_member_version().get_version() ==
local_member_info->get_member_version().get_version();

if (is_online && not_self && supports_clone) {
suitable_donors.push_back(member);
} else {
delete member;
}
}

delete all_members_info;
}

該函式的處理流程如下:

  1. 獲取叢集所有節點的資訊,儲存到 all_members_info 中。

    all_members_info 是個動態陣列,陣列中的元素是按照節點 server_uuid 從小到大的順序依次儲存的。

  2. 透過vector_random_shuffle函式將 all_members_info 進行隨機重排。

  3. 選擇 ONLINE 狀態且版本大於等於 8.0.17 的節點新增到 suitable_donors 中。

    為什麼是 8.0.17 呢,因為克隆外掛是 MySQL 8.0.17 引入的。

    注意,這裡只是比較了版本,沒有判斷克隆外掛是否真正載入。

函式中的 suitable_donors 實際上就是 m_suitable_donors。

get_clone_donors(m_suitable_donors);

基於前面的分析,可以看到,在 MGR 中,作為被克隆節點的 Donor 是隨機選擇的。

既然 Donor 的選擇是隨機的,想不從 Primary 節點克隆資料似乎是實現不了的。

分析到這裡,問題似乎是無解了。

別急,接下來讓我們分析下 MGR 克隆操作的實現邏輯。

MGR 克隆操作的實現邏輯

MGR 克隆操作是在Remote_clone_handler::clone_thread_handle函式中實現的。

// plugin/group_replication/src/plugin_handlers/remote_clone_handler.cc
[[noreturn]] void Remote_clone_handler::clone_thread_handle() {
...
while (!empty_donor_list && !m_being_terminated) {
stage_handler.set_completed_work(number_attempts);
number_attempts++;

std::string hostname("");
std::string port("");
std::vector<std::pair<std::string, uint>> endpoints;

mysql_mutex_lock(&m_donor_list_lock);
// m_suitable_donors 是所有符合 Donor 條件的節點
empty_donor_list = m_suitable_donors.empty();
if (!empty_donor_list) {
// 獲取陣列中的第一個元素
Group_member_info *member = m_suitable_donors.front();
Donor_recovery_endpoints donor_endpoints;
// 獲取 Donor 的端點資訊
endpoints = donor_endpoints.get_endpoints(member);
...
// 從陣列中移除第一個元素
m_suitable_donors.pop_front();
delete member;
empty_donor_list = m_suitable_donors.empty();
number_servers = m_suitable_donors.size();
}
mysql_mutex_unlock(&m_donor_list_lock);

// No valid donor in the list
if (endpoints.size() == 0) {
error = 1;
continue;
}
// 迴圈遍歷 endpoints 中的每個端點
for (auto endpoint : endpoints) {
hostname.assign(endpoint.first);
port.assign(std::to_string(endpoint.second));

// 設定 clone_valid_donor_list
if ((error = update_donor_list(sql_command_interface, hostname, port))) {
continue; /* purecov: inspected */
}

if (m_being_terminated) goto thd_end;

terminate_wait_on_start_process(WAIT_ON_START_PROCESS_ABORT_ON_CLONE);
// 執行克隆操作
error = run_clone_query(sql_command_interface, hostname, port, username,
password, use_ssl);

// Even on critical errors we continue as another clone can fix the issue
if (!critical_error) critical_error = evaluate_error_code(error);

// On ER_RESTART_SERVER_FAILED it makes no sense to retry
if (error == ER_RESTART_SERVER_FAILED) goto thd_end;

if (error && !m_being_terminated) {
if (evaluate_server_connection(sql_command_interface)) {
critical_error = true;
goto thd_end;
}

if (group_member_mgr->get_number_of_members() == 1) {
critical_error = true;
goto thd_end;
}
}

// 如果失敗,則選擇下一個端點進行重試。
if (!error) break;
}

// 如果失敗,則選擇下一個 Donor 進行重試。
if (!error) break;
}
...
}

該函式的處理流程如下:

  1. 首先會選擇一個 Donor。可以看到,程式碼中是透過front()函式來獲取 m_suitable_donors 中的第一個元素。
  2. 獲取 Donor 的端點資訊。
  3. 迴圈遍歷 endpoints 中的每個端點。
  4. 設定 clone_valid_donor_list。
  5. 執行克隆操作。如果操作失敗,則會進行重試,首先是選擇下一個端點進行重試。如果所有端點都遍歷完了,還是沒有成功,則會選擇下一個 Donor 進行重試,直到遍歷完所有 Donor。

當然,重試是有條件的,出現以下情況就不會進行重試:

  1. error == ER_RESTART_SERVER_FAILED:例項重啟失敗。

    例項重啟是克隆操作的最後一步,之前的步驟依次是:1. 獲取備份鎖。2. DROP 使用者表空間。3. 從 Donor 例項複製資料。

    既然資料都已經複製完了,就沒有必要進行重試了。

  2. 執行克隆操作的連線被 KILL 了且重建失敗。

  3. group_member_mgr->get_number_of_members() == 1:叢集只有一個節點。

既然克隆操作失敗了會進行重試,那麼思路來了,如果不想克隆操作在 Primary 節點上執行,很簡單,讓 Primary 節點上的克隆操作失敗了就行。

怎麼讓它失敗呢?

一個克隆操作,如果要在 Donor(被克隆節點)上成功執行,Donor 需滿足以下條件:

  1. 安裝克隆外掛。
  2. 克隆使用者需要 BACKUP_ADMIN 許可權。

所以,如果要讓克隆操作失敗,任意一個條件不滿足即可。推薦第一個,即不安裝或者解除安裝克隆外掛。

為什麼不推薦回收許可權這種方式呢?

因為解除安裝克隆外掛這個操作(uninstall plugin clone)不會記錄 Binlog,而回收許可權會。

雖然回收許可權的操作也可以透過SET SQL_LOG_BIN=0 的方式不記錄 Binlog,但這樣又會導致叢集各節點的資料不一致。所以,非常不推薦回收許可權這種方式。

所以,如果不想 MGR 從 Primary 節點克隆資料,直接解除安裝 Primary 節點的克隆外掛即可。

問題雖然解決了,但還是有一個疑問:endpoints 中為什麼會有多個端點呢?不應該就是 Donor 的例項地址,只有一個麼?這個實際上與 group_replication_advertise_recovery_endpoints 有關。

group_replication_advertise_recovery_endpoints

group_replication_advertise_recovery_endpoints 引數是 MySQL 8.0.21 引入的,用來自定義恢復地址。

看下面這個示例。

group_replication_advertise_recovery_endpoints= "127.0.0.1:3306,127.0.0.1:4567,[::1]:3306,localhost:3306"

在設定時,要求埠必須來自 port、report_port 或者 admin_port。

而主機名只要是伺服器上的有效地址即可(一臺伺服器上可能存在多張網路卡,對應的會有多個 IP),無需在 bind_address 或 admin_address 中指定。

除此之外,如果要透過 admin_port 進行分散式恢復操作,使用者還需要授予 SERVICE_CONNECTION_ADMIN 許可權。

下面我們看看 group_replication_advertise_recovery_endpoints 的生效時機。

在選擇完 Donor 後,MGR 會呼叫get_endpoints來獲取這個 Donor 的 endpoints。

// plugin/group_replication/src/plugin_variables/recovery_endpoints.cc
Donor_recovery_endpoints::get_endpoints(Group_member_info *donor) {
...
std::vector<std::pair<std::string, uint>> endpoints;
// donor->get_recovery_endpoints().c_str() 即 group_replication_advertise_recovery_endpoints 的值
if (strcmp(donor->get_recovery_endpoints().c_str(), "DEFAULT") == 0) {
error = Recovery_endpoints::enum_status::OK;
endpoints.push_back(
std::pair<std::string, uint>{donor->get_hostname(), donor->get_port()});
} else {
std::tie(error, err_string) =
check(donor->get_recovery_endpoints().c_str());
if (error == Recovery_endpoints::enum_status::OK)
endpoints = Recovery_endpoints::get_endpoints();
}
...
return endpoints;
}

如果 group_replication_advertise_recovery_endpoints 為 DEFAULT(預設值),則會將 Donor 的 hostname 和 port 設定為 endpoint。

注意,節點的 hostname、port 實際上就是 performance_schema.replication_group_members 中的 MEMBER_HOST、 MEMBER_PORT。

hostname 和 port 的取值邏輯如下:

// sql/rpl_group_replication.cc
void get_server_parameters(char **hostname, uint *port, char **uuid,
unsigned int *out_server_version,
uint *out_admin_port) {
...
if (report_host)
*hostname = report_host;
else
*hostname = glob_hostname;

if (report_port)
*port = report_port;
else
*port = mysqld_port;
...
return;
}

優先使用 report_host、report_port,其次才是主機名、mysqld 的埠。

如果 group_replication_advertise_recovery_endpoints 不為 DEFAULT,則會該引數的值設定為 endpoints。

所以,一個節點,只有被選擇為 Donor,設定的 group_replication_advertise_recovery_endpoints 才會有效果。

而節點有沒有設定 group_replication_advertise_recovery_endpoints 與它能否被選擇為 Donor 沒有任何關係。

總結

  1. MGR 選擇 Donor 是隨機的。
  2. MGR 在執行克隆操作之前,會將 clone_valid_donor_list 設定為 Donor 的 endpoint,所以,在啟動組複製之前,在 mysql 客戶端中顯式設定 clone_valid_donor_list 是沒有效果的。
  3. MGR 執行克隆操作,實際上呼叫的就是CLONE INSTANCE命令。
  4. performance_schema.replication_group_members 中的 MEMBER_HOST 和 MEMBER_PORT,優先使用 report_host、report_port,其次才是主機名、mysqld 的埠。
  5. 一個節點,只有被選擇為 Donor,設定的 group_replication_advertise_recovery_endpoints 才會有效果。
  6. 如果不想 MGR 從 Primary 節點克隆資料,直接解除安裝 Primary 節點的克隆外掛即可。

延伸閱讀

  1. MySQL 8.0 新特性之 Clone Plugin
  2. 《MySQL實戰》組複製章節

相關文章