复制指的是在通过网络连接的多台机器上保留相同数据的副本。

基于领导者-跟随者的复制

复制的出现是因为各种各样的原因,主要包括:

  • 通过降低数据和访问用户在空间上的距离从而减少访问延迟
  • 在系统一部分出现故障时,系统能够对外提供服务,保证服务高可用
  • 增加提供读取数据服务的节点,增加数据读取吞吐量

数据复制困难之处在于处理复制数据的变更,为了解决变更带来的问题,有多种处理变更的算法:单领导者、多领导者和无领导者

复制过程是同步复制还是异步复制对于数据库实现来说也是一个重要的因素。

另外,数据复制带来的另外一个问题——数据一致性在分布式系统中也广泛存在,最终一致性等问题也需要我们了解研究。

数据复制的过程

存储数据库副本的每个节点被称为副本(replica)。当存在多个副本的时候,如何才能确保所有数据都存在于每个副本上?

常见的方案是基于领导者的复制(leader-based replication),也称为主从复制(master/slave)。它的大致工作流程如下:

  1. 副本之一被选定为领导者。当客户端要写入数据时,它必须将写入请求发送给领导者,领导者负责将数据写入本地存储;
  2. 其他副本被指定为追随者。每当领导者将新数据写入本地存储时,它也会将数据变更发送给追随者,这些变更构成了复制日志(relication log)或者变更流(change stream)。每个跟随者从领导者拉取复制日志,并相应地按照领导者处理的相同顺序更新其本地数据库副本;
  3. 当客户想要从数据库中读取数据时,可以向领导者或者追随者查询。但是对于写操作,只有领导者才能相应请求。

上述的复制过程是很多关系型数据库(mysql,sql server)内置功能,它也被用于一些非关系型数据库(mongodb)或者分布式消息代理(rabbitmq、kafka)等。

同步复制和异步复制

复制系统的一个重要细节:复制是同步还是异步。这同时会影响到数据写入过程和数据读取过程:

  • 同步复制的优点是:跟随者保证有与领导者一致的最新数据副本。如果领导者突然失效,我们可以确信这些数据仍然能够在跟随者中找到;但是完全同步复制的缺点是:如果同步跟随者没有响应,领导者就无法处理写入操作,领导者会阻止所有写入并等待跟随者再次可用;
  • 将所有从库都设置为同步是不切实际的:任何一个跟随者中断都会导致整个系统的不可用。通常在数据库上启用同步复制往往意味着只需要其中一部分跟随者保持同步复制即可。这样可以保证至少有两个及以上的副本拥有最新的数据副本。这种配置有时也被称为半同步
  • 通常情况下,基于领导者的复制配置都是完全异步的,这种情况下,如果领导者失效且不可恢复,任何尚未复制给跟随者的写入都会丢失,难以保证数据的持久性;完全异步的优势在于,即使所有的跟随者都落后于领导者,领导者也仍然能够继续处理写入。

增加新的跟随者

系统运行过程中,有时会需要增加一个新的副本。对于新增加的副本来说,如何将副本完整的同步到本地存储,大致过程如下:

  1. 在某个时刻获取主库的一致性快照;
  2. 将快照复制到新的跟随者节点;
  3. 跟随者连接领导者,并拉取快照之后的所有数据变更;
  4. 跟随者处理完成积压的复制日志后,我们就可以说新增加的这个跟随者**赶上(catch up)**了领导者,它现在可以继续处理新的数据库变更了。

处理宕机副本

如何基于领导者的复制实现系统高可用?

从库失效:追赶恢复

对于跟随者而言,它记录从领导者处收到的数据变更。如果跟随者崩溃并重新启动,或者由于网络原因导致跟随者和领导者之间暂时网络中断,这种情况比较容易恢复:跟随者从复制日志中可以指导,在发生故障之前处理的最后一个写操作,所以在跟随者重新连接到领导者之后,它可以向领导者请求最后一个写操作之后的所有数据变更。当处理完所有变更后,跟随者就重新赶上了领导者,可以继续处理接收并处理变更流。

主库失效:故障切换

领导者失效处理过程相对于从库失效来说会更加麻烦:其中一个跟随者需要被提升为信任领导者,并完成副本配置——后续写入请求都将转发到新任领导者,其他跟随者也要拉取来自新任领导者的数据变更。上述的过程被称为故障切换(failover)

故障切换过程可以手动进行或者自动进行:

  1. 确认领导者失效,引起领导者失效的原因会有很多:断网、断电、系统崩溃等等。常见的实现机制是超时(timeout):副本节点之间频繁的互相发送心跳,如果一个节点在一段时间内没有相应,就认为它失效了;
  2. 选择一个新任领导者:这个过程可由选举算法完成或者由运维人员手动指定。通常新任领导者拥有失效领导者最新的数据集副本;
  3. 重新配置系统以启用新任领导者。系统后续的写入操作都有新任领导者处理。

故障切换会有很多麻烦问题需要处理:

  • 如果使用异步复制,新的领导者可能没有收到旧领导者最新的写入操作。这在老的领导者重新加入系统后会导致数据冲突:常见的解决方案是简单丢弃旧领导者未被复制的写入——这会影响到系统数据的持久性;
  • 如果数据库需要和其他外部系统协调,丢弃写入内容非常危险,会带来难以预料的风险;
  • 领导者发生故障时,可能会出现多个跟随者认为自己是新任领导者,这种情况被称为脑裂(split brain)。如果有多个主库同时接收写操作但却没有冲突消解机制,就会造成数据写入的丢失或损坏;
  • 如何判定领导者失效,对于超时时间的设置也需要考虑:如果超时时间太短会造成不必要的故障切换过程,在系统负载较高的时候,发生故障切换可能会让情况变得更糟;如果超时时间设置太长,就意味着恢复时间会更长。

复制日志的实现

基于领导者的复制底层实现方式有多种:

基于语句的复制

最简单的情况下,领导者记录它执行的每个写入语句:每个INSERT、UPDATE、DELETE等都被转发给每个跟随者,每个跟随者解析并执行写入语句,就像从客户端收到请求的处理一样。

这种实现可能会有一些问题:

  • 任何调用**非确定性函数(nondeterministic)**的语句,可能在每个副本上产生不同的效果,如NOW()或者RAND()等;
  • 如果语句使用自增列或者依赖数据库中现有数据,则必须在每个副本上按照完全相同的顺序执行他们,否则会产生意料外的效果;
  • 有副作用的语句可能在每个副本上产生不同的效果。

上述的问题,可以由领导者使用固定的返回内容代替上述不确定函数的调用。但是这种方案需要考虑的情况实在是太多了。

传输预写式日志(WAL)

存储引擎的写操作通常都是追加到日志中的,对于日志结构存储或者是基于B树的块存储均是这样。

日志都包含了所有数据库写入的仅追加字节序列。可以使用完全相同的日志在另一个节点上构建副本:除了将日志写入磁盘之外,领导者还可以通过网络将其发送给跟随者。

这种方式的确定是:日志记录的数据非常底层,WAL甚至包含了哪些磁盘块中的哪些字节发生了变更。这使得复制与存储引擎紧密耦合,不利于后续存储引擎版本的升级或者整个副本存在不同版本的数据库软件。

逻辑日志复制(基于行的复制)

另外一种方法是,复制和存储引擎使用不同的日志格式,这样使得复制日志可以从存储引擎分离出来,这种复制日志被称为逻辑日志,将其与基于存储引擎的数据复制区分开来。

基于触发器的复制

pass

复制延迟处理

数据复制能够将系统的读请求分散到多个跟随者上,对于读多写少的应用来说,数据复制非常有价值。对于同步复制来说,单点故障会影响到整个系统的可用性,在真实应用过程中,更多的系统还是采用了异步同步的方式来进行数据复制。

当应用程序从异步跟随者读取数据时,如果跟随者落后,它可能会看到过时的信息。同时对领导者和跟随者发起相同的查询,可能会得到不同的结果,因为并非所有的写入都反应到了跟随者中。这种不一致只是一个暂时的状态——如果停止写入操作并等待一段时间,所有跟随者最终都会赶上领导者并保持一致。上述的这种效应被称为最终一致性

最终一致性并不仅仅针对于NOSQL,关系型数据库的异步复制追随者也有相同的特性。

最终一致性中的最终,在实际情况中跟随者落后于领导者的程度是没有限制的。这种落后程度可以通过**复制延迟(replication lag)**来反应:写入领导者到同步至跟随者之间的延迟,可能是很短的时间,也可能会因为网络问题变为几秒或者几分钟。

为了解决复制延迟问题,通常的方案有以下几种:

读己之写

在一些需要保证读写一致性的应用来说,通过读己之写来保证。这取决于应用程序的业务逻辑、应用场景和数据库的具体实现细节。

单调读

对于同一个客户端,多次分别向不同跟随者发起读请求后得到的查询结果不一致现象。通过单调读来避免上述问题:在读取数据时,可能会得到一个旧值,单调读意味着如果一个用户顺序的进行多次读取,他们不会看到时间上的后退,即如果先前读到较新的数据,后续读取不会得到更旧的数据。

单调读的一个实现方式:确保每个用户总是从同一个副本读取。

一致前缀读

对于一系列按照某个顺序发生的写入读取操作来说,任何人读取数据时,他们也期望以同样的顺序读取到相应的数据。

由于分区的存在,在分布式系统中,不同的分区质检不存在全局写入顺序:用户从数据库中读取数据时,可能会看到数据库某些部分处于较旧的状态,而某些部分处于较新的状态。

一种解决方案是:确保任何有因果关系的写入都写入分布式系统相同的分区。

复制延迟的解决方案

在使用最终一致性的系统时,由于各种可能的原因,复制延迟可能会增加到几秒或者十几分钟。为了避免由于复制延迟带来应用层出现的问题,常用的方法一般有两种:

  1. 由应用层代码来提供更为精细的控制,从而为系统提供更有力的保障,例如读己之写等等;
  2. 通过数据库提供的事务功能,并信赖数据库可以做正确的事情。这里的事务更多的指的是分布式事务。

多主复制

基于领导者-跟随者模式的复制,一个最大的问题是同一时间只能有一个领导者处理写入请求。基于领导者-跟随者模型的一个延伸——允许多个节点接收写入操作。复制仍然以原有的方式进行:处理写入的每个节点将写入操作转发到其他所有节点,这种模式也被称为多领导模式。

多主复制模式通常的应用场景:

  • 运维多个数据中心
  • 需要客户端离线操作
  • 协同编辑

这种模式虽然能够同时有多个节点可以接收写入操作,但是会带来另一个问题:写入冲突。A节点接收写入操作,并把这个写入同步到B节点,同样作为领导者的B节点发现这条写入操作和它已经处理过的写入是有冲突的,如何处理冲突就成为多领导模式的一个主要问题。关于如何消解冲突,后续在研究一致性共识的时候,会有更深入的了解。

多主复制拓扑

复制拓扑(replication topology)描述写入从一个节点到另一个节点的通信链路。常见的拓扑模式有:

  • 环形拓扑:每个节点接收来自一个节点的写入,并将这些写入外加自身的写入传播给另外一个节点
  • 星型拓扑:一个指定的根节点将写入传播给所有其他节点
  • 全部到全部拓扑:每个领导者都将其所有写入传播给其他所有领导者

了解这些内容有助于我们做出更好的技术选型以及在出现故障的时候更好的排查问题。

环形或星型拓扑的问题

在环形或者星型拓扑中,每个写入可能需要在到达其他所有副本之前经过多个节点。每个节点需要转发从其他节点收到的数据更改。这就需要在转发和接收数据的时候避免复制内容成环导致无限循环。通常每个节点被赋予一个唯一的标识符,并且在复制日志中,每个写入都被标记了所有已经过的节点的标识符。当一个节点收到用自己的标识符标记的数据更改时,该数据更改将被忽略,因为节点知道它已经被处理过。

另外,如果复制传播链路中如果有一个节点发生故障,可能会导致后续的传播流程中断,直到这个节点被修复。这种情况往往只能通过手动重新配置来解决。

全部到全部拓扑的问题

更密集的拓扑结构容错性会更好,因为复制的传播链路在全部到全部的拓扑结构中会有多条路径,避免单点故障。但是全部到全部拓扑结构可能会带来其他问题:由于复制传播路径不确定,可能会由于网络问题导致写入的时序和其他节点接收处理的时间不一致。这个问题类似于一致前缀读:这是一个因果关系的问题。后续会在检测并发写入时做更深入的了解。

无主复制

这种模式在实际应用中接触的较少,可以先不做了解。

原文地址:https://ddia.vonng.com/#/ch5