从分布式系统的CAP理论出发,关注分布式一致性,以及区块链的共识问题及解决。node
区块链首先是一个大规模分布式系统,共识问题本质就是分布式系统的一致性问题,可是又有很大的不一样。
工程开发中,认为系统中存在故障(fault),但不存在恶意(corrupt)节点,而区块链,特别是公开链是落地到物理世界中,涉及到人性和利益关系,不可避免的存在信任以及恶意攻击问题。git
分布式一致性处理的是节点失效状况(便可能消息丢失或重复,但无错误消息)的共识达成(Consensus)问题,主要是Paxos算法及衍生的Raft算法。github
CAP理论的核心思想是任何基于网络的数据共享系统最多只能知足数据一致性(Consistency)、可用性(Availability)和网络分区容忍(Partition Tolerance)三个特性中的两个。web
Consistency 一致性
一致性指“all nodes see the same data at the same time”,即更新操做成功并返回客户端完成后,全部节点在同一时间的数据彻底一致。等同于全部节点拥有数据的最新版本。算法
Availability 可用性数据库
可用性指“Reads and writes always succeed”,即服务一直可用,并且是正常响应时间。
对于一个可用性的分布式系统,每个非故障的节点必须对每个请求做出响应。也就是,该系统使用的任何算法必须最终终止。当同时要求分区容忍性时,这是一个很强的定义:即便是严重的网络错误,每一个请求必须终止。promise
Tolerance也能够翻译为容错,分区容忍性具体指“the system continues to operate despite arbitrary message loss or failure of part of the system”,即系统容忍网络出现分区,分区之间网络不可达的状况,分区容忍性和扩展性紧密相关,Partition Tolerance特指在遇到某节点或网络分区故障的时候,仍然可以对外提供知足一致性和可用性的服务。网络
提升分区容忍性的办法就是一个数据项复制到多个节点上,那么出现分区以后,这一数据项就可能分布到各个区里。分区容忍就提升了。然而,要把数据复制到多个节点,就会带来一致性的问题,就是多个节点上面的数据多是不一致的。要保证一致,每次写操做就都要等待所有节点写成功,而这等待又会带来可用性的问题。并发
如图,Client A能够发送指令到Server而且设置更新X的值,Client 1从Server读取该值,在单点状况下,即没有网络分区的状况下,或者经过简单的事务机制,能够保证Client 1读到的始终是最新的值,不存在一致性的问题。分布式
若是在系统中增长一组节点,Write操做可能在Server 1上成功,在Server 1上失败,这时候对于Client 1和Client 2,就会读取到不一致的值,出现不一致。若是要保持x值的一致性,Write操做必须同时失败,下降系统的可用性。
能够看到,在分布式系统中,同时知足CAP定律中“一致性”、“可用性”和“分区容错性”三者是不可能的。
在一般的分布式系统中,为了保证数据的高可用,一般会将数据保留多个副本(replica),网络分区是既成的现实,因而只能在可用性和一致性二者间作出选择。CAP理论关注的是绝对状况下,在工程上,可用性和一致性并非彻底对立,咱们关注的每每是如何在保持相对一致性的前提下,提升系统的可用性。
在互联网领域的绝大多数的场景,都须要牺牲强一致性来换取系统的高可用性,系统每每只须要保证“最终一致性”,只要这个最终时间是在用户能够接受的范围内便可。
对于一致性,能够分为从服务端和客户端两个不一样的视角,即内部一致性和外部一致性。
没有全局时钟,绝对的内部一致性是没有意义的,通常来讲,咱们讨论的一致性都是外部一致性。外部一致性主要指的是多并发访问时更新过的数据如何获取的问题。
强一致性:
当更新操做完成以后,任何多个后续进程或者线程的访问都会返回最新的更新过的值。这种是对用户最友好的,就是用户上一次写什么,下一次就保证能读到什么。根据 CAP 理论,这种实现须要牺牲可用性。
弱一致性:
系统并不保证续进程或者线程的访问都会返回最新的更新过的值。用户读到某一操做对系统特定数据的更新须要一段时间,咱们称这段时间为“不一致性窗口”。系统在数据写入成功以后,不承诺当即能够读到最新写入的值,也不会具体的承诺多久以后能够读到。
最终一致性:
是弱一致性的一种特例。系统保证在没有后续更新的前提下,系统最终返回上一次更新操做的值。在没有故障发生的前提下,不一致窗口的时间主要受通讯延迟,系统负载和复制副本的个数影响。
最终一致性模型根据其提供的不一样保证能够划分为更多的模型,包括因果一致性和读自写一致性等。
在分布式系统中,各个节点之间在物理上相互独立,经过网络进行沟通和协调。
典型的好比关系型数据库,因为存在事务机制,能够保证每一个独立节点上的数据操做能够知足ACID。
可是,相互独立的节点之间没法准确的知道其余节点中的事务执行状况,因此两台机器理论上没法达到一致的状态。
若是想让分布式部署的多台机器中的数据保持一致性,那么就要保证在全部节点的数据写操做,要不所有都执行,要么所有的都不执行。
可是,一台机器在执行本地事务的时候没法知道其余机器中的本地事务的执行结果。因此节点并不知道本次事务到底应该commit仍是 roolback。
因此实现分布式事务,须要让当前节点知道其余节点的任务执行状态。常规的解决办法就是引入一个“协调者”的组件来统一调度全部分布式节点的执行。著名的是二阶段提交协议(Two Phase Commitment Protocol)和三阶段提交协议(Three Phase Commitment Protocol)。
Two Phase指的是Commit-request阶段Commit阶段。
请求阶段
在请求阶段,协调者将通知事务参与者准备提交或取消事务,而后进入表决过程。
在表决过程当中,参与者将告知协调者本身的决策:赞成(事务参与者本地做业执行成功)或取消(本地做业执行故障)。
提交阶段
在该阶段,协调者将基于第一个阶段的投票结果进行决策:提交或取消。
当且仅当全部的参与者赞成提交事务协调者才通知全部的参与者提交事务,不然协调者将通知全部的参与者取消事务。参与者在接收到协调者发来的消息后将执行响应的操做。
能够看出,两阶段提交协议存在明显的问题:
同步阻塞
执行过程当中,全部参与节点都是事务独占状态,当参与者占有公共资源时,第三方节点访问公共资源被阻塞。
单点问题
一旦协调者发生故障,参与者会一直阻塞下去。
数据不一致性
在第二阶段中,假设协调者发出了事务commit的通知,可是由于网络问题该通知仅被一部分参与者所收到并执行commit,其他的参与者没有收到通知一直处于阻塞状态,这段时间就产生了数据的不一致性。
Three Phase分别为CanCommit、PreCommit、DoCommit。
三阶段提交针对两阶段提交作了改进:
二阶段提交仍是三阶段提交都没法很好的解决分布式的一致性问题,直到Paxos算法的提出,Paxos协议由Leslie Lamport最先在1990年提出,目前已经成为应用最广的分布式一致性算法。
Google Chubby的做者Mike Burrows说过这个世界上只有一种一致性算法,那就是Paxos,其它的算法都是残次品。
Paxos 协议中,有三类节点:
Proposer 能够有多个,Proposer 提出议案(value)。所谓 value,在工程中能够是任何操做,例如“修改某个变量的值为某个值”、“设置当前 primary 为某个节点”等等。Paxos 协议中统一将这些操做抽象为 value。
不一样的 Proposer 能够提出不一样的甚至矛盾的 value,例如某个 Proposer 提议“将变量 X 设置为 1”,另外一个 Proposer 提议“将变量 X 设置为 2”,但对同一轮 Paxos 过程,最多只有一个 value 被批准。
Acceptor 有 N 个,Proposer 提出的 value 必须得到超过半数(N/2+1)的
Acceptor 批准后才能经过。Acceptor 之间彻底对等独立。
Learner 学习被批准的 value。所谓学习就是经过读取各个 Proposer 对 value 的选择结果,若是某个 value 被超过半数 Proposer 经过,则 Learner 学习到了这个 value。
这里相似 Quorum 议会机制,某个 value 须要得到 W=N/2 + 1 的 Acceptor 批准,Learner 须要至少读取 N/2+1 个 Accpetor,至多读取 N 个 Acceptor 的结果后,能学习到一个经过的 value。
上述三类角色只是逻辑上的划分,实践中一个节点能够同时充当这三类角色。有些文章会添加一个Client角色,做为产生议题者,实际不参与选举过程。
Paxos中 proposer 和 acceptor 是算法的核心角色,paxos 描述的就是在一个由多个 proposer 和多个 acceptor 构成的系统中,如何让多个 acceptor 针对 proposer 提出的多种提案达成一致的过程,而 learner 只是“学习”最终被批准的提案。
Paxos协议流程还须要知足几个约束条件:
每轮 Paxos 协议分为准备阶段和批准阶段,在这两个阶段 Proposer 和 Acceptor 有各自的处理流程。
Proposer与Acceptor之间的交互主要有4类消息通讯,以下图:
这4类消息对应于paxos算法的两个阶段4个过程:
Proposer 生成全局惟一且递增的ProposalID,向 Paxos 集群的全部机器发送 Prepare请求,这里不携带value,只携带N即ProposalID 。
Acceptor 收到 Prepare请求 后,判断:收到的ProposalID 是否比以前已响应的全部提案的N大:
若是是,则:
(1) 在本地持久化 N,可记为Max_N。
(2) 回复请求,并带上已Accept的提案中N最大的value(若此时尚未已Accept的提案,则返回value为空)。
(3) 作出承诺:不会Accept任何小于Max_N的提案。
若是否:不回复或者回复Error。
P2a:Proposer 发送 Accept
通过一段时间后,Proposer 收集到一些 Prepare 回复,有下列几种状况:
(1) 回复数量 > 一半的Acceptor数量,且全部的回复的value都为空,则Porposer发出accept请求,并带上本身指定的value。
(2) 回复数量 > 一半的Acceptor数量,且有的回复value不为空,则Porposer发出accept请求,并带上回复中ProposalID最大的value(做为本身的提案内容)。
(3) 回复数量 <= 一半的Acceptor数量,则尝试更新生成更大的ProposalID,再转P1a执行。
P2b:Acceptor 应答 Accept
Accpetor 收到 Accpet请求 后,判断:
(1) 收到的N >= Max_N (通常状况下是 等于),则回复提交成功,并持久化N和value。
(2) 收到的N < Max_N,则不回复或者回复提交失败。
P2c: Proposer 统计投票
通过一段时间后,Proposer 收集到一些 Accept 回复提交成功,有几种状况:
(1) 回复数量 > 一半的Acceptor数量,则表示提交value成功。此时,能够发一个广播给全部Proposer、Learner,通知它们已commit的value。
(2) 回复数量 <= 一半的Acceptor数量,则 尝试 更新生成更大的 ProposalID,再转P1a执行。
(3) 收到一条提交失败的回复,则尝试更新生成更大的 ProposalID,再转P1a执行。
Paxos算法的核心思想:
(1)引入了多个Acceptor,单个Acceptor就相似2PC中协调者的单点问题,避免故障
(2)Proposer用更大ProposalID来抢占临时的访问权,能够对比2PC协议,防止其中一个Proposer崩溃宕机产生阻塞问题
(3)保证一个N值,只有一个Proposer能进行到第二阶段运行,Proposer按照ProposalID递增的顺序依次运行
(3) 新ProposalID的proposer好比认同前面提交的Value值,递增的ProposalID的Value是一个继承关系
为何在Paxos运行过程当中,半数之内的Acceptor失效都能运行?
(1) 若是半数之内的Acceptor失效时 还没肯定最终的value,此时,全部Proposer会竞争 提案的权限,最终会有一个提案会 成功提交。以后,会有半过数的Acceptor以这个value提交成功。
(2) 若是半数之内的Acceptor失效时 已肯定最终的value,此时,全部Proposer提交前 必须以 最终的value 提交,此值也能够被获取,并再也不修改。
如何产生惟一的编号呢?
在《Paxos made simple》中提到的是让全部的Proposer都从不相交的数据集合中进行选择,例如系统有5个Proposer,则可为每个Proposer分配一个标识j(0~4),则每个proposer每次提出决议的编号能够为5*i + j(i能够用来表示提出议案的次数)。
推荐larmport和paxos相关的三篇论文:
The Part-Time Parliament
Paxos made simple
Fast Paxos
2PC/3PC和Paxos协议是经典的分布式协议,理解了它们之后,学习其余分布式协议会简单不少。
参考:
CAP theorem
浅谈分布式系统的基本问题:可用性与一致性
分布式系统入门到实战
图解分布式一致性协议Paxos
Paxos协议学习小结