问题

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实战》组复制章节

如何让 MGR 不从 Primary 节点克隆数据?的更多相关文章

  1. jQuery实现节点克隆、替换和互换

    <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/ ...

  2. MongoDB之我是怎么成为Primary节点的

    此文已由作者温正湖授权网易云社区发布. 欢迎访问网易云社区,了解更多网易技术产品运营经验. Primary(主)是MongoDB复制集中的最重要的角色,是能够接受客户端/Driver写请求的节点,(读 ...

  3. DOM节点克隆

    var newDiv = red.cloneNode();document.body.appendChild(newDiv);注意:1.默认只克隆元素本身:设置参数为true,进行深度克隆,可以克隆元 ...

  4. 答:SQLServer DBA 三十问之一: char、varchar、nvarchar之间的区别(包括用途和空间占用);xml类型查找某个节点的数据有哪些方法,哪个效率高;使用存储 过程和使用T-SQL查询数据有啥不一样;

    http://www.cnblogs.com/fygh/archive/2011/10/18/2216166.html 1. char.varchar.nvarchar之间的区别(包括用途和空间占用) ...

  5. hdfs 名称节点和数据节点

    名字节点(NameNode )是HDFS主从结构中主节点上运行的主要进程,它指导主从结构中的从节点,数据节点(DataNode)执行底层的I/O任务. 名字节点是HDFS的书记员,维护着整个文件系统的 ...

  6. Windows Server 2008R2配置MySQL Cluster并将管理节点和数据节点配置成windows服务

    说明:将mysql的管理节点和数据节点配置成windows服务是为了防止有人手误关闭管理节点或数据节点的dos命令窗口,管理节点或数据节点的命令窗口误关闭可能会造成mysql某台或某几台mysql不能 ...

  7. C#程序中:如何修改xml文件中的节点(数据)

    要想在web等程序中实现动态的数据内容给新(如网页中的Flash),不会更新xml文件中的节点(数据)是远远不够的,今天在这里说一个简单的xml文件的更新,方法比较基础,很适合初学者看的,保证一看就懂 ...

  8. C#程序中:如何向xml文件中插入节点(数据)

    向xml文件中动态的添加节点(数据)是一件很爽的事,可以给你的程序带来很多的方便,比如在web中,如果你的Flash用到了xml文件,这个方法可以让你在后台就轻轻松松的更新你的Flash内容哦!一起研 ...

  9. 【比特币】通过dns seeds获取节点列表数据

    通过dns seeds获取节点列表数据 dns seed是什么 返回比特币网络上完整节点IP地址的DNS服务器,用于协助发现节点. 哪里可以查看到 我们在bitcoinj库中,params文件夹内为网 ...

  10. 树莓派与Arduino Leonardo使用NRF24L01无线模块通信之基于RF24库 (五) 树莓派单子节点发送数据

    本项目中各个节点和树莓派的通信不区分信道,因此如果由树莓派发送给特定节点的数据会被所有节点接收到,因此子节点可以判别该数据是否发给自己的,需要在数据的第二个字节中加入目标节点的编号(第一个字节为源节点 ...

随机推荐

  1. 领域驱动设计(Domain-Driven Design,简称DDD)【简介 个人学习笔记】

    找到了第 1 篇资料:领域驱动设计详解:是什么.为什么.怎么做? - 知乎 找到了第 2 篇资料:领域驱动架构(DDD)建模中的模型到底是什么? - 知乎 找到了第 3 篇资料:一文看懂DDD 领域驱 ...

  2. swift_slowAlloc Crash 分析

    一.Crash详情 Crash类型 exception EXC_BREAKPOINT (SIGTRAP) reason EXC_BREAKPOINT EXC_ARM_BREAKPOINT fault_ ...

  3. numpy基础--通用函数:快速的元素级数组函数

    以下代码的前提:import numpy as np 通用函数(即ufunc)是一种对narray中的数组执行元素级运算的函数.可以看作简单函数(接受一个或多个标量值,并产生一个或多个标量值)的矢量化 ...

  4. Manim使用心得

    Manim 使用心得 manim 做视频还是挺方便的. 当然,如果你每一次都从 0 开始写,那么你会崩溃. 所以需要找到自己做视频的风格,以此总结出一套通用的 python 模板代码,然后调用. 例如 ...

  5. 随机化 base64 加密

    随机化 base64 加密 项目原址:Jeefy / jtim-enc · GitLab 技术 采用双重随机化技术. 第一重随机化是利用固定的随机种子改变 base64 解码的映射数组. 第二重随机化 ...

  6. 如何使用 Loadgen 来简化 HTTP API 请求的集成测试

    引言 在编写 HTTP 服务的过程中,集成测试 [1] 是保证程序正确性的重要一环,如下图所示,其基本的流程就是不断向服务发起请求然后校验响应的状态和数据等: 为大量的 API 和用例编写测试是一件繁 ...

  7. LINQ to Entities does not recognize the method 'System.String ToString()' method

    LINQ to Entities does not recognize the method 'System.String ToString()' method, and this method ca ...

  8. 如何解决Win10删除文件慢的办法

    问题:最近使用KMS激活了一些工具,今天删除不需要的文件时发现删除文件很慢很慢,删除一个几百k的文件都很慢. 解决办法通过控制面板→管理工具→服务→找到该进程并设为禁用就OK了.

  9. 泛型模板化设计DEMO

    泛型模板化设计DEMO 1. 定义Result泛型类 package com.example.core.mydemo.java.fanxing; public class Result<T> ...

  10. redis zset 延迟合并任务处理

    redis zset 延迟合并任务处理 @Autowired public RedisTemplate redisTemplate; ##1.发送端:在接口中收集任务ID,累计时间段之后,合并处理. ...