前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >主从同步中的关键技术解析

主从同步中的关键技术解析

原创
作者头像
郝阳
发布2018-01-15 16:58:30
4.1K3
发布2018-01-15 16:58:30
举报
文章被收录于专栏:郝阳的专栏郝阳的专栏

主从同步的整体思路不外乎“数据镜像(image) + 流水(binlog)”,但是仔细考虑,会有一些值得思考的细节问题,看看你是否考虑过?(术语:主机=master,从机=slave)

问题提出:

  1. “数据镜像”也称之为“快照”(snapshot),是指保留某个瞬间状态的切片数据。但是毕竟保存数据是个过程(可能需要数分钟、数十分钟、甚至数小时不等),如何保证这个过程中产生的修改操作,不会“弄脏”数据镜像呢?
  2. master生成“数据镜像”并成功传输给slave之后,还不能称之为主从数据一致。从镜像数据产生到传输完成过程中累计的修改操作,如何再增量的同步给slave?
  3. 什么是binlog?字面意思很简单——binary log。仅仅是记录修改数据的一个过程么?有没有其它格式?分别的特点和优缺点是什么?
  4. binlog的同步可以是slave向master拉取(pull),也可以是master向slave推送(push),应该选择哪种方式?各有什么优缺点?
  5. master产生一条修改数据操作,实时同步给slave后,是否需要等待slave确认收到的ack?ack的必要性和优缺点分别是什么?

如果上述5个问题,你都知道答案,那一定是对主从同步机制非常了解,可以不用再继续看后文啦。如果有些问题你不确定或者没有明确答案的话,接下来,会参考一些成熟系统(Redis、Mysql和TCaplus)的主从同步机制,看看它们是怎么解决上述几个问题的,最后给出对比和总结。

Question 1: 如何保证“数据镜像”的数据一致性?

1. Redis复用了linux的fork时的Copy-On-Write(COW)技术。

master收到slave的同步请求之后,master会fork出一个子进程用于产生镜像数据(RDB文件)。fork之后,父子进程的进程空间有些资源共享,大多数资源(如堆栈内存)会复制一份到子进程。但是内核为了更加高效,此时的复制并不是真正的复制,而是写时复制(Copy-On-Write)。

Copy-On-Write的示意图

示意图中可以看出,fork后父子进程的虚拟地址不同,但是都指向父进程的物理地址。当父子进程有任意一方,尝试修改内存的时候,都会产生缺页中断,由内核分配内存page给子进程独享。(正文段除外,因为是只读段,所以可以复用物理内存)

所以,子进程可以很好的保持fork瞬间的内存状态,并把数据保存成镜像文件。父进程此时继续处理业务请求,期间即使再有修改操作,也只是修改主进程对应的内存,子进程感知不到。从而很好的解决的此问题。

2. Mysql的Innodb数据引擎采用的是MVCC技术。

Mysql的镜像数据通常利用mysqldump工具生成。

1) 对于不支持事务的引擎,如MyISAM。为了保证dump时的数据一致性,通常都会采用锁表操作,即数据库在dump过程中变为只读,不允许写操作。这种方式通常只能在业务低峰期,或者备机上使用。

2) 对于支持事务的引擎,如InnoDB。mysqldump加上--single-transaction参数,可以在不停写、且保证数据一致性的基础上,进行dump数据生成镜像文件。其中采用的就是MVCC技术。

MVCC全称为Multi-Version Concurrency Control,即多版本并发控制技术。目前支持的数据库有:Oracle、InnoDB、PostgreSQL等。类似于前面介绍的Copy-On-Write技术,也是在修改数据时,copy数据并标记对应的版本号,但是操作粒度更加精细。MVCC的实现原理简单介绍如下:

MVCC实现的原理图

上图中,读操作采用READ-COMMITTED级别,也就是说读取到的一定是事务提交之后的结果,已经开始未提交的事务结果是读取不到的。大括号的范围表示事务的执行时间,其中绿色为修改类的事务,黄色为查询类的事务。每个事务对应一个事务id,按照事务开始的时间顺序排列,其事务id分别为R1->W2->R3->W4->R5(前缀R和W是为了直观区分读写)。

1) 写事务W2,把数值从V1修改成了V2。此时V2是最新值,V1是旧的副本;

2) 写事务W4,把数值从V2修改成功V3。此时V3是最新值,V2和V1是旧的副本;

3) R1是先于W2开始的读事务。所以R1读取到的是V1;

4) R3是W2提交之前发起的读事务。所以R3也只能读到V1;

5) R5是W4提交之前,W2提交之后发起的读事务。所以R5读到的是V2;

6) 当R3结束之后,没有事务需要访问V1了,所以V1可以从副本中删除;

7) 当R5结束之后,没有事务需要访问V2了,所以V2可以从副本中删除,只剩下最新值V3;

上述流程可以看出:类似于Copy-On-Write,当有读事务访问一个旧值时,修改类事务就会copy出来一个副本进行修改,保证读写不会冲突,而且不会锁住流程。

MVCC的原理如上面介绍,不过具体实现各数据库略有不同:

1) PostgreSQL中副本的表现形式就是多记录同时存在,但是每个记录会对应不同的事务ID(所有的UPDATE操作都会变成INSERT操作并记录事务ID的方式,延迟DELETE旧数据),根据读事务级别以及读事务ID判断需要读哪个副本。所以tcpdump过程中,极端情况下如果Update全表,会导致表大小增加一倍。独立的AutoVacuum进程负责异步回收不再需要使用的副本数据。

2) InnoDB中副本是保存在Undo Log中的,所以表数据不会膨胀。每次对记录的修改时,都会把修改前的数据写入Undo log中。所有读事务会对应一份ReadView的表,当有修改事务操作数据时,把操作事务的事务id记录在ReadView中,这样读事务在读取某条记录数据时,可以根据记录最新的事务id,在ReadView中查找,查找到的话,就意味着数据已经修改过,需要结合Undo log找到自己可以读到的那个值,否则就可以直接读取最新的数值。

3. TCaplus是利用“加锁”的思想。

镜像文件由slave生成,而且为了保证数据一致性,slave需要短暂停止同步,然后dump数据到磁盘后,再恢复同步。但是这种方式会导致有一段时间主备数据不同步,存在一定隐患。(TCaplus同学也有计划把这块做成边同步边落地数据的方式)

问题总结:

1) COW技术。优点:实现非常简单,fork之后完全跟主进程访问内存的方式相同。缺点:粒度相对粗犷,一次缺页中断加载的就是一个内存page大小(默认2kB);

2) MVCC技术。优点:控制更加精细,可以到数据记录级别。缺点:实现相对复杂,需要维护各种版本号,已经无效副本的回收。

3) “加锁”。优点:实现最简单。缺点:业务需要停写,影响较大。

Question 2:如何同步积压数据?如何增量同步?

1. Redis分两种情况处理:

1) 积压数据同步。master在主进程fork返回之后,子进程开始生成镜像数据,主线程此时就把之后产生的修改类指令存放在slave的发送缓冲区中,等待镜像文件发送完之后,一并把缓冲区中积压的指令发送给slave。

衍生的问题:会不会由于生成镜像文件时间较长,而这段时间的修改操作又很多,导致缓冲区爆掉?redis采用固定buffer+变长内存块队列的方式,保证缓冲区大小比较灵活,有伸缩性。

其中16k是缓冲区buffer的默认大小,静态分配;如果还不够,每次就动态分配2K的block,挂在一个链表上。

2) 增量数据同步。这种方式,是在slave与master连接断开,或者长时间没有心跳后(也会断连接)触发的增量同步逻辑。

Redis增量同步示意图

slave发送PSYNC+runid+offset请求给master,master侧维护一个循环队列(内存数据结构),称之为backlog,大小可配置。runid是为了确保master的实例没有变化过(没有重启过),offset是记录了slave当前已经同步的位置,master就根据offset在自己维护的backlog中查找,找到的话就把backlog中剩余的增量数据一并发送给slave。

2. Mysql的增量同步实现。

Mysql增量同步示意图

master收到slave的请求之后,就创建一个Dump-Thread线程与之对接(多个slave,就有多个线程)。与Redis类似,master根据slave传过来的binlog的filename和position,确定slave当前的同步位置,由Dump-Thread负责把剩余的binlog数据发送给slave。slave侧有两个线程与同步有关系,一个是IO-Thread,负责接收主机同步过来的binlog数据,并把数据写入Relay-Log的文件中;另一个是SQL-Thread,负责从Relay-Log中读取binlog并执行语句。

3. TCaplus的增量同步实现。

TCaplus增量同步示意图

TCaplus与Mysql的流程基本一致,不过slave侧没有Relay-Log这个中转文件,而是收到同步的binlog之后,将其转换成正常的请求,直接在备机上重做。

衍生的问题:Mysql上的relay-log有存在的价值吗?IO-Thread和SQL-Thread需要独立开来吗?

Mysql分两个线程是有必要的!因为SQL语句的执行时间很难估计,有些操作的执行时间会非常长(例如Update全表,或者没有命中索引的修改操作),如果没有Relay-Log而是直接由缓冲区接收,那么缓冲区很容易积压满,master就无法继续同步数据过来,从而导致同步延时增加。所以把接收数据和执行操作两个步骤拆分开来进行解耦,尽可能让同步消息先落地到Relay-log中是非常有必要的。

TCaplus这方便的问题不突出,批量更新的操作数量不多,特别是热点数据的修改都直接在内存中完成,所以整个同步数据接收和执行的总时间都是可控的。那么Relay-Log的存在意义就不大了。

问题总结:

1. Redis都是基于内存的数据结构实现的增量同步机制。通过变长的发送缓冲区和backlog的循环对列实现同步。

2. Mysql和TCaplus都是基于文件的binlog,实现增量同步机制。两者的区别在于:TCaplus的写操作耗时可控,所以没有Relay-log的中转文件。

Question 3:binlog的格式有哪几种格式?

binlog的全称为binary log,是一种二进制日志格式,主要用于主从同步或数据恢复。传统意义上的binlog一般都按顺序记录了修改类的操作指令(增加、修改、删除),并把这些操作流保存在磁盘上。广义上,binlog分为两种格式:

1) statement-based binlog。这种是基于操作流的,收到的请求是什么,就保存下来什么。

2) row-based binlog。这种是基于操作结果的,把操作之后row的数据保存下来。

这两种binlog各有什么特点?

1) statement-based binlog。

优点:表达更加精简,可以很好的保留数据的修改过程。

缺点:某些情况下,无法精确表达,重做可能会产生不同的执行结果。(例如Insert...select...操作,如果select没有带order by,返回顺序不同而导致auto_increment的值不同;还有其它复杂的机制,如触发器、存储过程、事务交叉执行等等都可能有不同结果)

2) row-based binlog。

优点:表达更加精确;有些操作的效率会更高(例如Insert...select...,重做的时候不需要重复select一遍了);具有幂等性,同一条日志多次执行的结果相同。

缺点:只是存储了修改结果,所以无法表达修改过程,不知道为什么变成了这样的状态;日志容易膨胀(假设一个操作是修改整个表的数据,基于row的话,就需要把整个表的数据结果都保存下来!!!)

对比下来,发现两种格式各有优缺点,所以产生了混合型的mix-based binlog:在能够精确表达的时候优先使用 statement-based binlog,对于无法精确表达的情况则采用row-based binlog。

  1. Redis固化数据的日志称之为AOF(append only file)。这种格式存储的是操作流,所以是一种statement-based binlog。
  2. Mysql支持上述三种binlog。并且建议使用mix-based binlog。
  3. TCaplus内部称之为ULog。是一种紧凑格式的基于statment-based的binlog。

Question 4:binlog同步什么时候pull,什么时候push?

  1. Redis:slave与master建立连接之后,会主动把已同步的offset发送给master,请求按照这个offset拉取数据,所以此时属于pull模式。当主从数据一致之后,master收到的修改类操作,都会实时传播(propagate)给slave,此时属于push模式。
  2. Mysql:也是push和pull结合。

前面介绍过,slave首先告诉master,自己当前的binlog filename和binlog position。master会创建独立线程Dump-thread与slave进行对接处理,根据传过来的(file,pos)在本地的binlog中查找,并把剩下的binlog发送给slave。这个过程也是pull模式。

Mysql实时广播binlog的流程图

master侧有新的binlog产生时,主线程会广播通知Dump-thread有新的binlog产生,然后由Dump-thread读取binlog发送给slave。该过程属于push模式。

  1. TCaplus:与Mysql类似。

问题总结:

三个系统都采用pull和push相结合的方式同步数据。

slave长时间与master不同步,slave什么时候具备重新同步的能力,只有slave才知道,所以这种情况下由slave拉取增量数据最合适;

master产生新数据需要同步给slave,此时只有master可以第一时间感知到,所以此时采用推送的方式通知slave更新数据一定是最实时的。试想,如果slave循环向master拉取数据,一方面效率会比较低,另一方面实时性也比较差。

Question 5:同步binlog是否需要ack?

Redis:master把增量修改操作发给slave,并没有确认slave是否收到。由于主备通过tcp直连,所以tcp层可以保证发送到对端的系统缓冲区中,而且也不会出现乱序或丢包的情况。

Mysql:默认的同步机制下,mysql也是不需要slave回复ack的。但是不确认ack对于要求强一致性的场合是不够的。例如,master把数据写入binlog并通知slave有修改后,就返回给客户端写入成功,恰恰master此时挂掉,slave没有收到刚才的binlog就升级为master,就会出现数据不一致。对此Mysql5.5版本之后,引入了半同步机制(semi-sync replication),这种机制下,是要求收到slave的ack回包的。(具体关于半同步的内容就不展开了,后面的参考资料里面可以进一步研究下。)

TCapuls:最终一致性,tcp直连,也是没有ack确认的。

问题总结:

1) tcp直连的情况下,可以不确认ack,简单实现最终一致性的主从同步。

2) 如果不使用tcp直连(例如通过proxy中转了同步请求),或者使用udp协议(不能保证时序、可靠性),那么必须要实现自己的ack。否则连最终一致性都无法保证。

3) 强一致性的情况下,需要确保slave回复ack成功后,再通知客户端成功。

各系统对比总结:

参考资料:

1 . Redis官网](http://redis.io/) 。上面有很多英文介绍,结合源码分析(源码写的很漂亮,而且注释丰富),很容易搞懂整个同步流程。

2 .Mysql的官网文档

3 .blog

1) Redis的增量同步流程分析

2) Mysql的半同步复制解析

3)介绍了 ysql如何实时推送同步

4 附件中还有一个介绍MVCC技术的ppt,网上搜到的,觉得讲的很清楚,可以参考下。

附件:

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 问题提出:
  • Question 1: 如何保证“数据镜像”的数据一致性?
    • 问题总结:
    • Question 2:如何同步积压数据?如何增量同步?
      • 问题总结:
      • Question 3:binlog的格式有哪几种格式?
      • Question 4:binlog同步什么时候pull,什么时候push?
        • 问题总结:
        • Question 5:同步binlog是否需要ack?
          • 问题总结:
          • 各系统对比总结:
          • 参考资料:
          • 附件:
          相关产品与服务
          云数据库 Redis
          腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档