MySQL在生产环境中被广泛地应用,大量的应用和服务都对MySQL服务存在重要的依赖关系,可以说如果数据层的MySQL实例发生故障,在不具备可靠降级策略的背景下就会直接引发上层业务,甚至用户使用的障碍;同时MySQL中存储的数据也是需要尽可能地减少丢失的风险,以避免故障时出现数据丢失引发的资产损失、客诉等影响。
在这样对服务可用性和数据可靠性需求的背景下,MySQL在Server层提供了一种可靠的基于日志的复制能力(MySQL Replication),在这一机制的作用下,可以轻易构建一个或者多个从库,提高数据库的高可用性、可扩展性,同时实现负载均衡:
具备包括但不限于以上特性的MySQL集群就可以覆盖绝大多数应用和故障场景,具备较高的可用性与数据可靠性,当前存储组提供的生产环境MySQL就是基于默认的异步主从复制的集群,向业务保证可用性99.99%,数据可靠性99.9999%的在线数据库服务。
本文将深入探讨MySQL的复制机制实现的方式, 同时讨论如何具体地应用复制的能力来提升数据库的可用性,可靠性等。
从比较宽泛的角度来探讨复制的原理,MySQL的Server之间通过二进制日志来实现实时数据变化的传输复制,这里的二进制日志是属于MySQL服务器的日志,记录了所有对MySQL所做的更改。这种复制模式也可以根据具体数据的特性分为三种:
最早的实现是基于语句格式,在3.23版本被引入MySQL,从最初起就是MySQL Server层的能力,这一点与具体使用的存储引擎没有关联;在5.1版本后开始支持基于行格式的复制;在5.1.8版本后开始支持混合格式的复制。
这三种模式各有优劣,相对来说,基于Row的行格式被应用的更广泛,虽然这种模式下对资源的开销会偏大,但数据变化的准确性以及可靠性是要强于Statement格式的,同时这种模式下的Binlog提供了完整的数据变更信息,可以使其应用不被局限在MySQL集群系统内,可以被例如Binlogserver,DTS数据传输等服务应用,提供灵活的跨系统数据传输能力, 目前互联网业务的在线MySQL集群全部都是基于Row行格式的Binlog。
2.2.1 Binlog事件类型
对于Binlog的定义而言,可以认为是一个个单一的Event组成的序列,这些单独的Event可以主要分为以下几类:
各类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。
只要开启了log_bin,MySQL就会具有File Position的位点记录,这一点不受GTID影响。
File: binlog.000001
Position: 381808617
这个概念相对来说更直观,可以直接理解为当前处在File对应编号的Binlog文件中,同时已经产生了合计Position bytes的数据,如例子中所示即该实例已经产生了381808617 bytes的Binlog,这个值在对应机器直接查看文件的大小也是匹配的,所以File Postion就是文件序列与大小的对应值。
基于这种模式开启复制,需要显式地在复制关系中指定对应的File和Position:
CHANGE MASTER TO MASTER_LOG_FILE='binlog.000001', MASTER_LOG_POSITION=381808617;
这个值必须要准确,因为这种模式下从库获取的数据完全取决于有效的开启点,那么如果存在偏差,就会丢失或执行重复数据导致复制中断。
MySQL 会在开启GTID_MODE=ON的状态下,为每一个事务分配唯一的全局事务ID,格式为:server_uuid:id
Executed_Gtid_Set: e2e0a733-3478-11eb-90fe-b4055d009f6c:1-753
其中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一样指定具体的值,只需要设置:
CHANGE MASTER TO MASTER_AUTO_POSITION=1;
从库在读取到Binlog后,会自动根据自身Executed_GTID_Set记录比对是否存在已执行或未执行的Binlog事务,并做对应的忽略和执行操作。
2.3.1 基本复制流程
当主库已经开启了binlog( log_bin = ON ),并正常地记录binlog,如何开启复制?
这里以MySQL默认的异步复制模式进行介绍:
总结来说,主库上只会有一个线程,而从库上则会有两个线程。
当集群进入运行的状态时,从库会持续地从主库接收到Binlog事件,并做对应的处理,那么这个过程中将会按照下述的数据流转方式:
上述过程都是异步操作,所以在某些涉及到大的变更,例如DDL改变字段,影响行数较大的写入、更新或删除操作都会导致主从间的延迟激增,针对延迟的场景,高版本的MySQL逐步引入了一些新的特性来帮助提高事务在从库重放的速度。
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个步骤:
从半同步复制的时序图来看,实际上只是在主库Commit的环节多了等待接收从库ACK的阶段,这里只需要收到一个从节点的ACK即可继续正常的处理流程,这种模式下,即使主库宕机了,也能至少保证有一个从库节点是可以用的,此外还减少了同步时的等待时间。
2.3.3 小结
在当前生产环境的在线数据库版本背景下,由MySQL官方提供的复制方式主要如上文介绍的内容,当然目前有还很多基于MySQL或兼容MySQL的衍生数据库产品,能在可用性和可靠性上做更大的提升,本文就不继续展开这部分的描述。
目前已经提及的复制方式,存在一个显著的特性:无法回避数据延迟的场景,异步复制会使得从库的数据落后,而半同步复制则会阻塞主库的写入,影响性能。
MySQL早期的复制模式中,从库的IO线程和SQL线程本质上都是串行获取事件并读取重放的,只有一个线程负责执行Relaylog,但主库本身接收请求是可以并发地,性能上限只取决于机器资源瓶颈和MySQL处理能力的上限,主库的执行和从库的执行(SQL线程应用事件)是很难对齐的,这里引用一组测试数据:
期望业务层限制使用是不现实的,MySQL则在5.6版本开始尝试引入可用的并行复制方案,总的来说,都是通过尝试加强在从库层面的应用速度的方式。
2.4.1 基于Schema级别的并行复制
基于库级别的并行复制是出于一个非常简易的原则,实例中不同Database/Schema内的数据以及数据变更是无关的,可以并行去处置。
在这种模式中,MySQL的从节点会启动多个WorkThread ,而原来负责回放的SQLThread会转变成Coordinator角色,负责判断事务能否并行执行并分发给WorkThread。
如果事务分别属于不同的Schema,并且不是DDL语句,同时没有跨Schema操作,那么就可以并行回放,否则需要等所有Worker线程执行完成后再执行当前日志中的内容。
MySQL Server
MySQL [(none)]> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| aksay_record |
| mysql |
| performance_schema |
| proxy_encrypt |
| sys |
| test |
+--------------------+
7 rows in set (0.06 sec)
对于从库而言,如果接收到了来自主库的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具体实现经历的两个阶段:
当前vivo的在线MySQL数据库服务标准架构是基于一主一从一离线的异步复制集群,其中一从用于业务读请求分离,离线节点不提供读服务,提供给大数据离线和实时抽数/DB平台查询以及备份系统使用;针对这样的应用背景,存储研发组针对MySQL场景提供了两种额外的扩展服务:
虽然MySQL的主从复制可以提高系统的高可用性,但是MySQL在5.6,5.7版本是不具备类似redis的自动故障转移的能力,如果主库宕机后不进行干预,业务实际上是无法正常写入的,故障时间较长的情况下,分离在从库上的读也会变得不可靠。
3.1.1 VSQL(原高可用2.0架构)
那么在当前这样标准一主二从架构的基础上,为系统增加HA高可用组件以及中间件组件强化MySQL服务的高可用性、读拓展性、数据可靠性:
3.1.2 数据可靠性强化
数据本身还是依赖MySQL原生的主从复制模式在集群中同步,这样仍然存在异步复制本身的风险,发生主库宕机时,如果从库上存在还未接收到的主库数据,这部分就会丢失,针对这个场景,我们提供了三种可行的方案:
配置HA的中心节点和全网MySQL机器的登录机器后,按照经典的MHA日志文件复制补偿方案来保障故障时的数据不丢失,操作上即HA节点会访问故障节点的本地文件目录读取候选主节点缺失的Binlog数据并在候选主上重放。
优势
劣势
依赖数据传输服务中的BinlogServer模块,提供Binlog日志的集中存储能力,HA组件同时管理MySQL集群以及BinlogServer,强化MySQL架构的健壮性,真实从库的复制关系全部建立在BinlogServer上,不直接连接主库。
优势
劣势
MySQL集群开启半同步复制,通过配置防止退化(风险较大),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.1 基于Binlog的跨系统数据流转
通过利用Binlog,实时地将MySQL的数据流转到其它系统,包括MySQL,ElasticSearch,Kafka等MQ已经是一种非常经典的应用场景了,MySQL原生提供的这种变化数据同步的能力使其可以有效地在各个系统间实时联动,DTS(数据传输服务)针对MySQL的采集也是基于和前文介绍的复制原理一致的方法,这里介绍我们是如何利用和MySQL 从节点相同的机制去获取数据的,也是对于完整开启复制的拓展介绍:
(1)如何获取Binlog
比较常规的方式有两种:
本文只介绍第二种,Fake Slave的实现方式
(2)注册Slave身份
这里以GO SDK为例,GO的byte范围是0~255,其它语言做对应转换即可。
data := make([]byte, 4+1+4+1+len(hostname)+1+len(b.cfg.User)+1+len(b.cfg.Password)+2+4+4)
(3)发起复制指令
data := make([]byte, 4+1+4+2+4+len(p.Name))
以上两个命令通过客户端连接执行后,就可以在主库上观察到一个有效的复制连接。
3.2.2 利用并行复制模式提升性能
以上两个命令通过客户端连接执行后,就可以在主库上观察到一个有效的复制连接。
根据早期的性能测试结果,不做任何优化,直接单连接重放源集群数据,在网络上的平均传输速度在7.3MB/s左右,即使是和MySQL的SQL Relay速度相比也是相差很远,在高压场景下很难满足需求。
DTS消费单元实现了对消费自kafka的事件的事务重组以及并发的事务解析工作,但实际最终执行还是串行单线程地向MySQL回放,这一过程使得性能瓶颈完全集中在了串行执行这一步骤。
(1)连接池改造
旧版的DTS的每一个消费任务只有一条维持的MySQL长连接,该消费链路的所有的事务都在这条长连接上串行执行,产生了极大的性能瓶颈,那么考虑到并发执行事务的需求,不可能对连接进行并发复用,所以需要改造原本的单连接对象,提升到近似连接池的机制。
go-mysql/client包本身不包含连接池模式,这里基于事务并发解析的并发度在启动时,扩展存活连接的数量。
// 初始化客户端连接数
se.conn = make([]*Connection, meta.MaxConcurrenceTransaction)
(2)并发选择连接
开启GTID复制的模式下,binlog中的GTID_EVENT的正文内会包含两个值:
LastCommitted int64
SequenceNumber int64
lastCommitted是我们并发的依据,原则上,LastCommitted相等事务可以并发执行,结合原本事务并发解析完成后会产生并发度(配置值)数量的事务集合,那么对这个列表进行分析判断,进行事务到连接池的分配,实现一种近似负载均衡的机制。
对于并发执行的场景,可以比较简单地使用类似负载均衡的机制,从连接池中遍历mysql connection执行对应的事务;但需要注意到的是,源的事务本身是具有顺序的,在logical-clock的场景下,存在部分并发prepare的事务是可以被并发执行的,但仍然有相当一部分的事务是不可并发执行,它们显然是分散于整个事务队列中,可以认为并发事务(最少2个)是被不可并发事务包围的:
假定存在一个事务队列有6个元素,其中只有t1、t2和t5、t6可以并发执行,那么执行t3时,需要t1、t2已经执行完毕,执行t5时需要t3,t4都执行完毕。
(3)校验点更新
在并发的事务执行场景下,存在水位低的事务后执行完,而水位高的事务先执行完,那么依照原本的机制,更低的水位会覆盖掉更高的水位,存在一定的风险:
但不论怎样进行优化,并发执行事务必然会引入更多的风险,例如并发事务的回滚无法控制,目标实例和源实例的因果一致性被破坏等,业务可以根据自身的需要进行权衡,是否开启并发的执行。
基于逻辑时钟并发执行事务改造后,消费端的执行性能在同等的测试场景下,可以从7.3MB/s提升到13.4MB/s左右。
(4)小结
基于消费任务本身的库、表过滤,可以实现另一种形式下的并发执行,可以启动复数的消费任务分别支持不同的库、表,这也是利用了kafka的多消费者组支持,可以横向扩展以提高并发性能,适用于数据迁移场景,这一部分可以专门提供支持。
而基于逻辑时钟的方式,对于目前现网大规模存在的未开启GTID的集群是无效的,所以这一部分我们也一直在寻找更优的解决方案,例如更高版本的特性Write Set的合并等,继续做性能优化。
最后,关于MySQL的复制能力不仅对于MySQL数据库服务本身的可用性、可靠性有巨大的提升,也提供了Binlog这一非常灵活的开放式的数据接口用于扩展数据的应用范围,通过利用这个“接口”,很容易就可以达成数据在多个不同存储结构、环境的实时同步,未来存储组也将会聚焦于BinlogServer这一扩展服务来强化MySQL的架构,包括但不限于数据安全性保障以及对下游数据链路的开放等。
参考资料: