进入到第五章了,来到了分布式系统之中最核心与复杂的内容:副本与一致性。通常分布式系统会通过网络连接的多台机器上保存相同数据的副本,所以在本篇之中,我们来展开看看如何去管理和维护这些副本,以及这个过程之中会遇到的各种问题。
1.副本
在数据系统之中,我们通常会有这样几个原因来使用副本技术:
- 保持地理位置接近用户,从而减少延迟(如:Cache,CDN技术)
- 提高系统的可用性和鲁棒性,即使系统中的某些部分已经失效了,仍然可以对外提供服务。(如:GFS三副本的设计)
- 通过扩展性来提供读查询,从而增加读取吞吐量。(如:ZooKeeper之中的Observer)
首先,如果副本的数据不随时间变化,那么副本的管理是比较简单的:只需要将数据复制到每个节点一次,就OK了。副本管理真正的困难在于对副本数据的修改,这会涉及到很多琐碎的问题。其次,副本复制时要考虑许多权衡,使用同步还是异步复制,以及如何处理失效的副本?接下来我们来一一探讨这个问题。
2.Leader-Follower机制
如何保障多个副本在不同节点上的一致性一直分布式系统之中的一个核心问题。分布式系统在写入数据时,需要由每个副本进行处理;否则,副本将不再包含相同的数据。Leader-Follower是一种常见的机制,我们来梳理一下它的原理:
-
- 一个节点上的副本被指定为Leader。当客户端需要向系统写入数据时,必须将写入请求发送给Leader,由Leader首先将新数据写入本地存储的副本。
-
- 管理其他副本的节点称为Follower。每当Leader将新数据写入本地存储d的副本时,也会将数据更改写入日志之中。每个Follower会从Leader那里获取修改日志,并相应地更新数据到的本地副本之中,这样,所有的在Follower上副本的修改顺序会和Leader保持相同的顺序。
-
- 当客户端需要从系统之中读取数据时,它可以查询Leader或其他Follower。(注:Follower与Leader之中的数据存在延迟,无法保证强一致性)写入请求只能由Leader来响应,或是由Follower转发给Leader。
许多关系数据库在同步副本时使用这样的机制,如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节点的日志序列号。
-
- 当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的信息丢弃,但是这显然违反了数据系统写入持久性的要求。
在某些故障场景中,可能会出现两个节点都认为他们