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

主从同步的整体思路不外乎“数据镜像(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,网上搜到的,觉得讲的很清楚,可以参考下。

附件:

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

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

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏iOSDevLog

Action API目录

3089
来自专栏.NET后端开发

ADO.NET入门教程(五) 细说数据库连接池

题外话 通过前几章的学习,不知道大家对ADO.NET有一定的了解了没有。撇开文章质量不讲,必须肯定的是,我是用心去写每一篇文章的。无论是是在排版上,还是在内容选...

4199
来自专栏安恒信息

安全漏洞公告

1 Linux Kernel 'linux-image-3.2.0-4-5kc-malta'软件包拒绝服务漏洞Linux Kernel 'linux-image...

3187
来自专栏信安之路

这些命令你用过多少?

在拿到一个 webshell 之后,大家首先会想到去把自己的权限提升到最高,windows 我们会提升到 SYSTEM 权限,而 Linux 我们会提升到 ro...

821
来自专栏魏艾斯博客www.vpsss.net

WP-Optimize 插件安装使用教程-WordPress 数据库优化效果明显

3154
来自专栏云计算教程系列

如何将Ubuntu从16.04升级到18.04

Ubuntu 18.04是一个长期支持(LTS)版本,LTS 版本每两年发布一次,而 Ubuntu 18.04 是自 2016 年以来的第一个长期支持版本。Ub...

2.3K4
来自专栏不忘初心

基于zookeeper的daemon框架方案——支持容灾和心跳监控

在线上项目中,很多时候需要起一个daemon做守护进程,用于不停地或以一定间隔地执行工作,比如每隔20s把内存中的数据做快照写磁盘。

2145
来自专栏Java架构沉思录

一文读懂数据库事务

什么是事务 根据维基百科的定义,一个数据库事务通常包含了一个序列的对数据库的读/写操作。它的存在包含有以下两个目的:1)为数据库操作序列提供了一个从失败中恢复...

3058
来自专栏java学习

教大家在如何Centos7系统中安装JDK、Tomcat、Mysql

1、jdk的安装 2、tomcat的安装 3、mysql的安装 远程工具:SSH Secure File Transfer Client

1372
来自专栏CSDN技术头条

Schemaless架构(二):Uber基于MySQL的Trip数据库

ber的Schemaless数据库是从2014年10月开始启用的,这是一个基于MySQL的数据库,本文就来探究一下它的架构。本文是系列文章的第二部分;第一部分是...

2507

扫码关注云+社区

领取腾讯云代金券