分布式系统概述
分布式系统概述
CAP理论
在理论计算机科学中,CAP定理(CAP theorem),又被称作布鲁尔定理(Brewer’s theorem),它指出对于一个分布式计算系统来说,不可能同时满足以下三点:
- 一致性(Consistency) (等同于所有节点访问同一份最新的数据副本)
- 可用性(Availability)(每次请求都能获取到非错的响应——但是不保证获取的数据为最新数据)
- 分区容错性(Partition tolerance)(以实际效果而言,分区相当于对通信的时限要求。系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况,必须就当前操作在C和A之间做出选择)
根据定理,分布式系统只能满足三项中的两项而不可能满足全部三项[4]。理解CAP理论的最简单方式是想象两个节点分处分区两侧。允许至少一个节点更新状态会导致数据不一致,即丧失了C性质。如果为了保证数据一致性,将分区一侧的节点设置为不可用,那么又丧失了A性质。除非两个节点可以互相通信,才能既保证C又保证A,这又会导致丧失P性质。
不是所谓的“3 选 2”
大部分人解释这一定律时,常常简单的表述为:“一致性、可用性、分区容忍性三者你只能同时达到其中两个,不可能同时达到”。实际上这是一个非常具有误导性质的说法,而且在 CAP 理论诞生 12 年之后,CAP 之父也在 2012 年重写了之前的论文。
当发生网络分区的时候,如果我们要继续服务,那么强一致性和可用性只能 2 选 1。也就是说当网络分区之后 P 是前提,决定了 P 之后才有 C 和 A 的选择。也就是说分区容错性(Partition tolerance)我们是必须要实现的。
简而言之就是:CAP 理论中分区容错性 P 是一定要满足的,在此基础上,只能满足可用性 A 或者一致性 C。
常见的可以作为注册中心的组件有:ZooKeeper、Eureka、Nacos…。
- ZooKeeper 保证的是 CP。 任何时刻对 ZooKeeper 的读请求都能得到一致性的结果,但是, ZooKeeper 不保证每次请求的可用性比如在 Leader 选举过程中或者半数以上的机器不可用的时候服务就是不可用的。
- Eureka 保证的则是 AP。 Eureka 在设计的时候就是优先保证 A (可用性)。在 Eureka 中不存在什么 Leader 节点,每个节点都是一样的、平等的。因此 Eureka 不会像 ZooKeeper 那样出现选举过程中或者半数以上的机器不可用的时候服务就是不可用的情况。 Eureka 保证即使大部分节点挂掉也不会影响正常提供服务,只要有一个节点是可用的就行了。只不过这个节点上的数据可能并不是最新的。
- Nacos 不仅支持 CP 也支持 AP
总结
在进行分布式系统设计和开发时,我们不应该仅仅局限在 CAP 问题上,还要关注系统的扩展性、可用性等等
在系统发生“分区”的情况下,CAP 理论只能满足 CP 或者 AP。要注意的是,这里的前提是系统发生了“分区”
如果系统没有发生“分区”的话,节点间的网络连接通信正常的话,也就不存在 P 了。这个时候,我们就可以同时保证 C 和 A 了。
总结:如果系统发生“分区”,我们要考虑选择 CP 还是 AP。如果系统没有发生“分区”的话,我们要思考如何保证 CA
BASE 理论
BASE是Basiclly Available(基本可用),Soft state(软状态),Eventually consistent(最终一致性)三个短语的缩写。
BASE是对CAP中一致性和可用性权衡的结果,其来源于对大规模互联网系统分布式实践的总结,是基于CAP定理逐步演化而来的,其核心思想是即使无法做到强一致性,但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性。
BASE 是 Basically Available(基本可用) 、Soft-state(软状态) 和 Eventually Consistent(最终一致性) 三个短语的缩写。BASE 理论是对 CAP 中一致性 C 和可用性 A 权衡的结果,其来源于对大规模互联网系统分布式实践的总结,是基于 CAP 定理逐步演化而来的,它大大降低了我们对系统的要求。
基本可用
基本可用是指分布式系统在出现不可预知的故障的时候,允许损失部分可用性——但请注意,这绝不等价于系统不可用。基本可用的两个例子:
- 响应事件上的损失:正常情况下,一个在线搜索引擎需要再0.5秒之内返回给用户响应的查询结果,但由于出现故障,查询结果的响应时间增加到了1~2秒。
- 功能上的损失:正常情况下,在一个电子商务网站上进行购物,消费者能够顺利完成每一笔订单,但是在一些节目大促销高峰,由于消费者的购物行为激增,为了保护购物系统的稳定性,部分消费者可能会被引导到一个降级的页面。
弱状态
弱状态也被称为软状态,和硬状态对应,是指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统不同节点的数据副本之间进行数据同步的过程存在延时。
最终一致性
最终一致性强调的是系统所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要试试保证系统数据的强一致性。
在实际工程实践中,最终一致性存在以下五类主要变种:
- 因果一致性(Causal consistency):如果进程A在更新完某个数据项后通知了进程B,那么进程B之后对数据项的访问都应该能够或得到进程A更新后的最新值,并且如果进程B要对改数据项进行更新操作的话,务必基于进程A更新后的最新值,即不能发生丢失更新情况。与此同时,与进程A无因果关系的进程C的数据访问则没有这样的显示。
- 读己之所写(Read your write):指进程A更新一个数据项之后,它自己总是能够访问到更新过的最新值,而不会看到旧值。也就是说,对于单个数据获取者来说,其读取到的数据,一定不会比自己上次写入的值旧。因此读己之所写,也是一种特殊的因果一致性。
- 会话一致性(Session consistency):会话一致性将对系统数据的访问过程框定在了一个会话中,系统能保证在同一个有效的会话中实现“读己之所写”的一致性,也就是说,执行更新操作之后,客户端能够在一个会话中始终读取到改数据项的最新值。
- 单调读一致性(Monotonic read consistency):读一致性是指如果一个进程从系统中读取出一个数据项的某个值后,那么系统对于该进程后续的任何数据访问都不应该返回更旧的值。
- 单调写一致性(Monotonic write consistency):单调写一致性:一个系统需要能够保证来自同一个进程的写操作被顺利的执行。
总的来说,BASE理论面向的是大型高可用可扩展的分布式系统,和传统事务的ACID特性是相反的,它完全不同于ACID 的强一致性模型,而是提出通过牺牲强一致性来获得可用性,并允许数据在同一段时间内是不一致的,但最终达到一致的状态。但同时,在实际的分布式场景中,不同业务单元和组件对数据一致性的要求是不同的,因此在具体的分布式系统架构设计过程中,ACID特性与BASE理论往往又会结合在一起使用。
Paxos 算法
兰伯特当时提出的 Paxos 算法主要包含 2 个部分:
- Basic Paxos 算法 : 描述的是多节点之间如何就某个值(提案 Value)达成共识。
- Multi-Paxos 思想 : 描述的是执行多个 Basic Paxos 实例,就一系列值达成共识。Multi-Paxos 说白了就是执行多次 Basic Paxos ,核心还是 Basic Paxos 。
由于 Paxos 算法在国际上被公认的非常难以理解和实现,因此不断有人尝试简化这一算法。到了2013 年才诞生了一个比 Paxos 算法更易理解和实现的共识算法—Raft 算法open in new window 。更具体点来说,Raft 是Multi-Paxos的一个变种,其简化了 Multi-Paxos 的思想,变得更容易被理解以及工程实现。
针对没有恶意节点的情况,除了 Raft 算法之外,当前最常用的一些共识算法比如 ZAB 协议、 Fast Paxos 算法都是基于 Paxos 算法改进的。
针对存在恶意节点的情况,一般使用的是工作量证明(POW,Proof-of-Work)、权益证明(PoS,Proof-of-Stake )等共识算法。这类共识算法最典型的应用就是区块链,就比如说前段时间以太坊官方宣布其共识机制正在从工作量证明(PoW)转变为权益证明(PoS)。
Basic Paxos 算法
Basic Paxos 中存在 3 个重要的角色:
- 提议者(Proposer):也可以叫做协调者(coordinator),提议者负责接受客户端发起的提议,然后尝试让接受者接受该提议,同时保证即使多个提议者的提议之间产生了冲突,那么算法都能进行下去;
- 接受者(Acceptor):也可以叫做投票员(voter),负责对提议者的提议投票,同时需要记住自己的投票历史;
- 学习者(Learner):如果有超过半数接受者就某个提议达成了共识,那么学习者就需要接受这个提议,并就该提议作出运算,然后将运算结果返回给客户端。
Multi Paxos 思想
因为兰伯特提到的 Multi-Paxos 思想,缺少代码实现的必要细节(比如怎么选举领导者),所以在理解上比较难。
Multi-Paxos 只是一种思想,这种思想的核心就是通过多个 Basic Paxos 实例就一系列值达成共识。
二阶段提交是达成共识常用的方式,Basic Paxos 就是通过二阶段提交的方式来达成共识。Basic Paxos 还支持容错,少于一般的节点出现故障时,集群也能正常工作。
Raft算法
当今的数据中心和应用程序在高度动态的环境中运行,为了应对高度动态的环境,它们通过额外的服务器进行横向扩展,并且根据需求进行扩展和收缩。同时,服务器和网络故障也很常见。因此,系统必须在正常操作期间处理服务器的上下线。它们必须对变故做出反应并在几秒钟内自动适应;对客户来说的话,明显的中断通常是不可接受的。
Raft是工程上使用较为广泛的强一致性、去中心化、高可用的分布式协议。Raft算法的头号目标就是容易理解(UnderStandable),这从论文的标题就可以看出来。当然,Raft增强了可理解性,在性能、可靠性、可用性方面是不输于Paxos的。
为了达到易于理解的目标,raft做了很多努力,其中最主要是两件事情:
- 问题分解
- 状态简化
raft会先选举出leader,leader完全负责replicated log的管理。leader负责接受所有客户端更新请求,然后复制到follower节点,并在“安全”的时候执行这些请求。如果leader故障,followes会重新选举出新的leader。
而这就涉及到了raft算法的两个核心问题:leader election 和 log replication
基础
节点类型
一个 Raft 集群包括若干服务器,以典型的 5 服务器集群举例。在任意的时间,每个服务器一定会处于以下三个状态中的一个:
Leader
:负责发起心跳,响应客户端,创建日志,同步日志。Candidate
:Leader 选举过程中的临时角色,由 Follower 转化而来,发起投票参与竞选。Follower
:接受 Leader 的心跳和日志同步数据,投票给 Candidate。
在正常的情况下,只有一个服务器是 Leader,剩下的服务器是 Follower。Follower 是被动的,它们不会发送任何请求,只是响应来自 Leader 和 Candidate 的请求。
可以看出所有节点启动时都是follower状态;在一段时间内如果没有收到来自leader的心跳,从follower切换到candidate,发起选举;如果收到majority的造成票(含自己的一票)则切换到leader状态;如果发现其他节点比自己更新,则主动切换到follower。
总之,系统中最多只有一个leader,如果在一段时间里发现没有leader,则大家通过选举-投票选出leader。leader会不停的给follower发心跳消息,表明自己的存活状态。如果leader故障,那么follower会转换成candidate,重新选出leader。
任期
raft 算法将时间划分为任意长度的任期(term),任期用连续的数字表示,看作当前 term 号。每一个任期的开始都是一次选举,在选举开始时,一个或多个 Candidate 会尝试成为 Leader。如果一个 Candidate 赢得了选举,它就会在该任期内担任 Leader。如果没有选出 Leader,将会开启另一个任期,并立刻开始下一次选举。raft 算法保证在给定的一个任期最少要有一个 Leader。
每个节点都会存储当前的 term 号,当服务器之间进行通信时会交换当前的 term 号;如果有服务器发现自己的 term 号比其他人小,那么他会更新到较大的 term 值。如果一个 Candidate 或者 Leader 发现自己的 term 过期了,他会立即退回成 Follower。如果一台服务器收到的请求的 term 号是过期的,那么它会拒绝此次请求。
日志
entry
:每一个事件成为 entry,只有 Leader 可以创建 entry。entry 的内容为<term,index,cmd>
其中 cmd 是可以应用到状态机的操作。log
:由 entry 构成的数组,每一个 entry 都有一个表明自己在 log 中的 index。只有 Leader 才可以改变其他节点的 log。entry 总是先被 Leader 添加到自己的 log 数组中,然后再发起共识请求,获得同意后才会被 Leader 提交给状态机。Follower 只能从 Leader 获取新日志和当前的 commitIndex,然后把对应的 entry 应用到自己的状态机中。
Leader选举
raft 使用心跳机制来触发 Leader 的选举。
如果一台服务器能够收到来自 Leader 或者 Candidate 的有效信息,那么它会一直保持为 Follower 状态,并且刷新自己的 electionElapsed,重新计时。
Leader 会向所有的 Follower 周期性发送心跳来保证自己的 Leader 地位。如果一个 Follower 在一个周期内没有收到心跳信息,就叫做选举超时,然后它就会认为此时没有可用的 Leader,并且开始进行一次选举以选出一个新的 Leader。
为了开始新的选举,Follower 会自增自己的 term 号并且转换状态为 Candidate。然后他会向所有节点发起 RequestVoteRPC 请求, Candidate
的状态会持续到以下情况发生:
- 赢得选举
- 其他节点赢得选举
- 一轮选举结束,无人胜出
赢得选举的条件是:一个 Candidate 在一个任期内收到了来自集群内的多数选票(N/2+1)
,就可以成为 Leader。
在 Candidate 等待选票的时候,它可能收到其他节点声明自己是 Leader 的心跳,此时有两种情况:
- 该 Leader 的 term 号大于等于自己的 term 号,说明对方已经成为 Leader,则自己回退为 Follower。
- 该 Leader 的 term 号小于自己的 term 号,那么会拒绝该请求并让该节点更新 term。
由于可能同一时刻出现多个 Candidate,导致没有 Candidate 获得大多数选票,如果没有其他手段来重新分配选票的话,那么可能会无限重复下去。
raft 使用了随机的选举超时时间来避免上述情况。每一个 Candidate 在发起选举后,都会随机化一个新的枚举超时时间,这种机制使得各个服务器能够分散开来,在大多数情况下只有一个服务器会率先超时;它会在其他服务器超时之前赢得选举。
日志复制
复制状态机
共识算法的实现一般是基于复制状态机(Replicated state machines),何为复制状态机:
If two identical, deterministic processes begin in the same state and get the same inputs in the same order, they will produce the same output and end in the same state.
简单来说:相同的初识状态 + 相同的输入 = 相同的结束状态。引文中有一个很重要的词deterministic
,就是说不同节点要以相同且确定性的函数来处理输入,而不要引入一下不确定的值,比如本地时间等。如何保证所有节点 get the same inputs in the same order
,使用replicated log是一个很不错的注意,log具有持久化、保序的特点,是大多数分布式系统的基石。
请求完整流程
一旦选出了 Leader,它就开始接受客户端的请求。每一个客户端的请求都包含一条需要被复制状态机(
Replicated State Mechine
)执行的命令。Leader 收到客户端请求后,会生成一个 entry,包含
<index,term,cmd>
,再将这个 entry 添加到自己的日志末尾后,向所有的节点广播该 entry,要求其他服务器复制这条 entry。如果 Follower 接受该 entry,则会将 entry 添加到自己的日志后面,同时返回给 Leader 同意。
如果 Leader 收到了多数的成功响应,Leader 会将这个 entry 应用到自己的状态机中,之后这个 entry 是 committed 的,并且向客户端返回执行结果。
Leader 通知 Follower 可以应用这个日志
在上面的流程中,leader只需要日志被复制到大多数节点即可向客户端返回,一旦向客户端返回成功消息,那么系统就必须保证log(其实是log所包含的command)在任何异常的情况下都不会发生回滚。这里有两个词:commit(committed),apply(applied),前者是指日志被复制到了大多数节点后日志的状态;而后者则是节点将日志应用到状态机,真正影响到节点状态。
Leader决定何时可以安全地将日志条目应用于状态机;这样的条目称为提交。Raft保证提交的条目是持久的,并且最终将由所有可用状态机执行。一旦创建日志条目的领导将其复制到大多数服务器上,就会提交日志条目
安全性
衡量一个分布式算法,有许多属性,如
- safety:nothing bad happens,
- liveness: something good eventually happens.
在任何系统模型下,都需要满足safety属性,即在任何情况下,系统都不能出现不可逆的错误,也不能向客户端返回错误的内容。比如,raft保证被复制到大多数节点的日志不会被回滚,那么就是safety属性。而raft最终会让所有节点状态一致,这属于liveness属性。
raft协议会保证以下属性
Election safety
选举安全性,即任一任期内最多一个leader被选出。这一点非常重要,在一个复制集中任何时刻只能有一个leader。系统中同时有多余一个leader,被称之为脑裂(brain split),这是非常严重的问题,会导致数据的覆盖丢失。
选举的约束是:
每个 Candidate 发送 RequestVoteRPC 时,都会带上最后一个 entry 的信息。所有节点收到投票信息时,会对该 entry 进行比较,如果发现自己的更新,则拒绝投票给该 Candidate。
判断日志新旧的方式:如果两个日志的 term 不同,term 大的更新;如果 term 相同,更长的 index 更新。
Leader Append-Only
Leader 需要保证自己存储全部已经提交的日志条目。这样才可以使日志条目只有一个流向:从 Leader 流向 Follower,Leader 永远不会覆盖已经存在的日志条目。
log matching
- 在两个日志里,有两个 entry 拥有相同的 index 和 term,那么它们一定有相同的 cmd
- 在两个日志里,有两个 entry 拥有相同的 index 和 term,那么它们前面的 entry 也一定相同
通过“仅有 Leader 可以生成entry”来保证第一个性质,第二个性质需要一致性检查来进行保证。
一般情况下,Leader 和 Follower 的日志保持一致,然后,Leader 的崩溃会导致日志不一样,这样一致性检查会产生失败。Leader 通过强制 Follower 复制自己的日志来处理日志的不一致。这就意味着,在 Follower 上的冲突日志会被领导者的日志覆盖。
为了使得 Follower 的日志和自己的日志一致,Leader 需要找到 Follower 与它日志一致的地方,然后删除 Follower 在该位置之后的日志,接着把这之后的日志发送给 Follower。
Leader
给每一个Follower
维护了一个 nextIndex
,它表示 Leader
将要发送给该追随者的下一条日志条目的索引。当一个 Leader
开始掌权时,它会将 nextIndex
初始化为它的最新的日志条目索引数+1。如果一个 Follower
的日志和 Leader
的不一致,AppendEntries
一致性检查会在下一次 AppendEntries RPC
时返回失败。在失败之后,Leader
会将 nextIndex
递减然后重试 AppendEntries RPC
。最终 nextIndex
会达到一个 Leader
和 Follower
日志一致的地方。这时,AppendEntries
会返回成功,Follower
中冲突的日志条目都被移除了,并且添加所缺少的上了 Leader
的日志条目。一旦 AppendEntries
返回成功,Follower
和 Leader
的日志就一致了,这样的状态会保持到该任期结束。
leader completeness
如果一个log entry在某个任期被提交(committed),那么这条日志一定会出现在所有更高term的leader的日志里面。这个跟leader election、log replication都有关。
节点崩溃
如果 Leader 崩溃,集群中的节点在 electionTimeout 时间内没有收到 Leader 的心跳信息就会触发新一轮的选主,在选主期间整个集群对外是不可用的。
如果 Follower 和 Candidate 崩溃,处理方式会简单很多。之后发送给它的 RequestVoteRPC 和 AppendEntriesRPC 会失败。由于 raft 的所有请求都是幂等的,所以失败的话会无限的重试。如果崩溃恢复后,就可以收到新的请求,然后选择追加或者拒绝 entry。
时间与可用性
raft 的要求之一就是安全性不依赖于时间:系统不能仅仅因为一些事件发生的比预想的快一些或者慢一些就产生错误。为了保证上述要求,最好能满足以下的时间条件:
1 | broadcastTime << electionTimeout << MTBF |
broadcastTime
:向其他节点并发发送消息的平均响应时间;electionTimeout
:选举超时时间;MTBF(mean time between failures)
:单台机器的平均健康时间;
broadcastTime
应该比electionTimeout
小一个数量级,为的是使Leader
能够持续发送心跳信息(heartbeat)来阻止Follower
开始选举;
electionTimeout
也要比MTBF
小几个数量级,为的是使得系统稳定运行。当Leader
崩溃时,大约会在整个electionTimeout
的时间内不可用;我们希望这种情况仅占全部时间的很小一部分。
由于broadcastTime
和MTBF
是由系统决定的属性,因此需要决定electionTimeout
的时间。
一般来说,broadcastTime 一般为 0.5~20ms
,electionTimeout
可以设置为 10~500ms,MTBF 一般为一两个月
总结
raft将共识问题分解成两个相对独立的问题,leader election,log replication。流程是先选举出leader,然后leader负责复制、提交log(log中包含command)
为了在任何异常情况下系统不出错,即满足safety属性,对leader election,log replication两个子问题有诸多约束
leader election约束:
- 同一任期内最多只能投一票,先来先得
- 选举人必须比自己知道的更多(比较term,log index)
log replication约束:
- 一个log被复制到大多数节点,就是committed,保证不会回滚
- leader一定包含最新的committed log,因此leader只会追加日志,不会删除覆盖日志
- 不同节点,某个位置上日志相同,那么这个位置之前的所有日志一定是相同的
- Raft never commits log entries from previous terms by counting replicas.