尽管分布式一致性算法强调了算法首要解决的问题是一致性,但其实这些算法不会仅仅考虑一致性问题。在介绍具体算法前先简要介绍一下相关的基础理论[1]。
ACID是事务的四大特性[2],分别为:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)。
事务必须是一个原子的操作序列单元,事务中包含的各项操作在一次执行过程中,要么全部执行成功,要么全部不执行,任何一项失败,整个事务回滚,只有全部都执行成功,整个事务才算成功。
事务的执行不能破坏数据库数据的完整性和一致性,事务在执行之前和之后,数据库都必须处于一致性状态。
个人感觉一致性是比较难以理解的,因为一致性和其他的特性有本质上的差别:一致性是针对业务的特性,而其他的AID都是针对数据库本身的特性[3]。举个例子,在一个银行系统中,如果A有100元,B有200元,当A向B转账50元后,A应当为50元,B应当为250元。注意,为什么这里就应当这样呢?因为这样才能保证A+B的总量保持不变。为什么A+B的总量保持不变呢?因为这是业务的特性,银行不能凭空增加总量或者减少总量。所以,所谓的一致性,不能脱离具体的应用场景,它是和业务绑定的,它需要满足一定的约束条件。
在并发环境中,并发的事务是相互隔离的,一个事务的执行不能被其他事务干扰。
SQL中4个事务隔离级别:
一个事务一旦提交,它对数据库中对应数据的状态变更就应该是永久性的,即使发生系统崩溃或机器宕机,只要数据库能够重新启动,那么一定能够将其恢复到事务成功结束时的状态。
一个分布式系统不可能同时满足一致性Consistency、可用性Availability、分区容错性Partition tolerance这三个基本需求,最多只能同时满足其中的两项[4]。
分布式环境中,一致性是指多个副本之间保持一致的特性。
系统提供的服务必须一直处于可用的状态,对于用户的每一个操作请求总是能够在有限的时间内返回结果。
分布式系统在遇到任何网络分区故障时,仍然需要能够保证对外提供满足一致性和可用性的服务,除非是整个网络环境都发生了故障。
BASE即指代Basically Available(基本可用)、Soft state(软状态)、Eventually consistent(最终一致性),基于CAP定理演化而来,核心思想是即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性[1]。
基本可用是指分布式系统在出现不可预知的故障的时候,允许损失部分可用性,但不等于系统不可用。如响应时间上的损失,当出现故障时,响应时间增加;功能上的损失,当流量高峰期时,屏蔽一些功能的使用以保证系统稳定性(服务降级)。
与硬状态相对,即是指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时。
强调系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。其本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。
最终一致性可分为如下几种:
本节将简单介绍几种常见的分布式一致性算法,具体包括:Lamport时间戳[5]、向量时钟[6]、2PC[7][8]、3PC[9][10][11]、Paxos[12]、Raft[13]、Zab[14]、NWR[15]及ISR[16]。图1展示了这些算法的提出时间,不过并不完全准确。有的算法在某个具体工具中使用,则参考其所在工具的发布时间作为提出时间,如ISR是在Kafka 0.8.0版本提出[16],NWR是在Amazon DynamoDB中提出[15]。而有的算法在在被赋予正式的名称前,其核心问题就已在研究了,如Paxos[17]。
因为分布式系统中的数据存放在不同的节点上,那么如何比较不同节点上数据的版本问题就成为一个重要问题。时间戳和向量图的出现就是为了解决这个问题[18]。时间戳和向量时钟都忽略了各个节点上的具体时间,而将按照某个规则不断增长的数字作为系统时间。不同点在于,时间戳设定了一个全局时钟;而向量时钟则是让每一个节点单独存储其他节点的更新时间。
Leslie Lamport在1978年提出逻辑时钟的概念,并描述了一种逻辑时钟的表示方法,这个方法被称为Lamport时间戳,本文简称时间戳。
分布式系统中按是否存在节点交互可分为三类事件,一类发生于节点内部,二是发送事件,三是接收事件[19]。时间戳原理如下:
假设有事件a、b,C(a)、C(b)分别表示事件a、b对应的时间戳,如果C(a) < C(b)
,则有a发生在b之前(happened before),记作a -> b
,例如上图中有C1 -> B1
。通过该定义,事件集中不同时间戳的事件可以进行比较,获得事件的偏序关系(Partial order[20])。
通过以上定义,我们可以对所有事件排序、获得事件的全序关系(Total order[21])。
时间戳帮助我们得到事件顺序关系,但还有一种顺序关系不能用时间戳很好地表示出来,那就是同时发生的事件。
向量时钟(Vector clock)是在时间戳基础上演进的另一种逻辑时钟方法,通过向量结构记录本节点的时间戳,同时也记录其他节点的时间戳。向量时钟的原理与时间戳类似,使用图例如下:
Two-Phase Commit,两阶段提交[5]。简化的时序图如下:
如果所有参与者的反馈都是Yes响应,那么就会执行事务提交;任何一个参与者反馈了No响应,或者在等待超时之后,协调者尚无法接收到所有参与者的反馈响应,那么就会中断事务。
执行事务提交:
中断事务:
优点:原理简单、实现方便。
缺点:
Three-Phase Commit,三阶段提交[22]。为了避免在2PC中通知所有参与者提交事务时,其中一个参与者crash导致不一致,就出现了三阶段提交的方式。三阶段提交在两阶段提交的基础上增加了一个PreCommit的过程,当所有参与者收到PreCommit后,并不执行动作,直到收到commit或超过一定时间后才完成操作。简化的时序图如下:
如果协调者接收到各参与者反馈都是Yes,那么执行事务预提交;如果任何一个参与者向协调者反馈了No响应,或者在等待超时后,协调者无法接收到所有参与者的反馈,那么就会中断事务。
执行事务预提交:
中断事务:
同PreCommit阶段,如果协调者接收到各参与者反馈都是Yes,那么执行事务提交;如果任何一个参与者向协调者反馈了No响应,或者在等待超时后,协调者无法接收到所有参与者的反馈,那么就会中断事务。
执行事务提交:
中断事务:
优点:由于参与者执行事务前进行了CanCommit询问,在一定程度上保证了后续PreCommit的正常执行;而参与者有问题的情况也可以及时中断。所以会比2PC的容错性好一些。
缺点:和2PC的情况一致。
Mike Burrows(Burrows–Wheeler transform[23]的共同作者)曾说:“there is only one consensus protocol, and that’s Paxos”。Paxos算法是基于消息传递且具有高度容错特性的一致性算法,是目前公认的解决分布式一致性问题最有效的算法之一[24]。但是Paxos也比较难懂,倒不是说它的流程太过复杂,而是其推导过程比较复杂。如果对这部分感兴趣可以参考资料《Paxos算法原理与推导》[24]。
上述三类角色只是逻辑上的划分,实践中一个节点可以同时充当这三类角色。
Paxos协议流程是一轮一轮的进行,每轮都有一个编号。每轮Paxos协议可能会批准一个value,也可能无法批准一个value。如果某一轮Paxos协议批准了某个value,则以后各轮Paxos只能批准这个value(这是整个协议正确性的基础)。
定义B为每个Acceptor中记录的最近一轮编号,b表示某个Proposer提出的轮数。Paxos协议的简化流程如下:
为了让一致性协议变得简单可理解,Raft协议主要使用了两种策略。一是将复杂问题进行分解,在Raft协议中,一致性问题被分解为:leader election、log replication、safety三个简单问题[25]。
在Raft协议中,将时间分成了一些任意长度的时间片,称为term,term使用连续递增的编号的进行识别,如下图所示:
Raft通过心跳机制发起leader选举。节点都是从follower状态开始的,如果收到了来自leader或candidate的RPC,那它就保持follower状态,避免争抢成为candidate。Leader会发送空的AppendEntries RPC作为心跳信号来确立自己的地位,如果follower一段时间(election timeout)没有收到心跳,它就会认为leader已经挂了,发起新的一轮选举。
选举发起后,一个follower会增加自己的当前term编号并转变为candidate。它会首先投自己一票,然后向其他所有节点并行发起RequestVote RPC,之后candidate状态将可能发生如下三种变化:
一旦leader被选举成功,就可以对客户端提供服务了。客户端提交每一条命令都会被按顺序记录到leader的日志中,每一条命令都包含term编号和顺序索引,然后向其他节点并行发送AppendEntries RPC用以复制命令(如果命令丢失会不断重发)。当复制成功也就是大多数节点成功复制后,leader就会提交命令,即执行该命令并且将执行结果返回客户端。Raft保证已经提交的命令最终也会被其他节点成功执行。leader会保存有当前已经提交的最高日志编号。顺序性确保了相同日志索引处的命令是相同的,而且之前的命令也是相同的。当发送AppendEntries RPC时,会包含leader上一条刚处理过的命令,接收节点如果发现上一条命令不匹配,就会拒绝执行。
在这个过程中可能会出现一种特殊故障:如果leader崩溃了,它所记录的日志没有完全被复制,会造成日志不一致的情况。follower相比于当前的leader可能会丢失几条日志,也可能会额外多出几条日志,这种情况可能会持续几个term。如下图所示:
在上图中,框内的数字是term编号,a、b丢失了一些命令,c、d多出来了一些命令,e、f既有丢失也有增多,这些情况都有可能发生。比如f可能发生在这样的情况下:f节点在term2时是leader,在此期间写入了几条命令,然后在提交之前崩溃了,在之后的term3中它很快重启并再次成为leader,又写入了几条日志,在提交之前又崩溃了,等他苏醒过来时新的leader来了,就形成了上图情形。在Raft中,leader通过强制follower复制自己的日志来解决上述日志不一致的情形,那么冲突的日志将会被重写。为了让日志一致,先找到最新的一致的那条日志(如f中索引为3的日志条目),然后把follower之后的日志全部删除,leader再把自己在那之后的日志一股脑推送给follower,这样就实现了一致。而寻找该条日志,可以通过AppendEntries RPC,该RPC中包含着下一次要执行的命令索引,如果能和follower的索引对上,那就执行;否则拒绝,然后leader将会逐次递减索引,直到找到相同的那条日志。
然而这样也还是会有问题,比如某个follower在leader提交时宕机了,也就是少了几条命令,然后它又经过选举成了新的leader,这样它就会强制其他follower跟自己一样,使得其他节点上刚刚提交的命令被删除,导致客户端提交的一些命令被丢失了。Raft通过为选举过程添加一个限制条件,解决了上面提出的问题,该限制确保leader包含之前term已经提交过的所有命令。Raft通过投票过程确保只有拥有全部已提交日志的candidate能成为leader。由于candidate为了拉选票需要通过RequestVote RPC联系其他节点,而之前提交的命令至少会存在于其中某一个节点上,因此只要candidate的日志至少和其他大部分节点的一样新就可以了, follower如果收到了不如自己新的candidate的RPC,就会将其丢弃。
Zab协议的全称是Zookeeper Atomic Broadcast(Zookeeper原子广播)[26]。
Zab借鉴了Paxos算法,是为分布式协调服务Zookeeper专门设计的一种支持崩溃恢复的原子广播协议,是Zookeeper保证数据一致性的核心算法。本节将介绍Zab算法的几个关键阶段,关于更多细节与异常处理建议阅读《ZAB-一致性算法》[26]。
Zab节点有三种状态:
相关术语说明:
在Amazon的Dynamo云存储系统中,使用了NWR来控制一致性[27]。其中,N代表同一份数据的Replica的份数,W是更新一个数据对象时需要确保成功更新的份数;R代表读取一个数据需要读取的Replica的份数。 公式W+R>N
,保证某个数据不被两个不同的事务同时读和写;公式W>N/2
保证两个事务不能并发写某一个数据。 在分布式系统中,数据的单点是不允许存在的。即线上正常存在的Replica数量为1的情况是非常危险的,因为一旦这个Replica再次出错,就可能发生数据的永久性错误。假如我们把N设置成为2,那么只要有一个存储节点发生损坏,就会有单点的存在,所以N必须大于2。N越高,系统的维护成本和整体成本就越高。工业界通常把N设置为3。例如,对于MySQL主从结构,其NWR数值分别是N= 2, W = 1, R = 1,没有满足NWR策略。NWR策略是在平衡读、写、备份的效率中使用到的一个非常经典的策略,在Amazon Dynamo等分布式存储系统中都应用的很关键。
配置NWR的时候要求W+R>N
。 因为W+R>N
,所以R>N-W,这个是什么意思呢?就是读取的份数一定要比总备份数减去确保写成功的份数的差值要大。也就是说,每次读取,都至少读取到一个最新的版本,从而不会读到一份旧数据。比如N=5
,W=3
,N-W=2
,即保证5个备份中有3个是正确的,另外2个可能没有写成功。如果只读取2份,那么这两份有可能都不是写成功的。
当我们需要高可写的环境的时候(例如amazon的购物车的添加请求应该是永远不被拒绝的)我们可以配置W=1,如果N=3,那么R=3。这个时候只要写任何节点成功就认为成功,但是读的时候必须从所有的节点都读出数据。如果我们要求读的高效率,我们可以配置W=N,R=1。这个时候任何一个节点读成功就认为成功,但是写的时候必须写所有三个节点成功才认为成功。需要注意的是,一个操作的耗时是几个并行操作中最慢一个的耗时。比如R=3的时候,实际上是向三个节点同时发了读请求,要三个节点都返回结果才能认为成功。假设某个节点的响应很慢,它就会严重拖累一个读操作的响应速度。
不同的NWR取值代表了不同的倾向,如果设定N=3,W=3,R=1,那么强调的是强一致性,写数据的时候一定要把所有的副本刷新,杜绝中间状态。如果N=3,R=1,W=1,则代表的是可用性,这种情况下一致性就被牺牲掉了。
ISR(In-Sync Replicas)是Kafka中同步副本时使用的一致性同步策略[28]。在Kafka的同步策略中有几个关键概念需要搞清楚:
其中AR = ISR + OSR
。ISR策略的核心是动态调整。处于ISR内部的follower都是可以和leader进行同步的,一旦出现故障或延迟,就会被踢出ISR;而OSR中的follower一旦赶上leader就会被加入ISR中。
[1] segmentfault 从ACID到CAP到BASE
[2] 知乎 事务的四大特性ACID
[3] 知乎 如何理解数据库事务中的一致性的概念?
[4] 知乎 谈谈分布式系统的CAP理论
[5] PDF Time, Clocks and the Ordering of Events in a Distributed System
[6] WIKIPEDIA Vector clock
[7] 博客园 Paxos 协议简单介绍
[7] PDF CONCURRENCY CONTROL AND RECOVERY IN DATABASE SYSTEMS
[8] WIKIPEDIA Two-phase commit protocol
[8] PDF Paxos Made Simple
[9] WIKIPEDIA Three-phase commit protocol
[10] PDF A QUORUM-BASED COMMIT PROTOCOL
[11] PDF INCREASING THE RESILIENCE OF DISTRIBUTED AND REPLICATED DATABASE SYSTEMS
[12] Paxos官网 Paxos
[13] PDF In Search of an Understandable Consensus Algorithm
[14] IEEE官网 Zab: High-performance broadcast for primary-backup systems
[15] WIKIPEDIA Amazon DynamoDB
[16] Kafka官网 DOWNLOAD
[17] WIKIPEDIA Paxos (computer science)
[18] CSDN 一致性算法之四: 时间戳和向量图
[19] 博客园 分布式系统理论基础 - 时间、时钟和事件顺序
[20] WIKIPEDIA Partially ordered set
[21] WIKIPEDIA Total order
[22] segmentfault 2PC到3PC到Paxos到Raft到ISR
[23] WIKIPEDIA Burrows–Wheeler transform
[24] 博客园 Paxos算法原理与推导
[25] 知乎 Raft协议原理详解
[26] 个人博客 ZAB-一致性算法
[27] 个人网站 Cap理论和nwr策略
[28] CSDN Kafka之ISR机制的理解