文章目录
前言
分布式系统是当前互联网领域的一个重要且大势所趋的一种解决计算,存储,网络相关问题的方向,利用数量庞大的服务器节点作为底层,在其之上搭建一个可以跨省甚至跨国的庞大系统。
它能够保证用户数据的一致性,对用户提供服务的高可用性,以及跨地域的分区容错性,而分布式一致性算法就是为了保证分布式系统能够满足其特性。
分布式一致性算法,我将其划分到了分布式存储技能树中的分布式理论中:
本文的总结,将按照以下导图进行知识点的总结和梳理
CAP理论
提到分布式系统,那么CAP理论便无法绕过了。自从2000年,Eric Brewer教授提出了分布式系统设计一定会遵循:一致性,可用性,分区容错性的三个原则之后,互联网界的分布式系统便都将其作为自己的衡量标准。后续的分布式系统设计也都按照CAP理论进行设计,但是任何一个系统都只能在三个原则中满足两个,无法三个都满足,即使像最近的亚马逊S3和google spanner等全球分布式系统也只能满足CP两种,对于A则也只能提供多个9的可用性。
后续该理论上升为定理之后也有相关的证明论文,本节只是将其作为一个基本理论扫盲的介绍。
C consistency 一致性
在分布式系统中有多个节点,整个系统对外提供的服务应该是一致的。即用户在不同的系统节点访问数据的时候应该是同样的结果,不能出现1号节点是结果1, 2号节点是结果2这种不一致的情况。
A availability 可用性
分布式系统为用户提供服务,需要保证能够在一些节点异常的情况下仍然支持为用户提供服务。
P partition tolerance 分区容错性
分布式系统的部署可能跨省,甚至跨国。不同省份或者国家之间的服务器节点是通过网络进行连接,此时如果两个地域之间的网络连接断开,整个分布式系统的体现就是分区容错性了。
在这种系统出现网络分区的情况下系统的服务就需要在一致性 和 可用性之间进行取舍。
如果为了在出现网络分区之后提供仍然能够提供可用性,那么一致性必然无法满足。因为可用性的要求是系统仍然能够提供服务,但是这个时候分布式系统在两个地域之间无法通信,那么America的请求无法同步到China的服务节点上,整个系统的一致性显然不能满足。
同理 为了保证一致性,那么可用性也必然不能满足,因为一致性的要求是请求A写入America之后从China中读,一定要能够读到这个请求(强一致性)或者 经过一段时间之后也能够读到(弱一致性)。但是两地域已经出现了网络分区,那么请求会阻塞从而降低可用性。
所以CAP理论一定是无法全部满足三者,只能满足其中的两者。
一致性模型
弱一致性
弱一致性体现的是最终一致性,即如上CAP理论中 ,两个地域的请求分别通过两地的节点写入,可以不用立即进行同步,而是经过一段时间之后两地的用户数据变为一致。这种一致性成为弱一致性,即最终一致性。
也就是用户一地写入之后从另外一个节点读取,无法立即读到刚才写入的数据。
比如DNS域名系统,Cassandra的 Gossip协议等都是最终一致性的体现。
全球目前的域名系统可以看作一个域名树,由一个根域名节点.cn
下辖多个.edu
,.org
,.com
等子域名节点,而这一些子域名之下🈶️更多的子域名(以企业为例www.kuaishou.com,之下有多个数据中心,不同的数据中心又数百上千的存储节点),当我们向某一个节点更新域名信息的时候,在其他节点立即访问该记录则不一定立即访问到最终的更新,而是经过一段时间之后我们能够看到更新。
强一致性
强一致性是非常有必要的,比如业界的银行系统,支付系统等对一致性要求非常高。
当下比较流行的强一致性算法 像 Paxos,以及基于paxos衍生的raft,ZAB都能提供强一致性的能力。
强一致性描述的是一个请求在一个节点写入之后在其他节点读取,则该数据的更新能够被读到。
接下来主要描述的是一些强一致性算法的原理。
强一致性算法
需要明确的问题
- 数据不能存放在单个节点上,当然这个是分布式系统的基础形态,不满足这个分布式系统也不能称为分布式系统了
- 分布式系统对分区容错的方案是 state machine replication (复制状态机),而强一致算法也被称为共识算法(consensus)
state状态机 简单来说就是一个函数,根据输入的不同值来执行对应的请求处理逻辑。
为了保证系统的最终一致性,不仅需要系统内部达成共识,也需要一些client的行为。
强一致算法: 主从同步
主从同步复制:
master
接受写请求master
复制日志到slavemaster
等待直到所有从节点返回
这是一个强一致性算法的基础,但是这样的模型会出现如下问题:
比如一个slave节点异常宕机,那么master迟迟等不到该节点的返回,此时client请求便会阻塞。这样的模型就是强一致模型,但是系统的可用性就会非常差,一个节点挂掉会导致整个系统不可用。
强一致性算法:多数派
为了避免主从同步那样一个节点异常,整个集群不可用的情况,推出了多数派。即每次写入只需要保证写入节点数大于N/2 个,读节点数保证大于N/2个节点,此时即可认为能够向客户端返回了。
这样既能够保证一致性,又提高了可用性。但是这个情况也只是针对单个请求写入的情况下,而我们实际生产环境中会存在大量的并发场景。
比如:
并发这个分布式系统的不同节点执行不同的操作,在满足多数派的情况下可能会出现顺序的问题,不同的节点因为请求同步的时间问题可能出现请求的顺序不一致。
强一致算法:Paxos
基础的Paxos算法包括如下三种:
- Basic Paxos
- Multi Paxos
- Fast Paxos
paxos算法是Lasile Lamport的发明,这个算法已经有很多论文对其基本原理进行描述,因为本身角色很多又要在不同的异常场景下描述清楚,很难真正让读者对该算法融会贯通的。
Lamport
为了更加形象的描述,提出了虚拟出来一个叫做Paxos的希腊城邦,这个城邦的管理者为了方便对民众的管理,提出了一系列法案用来对民众进行约束,当民众有民事请求时,会提交民事诉讼给国会,有一个组织者将该诉讼整理成法案交给一个个议员进行处理,议员之间又分为投票者和接受者用来实际进行该法案最终是否要处理的决定,最终将结果反馈给组织者,由组织者将最终的结果再返还给民众。
具体的角色如下:
Client
:系统外部角色,请求的发起者。就像是故事中的民众Proposer
: 接受Client
请求,想集群提出提议(propose)。并在投票的国会发生冲突的时候进行调节,经结果反馈给民众Accpetor
(Voter): 投票者,就是负责在集群内部对法案是否要写入进行投票。只有满足多数派(法定人数Quorum的由来)的情况下提议才会被接受,像是国会。Learner
:接受者,即进行backup,将民众的请求记录下来。这个角色对集群一致性本身并没有什么影响。
Basic Paxos
主要分为如下几个阶段(phases):
Phase 1a
: Prepare阶段
这个阶段Proposer
提出一个提案编号为N;此时N大于这个Proposer
之前提出的议案,请求这个集群的Acceptor
Quorum接受。Phase 1b
: Promise阶段
如果N大于此Acceptor
之前接受的任何提案编号则接受,否则拒绝。Phase 2a
: Accept阶段
如果Acceptor
内部投票通过的人数达到了多数派,则Proposer
会发出accept的请求,该请求包括提案编号为N,以及提案的内容(用户请求的内容)Phase2b
: Accepted阶段
如果此acceptor在此期间没有收到任何编号大于N的提案,则接受此提案内容,否则忽略。接受之后会向Proposer返回成功,且将议案的内容交给Learner
进行backup
正常的处理流程如下:
客户端发起request请求到Proposer,然后Proposer发出提案并携带提案的编号1,分别发给每一个Acceptor;每一个Acceptor根据天都编号是否满足大于1,将投票结果通过Propose阶段反馈给Proposer;Proposer收到Acceptor的结果判断是否达到了多数派,达到了多数派则向Acceptor发出Accept请求,并携带提案的具体内容V;Acceptor收到提案内容后向Proposer反馈Accepted表示收到,并将提案内容交给Learner进行备份。
Learner写入提案内容之后向Client发送完成请求的响应。
当Acceptor出现异常时流程如下:
虽然Accetor3 出现异常,没有向Proposer反馈,但是Proposer此时收到的接受提案的反馈有2个Acceptor,仍然满足多数派的情况,此时仍然能够将提案内容继续写入的,所以后续的Accept的发送只需要发送给剩下的两个Acceptor即可。
当Proposer出现异常时的情况如下:
Proposer失败的话表示收到Acceptor的Propose请求之后无法继续发送Accept请求,这个时候集群会重新选出另一个新的能够工作的Proposer,再从prepare阶段开始处理,同时Prepare的提案版本号会增加一个,但是提案的内容还是之前的内容。
当出现活锁的情况:
简化了如下处理流程,当然其中的 Proposers和Acceptors 不只一个,是由多个组成的。
基本情况就是集群中有多个Propser,当proposer1发送prepare版本为1并收到propose的时候节点发生了异常,集群切换到了新的proposer,并重新prepare 版本2,准备好和版本1相同的内容的提案。(因为acceptor处理的过程中发现更高版本的提案,会丢弃当前的版本,转向更高版本去处理)。
当proposer2等待acceptor的propose返回时,proposer1有上线了,发现自己prepare(1)提案被打断,此时又准备了一个更高版本的prepare(3)提案,打断了proposer2的2版本提案;当proposer2发现自己的2号版本被打断,又准备了更高的4号版本,从而打断了propose1的3号提案版本;依此下去,整个集群将会阻塞在相同的提案的不断提交之中,这种情况就是集群出现了活锁。
当然也有较好的解决措施,比如:proposer1的上线之后 重新提交法案使用随机时间机制,即随机生成一个时间戳,在这段时间内不向Acceptor发送消息;这样proposer2的提案能够被处理完成,这个时候proposer1再次提交新的提案。
综上这一些基本异常情况来看,basic paxos还是能够保证系统的一致性和可用性,共识算法还是能够正常工作。
但是basic还是有一些缺点:
- 难实现
有太多的角色需要实现 - 效率低(2轮 rpc)
每一个请求需要proposer和acceptor 至少交互两次才能完成请求的处理 - 活锁
这个时候推出了Multi Paxos,其中的特色就是Leader概念,Leader是由唯一的Proposer表示,所有的请求都需要经过这个Leader进行转发。
Multi Paxos
第一个版本:使用Proposer表示唯一的一个Leader
Proposer可以直接用来做提案是否被接受的决定,而不用所有的acceptor各自决定是否要接受提案。
正常处理流程如下:
- 处理第一个请求的时候因为需要选择leader,所以还是会走2轮rpc,从prepare 到最后的Accepted。
- 从第二个请求开始,所有的请求只需要交给Proposer,Proposer即可完成是否接受请求的决定,一次Accept即可达成将请求写入Learner的过程。
第二个版本:将算法角色进一步简化
这个简化之后便更加接近我们实际的分布式系统的情况
当然这个还是正常请求的处理流程,不过只会通过一轮Accept通信即可完成请求的处理。
ps:简化后的集群第一个请求还是会走2轮rpc,为了从servers中选举出一个leader,这个过程还是会从prepare开始到Accepted完成。
强一致算法: Raft(基于log replicated的共识算法)
raft是更为简化的multi paxos
(其实也就是上一个图中的paxos)算法,相比于paxos的复杂实现来说角色更少,问题更加精简。
rafti整体来说可以拆分成三个子问题来达成共识:
Leader Election
如何选择出leaderLog Replication
如何将log复制到其他的节点Safety
保证复制之后集群的数据是一致的
重新定义了新的角色:
Leader
一个集群只有一个leaderFollower
一个服从leader决定的角色Cadidate
follower发现集群没有leader,会重新选举leader,参与选举的节点会变成candidate
除了以上的精简之外,raft还提供了完善的社区 甚至 完整共识过程的可视化展示,这里可以通过原理动画展示 : http://thesecretlivesofdata.com/raft/ 浏览。
主体过程如下:
-
三个基本的节点角色介绍一下
-
Leader Election的选举过程
初始所有节点都是follower状态,当开始选举的时候将待选举节node a
点置为candidate状态
canditdate会向其他的follower节点发送请求进行leader投票,其他节点投票通过,则将candidate节点的状态置为leader状态。
接下来通过两种心跳时间来详细看一下选举过程的实现
在leader election选举的过程中会有两种timeout的设置来控制整个的选举过程:
-
Election timeout
表示follower等待请求的超时时间,如果这个时间结束前还没有收到来自leader或者选举leader的请求,那么当前follower便会变为candidate
。 raft给的设定值一般在150ms-300ms之间的随机值。
变成candidate之后,当前节点会立即发送leader的投票请求,其他的follower节点会重置选举的超时时间。 -
heartbeat timeout
新选举出来的leader每隔一段时间会发送一个请求给follower,这个时间就是心跳时间。
同样follower在相同的时间间隔内回复leader一个请求,表示自己已经收到。这样的心跳间隔将会一直持续,直到一个follower停止接受心跳,变成candidate。
重新选举的过程就是candidate发送选举请求,follower接受到之后返回对应的心跳回应,candidate根据心跳回应的个数判断是否满足多数派,从而变成leader。变成leader之后,会持续发送心跳包来保证follower的存活。
Log Replication
过程
主要过程如下:
客户端发送请求到leader,leader接受之后将请求封装成log entry,并将log副本发送给其他的follower节点。
等待其他的follower节点回复,发现达到了多数派之后leader便将该entry写入到自己的文件系统之中;
写入之后再发送请求,表示follower节点也可以写入了。
接下来我们详细看看log Replicated的实现过程,我们的leader被选举出来之后用于请求的转发,将接受到的用户请求封装成log entry,并将log entry的副本转发给follower,这个log enry发送到follower之后也会用于重置follower的心跳时间。- 客户端会发送一条请求到leader,这个请求会追加到leader的log上,但此时还没有写入leader所在节点的文件系统之上
- 这个条leader的log 会在leader发送下一条心跳包的时候携带该请求的信息 一起发送给follower
- 当entry提交到follower,且收到多数派的回复之后会给client一个回复,表示集群已经写入完成。同时将leader的log写入自己的文件系统,并且发送command让从节点也进行写入。这个过程就是multi paxos中的accepted阶段。
关于raft更加详细严谨的细节介绍和性能测试 可以参考论文raft paper
强一致算法:ZAB
基本和raft相同,只是在一些名词的叫法上有一些区别
比如ZAB 将某一个leader的周期称为epoch,而raft称为 term。
实现上的话 raft为了保证日志连续性,心跳方向是从leader到follower,ZAB则是相反的。
总结
本篇从理论和实现逻辑上描述了CAP理论、paxos相关的一致性算法变种以及基于multi paxos实现的简化版强一致算法协议raft和ZAB,后续会从源码层面一一观察以及自实现基础raft的通信来加深对分布式系统设计的理解。
可以看到分布式系统的核心算法还是围绕CAP理论来进行取舍,从系统前期的角色设计,到每个角色承担的任务,到最后异常情况下的各个角色做出的反应都能看到CAP的影子。这一些设计原则最终还是会反馈到用户体验之上,双十一的巨量流量,国外的APP群体和国内实时通信交互,看似简单的一个用户功能背后的每一个RPC都是一段跨省,跨国的异地恋,即使异国之路被封锁,合理的系统设计也能够让这段恋情持续。
各自安好,静待他音。
想猪!!!