探讨MySQL的复制机制实现的方式

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.000001position: 381808617  
这个概念相对来说更直观,可以直接理解为当前处在file对应编号的binlog文件中,同时已经产生了合计position bytes的数据,如例子中所示即该实例已经产生了381808617 bytes的binlog,这个值在对应机器直接查看文件的大小也是匹配的,所以file postion就是文件序列与大小的对应值。
基于这种模式开启复制,需要显式地在复制关系中指定对应的file和position:
change master to master_log_file='binlog.000001', master_log_position=381808617;  
这个值必须要准确,因为这种模式下从库获取的数据完全取决于有效的开启点,那么如果存在偏差,就会丢失或执行重复数据导致复制中断。
gtid
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 复制的具体流程
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 - 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具体实现经历的两个阶段:
基于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,其它语言做对应转换即可。
data := make([]byte, 4+1+4+1+len(hostname)+1+len(b.cfg.user)+1+len(b.cfg.password)+2+4+4)  
第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)发起复制指令
data := make([]byte, 4+1+4+2+4+len(p.name))
第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包本身不包含连接池模式,这里基于事务并发解析的并发度在启动时,扩展存活连接的数量。
// 初始化客户端连接数se.conn = make([]*connection, meta.maxconcurrencetransaction)  
(2)并发选择连接
利用逻辑时钟
开启gtid复制的模式下,binlog中的gtid_event的正文内会包含两个值:
lastcommitted int64sequencenumber int64  
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的架构,包括但不限于数据安全性保障以及对下游数据链路的开放等。


美图M4手机怎么样 为什么会被称为自拍神器
浮点数基础知识科普
为什么说可溯源就是区块链
快速识别需要更换有故障的自愈式低压并联电容器的技巧
企业应该对网站的网络安全性能重视起来
探讨MySQL的复制机制实现的方式
新火种AI|“赌城”上演“科技春晚”,AI硬件将在2024年大爆发
中国5G新进展:运营商启动招标,临时牌照有望上半年发放
光学计算的未来:一种新的可编程电路方法
深入理解FPGA Verilog HDL语法(一)
智慧农业物联网平台助力农业生产管理更加高效便捷?
小米max拆解 骁龙652平板手机好吗?
ICC2中的physical_status属性值都有哪些区别呢
创建 USB-PD 源设备编译运行
年产200万平方米高精密多层电路板 江西九江仁创艺电子项目开工
WiFi6是什么意思?有哪些技术提升?
如何优化Windows计算机性能
螺旋板式换热器结垢的解决方法
手机中文输入法
不会还有人还只用CAM350吧