MySQL 主从复制原理剖析与应用实践
vivo 互联网服务器团队- Shang Yongxing
MySQL Replication(主从复制)是指数据变化可以从一个 MySQL Server 被复制到另一个或多个 MySQL Server 上,通过复制的功能,可以在单点服务的基础上扩充数据库的高可用性、可扩展性等。
一、背景
MySQL 在生产环境中被广泛地应用,大量的应用和服务都对 MySQL 服务存在重要的依赖关系,可以说如果数据层的 MySQL 实例发生故障,在不具备可靠降级策略的背景下就会直接引发上层业务,甚至用户使用的障碍;同时 MySQL 中存储的数据也是需要尽可能地减少丢失的风险,以避免故障时出现数据丢失引发的资产损失、客诉等影响。
在这样对服务可用性和数据可靠性需求的背景下,MySQL 在 Server 层提供了一种可靠的基于日志的复制能力(MySQL Replication),在这一机制的作用下,可以轻易构建一个或者多个从库,提高数据库的高可用性、可扩展性,同时实现负载均衡:
实时数据变化备份
主库的写入数据会持续地在冗余的从库节点上被执行保留,减少数据丢失的风险
横向拓展节点,支撑读写分离
当主库本身承受压力较大时,可以将读流量分散到其它的从库节点上,达成读扩展性和负载均衡
高可用性保障
当主库发生故障时,可以快速的切到其某一个从库,并将该从库提升为主库,因为数据都一样,所以不会影响系统的运行
具备包括但不限于以上特性的 MySQL 集群就可以覆盖绝大多数应用和故障场景,具备较高的可用性与数据可靠性,当前存储组提供的生产环境 MySQL 就是基于默认的异步主从复制的集群,向业务保证可用性 99.99%,数据可靠性 99.9999%的在线数据库服务。
本文将深入探讨 MySQL 的复制机制实现的方式, 同时讨论如何具体地应用复制的能力来提升数据库的可用性,可靠性等。
二、复制的原理
2.1 Binlog 的引入
从比较宽泛的角度来探讨复制的原理,MySQL 的 Server 之间通过二进制日志来实现实时数据变化的传输复制,这里的二进制日志是属于 MySQL 服务器的日志,记录了所有对 MySQL 所做的更改。这种复制模式也可以根据具体数据的特性分为三种:
Statement:基于语句格式
Statement 模式下,复制过程中向获取数据的从库发送的就是在主库上执行的 SQL 原句,主库会将执行的 SQL 原有发送到从库中。
Row:基于行格式
Row 模式下,主库会将每次 DML 操作引发的数据具体行变化记录在 Binlog 中并复制到从库上,从库根据行的变更记录来对应地修改数据,但 DDL 类型的操作依然是以 Statement 的格式记录。
Mixed:基于混合语句和行格式
MySQL 会根据执行的每一条具体的 SQL 语句来区分对待记录的日志形式,也就是在 statement 和 row 之间选择一种。
最早的实现是基于语句格式,在 3.23 版本被引入 MySQL,从最初起就是 MySQL Server 层的能力,这一点与具体使用的存储引擎没有关联;在 5.1 版本后开始支持基于行格式的复制;在 5.1.8 版本后开始支持混合格式的复制。
这三种模式各有优劣,相对来说,基于 Row 的行格式被应用的更广泛,虽然这种模式下对资源的开销会偏大,但数据变化的准确性以及可靠性是要强于 Statement 格式的,同时这种模式下的 Binlog 提供了完整的数据变更信息,可以使其应用不被局限在 MySQL 集群系统内,可以被例如 Binlogserver,DTS 数据传输等服务应用,提供灵活的跨系统数据传输能力, 目前互联网业务的在线 MySQL 集群全部都是基于 Row 行格式的 Binlog。
2.2 Binlog 的要点
2.2.1 Binlog 事件类型
对于 Binlog 的定义而言,可以认为是一个个单一的 Event 组成的序列,这些单独的 Event 可以主要分为以下几类:
各类 Event 出现是具有显著的规律的:
XID_EVENT 标志一个事务的结尾
当发生了 DDL 类型的 QUERY_EVENT,那么也是一次事务的结束提交点,且不会出现 XID_EVENT
GTID_EVENT 只有开启了 GTID_MODE(MySQL 版本大于 5.6)
TABLE_MAP_EVENT 必定出现在某个表的变更数据前,存在一对多个 ROW_EVENT 的情况
除了上面和数据更贴近的事件类型外,还有 ROTATE_EVENT(标识 Binlog 文件发生了切分),FORMAT_DESCRIPTION_EVENT(定义元数据格式)等。
2.2.2 Binlog 的生命周期
Binlog 和 Innodb Log(redolog)的存在方式是不同的,它并不会轮转重复覆写文件,Server 会根据配置的单个 Binlog 文件大小配置不断地切分并产生新的 Binlog,在一个.index 文件记录当前硬盘上所有的 binlog 文件名,同时根据 Binlog 过期时间回收删除掉过期的 Binlog 文件,这两个在目前自建数据库的配置为单个大小 1G,保留 7 天。
所以这种机制背景下,只能在短期内追溯历史数据的状态,而不可能完整追溯数据库的数据变化的,除非是还没有发生过日志过期回收的 Server。
2.2.3 Binlog 事件示例
Binlog 是对 Server 层生效的,即使没有从库正在复制主库,只要在配置中开启了 log_bin,就会在对应的本地目录存储 binlog 文件,使用 mysqlbinlog 打开一个 Row 格式的示例 binlog 文件:
如上图,可以很明显地注意到三个操作,创建数据库 test, 创建数据表 test, 一次写入引发的行变更,可读语句(create, alter, drop, begin, commit.....)都可以认为是 QUERY_EVENT,而 Write_rows 就属于 ROW_EVENT 中的一种。
在复制的过程中,就是这样的 Binlog 数据通过建立的连接发送到从库,等待从库处理并应用。
2.2.4 复制基准值
Binlog 在产生时是严格有序的,但它本身只具备秒级的物理时间戳,所以依赖时间进行定位或排序是不可靠的,同一秒可能有成百上千的事件,同时对于复制节点而言,也需要有效可靠的记录值来定位 Binlog 中的水位,MySQL Binlog 支持两种形式的复制基准值,分别是传统的 Binlog File:Binlog Position 模式,以及 5.6 版本后可用的全局事务序号 GTID。
FILE Position
只要开启了 log_bin,MySQL 就会具有 File Position 的位点记录,这一点不受 GTID 影响。
这个概念相对来说更直观,可以直接理解为当前处在 File 对应编号的 Binlog 文件中,同时已经产生了合计 Position bytes 的数据,如例子中所示即该实例已经产生了 381808617 bytes 的 Binlog,这个值在对应机器直接查看文件的大小也是匹配的,所以 File Postion 就是文件序列与大小的对应值。
基于这种模式开启复制,需要显式地在复制关系中指定对应的 File 和 Position:
这个值必须要准确,因为这种模式下从库获取的数据完全取决于有效的开启点,那么如果存在偏差,就会丢失或执行重复数据导致复制中断。
GTID
MySQL 会在开启 GTID_MODE=ON 的状态下,为每一个事务分配唯一的全局事务 ID,格式为:server_uuid:id
其中 e2e0a733-3478-11eb-90fe-b4055d009f6c 用于唯一地标识产生该 Binlog 事件的实例,1-753 表示已经产生或接收了由 e2e0a733-3478-11eb-90fe-b4055d009f6c 实例产生的 753 个事务;
从库在从主库获取 Binlog Event 时,自身的执行记录会保持和获取的主库 Binlog GTID 记录一致,还是以 e2e0a733-3478-11eb-90fe-b4055d009f6c:1-753,如果有从库对 e2e0a733-3478-11eb-90fe-b4055d009f6c 开启了复制,那么在从库自身执行 show master status 也是会看到相同的值。
如果说从库上可以看到和复制的主库不一致的值,那么可以认为是存在 errant GTID,这个一般是由于主从切换或强制在从库上执行了写操作引发,正常情况下从库的 Binlog GTID 应该和主库的保持一致;
基于这种模式开启复制,不需要像 File Position 一样指定具体的值,只需要设置:
从库在读取到 Binlog 后,会自动根据自身 Executed_GTID_Set 记录比对是否存在已执行或未执行的 Binlog 事务,并做对应的忽略和执行操作。
2.3 复制的具体流程
2.3.1 基本复制流程
当主库已经开启了 binlog( log_bin = ON ),并正常地记录 binlog,如何开启复制?
这里以 MySQL 默认的异步复制模式进行介绍:
首先从库启动 I/O 线程,跟主库建立客户端连接。
主库启动 binlog dump 线程,读取主库上的 binlog event 发送给从库的 I/O 线程,I/O 线程获取到 binlog event 之后将其写入到自己的 Relay Log 中。
从库启动 SQL 线程,将等待 Relay 中的数据进行重放,完成从库的数据更新。
总结来说,主库上只会有一个线程,而从库上则会有两个线程。
时序关系
当集群进入运行的状态时,从库会持续地从主库接收到 Binlog 事件,并做对应的处理,那么这个过程中将会按照下述的数据流转方式:
Master 将数据更改记录在 Binlog 中,BinlogDump Thread 接到写入请求后,读取对应的 Binlog
Binlog 信息推送给 Slave 的 I/O Thread。
Slave 的 I/O 线程将读取到的 Binlog 信息写入到本地 Relay Log 中。
Slave 的 SQL 线程读取 Relay Log 中内容在从库上执行。
上述过程都是异步操作,所以在某些涉及到大的变更,例如 DDL 改变字段,影响行数较大的写入、更新或删除操作都会导致主从间的延迟激增,针对延迟的场景,高版本的 MySQL 逐步引入了一些新的特性来帮助提高事务在从库重放的速度。
Relay Log 的意义
Relay log 在本质上可以认为和 binlog 是等同的日志文件,即使是直接在本地打开两者也只能发现很少的差异;
Binlog Version 3 (MySQL 4.0.2 - < 5.0.0)
added the relay logs and changed the meaning of the log position
在 MySQL 4.0 之前是没有 Relay Log 这部分的,整个过程中只有两个线程。但是这样也带来一个问题,那就是复制的过程需要同步的进行,很容易被影响,而且效率不高。例如主库必须要等待从库读取完了才能发送下一个 binlog 事件。这就有点类似于一个阻塞的信道和非阻塞的信道。
在流程中新增 Relay Log 中继日志后,让原本同步的获取事件、重放事件解耦了,两个步骤可以异步的进行,Relay Log 充当了缓冲区的作用。Relay Log 包含一个 relay-log.info 的文件,用于记录当前复制的进度,下一个事件从什么 Pos 开始写入,该文件由 SQL 线程负责更新。
对于后续逐渐引入的特殊复制模式,会存在一些差异,但整体来说,是按照这个流程来完成的。
2.3.2 半同步复制
异步复制的场景下,不能确保从库实时更新到和主库一致的状态,那么如果在出现延迟的背景下发生主库故障,那么两者间的差异数据还是无法进行保障,同时也无法在这种情况下进行读写分离,而如果说由异步改为完全同步,那么性能开销上又会大幅提高,很难满足实际使用的需求。
基于这一的背景,MySQL 从 5.5 版本开始引入了半同步复制机制来降低数据丢失的概率,在这种复制模式中,MySQL 让 Master 在某一个时间点等待一个 Slave 节点的 ACK(Acknowledge Character)消息,接收到 ACK 消息后才进行事务提交,这样既可以减少对性能的影响,还可以相对异步复制获得更强的数据可靠性。
介绍半同步复制之前先快速过一下 MySQL 事务写入碰到主从复制时的完整过程,主库事务写入分为 4 个步骤:
InnoDB Redo File Write (Prepare Write)
Binlog File Flush & Sync to Binlog File
InnoDB Redo File Commit(Commit Write)
Send Binlog to Slave
当 Master 不需要关注 Slave 是否接受到 Binlog Event 时,即为异步主从复制
当 Master 需要在第 3 步 Commit Write 回复客户端前等待 Slave 的 ACK 时,为半同步复制(after-commit)
当 Master 需要在第 2 步 Flush&Sync,即 Commit 前等待 Slave 的 ACK 时,为增强半同步复制(after-sync)
时序关系
从半同步复制的时序图来看,实际上只是在主库 Commit 的环节多了等待接收从库 ACK 的阶段,这里只需要收到一个从节点的 ACK 即可继续正常的处理流程,这种模式下,即使主库宕机了,也能至少保证有一个从库节点是可以用的,此外还减少了同步时的等待时间。
2.3.3 小结
在当前生产环境的在线数据库版本背景下,由 MySQL 官方提供的复制方式主要如上文介绍的内容,当然目前有还很多基于 MySQL 或兼容 MySQL 的衍生数据库产品,能在可用性和可靠性上做更大的提升,本文就不继续展开这部分的描述。
2.4 复制的特性
目前已经提及的复制方式,存在一个显著的特性:无法回避数据延迟的场景,异步复制会使得从库的数据落后,而半同步复制则会阻塞主库的写入,影响性能。
MySQL 早期的复制模式中,从库的 IO 线程和 SQL 线程本质上都是串行获取事件并读取重放的,只有一个线程负责执行 Relaylog,但主库本身接收请求是可以并发地,性能上限只取决于机器资源瓶颈和 MySQL 处理能力的上限,主库的执行和从库的执行(SQL 线程应用事件)是很难对齐的,这里引用一组测试数据:
机器:64 核 256G,MySQL 5.7.29
测试场景:常规的 INSERT,UPDATE 压测场景
结果:MySQL Server 的 IO 线程速度以网络上的数据量评估,每秒超过 100MB,正常是可以覆盖业务使用的,然而 SQL 线程的预估速度只有 21~23MB/s,如果是涉及 UPDATE 场景,性能还会减少;
需要注意的是,以上结果是在高版本的 MySQL 具备并行复制能力的前提下取得,如果是不具备该特性的版本,性能会更差。
期望业务层限制使用是不现实的,MySQL 则在 5.6 版本开始尝试引入可用的并行复制方案,总的来说,都是通过尝试加强在从库层面的应用速度的方式。
2.4.1 基于 Schema 级别的并行复制
基于库级别的并行复制是出于一个非常简易的原则,实例中不同 Database/Schema 内的数据以及数据变更是无关的,可以并行去处置。
在这种模式中,MySQL 的从节点会启动多个 WorkThread ,而原来负责回放的 SQLThread 会转变成 Coordinator 角色,负责判断事务能否并行执行并分发给 WorkThread。
如果事务分别属于不同的 Schema,并且不是 DDL 语句,同时没有跨 Schema 操作,那么就可以并行回放,否则需要等所有 Worker 线程执行完成后再执行当前日志中的内容。
对于从库而言,如果接收到了来自主库的 aksay_record 以及 proxy_encrypt 内的数据变更,那么它是可以同时去处理这两部分 Schema 的数据的。
但是这种方式也存在明显缺陷和不足,首先只有多个 Schema 流量均衡的情况下才会有较大的性能改善,但如果存在热点表或实例上只有一个 Schema 有数据变更,那么这种并行模式和早期的串行复制也不存在差异;同样,虽然不同 Schema 的数据是没有关联,这样并行执行也会影响事务的执行顺序,某种程度来说,整个 Server 的因果一致性被破坏了。
2.4.2 基于组提交的复制(Group Commit)
基于 Schema 的并行复制在大部分场景是没有效力的,例如一库多表的情况下,但改变从库的单执行线程的思路被延续了下来,在 5.7 版本新增加了一种基于事务组提交的并行复制方式,在具体介绍应用在复制中的组提交策略前,需要先介绍 Server 本身 Innodb 引擎提交事务的逻辑:
Binlog 的落盘是基于 sync_binlog 的配置来的,正常情况都是取 sync_binlog=1,即每次事务提交就发起 fsync 刷盘。
主库在大规模并发执行事务时,因为每个事务都触发加锁落盘,反而使得所有的 Binlog 串行落盘,成为性能上的瓶颈。针对这个问题,MySQL 本身在 5.6 版本引入了事务的组提交能力(这里并不是指在从库上应用的逻辑),设计原理很容易理解,只要是能在同一个时间取得资源,开启 Prepare 的所有事务,都是可以同时提交的。
在主库具有这一能力的背景下,可以很容易得发现从库也可以应用相似的机制来并行地去执行事务,下面介绍 MySQL 具体实现经历的两个阶段:
基于 Commit-Parents-Based
MySQL 中写入是基于锁的并发控制,所以所有在 Master 端同时处于 Prepare 阶段且未提交的事务就不会存在锁冲突,在 Slave 端执行时都可以并行执行。
因此可以在所有的事务进入 prepare 阶段的时候标记上一个 logical timestamp(实现中使用上一个提交事务的 sequence_number),在 Slave 端同样 timestamp 的事务就可以并发执行。
但这种模式会依赖上一个事务组的提交,如果本身是不受资源限制的并发事务,却会因为它的 commit-parent 没有提交而无法执行;
基于 Logic-Based
针对 Commit-Parent-Based 中存在的限制进行了解除,纯粹的理解就是只有当前事务的 sequence_number 一致就可以并发执行,只根据是否能取得锁且无冲突的情况即可以并发执行,而不是依赖上一个已提交事务的 sequence_number。
三、应用
当前 vivo 的在线 MySQL 数据库服务标准架构是基于一主一从一离线的异步复制集群,其中一从用于业务读请求分离,离线节点不提供读服务,提供给大数据离线和实时抽数/DB 平台查询以及备份系统使用;针对这样的应用背景,存储研发组针对 MySQL 场景提供了两种额外的扩展服务:
3.1 应用高可用系统+中间件
虽然 MySQL 的主从复制可以提高系统的高可用性,但是 MySQL 在 5.6,5.7 版本是不具备类似 Redis 的自动故障转移的能力,如果主库宕机后不进行干预,业务实际上是无法正常写入的,故障时间较长的情况下,分离在从库上的读也会变得不可靠。
3.1.1 VSQL(原高可用 2.0 架构)
那么在当前这样标准一主二从架构的基础上,为系统增加 HA 高可用组件以及中间件组件强化 MySQL 服务的高可用性、读拓展性、数据可靠性:
HA 组件管理 MySQL 的复制拓扑,负责监控集群的健康状态,管理故障场景下的自动故障转移;
中间件 Proxy 用于管理流量,应对原有域名场景下变更解析慢或缓存不生效的问题,控制读写分离、实现 IP、SQL 的黑白名单等;
3.1.2 数据可靠性强化
数据本身还是依赖 MySQL 原生的主从复制模式在集群中同步,这样仍然存在异步复制本身的风险,发生主库宕机时,如果从库上存在还未接收到的主库数据,这部分就会丢失,针对这个场景,我们提供了三种可行的方案:
日志远程复制
配置 HA 的中心节点和全网 MySQL 机器的登录机器后,按照经典的 MHA 日志文件复制补偿方案来保障故障时的数据不丢失,操作上即 HA 节点会访问故障节点的本地文件目录读取候选主节点缺失的 Binlog 数据并在候选主上重放。
优势
与 1.0 的 MHA 方案保持一致,可以直接使用旧的机制
机制改造后可以混合在高可用的能力内,不需要机器间的免密互信,降低权限需求和安全风险
劣势
不一定可用,需要故障节点所在机器可访达且硬盘正常,无法应对硬件或网络异常的情况
网络上链路较长,可能无法控制中间重放日志的耗时,导致服务较长时间不可用
日志集中存储
依赖数据传输服务中的 BinlogServer 模块,提供 Binlog 日志的集中存储能力,HA 组件同时管理 MySQL 集群以及 BinlogServer,强化 MySQL 架构的健壮性,真实从库的复制关系全部建立在 BinlogServer 上,不直接连接主库。
优势
可以自定义日志的存储形式:文件系统或其它共享存储模式
不涉及机器可用和权限的问题
间接提高 binlog 的保存安全性(备份)
劣势
额外的资源使用,如果需要保留较长时间的日志,资源使用量较大
如果不开启半同步,也不能保证所有的 binlog 日志都能被采集到,即使采集(相当于 IO 线程)速度远超 relay 速度,极限约 110MB/s
系统复杂度提升,需要承受引入额外链路的风险
改变为半同步复制
MySQL 集群开启半同步复制,通过配置防止退化(风险较大),Agent 本身支持半同步集群的相关监控,可以减少故障切换时日志丢失的量(相比异步复制)
优势
MySQL 原生的机制,不需要引入额外的风险
本质上就是在强化高可用的能力(MySQL 集群本身)
HA 组件可以无缝接入开启半同步的集群,不需要任何改造
劣势
存在不兼容的版本,不一定可以开启
业务可能无法接受性能下降的后果
半同步不能保证完全不丢数据,Agent 本身机制实际上是优先选择“执行最多”的从节点而不是“日志最多”的从节点
orchestrator will promote the replica which has executed more events rather than the replica which has more data in the relay logs.
目前来说,我们采用的是日志远程复制的方案,同时今年在规划集中存储的 BinlogServer 方案来强化数据安全性;不过值得一提的是,半同步也是一种有效可行的方式,对于读多写少的业务实际上是可以考虑升级集群的能力,这样本质上也可以保证分离读流量的准确性。
3.2 数据传输服务
3.2.1 基于 Binlog 的跨系统数据流转
通过利用 Binlog,实时地将 MySQL 的数据流转到其它系统,包括 MySQL,ElasticSearch,Kafka 等 MQ 已经是一种非常经典的应用场景了,MySQL 原生提供的这种变化数据同步的能力使其可以有效地在各个系统间实时联动,DTS(数据传输服务)针对 MySQL 的采集也是基于和前文介绍的复制原理一致的方法,这里介绍我们是如何利用和 MySQL 从节点相同的机制去获取数据的,也是对于完整开启复制的拓展介绍:
(1)如何获取 Binlog
比较常规的方式有两种:
监听 Binlog 文件,类似日志采集系统的操作
MySQL Slave 的机制,采集者伪装成 Slave 来实现
本文只介绍第二种,Fake Slave 的实现方式
(2)注册 Slave 身份
这里以 GO SDK 为例,GO 的 byte 范围是 0~255,其它语言做对应转换即可。
第 0-3 位为 0,无意义
第 4 位是 MySQL 协议中的 Command_Register_Slave,byte 值为 21
第 5-8 位是当前实例预设的 server_id(非 uuid,是一个数值)使用小端编码成的 4 个字节
接下来的若干位是把当前实例的 hostname,user,password
接下来的 2 位是小端编码的 port 端口值
最后 8 位一般都置为 0,其中最后 4 位指 master_id,伪装 slave 设置为 0 即可
(3)发起复制指令
第 0-3 位同样置为 0,无特殊意义
第 4 位是 MySQL 协议的 Command_Binlog_Dump,byte 值为 18
第 5-8 位是 Binlog Position 值的小端序编码产生的 4 位字节
第 9-10 位是 MySQL Dump 的类别,默认是 0,指 Binlog_Dump_Never_Stop,即编码成 2 个 0 值
第 11-14 位是实例的 server_id(非 uuid)基于小端编码的四个字节值
最后若干位即直接追加 Binlog File 名称
以上两个命令通过客户端连接执行后,就可以在主库上观察到一个有效的复制连接。
3.2.2 利用并行复制模式提升性能
以上两个命令通过客户端连接执行后,就可以在主库上观察到一个有效的复制连接。
根据早期的性能测试结果,不做任何优化,直接单连接重放源集群数据,在网络上的平均传输速度在 7.3MB/s 左右,即使是和 MySQL 的 SQL Relay 速度相比也是相差很远,在高压场景下很难满足需求。
DTS 消费单元实现了对消费自 kafka 的事件的事务重组以及并发的事务解析工作,但实际最终执行还是串行单线程地向 MySQL 回放,这一过程使得性能瓶颈完全集中在了串行执行这一步骤。
MySQL 5.7 版本以前,会利用事务的 Schema 属性,使不同 db 下的 DML 操作可以在备库并发回放。在优化后,可以做到不同表 table 下并发。但是如果业务在 Master 端高并发写入一个库(或者优化后的表),那么 slave 端就会出现较大的延迟。基于 schema 的并行复制,Slave 作为只读实例提供读取功能时候可以保证同 schema 下事务的因果序(Causal Consistency,本文讨论 Consistency 的时候均假设 Slave 端为只读),而无法保证不同 schema 间的。例如当业务关注事务执行先后顺序时候,在 Master 端 db1 写入 T1,收到 T1 返回后,才在 db2 执行 T2。但在 Slave 端可能先读取到 T2 的数据,才读取到 T1 的数据。
MySQL 5.7 的 LOGICAL CLOCK 并行复制,解除了 schema 的限制,使得在主库对一个 db 或一张表并发执行的事务到 slave 端也可以并行执行。Logical Clock 并行复制的实现,最初是 Commit-Parent-Based 方式,同一个 commit parent 的事务可以并发执行。但这种方式会存在可以保证没有冲突的事务不可以并发,事务一定要等到前一个 commit parent group 的事务全部回放完才能执行。后面优化为 Lock-Based 方式,做到只要事务和当前执行事务的 Lock Interval 都存在重叠,即保证了 Master 端没有锁冲突,就可以在 Slave 端并发执行。LOGICAL CLOCK 可以保证非并发执行事务,即当一个事务 T1 执行完后另一个事务 T2 再开始执行场景下的 Causal Consistency。
(1)连接池改造
旧版的 DTS 的每一个消费任务只有一条维持的 MySQL 长连接,该消费链路的所有的事务都在这条长连接上串行执行,产生了极大的性能瓶颈,那么考虑到并发执行事务的需求,不可能对连接进行并发复用,所以需要改造原本的单连接对象,提升到近似连接池的机制。
go-mysql/client 包本身不包含连接池模式,这里基于事务并发解析的并发度在启动时,扩展存活连接的数量。
(2)并发选择连接
利用逻辑时钟
开启 GTID 复制的模式下,binlog 中的 GTID_EVENT 的正文内会包含两个值:
lastCommitted 是我们并发的依据,原则上,LastCommitted 相等事务可以并发执行,结合原本事务并发解析完成后会产生并发度(配置值)数量的事务集合,那么对这个列表进行分析判断,进行事务到连接池的分配,实现一种近似负载均衡的机制。
非并发项互斥
对于并发执行的场景,可以比较简单地使用类似负载均衡的机制,从连接池中遍历 mysql connection 执行对应的事务;但需要注意到的是,源的事务本身是具有顺序的,在 logical-clock 的场景下,存在部分并发 prepare 的事务是可以被并发执行的,但仍然有相当一部分的事务是不可并发执行,它们显然是分散于整个事务队列中,可以认为并发事务(最少 2 个)是被不可并发事务包围的:
假定存在一个事务队列有 6 个元素,其中只有 t1、t2 和 t5、t6 可以并发执行,那么执行 t3 时,需要 t1、t2 已经执行完毕,执行 t5 时需要 t3,t4 都执行完毕。
(3)校验点更新
在并发的事务执行场景下,存在水位低的事务后执行完,而水位高的事务先执行完,那么依照原本的机制,更低的水位会覆盖掉更高的水位,存在一定的风险:
Write_Event 的构造 SQL 调整为 replace into,可以回避冲突重复的写事件;Update 和 Delete 可以基于逻辑时钟的并发保障,不会出现。
水位只会向上提升,不会向下降低。
但不论怎样进行优化,并发执行事务必然会引入更多的风险,例如并发事务的回滚无法控制,目标实例和源实例的因果一致性被破坏等,业务可以根据自身的需要进行权衡,是否开启并发的执行。
基于逻辑时钟并发执行事务改造后,消费端的执行性能在同等的测试场景下,可以从 7.3MB/s 提升到 13.4MB/s 左右。
(4)小结
基于消费任务本身的库、表过滤,可以实现另一种形式下的并发执行,可以启动复数的消费任务分别支持不同的库、表,这也是利用了 kafka 的多消费者组支持,可以横向扩展以提高并发性能,适用于数据迁移场景,这一部分可以专门提供支持。
而基于逻辑时钟的方式,对于目前现网大规模存在的未开启 GTID 的集群是无效的,所以这一部分我们也一直在寻找更优的解决方案,例如更高版本的特性 Write Set 的合并等,继续做性能优化。
四、总结
最后,关于 MySQL 的复制能力不仅对于 MySQL 数据库服务本身的可用性、可靠性有巨大的提升,也提供了 Binlog 这一非常灵活的开放式的数据接口用于扩展数据的应用范围,通过利用这个“接口”,很容易就可以达成数据在多个不同存储结构、环境的实时同步,未来存储组也将会聚焦于 BinlogServer 这一扩展服务来强化 MySQL 的架构,包括但不限于数据安全性保障以及对下游数据链路的开放等。
参考资料:
版权声明: 本文为 InfoQ 作者【vivo互联网技术】的原创文章。
原文链接:【http://xie.infoq.cn/article/03012aefb6ffe23d283d3e766】。文章转载请联系作者。
评论