問題
MGR 中,新節點在加入時,為了與組內其它節點的數據保持一致,它會首先經歷一個分布式恢復階段。在這個階段,新節點會隨機選擇組內一個節點(Donor)來同步差異數據。
在 MySQL 8.0.17 之前,同步的方式只有一種,即基于 Binlog 的異步復制,這種方式適用于差異數據較少或需要的 Binlog 都存在的場景。
從 MySQL 8.0.17 開始,新增了一種同步方式-克隆插件,克隆插件可用來進行物理備份恢復,這種方式適用于差異數據較多或需要的 Binlog 已被 purge 的場景。
克隆插件雖然極大提升了恢復的效率,但備份畢竟是一個 IO 密集型的操作,很容易影響備份實例的性能,所以,我們一般不希望克隆操作在 Primary 節點上執行。
但 Donor 的選擇是隨機的(后面會證明這一點),有沒有辦法讓 MGR 不從 Primary 節點克隆數據呢?
本文主要包括以下幾部分:
- MGR 是如何執行克隆操作的?
- 可以通過 clone_valid_donor_list 設置 Donor 么?
- MGR 是如何選擇 Donor 的?
- MGR 克隆操作的實現邏輯。
- 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;");elsequery.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 可分為以下兩步:
- 首先,判斷哪些節點適合當 Donor。滿足條件的節點會放到一個動態數組(m_suitable_donors)中, 這個操作是在
Remote_clone_handler::get_clone_donors
函數中實現的。 - 其次,循環遍歷 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;
}
該函數的處理流程如下:
-
獲取集群所有節點的信息,存儲到 all_members_info 中。
all_members_info 是個動態數組,數組中的元素是按照節點 server_uuid 從小到大的順序依次存儲的。
-
通過
vector_random_shuffle
函數將 all_members_info 進行隨機重排。 -
選擇 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?listif?(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_listif?((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?issueif?(!critical_error)?critical_error?=?evaluate_error_code(error);//?On?ER_RESTART_SERVER_FAILED?it?makes?no?sense?to?retryif?(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;}
...
}
該函數的處理流程如下:
- 首先會選擇一個 Donor。可以看到,代碼中是通過
front()
函數來獲取 m_suitable_donors 中的第一個元素。 - 獲取 Donor 的端點信息。
- 循環遍歷 endpoints 中的每個端點。
- 設置 clone_valid_donor_list。
- 執行克隆操作。如果操作失敗,則會進行重試,首先是選擇下一個端點進行重試。如果所有端點都遍歷完了,還是沒有成功,則會選擇下一個 Donor 進行重試,直到遍歷完所有 Donor。
當然,重試是有條件的,出現以下情況就不會進行重試:
-
error == ER_RESTART_SERVER_FAILED
:實例重啟失敗。實例重啟是克隆操作的最后一步,之前的步驟依次是:1. 獲取備份鎖。2. DROP 用戶表空間。3. 從 Donor 實例拷貝數據。
既然數據都已經拷貝完了,就沒有必要進行重試了。
-
執行克隆操作的連接被 KILL 了且重建失敗。
-
group_member_mgr->get_number_of_members() == 1
:集群只有一個節點。
既然克隆操作失敗了會進行重試,那么思路來了,如果不想克隆操作在 Primary 節點上執行,很簡單,讓 Primary 節點上的克隆操作失敗了就行。
怎么讓它失敗呢?
一個克隆操作,如果要在 Donor(被克隆節點)上成功執行,Donor 需滿足以下條件:
- 安裝克隆插件。
- 克隆用戶需要 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 沒有任何關系。
總結
- MGR 選擇 Donor 是隨機的。
- MGR 在執行克隆操作之前,會將 clone_valid_donor_list 設置為 Donor 的 endpoint,所以,在啟動組復制之前,在 mysql 客戶端中顯式設置 clone_valid_donor_list 是沒有效果的。
- MGR 執行克隆操作,實際上調用的就是
CLONE INSTANCE
命令。 - performance_schema.replication_group_members 中的 MEMBER_HOST 和 MEMBER_PORT,優先使用 report_host、report_port,其次才是主機名、mysqld 的端口。
- 一個節點,只有被選擇為 Donor,設置的 group_replication_advertise_recovery_endpoints 才會有效果。
- 如果不想 MGR 從 Primary 節點克隆數據,直接卸載 Primary 節點的克隆插件即可。