跳转至

分布式系统之复制

复制:在多台通过网络连接的机器上保存相同数据的拷贝。

为什么需要复制数据? - 允许系统在部分节点出现故障后继续工作(增加可用性) - 地理上保持数据离用户更近(减少延迟) - 扩展可以提供查询的机器数量(增加读吞吐)

复制算法: 1. 单主复制:所有客户端都将写入操作发送到主节点上,该节点负责将数据更改事件发送到其它副本。每个副本都可以接受读请求,但内容可能是过期值。 2. 多主复制:系统中存在多个主节点,每个都可以接受请求,客户端将写请求发送到其中一个主节点上,该节点负责将数据更改事件同步到其它主节点和自己的从节点。 3. 无主复制:客户端将写请求发送多个节点上,读取时从多个节点上并行读取,以此检测和纠正某些过期数据。

三种不同的算法有各自不同的优势和劣势。

复制权衡:同步复制、异步复制,如何处理失效的副本。

主从复制

每个节点都保存数据库的一个副本,如何保证所有数据最终存在于所有副本中?所有的写入需要被每个副本处理。最常见的解决办法是:基于主节点的复制。

主从复制流程如下:

  1. 一个节点被指定为主节点,当客户端写入时,必须将请求发送到主节点
  2. 其它节点作为从节点,主节点将新数据写入本地存储时,还将数据变更信息发送给从节点(复制日志或者变更流)。从节点收到日志后更新本地数据,与主节点相同的顺序应用所有写入。
  3. 当客户端从数据库读时,可以从主节点读,或者从任意一个从节点读。

image-20190824202427656

这种模式被很多系统所使用:

关系数据库:PG/MySQL/Oracle/SQL Server

非关系数据库:MongoDB/RethinkDB/Espresso

分布式消息队列:Kafka/RabbitMQ

还有一些网络文件系统和复制块设备入DRBD使用。

同步异步

在主从复制中,客户端发送更新请求给主节点,主收到请求数据,在某个时刻主节点将数据变更发送给从节点,最终主节点通知客户端更新成功。

一主两从,一个同步从,一个异步从:

image-20190824203118027

同步:从1返回应答后才给客户端返回成功,从1是同步的。

异步:主发送消息,但是不等从返回应答,从2是异步的。

延迟:在从2处理消息前,可能会有很大的延迟,大部分数据库在 1s 内应用数据变更。但是并无法保证延迟在什么范围内。有一些场景可能会落后主几分钟甚至更长时间,例如从故障恢复,系统达到最大容量时,或者节点间出现网络故障。

同步复制的优势:数据副本保持最新,并且与主保持一致。如果主出现故障,我们可以确保数据在从上可用。 同步复制的缺点:如果从节点不返回应答(从机故障,网络错误或其他原因),则无法写入。主节点必须阻塞所有写入等待同步副本再次可用。

半同步:如果一个同步从不可用或者很慢,则异步从中的一个变为同步。可以保证至少有两个节点的数据是更新的。

异步复制:基于主的复制通常被配置为异步的,如果主故障,则所有写入主但是没有复制到从的数据都会丢失。削弱持久性可能听起来像是一种糟糕的权衡,但异步复制仍然被广泛使用,特别是如果有很多从或者它们在地理上分布的时候。

其它复制方式:链式复制

复制和共识有很强的关系,后面介绍。

新的从节点

增加副本数或者替换故障节点,都需要增加新的从节点。如果保证新的从节点与主数据有完全一样的拷贝数据?

将数据文件拷贝到另一个节点是否可以解决问题?由于客户端持续写入数据到数据库,所以文件拷贝不行。

将磁盘文件锁定(对所有写不可用),但是这样无法保证高可用。但幸运的是,增加新节点并不需要停机。

增加新节点流程: 1. 在某个时间点获取数据库的快照。大多数数据库支持这个特性,例如用于备份。 2. 将快照拷贝到从节点。 3. 从节点连接主节点,请求快照后的所有变更数据。这需要将快照与主节点复制日志的准确位置相关联,例如:PG中称之为LSN,MySQL中称之为binlog coordinates。 4. 当从节点处理了所有自快照以来的所有积压的数据后,则称之为追赶上了。从节点可以继续处理来自主节点的数据变更。

节点故障

如何保证在主复制模型下获得高可用?

从节点故障:追赶式恢复

如果从节点故障后重启,或者网络闪断,从节点恢复很容易:根据其日志,知道最后一次事务处理信息,从节点连接到主节点,然后请求故障以后的所有数据变更即可。当应用这些变更后,就追赶上了主节点,可以持续接受数据变更,跟原来一样。

主节点故障:故障转移

一个从接到需要被推选为主节点,客户端需要重新配置,然后发送写入操作到新的主节点,其它从节点需要从新的主节点获取数据变更。

故障转移流程: 1. 确定主节点故障:有许多事情可能会出错:崩溃,停电,网络问题等等。没有万无一失的方法来检测出错的地方,因此大多数系统只是使用超时:节点经常在彼此之间来回反弹消息,如果节点在一段时间内没有响应(比如30秒)它被认为发生故障。(如果故意将领导者下线,进行有计划维护,则不适用。) 2. 选举新的主节点:这可以通过选举过程(其中领导者由大多数剩余的副本选择)来完成,或者可以由先前选出的控制器节点指定新的领导者。主节点的最佳候选者通常是具有来自旧领导者的最新数据变化的复制品(以最小化任何数据丢失)。让所有节点就新领导者达成一致是一个共识问题,在后面有详细讨论。 3. 重新配置系统以使用新的主节点。客户端现在需要将他们的写请求发送给新的主节点(“请求路由”)。 如果旧主节点回来,它可能仍然认为它是主,没有意识到其他副本已迫使它下台。系统需要确保旧的领导者成为从节点并认可新的主节点。

故障转移有各种出错的情况: - 如果是异步复制:选主后会丢失旧主节点的更新,新旧主节点相互矛盾的写入。最普遍的解决方案是让旧主节点的未复制的写入被简单地丢弃,这可能会违反持久性。 - 丢失写入可能会非常危险:例如这个系统是其它系统的协调者。 - 脑裂:两个节点都认为自己是主节点,都接受写入,无法解决冲突。检测多个节点,如果出现脑裂,则杀死其中一个。但是如果设计不好,则会出现杀死两个的情况。 - 主节点的超时时间如何设置:太长会导致恢复时间过长,太短导致不必要的故障转移。高压力、网络出现严重拥堵,切换只会导致情况更糟糕。

手动还是自动故障转移。

节点失效、网络不可靠、副本一致性、持久性、可用性与延迟之间存在各种细微的协调,这些问题实际上也正是分布式系统核心的基本问题。

实现日志复制

  1. 基于语句的复制:记录写相关的语句如 INSERT/UPDATE/DELETE,将其发送给备节点。但是时间、自增列、有副作用的语句(触发器、存储过程、UDF)会导致不同副本出现副作用。
  2. 基于WAL的复制:与存储系统耦合。不同版本无法共存。无法滚动升级,停机。(LSM-Tree日志是主要存储方式;对于覆盖写 B-tree 结构,每次修改页面会进行预写日志。)
  3. 基于行的逻辑复制:与物理表示解耦。更容易实现滚动升级,外部程序更容易解析,构建定制索引,缓存,change data capture。 (对于行插入,日志包含所有相关列的新值;对于行删除,日志里有足够信息来标识已删除的行,通常是主键,但是如果没有定义主键,就需要记录所有列的旧值;对于行更新,日志里有足够信息来标识更新的行,以及所有列的新值,或者至少包含所有列的新值)
  4. 基于触发器的复制:更高的灵活性,例如仅复制部分数据,到另一种数据库复制,冲突解决逻辑,这些场景可能需要将复制上升到应用层来解决。基于触发器的复制通常比其他复制方法具有更大的开销,并且比数据库的内置复制更容易出现错误和限制。 然而,由于其灵活性,它仍然是有用的。

复制滞后问题

从异步的从节点读取数据,该副本落后于主节点,则会读取到过期的信息。这种不一致是一个暂时的状态,如果停止写数据库,经过一段时间后,从节点最终会赶上,并与主节点保持一致。这种效应也称之为:最终一致性

读自己的写

用户在写入不久后查看自己的数据,如果可以在任意节点读取,则如果异步复制,则会出现从节点还没有最新数据的情况。对用户来说刚才写入的数据丢失了。此时需要写后读一致性,也就是读写一致性。

image-20190824204742104

基于主从复制的系统该如何实现写后读一致性呢?有多种可行的方案:

  • 如果用户访问可能被修改的内容,在主节点读取,否则在从节点读取。
  • 跟踪最近更新的时间,监控从节点的复制滞后程度,避免从那些滞后的节点读取。
  • 客户端跟踪更新时间,附加在读请求中,系统确保该用户读取的内容至少包含了该时间戳的更新。时间戳可以是逻辑时间戳或者实际的系统时钟。
  • 如果副本分布在多数据中心,必须先把请求路由到主节点的数据中心。

多设备的写后读一致性。

  • 记住用户上次更新时间戳实现起来比较困难。元数据必须做到全局共享。
  • 副本分布在多个数据中心,无法保证来自所有设备到达同一个数据中心。例如台式机家庭宽带网络,移动设备使用蜂窝数据网络,如果要求方案要求必须从主节点读取,则首先要将不同设备路由到同一个数据中心。

单调读

单调读要求某个用户一次进行多次读取时,不会看到回滚的情况。单调读比强一致性弱,比最终一致性强。

image-20190824214435501

单调读的一种实现方式是,确保每个用户总是从固定的同一个副本执行读取(不同的用户可以从不同的副本读取)。例如基于用户 ID 的哈希的方法而不是随机选择一个副本读取。但如果该副本发生失效,则用户的查询必须重新路由到另一个副本。

前缀一致读

第三个由于复制滞后导致因果反常的例子。

image-20190824225414422

要防止这种类型的异常,需要引入:前缀一致读。对于一系列按照某种顺序发生的写请求,读取这些内容也按照当时写入的顺序。

这是分片数据库中出现的一个特殊的问题,许多分布式数据库中,不同的分区独立运行,因此没有全局顺序,这就会导致读取数据时,可能会看到某部分旧值和另一部分新值。一个解决方案是具有因果关系的写入都交给同一个分区完成,但实际该方案的失效效率会大打折扣,例如“Happened-befor 关系与并发”会继续讨论该问题。

复制滞后解决方案

使用最终一致性系统时,最好事先就考虑好这个问题:如果复制延迟增加到几分钟甚至几小时,那么应用层的行为会是什么样子?

应用层保证,但需要复杂的业务逻辑。

数据库如果能保证,则情况就会变得更简单。事务可以提供更强保证的一种方式。

单节点事务比较成熟。在分布式数据库中(支持复制和分区)的过程中,许多系统选择放弃事务,声称事务在性能和可用性方面代价过高,只能提供最终一致性,有一定的道理,但是情况远比所说的那么简单。

TODO 多主复制

TODO 无主复制