安
网易游戏 MySQL DBA, 主要负责网易游戏 MySQL SaaS 平台的设计与维护,也有关注 TiDB,CockRoachDB 等分布式数据库。前言
Oracle 在 19 年 7 月下旬发布了 MySQL 的 8.0.17 版本,在这个版本中包含了一个全新的功能性插件:Clone。这个插件最初的目的是简化添加 Group Replication 新节点的操作,而这个插件实际发布之后,简化的操作不仅是添加 Group Replication 新节点,还包括了所有和备份恢复相关的操作,花费的时间远也低于常规的备份恢复手段。
原理介绍
MySQL Clone 插件如字面意思一般,它的作用就是 “Clone” 一个 MySQL 实例。安装这个插件之后,可以通过 MySQL client 登录到一个空的 MySQL 实例(推荐空实例),通过 Clone 命令获取到一份远端 MySQL 实例的一致性数据镜像,且整个过程和进度可以在 information_schema 的系统表中监测到。
简单的原理图如下:
Recipient 节点就是发起 Clone 操作的实例,推荐使用一个空实例;Donor 节点就是目标实例,是 Clone 操作的对象。完成 Clone 操作后,Recipient 节点就拥有 Donor 节点完整的 InnoDB 数据。从 8.0 开始,所有的 MySQL 系统表已经全部转换为 InnoDB 引擎,所以 Clone 操作也会把用户和授权信息完整的 Clone 到 Recipient 节点。
Clone 操作的流程如下:
清空 Recipient 节点的数据
复制 Donor 节点的数据文件
复制 Donor 节点的数据页
复制 Donor 节点的 redo log
同步 Donor 节点的数据文件
重启 Recipient 节点
在 Recipient 节点上完成 Crash recovery
使用 Clone 插件需要注意以下几点:
Recipient 节点并不会 Clone Donor 节点的 MySQL 配置文件,因为 Recipient 节点一般会有不同的 IP 或者端口。但是涉及到一些存储相关的参数(例如 innodb_page_size)出现不一致的时候,Clone 插件会报错,这些参数可能会导致 Recipient 节点重启失败,因此要提前准备好 Recipient 节点的配置文件。
Clone 插件复制的文件不包括 binlog,因为加入原有的 HA 结构并不需要 Donor 节点的 binlog。
Clone 插件当前仅支持 InnoDB 引擎的表,MyISAM 和 CSV 等引擎的表在 Clone 之后是一个空表。Clone 插件最初设计的时候会支持 MySQL 的所有引擎,但是目前仅实现了 InnoDB 引擎的功能,并通过了测试。
Clone 操作会阻塞 Donor 节点上的所有 DDL 操作。
Clone 操作会清空 Recipient 节点的所有数据和 binlog,因此要特别注意是否要在 Clone 操作执行之前备份 Recipient 节点的数据。
Clone 操作最好是挂到后台执行。
使用限制
MySQL 版本不低于 8.0.17
到 8.0.17 版本为止,Clone 插件仅支持 InnoDB 引擎的表。
Donor 节点在 Clone 的过程中,IO 读取的吞吐量和 CPU 使用率会有非常明显的上涨。
使用场景
由于 Clone 插件会直接生成一份完整的一致性备份,因此在搭建新的从库,创建临时测试库,本地、远端的一致性备份等可以直接访问源实例的场景会非常适合 Clone 插件。同时 Clone 插件也能满足 TB 级 MySQL 实例的备份需求,而 xtrabackup 和 mysqlbackup 会受到 redo log rotate 的影响,绝大多数场景下无法对 TB 级的 MySQL 进行备份,dump,快照等方式则存在磁盘空间或者时间上的缺陷。
操作流程
安装 Clone 插件
在 MySQL Client 安装 Clone 插件:
INSTALL PLUGIN CLONE SONAME "mysql_clone.so";
CREATE USER clone_user IDENTIFIED BY "clone_password";
GRANT BACKUP_ADMIN ON *.* to clone_user;
授予查看 Clone 进度等相关信息的权限:
GRANT SELECT ON performance_schema.* TO clone_user;
GRANT EXECUTE ON *.* to clone_user;
登录到 Recipient 节点执行 Clone 命令
##登录到Recipient的mysql client端执行
SET GLOBAL clone_valid_donor_list = 'donor.host.com:3306';
##Clone 操作推荐在shell命令行下挂到后台执行
CLONE INSTANCE FROM clone_user@donor.host.com:3306 IDENTIFIED BY "clone_password";
查看 Clone 的信息
检查 Clone 的运行状态:
SELECT STATE, CAST(BEGIN_TIME AS DATETIME) as "START TIME",
CASE WHEN END_TIME IS NULL THEN
LPAD(sys.format_time(POWER(10,12) * (UNIX_TIMESTAMP(now()) - UNIX_TIMESTAMP(BEGIN_TIME))), 10, ' ')
ELSE
LPAD(sys.format_time(POWER(10,12) * (UNIX_TIMESTAMP(END_TIME) - UNIX_TIMESTAMP(BEGIN_TIME))), 10, ' ')
END as DURATION
FROM performance_schema.clone_status;
结果展示:
+-------------+---------------------+------------+
| STATE | START TIME | DURATION |
+-------------+---------------------+------------+
| In Progress | 2019-07-17 17:23:26 | 4.84 m |
+-------------+---------------------+------------+
检查 Clone 的操作进度:
SELECT STAGE, STATE, CAST(BEGIN_TIME AS TIME) as "START TIME",
CASE WHEN END_TIME IS NULL THEN
LPAD(sys.format_time(POWER(10,12) * (UNIX_TIMESTAMP(now()) - UNIX_TIMESTAMP(BEGIN_TIME))), 10, ' ')
ELSE
LPAD(sys.format_time(POWER(10,12) * (UNIX_TIMESTAMP(END_TIME) - UNIX_TIMESTAMP(BEGIN_TIME))), 10, ' ')
END as DURATION,
LPAD(CONCAT(FORMAT(ROUND(ESTIMATE/1024/1024,0), 0), " MB"), 16, ' ') as "Estimate",
CASE WHEN BEGIN_TIME IS NULL THEN LPAD('0%', 7, ' ')
WHEN ESTIMATE > 0 THEN
LPAD(CONCAT(CAST(ROUND(DATA*100/ESTIMATE, 0) AS BINARY), "%"), 7, ' ')
WHEN END_TIME IS NULL THEN LPAD('0%', 7, ' ')
ELSE LPAD('100%', 7, ' ') END as "Done(%)"
FROM performance_schema.clone_progress;
结果展示:
+-----------+-------------+------------+------------+-----------+---------+
| STAGE | STATE | START TIME | DURATION | Estimate | Done(%) |
+-----------+-------------+------------+------------+-----------+---------+
| DROP DATA | Completed | 17:23:26 | 790.86 ms | 0 MB | 100% |
| FILE COPY | In Progress | 17:23:27 | 4.85 m | 94,729 MB | 47% |
| PAGE COPY | Not Started | NULL | NULL | 0 MB | 0% |
| REDO COPY | Not Started | NULL | NULL | 0 MB | 0% |
| FILE SYNC | Not Started | NULL | NULL | 0 MB | 0% |
| RESTART | Not Started | NULL | NULL | 0 MB | 0% |
| RECOVERY | Not Started | NULL | NULL | 0 MB | 0% |
+-----------+-------------+------------+------------+-----------+---------+
等待 Recipient 节点完成 Clone
重启完成 Crash Recovery 之后,整个 Clone 操作就结束了。
Clone 插件的效率测试
测试环境
类型 | 详情 |
---|---|
品牌 | 惠普 DL360 Gen9 |
CPU | E5-2630 v4*2 |
内存 | DDR4 2400MHz 16GB*8 |
HDD | 希捷 1800G 2.5寸 SAS 10K 12Gb/s,2块盘,RAID 1 |
SSD | 英特尔 S3510 800G 2.5寸 SATA 0 6Gb/s,6块盘,RAID 10 |
数据集 | 4张表,每张表10亿行数据,总共约1000GB |
软件版本 | MySQL 8.0.17, sysbench 1.0.16 |
测试内容
测试主要对比的指标为时间,具体指从一个运行的实例上建立一个完整的镜像实例所花费的时间。测试的对比对象为 LVM 快照和 Xtrabackup 工具。
MySQL 实例的配置使用相同的参数配置,数据目录均使用 SSD 磁盘,备份目录均使用 SAS 磁盘,redo log 为 2G 大小,总共 4 个文件。本次对比中会记录操作的各个阶段的耗时,数据仅作为实际备份恢复耗时的参考(部分数据可作为 TB 级 MySQL 实例的 RTO 的参考)。
Xtrabackup 备份开启了压缩与流数据打包,备份的数据流存储到 SAS 盘,恢复过程中,整个解包和解压缩均在 SAS 盘完成。因此 Xtrabackup 的恢复备份流程总共包括以下几个步骤:打包压缩 -> 解包 -> 解压缩 -> 恢复备份 -> 本地 copy 数据文件 -> 成功启动,本次测试参考的流程为本地备份恢复。
快照完成的时间定为把完整的快照数据拷贝到 SAS 盘之后,本次测试不做实际快照备份的操作,仅以磁盘写入吞吐量的平均值来做粗略估计。快照备份的速度按照 SAS 盘的平均写入速度(200MB/s)来计算。快照恢复的速度按照 SAS 盘的平均读取速度(300MB/s)来计算。总共 1000GB 的数据量。
MySQL Clone 插件直接使用 Recipient 节点的数据目录(SSD 磁盘)为目标目录,Clone 插件的配置如下:
##设置为ON时,MySQL会忽略用户的设置,自动调整Clone的参数, 速度非常慢
##但是对服务器的CPU和IO资源几乎没有影响。
clone_autotune_concurrency = OFF
##设置传输数据时可用的buffer大小,默认大小为4MB,增加这个参数允许Clone插件更充分的利用IO设备的性能,加快Clone操作的速度。
clone_buffer_size = 268435456
##设置Clone操作尝试获取相关锁的等待时间
clone_ddl_timeout = 300
##设置Clone操作是否开启压缩功能来进行数据传输,会减少网络带宽的消耗,增加CPU的消耗
clone_enable_compression = ON
##设置Clone操作使用的并发线程数
clone_max_concurrency = 24
##设置Clone操作可使用的最大数据传输吞吐量
clone_max_data_bandwidth = 0
##设置Clone操作可使用的最大网络带宽
clone_max_network_bandwidth = 0
测试中模拟的实际业务场景分为四个:
无写入负载
低写入负载
中等写入负载
标准读写负载
Xtrabackup 会因为 redo log 循环写入的原因导致备份失败,因此本次测试中不对高写入负载的场景进行测试,需要 Xtrabackup 支持 8.0.17 的 redo log archives 特性之后才能解决此类问题。
测试场景的几种负载由 sysbench 的脚本来进行模拟,对应的并发压力为:
无写入负载(None)
并发线程 = 0
低写入负载(LOW,write_only)
并发线程 = 4
中等写入负载(MID,write_only)
并发线程 = 8
标准读写负载(RW,read_write)
并发线程 = 16
CPU idle% 约 60%
结论
如果从一个正常的 MySQL 实例上获取一个镜像实例或者例行备份,Clone 插件的效率远远高于(超过 100% 的速度提升)常规的备份恢复手段(mydumper,mysqlbackup,xtrabackup,快照等)。
Clone 插件会占用 donor 实例的资源,会比较明显的影响 SELECT 操作,对 DML 的影响较小。
如果 MySQL 实例处于异常状态,无法访问,那么 Clone 插件就无法使用了,依旧需要使用常规的手段来进行灾难恢复。
耗时对比
实测过程中,Clone 插件对 donor 实例的主要压力在于 CPU,以及 IO 设备的读取性能上,对写入性能的影响相对比较小,即使达到中等量级的写入压力,Clone 的过程中也只达到了约 8% 的写入性能损失,但是在标准的读写负载场景下,性能损失达到了约 28%,主要是因为 CPU 的资源发生了争抢。
虽然在规划中没有加入高写入负载的测试,但是实际发现即使 sysbench 的写入并发设置为 1(写入 QPS 约 4000),Xtrabackup 依旧不能完成备份操作(由于 redo log 的循环写入),因此实际测试的结果中,Xtrabackup 仅有无负载的测试数据。
横向对比几种不同的备份恢复方案(所有图例的 None 字段代表负载):
从图中可以看到 Clone 插件花费的时间仅有快照的 40%,Xtrabackup 的 18%,这是因为 Clone 的过程几乎就是从远端实例把数据 rsync 到本地的 SSD 磁盘,整个过程没有 SAS 盘的参与。
纵向对比 Clone 插件在不同场景下的表现:
可以看到随着写入负载,或者是读写负载的增加,Clone 插件的耗时也会增长,增加的幅度最多达到了 50%,因此 Clone 操作最好在业务比较少的时候进行,或者是在其他空闲 MySQL 实例进行。
Xtrabackup 各阶段的耗时
Xtrabackup 备份恢复的整个流程的如下。
打包压缩(完成备份) -> 解包 -> 解压缩 -> 恢复备份 -> 本地 copy 数据文件 -> 成功启动(完成恢复)
本次测试中也分别记录了 Xtrabackup 各个阶段的耗时,参见下图:
恢复备份阶段没有耗时的原因是备份过程中没有进行数据变更,因此无需prepare/apply-log操作。从图中可以看到,绝大部分时间都花费在解压缩的部分,因为 SAS 盘的性能偏弱。
如果需要利用历史备份进行灾难恢复,TB 级的 MySQL 实例进行恢复的操作还需要花费约 300 分钟(去掉打包压缩的流程)。
往期精彩
﹀
﹀
﹀
MySQL 8.0 New Feature:NOWAIT and SKIP LOCK
Python 简洁编码之道
mycli,一款让你忘记 mysql-client 的命令行客户端神器
记一次 Python Popen 阻塞的问题
记一次 Gitlab Runner 卡顿的详情分析