分布式系统是一个硬件或软件组件分布在不同的网络计算机上,彼此之间仅仅通过消息传递进行通信和协调的系统。
通俗的理解,所谓分布式系统,就是一个业务拆分成多个子业务,分布在不同的服务器节点,共同构成的系统称为分布式系统,同一个分布式系统中的服务器节点在空间部署上是可以随意分布的,这些服务器可能放在不同的机柜中,也可能在不同的机房中,甚至分布在不同的城市。
Tip:
分布式与集群的区别:
分布式系统的特点:
(1)分布性
(2)对等性
(3)并发性
(4)缺乏全局时钟
(5)故障总是会发生
阿里巴巴发起的"去 IOE"运动 (IOE 指的是 IBM 小型机、Oracle 数据库、EMC 的高端存储)。阿里巴巴2009 年“去IOE”战略技术总监透露,截止到 2013 年 5 月 17 日阿里巴巴最后一台 IBM 小型机在支付宝下线。
为什么要去IOE
分布式数据一致性,指的是数据在多份副本中存储时,各副本中的数据是一致的。
分布式系统当中,数据往往会有多个副本。如果是一台数据库处理所有的数据请求,那么通过ACID四原则,基本可以保证数据的一致性。而多个副本就需要保证数据会有多份拷贝。这就带来了同步的问题,因为我们几乎没有办法保证可以同时更新所有机器当中的包括备份所有数据。 网络延迟,即使我在同一时间给所有机器发送了更新数据的请求,也不能保证这些请求被响应的时间保持一致存在时间差,就会存在某些机器之间的数据不一致的情况。
总得来说,我们无法找到一种能够满足分布式系统所有系统属性的分布式一致性解决方案。因此,如何既保证数据的一致性,同时又不影响系统运行的性能,是每一个分布式系统都需要重点考虑和权衡的。于是,一致性级别由此诞生:
CAP 定理
CAP 理论含义是,一个分布式系统不可能同时满足一致性(C:Consistency)
,可用性(A: Availability)
和分区容错性(P:Partition tolerance)
这三个基本需求,最多只能同时满足其中的2个。
1、C - Consistency
一致性是指写操作后读操作可以读到最新的数据状态,当数据分布在多个节点上时,从任意节点读取到的数据都是最新的。
商品信息读写要满足一致性需要实现如下目标:
如何实现一致性?
分布式一致性的特点:
2、A - Availability
可用性是指任何操作都可以得到响应的结果,且不会出现响应超时或响应错误。
商品信息读写要满足可用性需要实现如下目标:
如何实现可用性?
3、P - Partition tolerance
分布式系统的各个节点部署在不同的子网中, 不可避免的会出现由于网络问题导致节点之间通信失败,此时仍可以对外提供服务,这个就是分区容错性 (分区容忍性).
商品信息读写要满足分区容错性需要实现如下目标:
如何实现分区容错性?
假设有一个系统如下:
有用户向N1发送了请求更改了数据,将数据库从V0更新成了V1。由于网络断开,所以N2数据库依然是V0,如果这个时候有一个请求发给了N2,但是N2并没有办法可以直接给出最新的结果V1,这个时候该怎么办呢?
这个时候无法两种方法,一种是将错就错,将错误的V0数据返回给用户。第二种是阻塞等待,等待网络通信恢复,N2中的数据更新之后再返回给用户。显然前者牺牲了一致性,后者牺牲了可用性。
这个例子虽然简单,但是说明的内容却很重要。在分布式系统当中,CAP三个特性我们是无法同时满足的,必然要舍弃一个。三者舍弃一个,显然排列组合一共有三种可能。
1、舍弃A(可用性),保留CP(一致性和分区容错性)
2、舍弃C(一致性),保留AP(可用性和分区容错性)
3、舍弃P(分区容错性),保留CA(一致性和可用性)
什么是BASE理论?
BASE:全称:Basically Available(基本可用),So state(软状态),和Eventually consistent(最终一致性)三个短语的缩写,来自 ebay 的架构师提出。
BASE是对CAP中一致性和可用性权衡的结果,BASE理论的核心思想是:即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。
1、Basically Available(基本可用)
基本可用是指分布式系统在出现不可预知故障的时候,允许损失部分可用性——但请注意,这绝不等价于系统不可用。以下就是两个"基本可用"的例子
2、So state(软状态)
3、Eventually consistent(最终一致性)
最终一致性强调的是系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。因此最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。
事务的基本特性:
我们知道事务有4个非常重要的特性,即我们常说的(ACID)。
其实分布式事务从实质上看与数据库事务的概念是一致的,既然是事务也就需要满足事务的基本特性(ACID),只是分布式事务相对于本地事务而言其表现形式有很大的不同。
2PC ( Two-Phase Commit缩写)即两阶段提交协议,是将整个事务流程分为两个阶段,准备阶段(Prepare phase)
、提交阶段(commit phase)
,2是指两个阶段,P是指准备阶段,C是指提交阶段。
在计算机中部分关系数据库如Oracle、MySQL支持两阶段提交协议.
两个阶段过程:
协议说明:
顾名思义,二阶段提交就是将事务的提交过程分成了两个阶段来进行处理。流程如下:
(1)成功执行事务事务提交流程
Tip: 什么是Ack
(2)中断事务步骤如下:
假如任何一个参与者向协调者反馈了No响应,或者在等待超时之后,协调者尚无法接收到所有参与者的反馈响应,那么就会中断事务。
优点: 原理简单,实现方便
缺点: 同步阻塞,单点问题,数据不一致,过于保守
3PC,全称 “three phase commit”,是 2PC 的改进版,将 2PC 的 “提交事务请求” 过程一分为二,共形成了由CanCommit、PreCommit和doCommit三个阶段组成的事务处理协议。
第一个阶段: CanCommit
1、事务询问
协调者向所有的参与者发送一个包含事务内容的canCommit请求,询问是否可以执行事务提交操作,并开始等待各参与者的响应。
2、各参与者向协调者反馈事务询问的响应
参与者在接收到来自协调者的包含了事务内容的canCommit请求后,正常情况下,如果自身认为可以顺利执行事务,则反馈Yes响应,并进入预备状态,否则反馈No响应。
协调者在得到所有参与者的响应之后,会根据结果有2种执行操作的情况:执行事务预提交,或者中断事务,假如所有参与反馈的都是Yes,那么就会执行事务预提交。
1、执行事务预提交分为 3 个步骤
若任一参与者反馈了No响应,或者在等待超时后,协调者尚无法接收到所有参与者反馈,则中断事务
2、中断事务也分为2个步骤:
该阶段做真正的事务提交或者完成事务回滚,所以就会出现两种情况:
1、执行事务提交
2、中断事务
注意:一旦进入阶段三,可能会出现 2 种故障:
如果出现了任一一种情况,最终都会导致参与者无法收到 doCommit 请求或者 abort 请求,针对这种情况,参与者都会在等待超时之后,继续进行事务提交。
问题:3PC协议并没有完全解决数据不一致问题。
Paxos由Lamport于1998年在《The Part-Time Parliament》论文中首次公开,最初的描述使用希腊的一个小岛Paxos作为比喻,描述了Paxos小岛中通过决议的流程,并以此命名这个算法,但是这个描述理解起来比较有挑战性。后来在2001年,Lamport觉得同行不能理解他的幽默感,于是重新发表了朴实的算法描述版本《Paxos Made Simple》
自Paxos问世以来就持续垄断了分布式一致性算法,Paxos这个名词几乎等同于分布式一致性。Google的很多大型分布式系统都采用了Paxos算法来解决分布式一致性问题,如Chubby、Megastore以及Spanner等。开源的ZooKeeper,以及MySQL 5.7推出的用来取代传统的主从复制的MySQL Group Replication等纷纷采用Paxos算法解决分布式一致性问题。
然而,Paxos的最大特点就是难,不仅难以理解,更难以实现。
答:解决了分布式系统一致性问题。
我们假设一种情况,在一个集群环境中,要求所有机器上的状态是一致的,其中有2台机器想修改某个状态,机器A 想把状态改为 A,机器 B 想把状态改为 B,那么到底听谁的呢?
有的人会想到,可以像 2PC,3PC 一样引入一个协调者,谁先到,听谁的
但是如果,协调者宕机了呢?所以需要对协调者也做备份,也要做集群。这时候,问题来了,这么多协调者,听谁的呢?
Paxos 算法就是为了解决这个问题而生的
首先一个很重要的概念叫提案(Proposal)。最终要达成一致的value就在提案里。
提案 (Proposal):Proposal信息包括提案编号 (Proposal ID) 和提议的值 (Value)
在Paxos算法中,有如下角色:
假设有一组可以提出提案的进程集合,那么对于一个一致性算法需要保证以下几点:
假设只有一个Acceptor(可以有多个Proposer),只要Acceptor接受它收到的第一个提案,则该提案被选定,该提案里的value就是被选定的value。这样就保证只有一个value会被选定。
但是,如果这个唯一的Acceptor宕机了,那么整个系统就无法工作了!
多个Acceptor的情况如下图。那么,如何保证在多个Proposer和多个Acceptor的情况下选定一个value呢?
下面开始寻找解决方案。
首先我们希望即使只有一个Proposer提出了一个value,该value也最终被选定。
那么,就得到下面的约束:
但是,这又会引出另一个问题:如果每个Proposer分别提出不同的value,发给不同的Acceptor。根据P1,Acceptor分别接受自己收到的第一个提案,就导致不同的value被选定。出现了不一致。如下图:
刚刚是因为『一个提案只要被一个Acceptor接受,则该提案的value就被选定了』才导致了出现上面不一致的问题。
因此,我们需要加一个规定:
这个规定又暗示了:『一个Acceptor必须能够接受不止一个提案!』不然可能导致最终没有value被选定。比如上图的情况。v1、v2、v3都没有被选定,因为它们都只被一个Acceptor的接受。
所以在这种情况下,我们使用一个全局的编号来标识每一个Acceptor批准的提案,当一个具有某value值的提案被半数以上的Acceptor批准后,我们就认为该value被选定了。
根据上面的内容,我们现在虽然允许多个提案被选定,但必须保证所有被选定的提案都具有相同的value值。否则又会出现不一致。
于是有了下面的约束:
一个提案只有被Acceptor接受才可能被选定,因此我们可以把P2约束改写成对Acceptor接受的提案的约束P2a。
只要满足了P2a,就能满足P2。
但是,考虑如下的情况:假设总的有5个Acceptor。Proposer2提出[M1,V1]的提案,Acceptor 2-5(半数以上)均接受了该提案,于是对于Acceptor2~5和Proposer2来讲,它们都认为V1被选定。Acceptor1刚刚从宕机状态恢复过来(之前Acceptor1没有收到过任何提案),此时Proposer1向Acceptor1发送了[M2,V2]的提案(V2≠V1且M2>M1),对于Acceptor1来讲,这是它收到的第一个提案。根据P1(一个Acceptor必须接受它收到的第一个提案。),Acceptor1必须接受该提案!同时Acceptor1认为V2被选定。这就出现了两个问题:
所以我们要对P2a约束进行强化!
P2a是对Acceptor接受的提案约束,但其实提案是Proposer提出来的,所有我们可以对Proposer提出的提案进行约束。得到P2b:
由P2b可以推出P2a进而推出P2。
那么,如何确保在某个value为v的提案被选定后,Proposer提出的编号更高的提案的value都是v呢?
只要满足P2c即可:
从上面的内容,可以看出,从P1到P2c的过程其实是对一系列条件的逐步增强,如果需要证明这些条件可以保证一致性,那么就可以进行反向推导:P2c =>P2b=>P2a=>P2,然后通过P2和P1来保证一致性。
情况一:多数派还没有接收过任何提案
提案者向acceptor发送提案,这个过程分为两步,第一步称为prepare准备阶段,第二步称为accept请求阶段。
最终结果如下图:
情况二:可能已经有半数以上的acceptor接收过提案【N, V】
接下来来学习,在P2c的基础上如何进行提案的生成
这里有个比较重要的思想:Proposer生成提案之前,应该先去『学习』已经被选定或者可能被选定的value,然后以该value作为自己提出的提案的value。如果没有value被选定,Proposer才可以自己决定value的值。这样才能达成一致。这个学习的阶段是通过一个『Prepare请求』实现的。
于是我们得到了如下的提案生成算法:
刚刚讲解了Paxos算法中Proposer的处理逻辑,怎么去生成的提案,下面来看看Acceptor是如何批准提案的。
根据刚刚的介绍,一个Acceptor可能会受到来自Proposer的两种请求,分别是Prepare请求和Accept请求,对这两类请求作出响应的条件分别如下
上面的内容中,分别从Proposer和Acceptor对提案的生成和批准两方面来讲解了Paxos算法在提案选定过程中的算法细节,同时也在提案的编号全局唯一的前提下,获得了一个提案选定算法,接下来我们再对这个初步算法做一个小优化,尽可能的忽略Prepare请求。
通过这个优化,每个Acceptor只需要记住它已经批准的提案的最大编号以及它已经做出Prepare请求响应的提案的最大编号,以便出现故障或节点重启的情况下,也能保证P2c的不变性,而对于Proposer来说,只要它可以保证不会产生具有相同编号的提案,那么就可以丢弃任意的提案以及它所有的运行时状态信息。
综合前面的讲解,我们来对Paxos算法的提案选定过程进行下总结,那结合Proposer和Acceptor对提案的处理逻辑,就可以得到类似于两阶段提交的算法执行过程。
阶段二:
当然,实际运行过程中,每一个Proposer都有可能产生多个提案,但只要每个Proposer都遵循如上所述的算法运行,就一定能够保证算法执行的正确性。
刚刚我们介绍了如何来选定一个提案,下面我们再来看看Learner学习被选定的value,如下图:
根据前面的内容讲解,我们已经基本上了解了Paxos算法的核心逻辑,那接下来再来看看Paxos算法在实际过程中的一些细节。
活性:最终一定会发生的事情:最终一定要选定value
假设存在这样一种极端情况,有两个Proposer依次提出了一系列编号递增的提案,导致最终陷入死循环,没有value被选定,具体流程如下:
解决:通过选取主Proposer,并规定只有主Proposer才能提出议案。这样一来只要主Proposer和过半的Acceptor能够正常进行网络通信,那么但凡主Proposer提出一个编号更高的提案,该提案终将会被批准,这样通过选择一个主Proposer,整套Paxos算法就能够保持活性。
首先说什么是 Raft 算法:Raft 是一种为了管理复制日志的一致性算法。
Raft 提供了和 Paxos 算法相同的功能和性能,但是它的算法结构和Paxos不同。Raft 算法更加容易理解并且更容易构建实际的系统。
Raft 将一致性算法分解成了3模块
Raft 算法分为两个阶段,首先是选举过程,然后在选举出来的领导人带领进行正常操作,比如日志复制等。
Raft 通过选举一个领导人,然后给予他全部的管理复制日志的责任来实现一致性。
在Raft中,任何时候一个服务器都可以扮演下面的角色之一:
而影响他们身份变化的则是选举。
Raft使用心跳机制来触发选举。当server启动时,初始状态都是follower。每一个server都有一个定时器,超时时间为election timeout(一般为150-300ms),如果某server没有超时的情况下收到来自领导者或者候选者的任何消息,定时器重启,如果超时,它就开始一次选举。
下面用图示展示这个过程:
初始状态下集群中的所有节点都处于 follower 状态。
某一时刻,其中的一个 follower 由于没有收到 leader 的 heartbeat 率先发生 election timeout 进而发起选举,进而升级为Candidate状态。
只要集群中超过半数的节点接受投票,candidate 节点将成为即切换 leader 状态。
成为 leader 节点之后,leader 将定时向 follower 节点同步日志并发送 heartbeat。
集群中各个节点的状态随时都有可能发生变化。从实际的变化上来分类的话,节点的异常大致可以分为四种类型:
下面将说明当集群中的 leader 节点不可用时,raft 集群是如何应对的。
➢ 一般情况下,leader 节点定时发送 heartbeat 到 follower 节点。
➢ 由于某些异常导致 leader 不再发送 heartbeat ,或 follower 无法收到 heartbeat 。
➢ 当某一 follower 发生 election timeout 时,其状态变更为 candidate,并向其他 follower 发起投票。
➢ 当超过半数的 follower 接受投票后,这一节点将成为新的 leader,leader 的步进数加 1 并开始向 follower 同步日志。
➢ 当一段时间之后,如果之前的 leader 再次加入集群,则两个 leader 比较彼此的步进数,步进数低的 leader 将切换自己的状态为 follower。
➢ 较早前 leader 中不一致的日志将被清除,并与现有 leader 中的日志保持一致。
follower 节点不可用的情况相对容易解决。因为集群中的日志内容始终是从 leader 节点同步的,只要这一节点再次加入集群时重新从 leader 节点处复制日志即可。
➢ 集群中的某个 follower 节点发生异常,不再同步日志以及接收 heartbeat。
➢ 经过一段时间之后,原来的 follower 节点重新加入集群。
➢ 这一节点的日志将从当时的 leader 处同步。
在集群中出现多个 candidate 或多个 leader 通常是由于数据传输不畅造成的。出现多个 leader 的情况相对少见,但多个 candidate 比较容易出现在集群节点启动初期尚未选出 leader 的“混沌”时期。
➢ 初始状态下集群中的所有节点都处于 follower 状态。
➢ 两个节点同时成为 candidate 发起选举。
➢ 两个 candidate 都只得到了少部分 follower 的接受投票。
➢ candidate 继续向其他的 follower 询问。
➢ 由于一些 follower 已经投过票了,所以均返回拒绝接受。
➢ candidate 也可能向一个 candidate 询问投票。
➢ 在步进数相同的情况下,candidate 将拒绝接受另一个 candidate 的请求。
➢ 由于第一次未选出 leader,candidate 将随机选择一个等待间隔(150ms ~ 300ms)再次发起投票。
➢ 如果得到集群中半数以上的 follower 的接受,这一 candidate 将成为 leader。
➢ 稍后另一个 candidate 也将再次发起投票。
➢ 由于集群中已经选出 leader,candidate 将收到拒绝接受的投票。
➢ 在被多数节点拒绝之后,并已知集群中已存在 leader 后,这一 candidate 节点将终止投票请求、切换为follower,从 leader 节点同步日志。
日志复制的过程
Leader选出后,就开始接收客户端的请求。Leader把请求作为日志条目(Log entries)加入到它的日志中,然后并行的向其他服务器发起 AppendEntries RPC复制日志条目。当这条日志被复制到大多数服务器上,Leader将这条日志应用到它的状态机并向客户端返回执行结果。
下图表示了当一个客户端发送一个请求给领导者,随后领导者复制给跟随者的整个过程。
4 个步骤:
可以看到,直到第四步骤,整个事务才会达成。中间任何一个步骤发生故障,都不会影响日志一致性。
分布式系统本质是通过低廉的硬件攒在一起以获得更好的吞吐量、性能以及可用性等。
在分布式环境下,有几个问题是普遍关心的,我们称之为设计策略:
在分布式环境中,我们提及过存在非常多的节点(Node),其实质是这些节点分担任务的运行、计算或者程序逻辑处理。那么就有一个非常重要的问题,如何检测一个节点出现了故障乃至无法工作了?
通常解决这一问题是采用心跳检测的手段,如同通过仪器对病人进行一些检测诊断一样。
心跳顾名思义,就是以固定的频率向其他节点汇报当前节点状态的方式。收到心跳,一般可以认为一个节点和现在的网络拓扑是良好的。当然,心跳汇报时,一般也会携带一些附加的状态、元数据信息,以便管理。
如图所示,Client请求Server,Server转发请求到具体的Node获取请求结果。Server需要与三个Node节点保持心跳连接,确保Node可以正常工作。
若Server没有收到Node3的心跳时,Server认为Node3失联。但是失联是失去联系,并不确定是否是Node3故障,有可能是Node3处于繁忙状态,导致调用检测超时;也有可能是Server与Node3之间链路出现故障或闪断。所以心跳不是万能的,收到心跳可以确认节点正常,但是收不到心跳也不能认为该节点就已经宣告“死亡”。此时,可以通过一些方法帮助Server做决定: 周期检测心跳机制、累计失效检测机制。
通过周期检测心跳机制、累计失效检测机制可以帮助判断节点是否“死亡”,如果判断“死亡”,可以把该节点踢出集群。
高可用(High Availability)是系统架构设计中必须考虑的因素之一,通常是指,经过设计来减少系统不能提供服务的时间。
系统高可用性的常用设计模式包括三种:主备(Master-SLave)、互备(Active-Active)和集群(Cluster)模式。
容错顾名思义就是IT系统对于错误包容的能力
容错的处理是保障分布式环境下相应系统的高可用或者健壮性,一个典型的案例就是对于缓存穿透 问题的解决方案。
我们在项目中使用缓存通常都是先检查缓存中是否存在,如果存在直接返回缓存内容,如果不存在就直接查询数据库然后再缓存查询结果返回。这个时候如果我们查询的某一个数据在缓存中一直不存在,就会造成每一次请求都查询DB,这样缓存就失去了意义,在流量大时,或者有人恶意攻击如频繁发起为id为“-1”的条件进行查询,可能DB就挂掉了。
那这种问题有什么好办法解决呢?
一个比较巧妙的方法是,可以将这个不存在的key预先设定一个值。比如,key=“null”。在返回这个null值的时候,我们的应用就可以认为这是不存在的key,那我们的应用就可以决定是否继续等待访问,还是放弃掉这次操作。如果继续等待访问,过一个时间轮询点后,再次请求这个key,如果取到的值不再是null,则可以认为这时候key有值了,从而避免了透传到数据库,把大量的类似请求挡在了缓存之中。
负载均衡:其关键在于使用多台集群服务器共同分担计算任务,把网络请求及计算分配到集群可用的不同服务器节点上,从而达到高可用性及较好的用户操作体验。
如图,不同的用户User1、User2、User3访问应用,通过负载均衡器分配到不同的节点。
负载均衡器有硬件解决方案,也有软件解决方案。硬件解决方案有著名的F5,软件有LVS、HAProxy、Nginx等。
以Nginx为例,负载均衡有以下几种策略: