如何让 MGR 不从 Primary 节点克隆数据?
问题
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;");
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 可分为以下两步:
- 首先,判断哪些节点适合当 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 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;
}
...
}
该函数的处理流程如下:
- 首先会选择一个 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 节点的克隆插件即可。
延伸阅读
- MySQL 8.0 新特性之 Clone Plugin
- 《MySQL实战》组复制章节
如何让 MGR 不从 Primary 节点克隆数据?的更多相关文章
- jQuery实现节点克隆、替换和互换
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/ ...
- MongoDB之我是怎么成为Primary节点的
此文已由作者温正湖授权网易云社区发布. 欢迎访问网易云社区,了解更多网易技术产品运营经验. Primary(主)是MongoDB复制集中的最重要的角色,是能够接受客户端/Driver写请求的节点,(读 ...
- DOM节点克隆
var newDiv = red.cloneNode();document.body.appendChild(newDiv);注意:1.默认只克隆元素本身:设置参数为true,进行深度克隆,可以克隆元 ...
- 答:SQLServer DBA 三十问之一: char、varchar、nvarchar之间的区别(包括用途和空间占用);xml类型查找某个节点的数据有哪些方法,哪个效率高;使用存储 过程和使用T-SQL查询数据有啥不一样;
http://www.cnblogs.com/fygh/archive/2011/10/18/2216166.html 1. char.varchar.nvarchar之间的区别(包括用途和空间占用) ...
- hdfs 名称节点和数据节点
名字节点(NameNode )是HDFS主从结构中主节点上运行的主要进程,它指导主从结构中的从节点,数据节点(DataNode)执行底层的I/O任务. 名字节点是HDFS的书记员,维护着整个文件系统的 ...
- Windows Server 2008R2配置MySQL Cluster并将管理节点和数据节点配置成windows服务
说明:将mysql的管理节点和数据节点配置成windows服务是为了防止有人手误关闭管理节点或数据节点的dos命令窗口,管理节点或数据节点的命令窗口误关闭可能会造成mysql某台或某几台mysql不能 ...
- C#程序中:如何修改xml文件中的节点(数据)
要想在web等程序中实现动态的数据内容给新(如网页中的Flash),不会更新xml文件中的节点(数据)是远远不够的,今天在这里说一个简单的xml文件的更新,方法比较基础,很适合初学者看的,保证一看就懂 ...
- C#程序中:如何向xml文件中插入节点(数据)
向xml文件中动态的添加节点(数据)是一件很爽的事,可以给你的程序带来很多的方便,比如在web中,如果你的Flash用到了xml文件,这个方法可以让你在后台就轻轻松松的更新你的Flash内容哦!一起研 ...
- 【比特币】通过dns seeds获取节点列表数据
通过dns seeds获取节点列表数据 dns seed是什么 返回比特币网络上完整节点IP地址的DNS服务器,用于协助发现节点. 哪里可以查看到 我们在bitcoinj库中,params文件夹内为网 ...
- 树莓派与Arduino Leonardo使用NRF24L01无线模块通信之基于RF24库 (五) 树莓派单子节点发送数据
本项目中各个节点和树莓派的通信不区分信道,因此如果由树莓派发送给特定节点的数据会被所有节点接收到,因此子节点可以判别该数据是否发给自己的,需要在数据的第二个字节中加入目标节点的编号(第一个字节为源节点 ...
随机推荐
- Android 13 - Media框架(18)- CodecBase
关注公众号免费阅读全文,进入音视频开发技术分享群! 从这一节开始我们会回到上层来看ACodec的实现,在这之前我们会先了解ACodec的基类CodecBase.CodecBase.h 中除了声明有自身 ...
- WPF 制作高性能的透明背景异形窗口(使用 WindowChrome 而不要使用 AllowsTransparency=True)
在 WPF 中,如果想做一个背景透明的异形窗口,基本上都要设置 WindowStyle="None".AllowsTransparency="True" 这两个 ...
- 互联网软件的安装包界面设计-Inno setup
https://blog.csdn.net/oceanlucy/article/details/50033773 "安装界面太丑了,不堪入目!" "这界面应该属于20年代 ...
- 23ai中的True Cache到底能做啥?
最近,Oracle的产品管理总监在Oracle数据库内幕中介绍了True Cache. 原文链接如下: https://blogs.oracle.com/database/post/introduci ...
- 在 Rainbond 中一键安装高可用 Nacos 集群
描述如何通过云原生应用管理平台 Rainbond 一键安装高可用 Nacos 集群.这种方式适合不太了解 Kubernetes.容器化等复杂技术的用户使用,降低了在 Kubernetes 中部署 Na ...
- 判断是不是ie浏览器 加上ie11
var b_version = navigator.appVersion; var version = b_version.split(";"); var trim_Version ...
- ETL工具-nifi干货系列 第九讲 处理器EvaluateJsonPath,根据JsonPath提取字段
1.其实这一节课本来按照计划一起学习RouteOnAttribute处理器(相当于java中的ifelse,switch case 控制语句),但是在学习的过程中遇到了一些问题.RouteOnAttr ...
- (一)requests-实战小练习
1.需求:爬取豆瓣电影分类排行榜 https://movie.douban.com/中的电影详情数据 (此处以抓取科幻电影分类的信息为例) import requests import json ur ...
- CTF反序列化wp(ciscn,nss,ctfshowweb入门)
[CISCN 2023 华北]ez_date 题目: <?php error_reporting(0); highlight_file(__FILE__); class date{ public ...
- Diffusers实战
Smiling & Weeping ---- 一生拥有自由和爱,是我全部的野心 1. 环境准备 %pip install diffusers from huggingface_hub impo ...