《分布式系统概念与设计》一书中对分布式系统概念的定义如下:分布式系统是一个硬件或软件组件分布在不同的网络计算机上,彼此之间仅仅通过消息传递进行通信和协调的系统
分布式系统的设计目标一般包括如下几个方面
分布式架构比单体式架构拥有更多的挑战
提到分布式系统,就不得不提CAP原理。
CAP的完整定义为:
CAP原理具有重大的指导意义:在任何分布式系统中,可用性、一致性和分区容忍性这三个方面都是相互矛盾的,三者不可兼得,最多只能取其二。
1)AP满足但C不满足:如果既要求系统高可用又要求分区容错,那么就要放弃一致性了。因为一旦发生网络分区(P),节点之间将无法通信,为了满足高可用(A),每个节点只能用本地数据提供服务,这样就会导致数据的不一致(!C)。一些信奉BASE(Basic Availability, Soft state, Eventually Consistency)原则的NoSQL数据库(例如,Cassandra、CouchDB等)往往会放宽对一致性的要求(满足最终一致性即可),以此来换取基本的可用性。
2)CP满足但A不满足:如果要求数据在各个服务器上是强一致的(C),然而网络分区(P)会导致同步时间无限延长,那么如此一来可用性就得不到保障了(!A)。坚持事务ACID(原子性、一致性、隔离性和持久性)的传统数据库以及对结果一致性非常敏感的应用(例如,金融业务)通常会做出这样的选择。
3)CA满足但P不满足:指的是如果不存在网络分区,那么强一致性和可用性是可以同时满足的。
正如热力学第二定律揭示了任何尝试发明永动机的努力都是徒劳的一样,CAP原理明确指出了完美满足CAP三种属性的分布式系统是不存在的。
了解CAP原理的目的在于,其能够帮助我们更好地理解实际分布式协议实现过程中的取舍。
分布式存储系统通常会通过维护多个副本来进行容错,以提高系统的可用性。这就引出了分布式存储系统的核心问题——如何保证多个副本的一致性?
“一致性”有三种含义:
Coherence这个单词只在Cache Coherence场景下出现过,其所关注的是多核共享内存的CPU架构下,各个核的Cache上的数据应如何保持一致。
Consensus是共识,它强调的是多个提议者就某件事情达成共识,其所关注的是达成共识的过程,例如Paxos协议、Raft选举等。Consensus属于replication protocol的范畴。
Consistency表达的含义相对复杂一些,广义上说,它描述了系统本身的不变量的维护程度对上层业务客户端的影响,以及该系统的并发状态会向客户端暴露什么样的异常行为。CAP、ACID中的C都有这层意思。
这里重点讨论的分布式系统中的一致性问题,属于上文中提到的Consensus和Consistency范畴。
分布式系统的一致性是一个具备容错能力的分布式系统需要解决的基本问题。通俗地讲,一致性就是不同的副本服务器认可同一份数据。一旦这些服务器对某份数据达成了一致,那么该决定便是最终的决定,且未来也无法推翻。
这里有一点需要注意:一致性与结果的正确性没有关系,而是系统对外呈现的状态是否一致(统一)。例如,所有节点都达成一个错误的共识也是一致性的一种表现。
一致性协议就是用来解决一致性问题的,它能使得一组机器像一个整体一样工作,即使其中的一些机器发生了错误也能正常工作。正因为如此,一致性协议在大规模分布式系统中扮演着关键角色。
一致性协议从20世纪80年代开始研究,一致性协议衍生出了很多算法。衡量一致性算法的标准具体如下:
在给定了与操作和状态相关的一些规则的情况下,系统中的操作历史应该总是遵循这些规则。我们称这些规则为一致性模型
对于一致性,可以分别从客户端和服务端两个不同的视角来理解。
从客户端来看,一致性主要是指多并发访问时如何获取更新过的数据的问题。
从服务端来看,则是更新如何复制分布到整个系统,以保证数据最终的一致性。
因此,可以从两个角度来查看一致性模型:以数据为中心的一致性模型和以用户为中心的一致性模型。
实现以下这几种一致性模型的难度会依次递减,对一致性强度的要求也依次递减。
严格一致性(Strong Consistency)
严格一致性也称强一致性,原子一致性或者是可线性化(Linearizability),是要求最高的一致性模型。严格一致性的要求具体如下:
**严格一致性维护的是一个绝对全局时间顺序。**单机系统遵守严格一致性,但对于分布式系统,为每个操作都分配一个准确的全局时间戳是不可能实现的,所以严格一致性只是存在于理论中的一致性模型。
顺序一致性(Sequential Consistency)
顺序一致性,也称为可序列化,比严格一致性要求弱一点,但也是能够实现的最高级别的一致性模型。
因为全局时钟导致严格一致性很难实现,因此顺序一致性放弃了全局时钟的约束,改为分布式逻辑时钟实现。顺序一致性是指所有的进程都以相同的顺序看到所有的修改。读操作未必能够及时得到此前其他进程对同一数据的写更新,但是每个进程读到的该数据不同值的顺序却是一致的。
满足顺序一致性的存储系统需要一个额外的逻辑时钟服务。
下图解释了严格一致性和顺序一致性
a) 顺序一致性,从这两个进程的角度来看,顺序应该是这样的:Write(y, 2)→Read(x, 0)→Write(x, 4)→Read(y, 2),完全没有冲突
b) 严格一致性,从两个进程看到的操作顺序与全局时钟的顺序一样,都是Write(y, 2)→Write(x, 4)→Read(x, 4)→Read(y, 2)。
c) 不满足顺序一致性,Write(x, 4)→Read(y, 0)→Write(y, 2)→Read(x, 0),这个有冲突
因果关系可以描述成如下情况:
不严格地说,因果一致性弱于顺序一致性。
因果一致性和顺序一致性的对比
P2写x=7,P2同步到P3,P3读到7
P1写x=2,P1同步到P3,P3读到2
P1写x=4,P1同步到P4,P4读到4
P2同步到P4,P4读到7
永远不会出现先读到4,再读到2的情况
因果关系只保证因果的顺序是正确的,其他的顺序不理会
可串行化一致性(Serializable Consis-tency)
如果说操作的历史等同于以某种单一原子顺序发生的历史,但对调用和完成时间没有说明,那么就可以获得称为可序列化的一致性模型。
在一个可序列化的系统中,有如下所示的这样一个程序:
x = 1
x = x + 1
puts x
在这里,我们假设每行代表一个操作,并且所有的操作都成功。因为这些操作可以以任何顺序进行,所以可能打印出nil、1或2。因此,一致性显得很弱。
但在另一方面,串行化的一致性又很强,因为它需要一个线性顺序。例如,下面的这个程序:
print x if x = 3
x = 1 if x = nil
x = 2 if x = 1
x = 3 if x = 2
它可能不会严格地以我们编写的顺序发生,但它能够可靠地将x从nil→1→2,更改为3,最后打印出3。
最终一致性(Eventual Consistency)
最终一致性是指如果更新的间隔时间比较长,那么所有的副本都能够最终达到一致性。
用户读到某一操作对系统特定数据的更新需要一段时间,我们将这段时间称为“不一致性窗口”。
在读多写少的场景中,例如CDN,读写之比非常悬殊,如果网站的运营人员修改了一张图片,最终用户延迟了一段时间才看到这个更新实际上问题并不是很大。
复制状态机的基本思想是:一个分布式的复制状态机系统由多个复制单元组成,每个复制单元均是一个状态机,它的状态保存在一组状态变量中。状态机的状态能够并且只能通过外部命令来改变。
上文提到的“一组状态变量”通常是基于操作日志来实现的。每一个复制单元存储一个包含一系列指令的日志,并且严格按照顺序逐条执行日志上的指令。
所以,在复制状态机模型下,一致性算法的主要工作就变成了如何保证操作日志的一致性。
复制状态机的运行过程如下图所示:
服务器上的一致性模块负责接收外部命令,然后追加到自己的操作日志中。它与其他服务器上的一致性模块进行通信以保证每一个服务器上的操作日志最终都以相同的顺序包含相同的指令。一旦指令被正确复制,那么每一个服务器的状态机都将按照操作日志的顺序来处理它们,然后将输出结果返回给客户端。
复制状态机在分布式系统中常被用于解决各种容错相关的问题,例如,GFS、HDFS、Chubby、ZooKeeper和etcd等分布式系统都是基于复制状态机模型实现的。
需要注意的是,指令在状态机上的执行顺序并不一定等同于指令的发出顺序或接收顺序。
复制状态机只是保证所有的状态机都以相同的顺序执行这些命令。
拜占庭位于如今土耳其的伊斯坦布尔,是东罗马帝国的首都。由于当时拜占庭罗马帝国幅员辽阔,出于防御的原因,每个军队都相隔甚远,将军与将军之间只能靠信差来传递消息。发生战争时,拜占庭军队内所有将军必需达成共识,决定是否攻击敌人。但是军队内可能存在叛徒和敌军的间谍扰乱将军们的决定,因此在进行共识交流时,结果可能并不能真正代表大多数人的意见。这时,在已知有成员不可靠的情况下,其余忠诚的将军如何排除叛徒或间谍的影响来达成一致的决定,就是著名的拜占庭将军问题。
拜占庭将军问题讲述的是一个共识问题。拜占庭将军问题是对现实世界的模型化。
拜占庭错误是一个overly pessimistic模型(最悲观、最强的错误模型)
研究这个模型的意义在于:如果某个一致性协议能够保证系统在出现N个拜占庭错误时,依旧可以做出一致性决定,那么这个协议也就能够处理系统出现N个其他任意类型的错误。
进程失败错误(fail-stop Failure,如同宕机)则是一个overly optimistic模型(最乐观、最弱的错误模型)
研究这个模型的意义在于:如果某个一致性协议在系统出现N个进程失败错误时都无法保证做出一致性决定,那么这个协议也就无法处理系统出现N个其他任意类型的错误。
Fred Schneider在前面提到的论文《Implementing fault-tolerant services using thestate machine approach》中指出了这样一个基本假设:
一个RSM(分布式状态机)系统要容忍N个拜占庭错误,至少需要2N+1个复制节点。
如果只是把错误的类型缩小到进程失败,则至少需要N+1个复制节点才能容错。
但是不是只要满足上文提到的2N+1个要求就能保证万无一失了呢?很不幸,答案是否定的。
FLP不可能性是分布式领域中一个非常著名的定理:
No completely asynchronous consensusprotocol can tolerate even a single unan-nounced process death.
在异步通信场景下,任何一致性协议都不能保证,即使只有一个进程失败,其他非失败进程也不能达成一致。
这里的进程失败(unannounced process death)指的是一个进程发生了故障,但其他节点并不知道,继续认为这个进程还没有处理完成或发生消息延迟了。
举个例子:
甲、乙、丙三个人各自分开进行投票(投票结果是0或1)。他们彼此可以通过电话进行沟通,但有人会睡着。例如:甲投票0,乙投票1,这时候甲和乙打平,丙的选票就很关键。然而丙睡着了,在他醒来之前甲和乙都将无法达成最终的结果。即使重新投票,也有可能陷入无尽的循环之中。
FLP定理实际上说明了在允许节点失效的场景下,基于异步通信方式的分布式协议,无法确保在有限的时间内达成一致性。用CAP理论解释的话,在P的条件下,无法满足C和A。
请注意,这个结论的前提是异步通信。在分布式系统中,“异步通信”与“同步通信”的最大区别是没有时钟、不能时间同步、不能使用超时、不能探测失败、消息可任意延迟、消息可乱序等。
所以,实际的一致性协议(Paxos、Raft等)在理论上都是有缺陷的,最大的问题是理论上存在不可终止性!但他们都做了一些调整,降低了概率。
大神Leslie Lamport对类似拜占庭将军这样的问题进行了深入研究,并发表了几篇论文。
总结起来就是回答如下的三个问题:
1)类似拜占庭将军这样的分布式一致性问题是否有解?
2)如果有解的话需要满足什么样的条件?
3)基于特定的前提条件,提出一种解法。
Leslie Lamport在论文“拜占庭将军问题”中已经给出了前两个问题的回答,而第三个问题在他的论文“The Part-Time Parliament”中提出了一种基于消息传递的一致性算法。
下面讲述的就是大神的日常操作:
1990年,Lamport向ACM Transac-tions on Computer Systems提交了他那篇关于Paxos算法的论文。主编回信建议他用数学而不是神话描述他的算法,否则他们不会考虑接受这篇论文。Lamport觉得那些人太迂腐,拒绝做任何修改,转而将论文贴在了自己的个人博客上。
起初Paxos算法由于难以理解并没有引起多少人的重视,直到2006年Google的三大论文初现“云”端,其中Chubby Lock服务使用了Paxos作为Chubby Cell的一致性算法,这件事使得Paxos算法的人气从此一路飙升,几乎垄断了一致性算法领域。在Raft算法诞生之前,Paxos几乎成了一致性协议的代名词。
Lamport本人觉得Paxos很简单,但事实上对于大多数人来说,Paxos还是太难理解了。
引用NSDI社区上的一句话就是:全世界真正理解Paxos算法的人只有5个!
这可能就是人和神之间的区别吧。
然后,更容易理解的一致性算法Raft诞生了。
终于讲到Raft了,我太不容易了。
Raft算法主要使用两种方法来提高可理解性。提高理解性主要通过两个常用手段
问题分解
尽可能地将问题分解成为若干个可解决的、更容易理解的小问题——这是众所周知的简化问题的方法论。例如,Raft算法把问题分解成了领袖选举(leader election)、日志复制(log repli-cation)、安全性(safety)和成员关系变化(membershipchanges)这几个子问题。
减少状态空间
Raft算法通过减少需要考虑的状态数量来简化状态空间。这将使得整个系统更加一致并且能够尽可能地消除不确定性。
Raft有几点重要的创新
Leader(领袖)
Candidate(候选人)
Follower(群众)
任期(Term):Raft算法将时间划分成为任意个不同长度的任期,任期是单调递增的,用连续的数字(1, 2, 3……)表示。在Raft的世界里,每一个任期的开始都是一次领导人的选举。如果一个候选人赢得了选举,那么它就会在该任期的剩余时间内担任领导人。在某些情况下,选票会被瓜分,导致没有哪位候选人能够得到超过半数的选票,这样本次任期将以没有选出领导人而结束。那么,系统就会自动进入下一个任期,开始一次新的选举。Raft算法保证在给定的一个任期内最多只有一个领导人。某些Term会由于选举失败,存在没有领导人的情况,如t3所示。
任期在Raft中起着逻辑时钟的作用,同时也可用于在Raft节点中检测过期信息——比如过期的领导人。每个Raft节点各自都在本地维护一个当前任期值,触发这个数字变化(增加)主要有两个场景:开始选举和与其他节点交换信息。如果一个节点(包括领导人)的当前任期号比其他节点的任期号小,则将自己本地的任期号自觉地更新为较大的任期号。如果一个候选人或者领导人意识到它的任期号过时了(比别人的小),那么它会立刻切换回群众状态;如果一个节点收到的请求所携带的任期号是过时的,那么该节点就会拒绝响应本次请求。
Raft通过选举一个权力至高无上的领导人,并采取赋予他管理复制日志重任的方式来维护节点间复制日志的一致性。
领导人从客户端接收日志条目,再把日志条目复制到其他服务器上,并且在保证安全性的前提下,告诉其他服务器将日志条目应用到它们的状态机中。领导人可以决定新的日志条目需要放在日志文件的什么位置,而不需要和其他服务器商议,并且数据都是单向地从领导人流向其他服务器。
领导人选举的过程,就是Raft三种角色切换的过程
开始的时候,系统有一个Leader和众多Follower
一旦某个领导人赢得了选举,那么它就会开始接收客户端的请求。领导人将把这条指令作为一条新的日志条目加入它的日志文件中,然后并行地向其他Raft节点发起AppendEntriesRPC,要求其他节点复制这个日志条目。当这个日志条目被“安全”地复制之后,Leader会将这条日志应用(apply,即执行该指令)到它的状态机中,并且向客户端返回执行结果。如果Follower发生错误,运行缓慢没有及时响应AppendEntries RPC,或者发生了网络丢包的问题,那么领导人会无限地重试AppendEntries RPC(甚至在它响应了客户端之后),直到所有的追随者最终存储了和Leader一样的日志条目。
日志由有序编号的日志条目组成。每一个日志条目一般均包含三个属性:整数索引(log index)、任期号(term)和指令(command)。一般如下所示:
一旦领导人创建的条目已经被复制到半数以上的节点上了,那么这个条目就称为可被提交的。
Raft日志复制主要流程如下:
从上面的步骤可以看出,针对Raft日志条目有两个操作,提交(commit)和应用(apply),应用必须发生在提交之后,即某个日志条目只有被提交之后才能被应用到本地状态机上。
流程图如下:https://www.processon.com/view/link/5fa6c1045653bb25634dea4a
本文介绍如何在领导人选举部分加入一个限制规则来保证——任何的领导人都拥有之前任期提交的全部日志条目。
怎样才能具有成为领导人的资格?- 必须包含所有已提交日志条目
RequestVote RPC的接收方有一个检查:如果Follower自己的日志比RPC调用方(候选人)的日志更加新,就会拒绝候选人的投票请求。
这就Raft算法使用投票的方式来阻止那些没有包含所有已提交日志条目的节点赢得选举。
如何判断日志已经提交?
在提交之前term的日志项时,必须保证当前term新建的日志项已经复制到超过半数节点。这样,之前term的日志项才算真正提交的。
broadcastTime << electionTimeout << MTBF
broadcastTime指的是一个节点向集群中其他节点发送RPC,并且收到它们响应的平均时间。
electionTimeout就是选举超时时间。
MTBF指的是单个节点发生故障的平均时间间隔。
为了使领导人能够持续发送心跳包来阻止下面的Follower发起选举,broadcastTime应该比electionTimeout小一个数量级。
####追随者/候选人异常
Raft算法通过领导人无限的重试来应对这些失败,直到故障的节点重启并处理了这些RPC为止。
因为Raft算法中的RPC都是幂等的,因此不会有什么问题。
Raft数据交换流程如上图所示,在任何时刻,领导人都有可能崩溃。
如果在这个阶段Leader出现故障,此时数据属于未提交状态,那么Client不会收到ACK,而是会认为超时失败可安全发起重试。Follower节点上没有该数据,重新选主后Client重试重新提交可成功。原来的Leader节点恢复之后将作为Follower加入集群,重新从当前任期的新Leader处同步数据,与Leader数据强制保持一致。
如果在这个阶段Leader出现故障,此时数据在Follower节点处于未提交状态(Uncommitted)且不一致,那么Raft协议要求投票只能投给拥有最新数据的节点。所以拥有最新数据的节点会被选为Leader,再将数据强制同步到Follower,数据不会丢失并且能够保证最终一致。
如果在这个阶段Leader出现故障,虽然此时数据在Fol-lower节点处于未提交状态(Uncommitted),但也能保持一致,那么重新选出Leader后即可完成数据提交,由于此时客户端不知到底有没有提交成功,因此可重试提交。针对这种情况,Raft要求RPC请求实现幂等性,也就是要实现内部去重机制。
网络分区将原先的Leader节点和Follower节点分隔开,Follower收不到Leader的心跳将发起选举产生新的Leader。这时就产生了双Leader,原先的Leader独自在一个区,向它提交数据不可能复制到大多数节点上,所以永远都是提交不成功。向新的Leader提交数据可以提交成功,网络恢复后旧的Leader发现集群中有更新任期(Term)的新Leader,则自动降级为Fol-lower并从新Leader处同步数据达成集群数据一致。
分布式系统和一般的业务系统区别还是挺大的,涉及到更多论文、数理知识,更加偏学术一些。随着计算机的不断发展,关于分布式的知识,还是需要进行掌握的。
关于Raft算法,建议看一下源码https://github.com/etcd-io/etcd/tree/master/raft。
Raft通过领导选举机制,简化了整体的复杂性。利用日志复制+复制状态机,保证状态执行的一致。同时设置了一些对应的安全规则,加强了日志复制的安全,维护了一致性。
如果大家时间有限,可以只看一下CAP、复制状态机和Raft。
https://www.infoq.cn/article/wechat-serial-number-generator-architecture/ 微信序列号生成器架构设计及演变
从微信朋友圈的评论可见性,谈因果一致性在分布式系统中的应用
https://www.jianshu.com/p/ab511132a34f raft 系列解读(3) 之 代码实现
https://blog.csdn.net/lanyang123456/article/details/109279234 raft协议中的日志安全性
http://www.duokan.com/book/180790 云原生分布式存储基石:etcd深入解析
大家如果喜欢我的文章,可以关注我的公众号(程序员麻辣烫)
我的个人博客为:https://shidawuhen.github.io/
往期文章回顾:
技术
读书笔记
思考