副本机制与副本同步------《Designing Data-Intensive Applications》读书笔记6

进入到第五章了,来到了分布式系统之中最核心与复杂的内容:副本与一致性。通常分布式系统会通过网络连接的多台机器上保存相同数据的副本,所以在本篇之中,我们来展开看看如何去管理和维护这些副本,以及这个过程之中会遇到的各种问题。

1.副本

在数据系统之中,我们通常会有这样几个原因来使用副本技术:

  • 保持地理位置接近用户,从而减少延迟(如:Cache,CDN技术
  • 提高系统的可用性和鲁棒性,即使系统中的某些部分已经失效了,仍然可以对外提供服务。(如:GFS三副本的设计
  • 通过扩展性来提供读查询,从而增加读取吞吐量。(如:ZooKeeper之中的Observer

首先,如果副本的数据不随时间变化,那么副本的管理是比较简单的:只需要将数据复制到每个节点一次,就OK了。副本管理真正的困难在于对副本数据的修改,这会涉及到很多琐碎的问题。其次,副本复制时要考虑许多权衡,使用同步还是异步复制,以及如何处理失效的副本?接下来我们来一一探讨这个问题。

2.Leader-Follower机制

如何保障多个副本在不同节点上的一致性一直分布式系统之中的一个核心问题。分布式系统在写入数据时,需要由每个副本进行处理;否则,副本将不再包含相同的数据。Leader-Follower是一种常见的机制,我们来梳理一下它的原理:

    1. 一个节点上的副本被指定为Leader。当客户端需要向系统写入数据时,必须将写入请求发送给Leader,由Leader首先将新数据写入本地存储的副本。
    1. 管理其他副本的节点称为Follower。每当Leader将新数据写入本地存储d的副本时,也会将数据更改写入日志之中。每个Follower会从Leader那里获取修改日志,并相应地更新数据到的本地副本之中,这样,所有的在Follower上副本的修改顺序会和Leader保持相同的顺序。
    1. 当客户端需要从系统之中读取数据时,它可以查询Leader或其他Follower。(注:Follower与Leader之中的数据存在延迟,无法保证强一致性)写入请求只能由Leader来响应,或是由Follower转发给Leader

Leader-Follower机制

许多关系数据库在同步副本时使用这样的机制,如PostgreSQL,MySQL,Oracle Data Guard 和SQL Server。同时许多非关系型数据库与分布式消息队列也采用这样的机制,包括MongoDB,Rethinkd,Kafka,RabbitMQ。

2.1 同步与异步复制

在副本进行主从复制时一个重要细节是复制是同步还是异步发生的?(在关系数据库中,这往往是一个可配置的选项。在其他系统之中,如Ceph,是系统默认的)

同步复制与异步复制的响应时间的比较

由上图可知,同步复制有相当大的延迟,而异步复制的响应相当快速。但是异步复制却不能保证完成所需要多长时间。有些情况下,Follower的数据可能比Leader上的数据落后几分钟或更多。如:节点之间存在网络问题或节点的故障恢复。如果Leader失败且不可恢复,则尚未复制到Follower的任何写操作都将丢失。

而同步复制的优点是保证了Follower与Leader之间的副本一致性,一旦任意一个Leader失效了,任何一个Follower的数据都与Leader相同。但是同步复制一旦出现网络或节点的故障,会导致无法处理写入。Leader必须阻止所有写入并等待Follow上的副本再次可用。如果所有的Follower都是同步复制,那么任何一个节点的中断都会导致整个系统瘫痪。在实际运用之中,如果在数据库上启用同步复制,通常其中一个副本是同步复制的,而另一个是异步复制的。如果同步的副本变得不可用或十分缓慢,可以将同步操作切换到另一个异步副本之中。这样保证了至少两个节点上有一个数据的最新副本:Leader和一个同步Follower。这种配置称之为半同步。(链式复制也是类似于半同步的一种复制机制,不丢失数据但仍能提供良好性能和可用性的复制方法。)

2.2 添加新的Follower

有时我们需要添加新的Follower来增加副本的数量,或者替换失败的节点。此时就需要确保新的Follower拥有一个正确的副本的数据。仅仅将数据文件从一个节点复制到另一个节点通常是不够的:客户端不停向系统写入数据,所以数据副本总是处于不断变化的状态。这里可以简单地通过锁定系统,使其拒绝客户端的写请求来使各个副本上保持一致,但这样会大大降低系统的可用性。所以我们需要一个不停机的方式来添加新的Follower:

  • 1.在某个时间点对Leader的副本进行快照,并且将快照复制到新加入的Follower节点。

  • 2 .Follower连接到Leader,并向Leader请求快照之后所有的数据更改。通常是Leader节点的日志序列号。

    1. 当Follower处理完快照之后的数据更改之后,它就可以正常处理来自Leader的数据更改了。
2.3 节点故障

在分布式系统之中,任何节点都可能出现故障,而能够在不停机的情况下重新启动单个节点是操作和维护是十分必要的。尽管每个节点故障,但我们需要让一个节点停机的影响尽可能小。

  • Follower故障

在Follower的本地磁盘上,都保存着从Leader收到的数据更改的日志。当一个Follower崩溃并重新启动,或者Leader与Follower之间的网络暂时中断。Follower可从它的日志找到故障发生之前处理的最后一个事务,然后连接到Leader并请求在Follower断开连接的时候发生的所有数据变化。(这个流程和添加新的Follower其实是同样的思路

  • Leader故障

在处理Leader的失败时显然会更为棘手:其中一个Follower需要被提升为新的Leader,客户端也需要识别并且将后续的请求发送给新的Leader,而其他的Follower则需要开始在新Leader之下工作。处理Leader故障通常是如下的流程:

  • 1、确认Leader失效。绝大多数系统使用超时机制:如果一个节点不响应一段时间,例如,30秒,它被认为是失效了。(如果是中心化的系统可以采用Lease机制。笔者在硕士生阶段对Cassandra数据库有过系统的调研,在Cassandra中采用了由日本学者Naohiro Hayashibara提出的《The Phi Accrual Failure Detector》失败探测算法,通过多维度累积量来判断节点是否失效,不失为一个好的解决方案,十分适合类P2P架构的分布式系统

  • 2、选取新的Leader。在中心化架构之中,如HDFS,新的Leader可以用中心化节点指定。而在非中心化的架构之中,则可以通过选举过程来完成,分布式系统之中的选举协议有很多:2PC,3PC,Paxos,Raft等等。

  • 3、调整系统配置以使用新的Leader。如果旧的Leader回归到集群,它可能仍然认为自己是Leader,这时需要确保旧的Leader成为Follower并承认新的Leader。

如果是异步复制的场景,新的Leader可能旧的Leader之前的完整的写入信息。最常见的解决方案是丢弃旧Leader之前写入多于新Leader的信息丢弃,但是这显然违反了数据系统写入持久性的要求。
在某些故障场景中,可能会出现两个节点都认为他们是Leader,这种情况被称为脑裂。此时两个Leader都会接受写请求,数据很可能会出现丢失或损坏。
什么时候进行故障切换也是一个值得探讨的问题:较长的超时时间意味着在Leader失效的情况下恢复时间更长。然而,如果时间太短,可能会有不必要的故障转移。例如,临时负载高峰时刻可能导致节点的响应时间增加到超时,那么不必要的故障转移会使情况变得更糟,而不是更好。为此,一些运营团队更愿意执行手动的故障转移,即使系统本身支持自动的故障转移。

3. 日志的复制

日志在副本的一致性之中是至关重要的,所以我们接下来简要的梳理一下日志复制可用的方法:

  • Statement-Based复制
    在最简单的情况下,Leader将每个写请求通过日志的形式发送给Follower。每个Follower解析和执行对应的操作语句,虽然这听起来很合理,但是实际操作中会存在一些坑:
    (1) 非确定性函数,如now()获得当前的日期和时间或rand()得到一个随机数,这样会导致副本之间的不一致。(这里可以转换思维,用一个确定的修改值,来替换不确定性的函数调用)
    (2) 如果使用一个自动递增的列,或如果他们依赖于数据库中的现有数据(例如,更新…在<条件>),他们必须执行完全相同的顺序在每个副本,否则也会产生不一致性。(异步转发,乱序到达。这个可以通过操作序列号等强制要求进行规避。
    (3) 有副作用的语句(例如触发器、存储过程、用户定义函数)可能会导致每个副本上出现不同的副作用。

  • Write-ahead日志复制
    日志是一个只包含所有写入操作的字节序列。我们可以使用完全相同的日志来在另一个节点上构建一个副本。Leader将日志写入磁盘之后,将它通过网络发送给Follower。当Follower处理这个日志时,它构建了一个与Leader完全相同的数据结构的副本。这种方式的缺点是:日志在非常低的级别上描述数据。这使得数据拷贝与存储引擎紧密耦合。

  • Row-based日志复制
    Row-based与Write-ahead的方法类似,但是它允许复制日志与存储引擎内部分离。这种日志称为逻辑日志,逻辑日志通常是描述在一个行的粒度上记录写入操作:
    对于插入的行,日志包含所有列的新值。
    对于已删除的行,日志包含足够的信息以唯一地标识删除的行。(主键
    对于更新的行,日志包含足够的信息以唯一地标识更新的行,以及所有列的新值。
    由于逻辑日志与存储引擎内部分离,因此可以更容易地保持向后兼容,从而允许Leader与Follower运行不同版本的数据系统,甚至是不同的存储引擎。同时,逻辑日志格式对外部应用程序也更容易解析。可以将逻辑日志的内容发送到外部系统(如用于离线分析的数据仓库),或者用于构建自定义索引和缓存。

4. 复制延迟

副本可以增加系统的可伸缩性(处理比单个机器处理更多的请求)和降低延迟(将副本放置在离用户更近的地方)。写入操作必须通过Leader副本,但是只读查询可以在任何副本上进行。 对于一次写入,多次读取的应用来说,采用读扩展架构是十分合理的。但是由于上文提及的原因,我们通常不会采用同步复制的方式。这将导致数据出现明显的不一致性:如果您同时对Leader和Follwer执行相同的查询,可能会得到不同的结果,因为并不是所有的写入实时在Follower上反馈。这种不一致性仅仅是暂时状态,所以这种情况被称为最终一致性。

对于这种情况我们应该这么去处理和理解,我们下回分解~~~(第五章的内容炒鸡多,接下来会通过多篇读书笔记来给大家梳理,讲解,下一篇再见~~

posted @ 2018-01-19 11:30  HappenLee  阅读(1362)  评论(0编辑  收藏  举报