前言
在传统单体应用下,请求从客户端到服务层,再到数据库,很多时候通过关系型数据库自身的事务机制,保证了数据在读写层面的一致性(ACID)。
对于分布式系统来说,就需要通过一些协议和算法来保证数据的一致性,数据的一致性是最基础也是最核心的问题。
1. 一致性问题
1.1 定义
一致性,早期也叫agreement,指分布式系统中的多个服务节点,给定一系列操作,在约定协议的保障下,试图使得他们对处理结果达成某种程度的认同。
一致性并不代表结果正确与否,而是系统对外呈现的状态一致与否。
1.2 问题与挑战
理想情况下,各个服务节点严格遵循相同的处理协议,构成相同的状态机,给定相同的初始状态和输入序列,则可以保障在处理过程中的每个环节的结果都是相同的。
但在实际的场景中,在以下几个方面很容易出现问题:
- 节点之间的网络通信是不可靠的,包括消息延迟、乱序、内容错误等;
- 节点的处理时间无法保障,结果可能出现错误,甚至节点自身宕机;
解决这些问题时,一个解决方式就是同步串行化调用,这会严重降低分布式系统的性能和可扩展性,是一种分布式单体应用。
1.3 一致性要求
分布式系统达成一致性的过程应该满足:
- 可终止性:一致的结果在有限时间完成,意味着服务可正常使用;
- 约同性:不同节点最终完成决策的结果是相同的;意味着要把多件事情进行排序,而且这个顺序还是大家都认可的;
- 合法性:决策的结果必须是某个节点提出的提案。
1.4 带约束的一致性
实现理想的严格一致性代价很大,越强的一致性要求往往会造成越弱的处理性能及越差的可扩展性。
一般来说,强一致性包含两类:
- 顺序一致性:保证所有进程看到的全局执行顺序一致,并且每个进程自身的执行顺序跟实际发生顺序一致。只是限制了各进程内指令的偏序关系,但不在进程间按照物理时间进行全局排序。
- 线性一致性:在顺序一致性的前提下加强了进程间的操作排序,形成唯一的全局顺序,是很强的原子性保证,很难实现。
大部分系统实现的都是最终一致性(弱一致性),即在某个时刻(而不是立刻),让系统达到一致的状态。
1.5 共识算法
一致性往往指分布式系统中多个副本对外呈现的数据的状态。顺序一致性、线性一致性,描述了多个节点对数据状态的维护能力。而共识描述了分布式系统中多个节点之间彼此对某个状态达成一致结果的过程。达成某种共识并不意味着就保障了一致性。
系统满足不同程度的一致性,核心过程需要通过共识算法来达成。共识算法解决的是对某个提案大家达成一致意见的过程。
把出现故障(crash或fail-stop)但不会伪造信息的情况称为“非拜占庭错误”或“故障错误";伪造信息恶意响应的情况称为”拜占庭错误“,对应的节点称为拜占庭节点。
1.5.1 常见算法
根据解决的是非拜占庭还是拜占庭错误,可以分为:
- CFT(crash fault tolerance):常见经典算法包括paxos、raft及其变种,这类容错算法性能较好,处理较快,容忍不超过一半的故障节点。
- BFT(byzantine fault tolerance):包括PBFT(practical byzantine fault tolerance)为代表的确定性系列算法(一旦达成共识,就不可逆转,共识是最终结果),pow为代表的概率算法(共识是临时的,随着时间推移或某种强化,共识结果被推翻的概率越来越小,成为事实上的最终结果)等。拜占庭类容错算法往往性能较差,容忍不超过1/3的故障节点。
- XFT(cross fault tolerance):改进算法,可以提供类似CFT的处理响应速度,并能在大多数节点正常工作时提供BFT保障。
FLP不可能原理:即便在网络通信可靠的情况下,可扩展的分布式系统的共识问题,其通用解法的理论下限是没有下限(无解)。这个原理可以看做是分布式领域里的”测不准原理“。
1.5.2 FLP不可能原理
在网络可靠,但允许节点失效的最小化异步模型系统观众,不存在一个可以解决一致性问题的确定性共识算法。不要浪费时间,去为异步分布式系统设计在任意场景下都能实现共识的算法。
在分布式系统中,同步和异步存在特殊的含义:
- 同步:系统中的各个节点的时钟误差存在上限,并且消息的传递必须在一定时间内完成,否则认为失败;同时各个节点完成处理消息的时间是一定的。比较容易判断消息是否丢失。
- 异步:系统中各个节点可能存在较大的时钟差异,同时消息传输时间是任意长的,各节点对消息进行处理的时间也可能是任意长的,无法判断消息问题。
科学告诉我们什么是不可能的,工程则告诉我们,付出一些代价,可以把他变为可行。
1.6 CAP
分布式系统不可能同时确保以下三个特性:
- 一致性:任何操作应该都是原子的,发生在后面的事件能看到前面事件发生导致的结果,这里指的是强一致性;
- 可用性:在有限时间内,任何非失败节点都能应答请求;
- 分区容忍性:网络可能发生分区,即节点之间的通信不可保障。
网络分区出现时,系统无法同时保障CA,要么节点收到请求后因为没有得到其他节点的确认而不应答(牺牲可用性),要么节点只能应答非一致的结果(牺牲一致性)。
在分布式数据库领域,通常满足ACID原则,允许付出可用性的代价。
与ACID相对的,是BASE原则(Base availability,soft-state,eventual Consistency):通过实现最终一致性,来换取。
1.7 Paxos算法和Raft算法
paxos是第一个广泛应用的共识算法,原理是基于”两阶段提交“算法并进行泛化和扩展,通过消息传递来逐步消除系统中的不确定状态,是很多算法(如zab、raft)设计的基础。
1.7.1 paxos算法
基本原理是将节点分为三种逻辑角色:
- 提案者(proposer):提出提案,等待大家批准为结案。系统中提案都用于一个自增的唯一提案号。往往由客户端担任该角色。
- 接收者(acceptor):负责对提案投票、接受提案。往往由服务端担任该角色。
- 学习者(learner):获取批准结果,并帮忙传播,不参与投票。可能为客户端或服务端。
算法需要满足safety和liveness两方面的约束要求,这也是大部分分布式算法应该考虑的两个基础属性。 - safety约束:保证决议(value)结果是对的,无歧义,不会出现错误情况。
- liveness约束:保证决议过程能在有限时间内完成。
决议有多种组合形式:
- 单个提案者+多接受者:简单,但一旦提案者故障,系统无法工作。
- 多个提案者+单个接受者:简单,容易发生单点故障。
- 多个提案者+多个接受者:一种方式是同一时间片段内(如:一个提案周期)内只有一个提案者,需要设计一种机制来保障提案者的正确产生。另一种是同一时间片段内可以出现多个提案者。
多个提案者意味着很可能单个提案人无法集齐足够多的票,另外提案者即便收到了足够的票,也不敢保证一定通过,因为投票者无法获知其他投票人的结果,也无法确认提案人是否收到了自己的投票(这样就无法确认自己是否应该投票)。
2PC:两阶段提交,分为准备阶段和提交阶段:
- 准备阶段:
a.Proposer选择一个提案编号 n ,然后向Acceptors的某个majority集合的成员发送编号为 n 的prepare请求。
b.如果一个Acceptor收到一个编号为 n 的prepare请求,且 n 大于它已经响应的所有prepare请求的编号,那么它就会保证不会再通过(accept)任何编号小于 n 的提案,同时将它已经通过的最大编号的提案(如果存在的话)作为响应。 [♠]译注 - 提交阶段
a.如果Proposer收到来自半数以上的Acceptor对于它的prepare请求(编号为 n )的响应,那么它就会发送一个针对编号为 n ,value值为 v 的提案的accept请求给Acceptors,在这里 v 是收到的响应中编号最大的提案的值,如果响应中不包含提案,那么它就是任意值。
b.如果Acceptor收到一个针对编号 n 的提案的accept请求,只要它还未对编号大于 n 的prepare请求作出响应,它就可以通过这个提案。
1.7.2 Raft算法
Raft算法是对paxos的优化,包含三种角色:Leader(领导者)、Candidate(候选领导者)、Follower(跟随者),决策前通过一个全局的leader来简化后续的决策过程,典型的过程包含两个主要阶段:
- Leader选举:开始所有节点都是follower,在随机超时发生后未收到来自leader或candidate的消息,则转变为candidate,提出选举请求。最后选举阶段得票超过一半者被选为leader,如果未选出,随机超时后进入新的阶段重试。Leader负责从客户端接收log,并分发到其他节点。
- 同步日志:Leader会找系统中日志(并非是输出消息,而是各种事件的发生记录)最新的记录,并强制所有的follower刷新到这个记录,数据的同步是单向的。
1.8 拜占庭问题与算法
PBFT算法采用密码学相关技术确保消息传递过程无法被篡改和破坏。
算法的基本过程:
1.首先通过轮换或随机算法选出某个节点作为主节点,此后只要主节点不切换,则称为一个试图;
2.在某个视图中,客户端将请求发给主节点,主节点负责关播请求到所有其他副本;
3.所有节点处理完成请求,将处理结果返回给客户端,客户端检查是否收到了只是f+1个不同节点的相同结果,作为最终结果。
主节点关播过程包括三个阶段的处理:预准备阶段、准备阶段、提交阶段。预准备和准备阶段确保在同一个视图内请求发送的顺序正确,准备和提交阶段则确保不同视图之间的确认请求是保存的。
1.预准备阶段:主节点为从客户端收到的请求分配提案编号,然后发出预准备消息给各副本节点。
2.准备阶段:副本节点收到预准备消息后,检查消息合法,检查通过则向其他节点发送准备消息,带上自己的id信息,同时接收来自其他节点的准备消息。收到准备消息的节点对消息进行合法性检查,验证通过则把这个准备消息写入消息日志中,集齐至少2f+1个验证过的消息才进入准备阶段。
3.提交阶段:广播commit消息,告诉其他节点某个提案n在视图v里已经处于准备状态,如果集齐至少2f+1个验证过的commit消息,则说明提案通过。
新的解决思路
拜占庭问题之所以难以解决,在于任何时候系统中都可能存在多个提案,提案成本很低,并且完成最终一致性确认过程十分困难,容易受到干扰。比特币的区块链网络在设计时提出了创新的Pow(Proof of word)概率算法思路,针对这两个环节进行了改进。首先,限制一段时间内整个网络中出现提案的个数(增加提案成本),其次放宽对最终一致性确认的需求,约定好大家都确认并沿着已知最长的链进行拓展。系统的最终确认是概率意义上的存在,即便有人试图恶意破坏,也会付出响应的经济代价(超过整体系统一半的计算力)。后来的Pox系列算法,也是这个思路进行改进,采用经济上的惩罚来制约破坏者。
1.9 可靠性指标
两个核心时间:MTBF(Mean time between failures)平均故障间隔时间、MTTR(Mean time to Repair)平均修复时间。
2 提交
2.1 2PC 二阶段提交
在二阶段中,会有两个角色:协调者,参与者。
协调者统一调度所有的参与者执行逻辑。
调度过程:
问题:
1.P1B参与者故障,参与者故障或网络错误,无法进行P2A,参与者会阻塞。超时回滚解决?
2.P2A协调者故障,参与者会阻塞。
3.P2A协调组未完全通知出现故障,参与者数据一致性,未收到通知的参与者阻塞。
4.P2A参与者故障,无法反馈所有执行结果,协调者结果如何认定?
疑问:为啥不做超时回滚或提交
思考:超时回滚会造成大概率数据不一致的问题,参与者不知道是2还是3的情况。
2.2 3PC 三阶段提交
问题:
1.P2B阶段,参与者和协调者故障,均存在事务短时阻塞情况。
2.P3A,协调者故障,存在事务短时阻塞情况。当P3A执行rollback时,协调者通知过程中故障或网络故障,会存在数据不一致问题。
2PC和3PC的比较,这里并不能完全解决数据不一致的问题,相比于2PC,增加了canCommit阶段,会降低故障发生时,事务阻塞和数据不一致的概率(毕竟进行过一轮节点状态的判断,事务的执行过程很短的情况下,再次发生故障的概率较低)。
3. 一致性协议是什么
一致性协议的根本目的是为了解决分布式系统中数据复制产生的数据一致性问题。
根据是否允许数据分歧可以分为两种:
- 单主协议(不允许数据分歧):所有写操作都由主节点(协调者)处理并同步其他节点。如:主备、2PC、3PC、paxos,写操作有序,一致性较强。
- 多主协议(允许数据分歧):写操作由不同节点发起并同步给其他副本。如:gossip、pow。操作不保证有序性,最终一致。
还可以从同步/异步来划分。
参考文献:
1.http://lamport.azurewebsites.net/pubs/paxos-simple.pdf
2.区块链设计