让 MGR 不从 Primary 的节点克隆数据?

问题

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;");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 可分为以下两步:

  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 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;}
...
}

该函数的处理流程如下:

  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 节点的克隆插件即可。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/74293.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

第三十二篇 深入解析Kimball维度建模:构建企业级数据仓库的完整框架

目录 一、维度建模设计原则深度剖析1.1 业务过程驱动设计1.2 星型模式VS雪花模式 二、维度建模五步法实战&#xff08;附完整案例&#xff09;2.1 业务需求映射2.2 模型详细设计2.3 缓慢变化维处理 三、高级建模技术解析3.1 渐变维度桥接表3.2 快照事实表设计 四、性能优化体系…

IntelliJ IDEA 中 Maven 的 `pom.xml` 变灰带横线?一文详解解决方法

前言 在使用 IntelliJ IDEA 进行 Java 开发时&#xff0c;如果你发现项目的 pom.xml 文件突然变成灰色并带有删除线&#xff0c;这可能是 Maven 的配置或项目结构出现了问题。 一、问题现象与原因分析 现象描述 文件变灰&#xff1a;pom.xml 在项目资源管理器中显示为灰色。…

缓存过期时间之逻辑过期

1. 物理不过期&#xff08;Physical Non-Expiration&#xff09; 定义&#xff1a;在Redis中不设置EXPIRE时间&#xff0c;缓存键永久存在&#xff08;除非主动删除或内存淘汰&#xff09;。目的&#xff1a;彻底规避因缓存自动过期导致的击穿&#xff08;单热点失效&#xff…

基于WebAssembly的浏览器密码套件

目录 一、前言二、WebAssembly与浏览器密码套件2.1 WebAssembly技术概述2.2 浏览器密码套件的需求三、系统设计思路与架构3.1 核心模块3.2 系统整体架构图四、核心数学公式与算法证明4.1 AES-GCM加解密公式4.2 SHA-256哈希函数五、异步任务调度与GPU加速设计5.1 异步任务调度5.…

Qt的内存管理机制

在Qt中&#xff0c;显式使用new创建的对象通常不需要显式调用delete来释放内存&#xff0c;这是因为Qt提供了一种基于对象树(Object Tree)和父子关系(Parent-Child Relationship)的内存管理机制。这种机制可以自动管理对象的生命周期&#xff0c;确保在适当的时候释放内存&…

数据结构之双向链表-初始化链表-头插法-遍历链表-获取尾部结点-尾插法-指定位置插入-删除节点-释放链表——完整代码

数据结构之双向链表-初始化链表-头插法-遍历链表-获取尾部结点-尾插法-指定位置插入-删除节点-释放链表——完整代码 #include <stdio.h> #include <stdlib.h>typedef int ElemType;typedef struct node{ElemType data;struct node *next, *prev; }Node;//初化链表…

【Linux网络-五种IO模型与阻塞IO】

一、引入 网络通信的本质就是进程间的通信&#xff0c;进程间通信的本质就是IO&#xff08;Input&#xff0c;Output&#xff09; I/O&#xff08;input/output&#xff09;也就是输入和输出&#xff0c;在冯诺依曼体系结构当中&#xff0c;将数据从输入设备拷贝到内存就叫作…

算法-最大公约数

1、约数&#xff1a; 1.1 试除法求约数 原理&#xff1a;只需要遍历最小的约数即可&#xff0c;较大的那个可以直接算出来。 import java.util.*; public class Main {static Scanner sc new Scanner(System.in);public static void main(String[] args) {int t sc.nextIn…

湖北楚大夫

品牌出海已成为众多企业拓展业务、提升竞争力的关键战略。楚大夫(chudafu.com)作为一家专注于品牌出海、海外网络营销推广以及外贸独立站搭建的公司&#xff0c;凭借其专业、高效、创新的服务模式&#xff0c;致力于成为中国企业走向国际市场的坚实后盾与得力伙伴。楚大夫通过综…

Flutter 学习之旅 之 flutter 使用 connectivity_plus 进行网路状态监听(断网/网络恢复事件监听)

Flutter 学习之旅 之 flutter 使用 connectivity_plus 进行网路状态监听&#xff08;断网/网络恢复事件监听&#xff09; 目录 Flutter 学习之旅 之 flutter 使用 connectivity_plus 进行网路状态监听&#xff08;断网/网络恢复事件监听&#xff09; 一、简单介绍 二、conne…

从零开始实现 C++ TinyWebServer 处理请求 HttpRequest类详解

文章目录 HTTP 请求报文HttpRequest 类实现 Init() 函数实现 ParseRequestLine() 函数实现 ParseHeader() 函数实现 ParsePath() 函数实现 ParseBody() 函数实现 ParsePost() 函数实现 ParseFromUrlEncoded() 函数实现 UserVerify() 函数实现 Parse() 函数HttpRequest 代码Http…

systemd-networkd 的 *.network 配置文件详解 笔记250323

systemd-networkd 的 *.network 配置文件详解 笔记250323 查看官方文档可以用 man systemd.network命令, 或访问: https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html 名称 systemd.network — 网络配置 概要 network.network 描述 一个纯…

自定义mavlink 生成wireshark wlua插件错误(已解决)

进入正题 python3 -m pymavlink.tools.mavgen --langWLua --wire-protocol2.0 --outputoutput/develop message_definitions/v1.0/development.xml 编译WLUA的时候遇到一些问题 1.ERROR:SCHEMASV:SCHEMAV_CVC_ENUMERATION_VALID 3765:0:ERROR:SCHEMASV:SCHEMAV_CVC_ENUMERAT…

计算机操作系统(四) 操作系统的结构与系统调用

计算机操作系统&#xff08;四&#xff09; 操作系统的结构与系统调用 前言一、操作系统的结构1.1 简单结构1.2 模块化结构1.3 分层化结构1.4 微内核结构1.5 外核结构 二、系统调用1.1 系统调用的基本概念1.2 系统调用的类型 总结&#xff08;核心概念速记&#xff09;&#xf…

深入解析 Spring IOC AOP:原理、源码与实战

深入解析 Spring IOC & AOP&#xff1a;原理、源码与实战 Spring 框架的核心在于 IOC&#xff08;控制反转&#xff09; 和 AOP&#xff08;面向切面编程&#xff09;。今天&#xff0c;我们将深入剖析它们的原理&#xff0c;结合源码解析&#xff0c;并通过 Java 代码实战…

LLM之RAG理论(十四)| RAG 最佳实践

RAG 的过程很复杂&#xff0c;包含许多组成部分。我们如何确定现有的 RAG 方法及其最佳组合&#xff0c;以确定最佳 RAG 实践&#xff1f; 论文 《Searching for Best Practices in Retrieval-Augmented Generation》给出了回答。 本文将从以下三方面进行介绍&#xff1a; 首先…

利用knn算法实现手写数字分类

利用knn算法实现手写数字分类 1.作者介绍2.KNN算法2.1KNN&#xff08;K-Nearest Neighbors&#xff09;算法核心思想2.2KNN算法的工作流程2.3优缺点2.4 KNN算法图示介绍 3.实验过程3.1安装所需库3.2 MNIST数据集3.3 导入手写数字图像进行分类3.4 完整代码3.5 实验结果 1.作者介…

C语言-适配器模式详解与实践

文章目录 C语言适配器模式详解与实践1. 什么是适配器模式&#xff1f;2. 为什么需要适配器模式&#xff1f;3. 实际应用场景4. 代码实现4.1 UML 关系图4.2 头文件 (sensor_adapter.h)4.3 实现文件 (sensor_adapter.c)4.4 使用示例 (main.c) 5. 代码分析5.1 关键设计点5.2 实现特…

Rust函数、条件语句、循环

文章目录 函数**语句与表达式**条件语句循环 函数 Rust的函数基本形式是这样的 fn a_func(a: i32) -> i32 {}函数名是蛇形风格&#xff0c;rust不在意函数的声明顺序&#xff0c;只需要有声明即可 函数参数必须声明参数名称和类型 语句与表达式 这是rust非常重要的基础…

maptalks图层交互 - 模拟 Tooltip

maptalks图层交互 - 模拟 Tooltip 图层交互-模拟tooltip官方文档 <!DOCTYPE html> <html><meta charsetUTF-8 /><meta nameviewport contentwidthdevice-width, initial-scale1 /><title>图层交互 - 模拟 Tooltip</title><style typet…