什么是分布式系统?
拿一个最简单的例子,就比如说我们的图书管理系统。之前的系统包含了所有的功能,比如用户注册登录、管理员功能、图书借阅管理等。这叫做集中式系统。也就是一个人干了好几件事。
后来随着功能的增多,用户量也越来越大。集中式系统维护太麻烦,拓展性也不好。于是就考虑着把这些功能分开。通俗的理解就是原本需要一个人干的事,现在分给n个人干,各自干各自的,最终取得和一个人干的效果一样。
稍微正规一点的定义就是:一个业务分拆多个子业务,部署在不同的服务器上。 然后通过一定的通信协议,能够让这些子业务之间相互通信。
既然分给了n个人,那就涉及到这些人的沟通交流协作问题。想要去解决这些问题,就需要先聊聊分布式系统中的CAP理论。
CAP原理
CAP原理指的是一个分布式系统最多只能同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance)这三项中的两项。
这张图不知道你之前看到过没,如果你看过书或者是视频,这张图应该被列举了好几遍了。下面我不准备直接上来就对每一个特性进行概述。我们先从案例出发逐步过渡。
1、一个小例子
首先我们看一张图。
现在网络中有两个节点N1和N2,他们之间网络可以连通,N1中有一个应用程序A,和一个数据库V,N2也有一个应用程序B2和一个数据库V。现在,A和B是分布式系统的两个部分,V是分布式系统的两个子数据库。
现在问题来了。突然有两个用户小明和小华分别同时访问了N1和N2。我们理想中的操作是下面这样的。
(1)小明访问N1节点,小华访问N2节点。同时访问的。
(2)小明把N1节点的数据V0变成了V1。
(2)N1节点一看自己的数据有变化,立马执行M操作,告诉了N2节点。
(4)小华读取到的就是最新的数据。也是正确的数据。
上面这是一种最理想的情景。它满足了CAP理论的三个特性。现在我们看看如何来理解满足的这三个特性。
Consistency 一致性
一致性指的是所有节点在同一时间的数据完全一致。
对于一致性,也可以分为从客户端和服务端两个不同的视角来理解。
(1)客户端
从客户端来看,一致性主要指的是多并发访问时更新过的数据如何获取的问题。也就是小明和小华同时访问,如何获取更新的最新的数据。
(2)服务端
从服务端来看,则是更新如何分布到整个系统,以保证数据最终一致。也就是N1节点和N2节点如何通信保持数据的一致。
对于一致性,一致的程度不同大体可以分为强、弱、最终一致性三类。
(1)强一致性
对于关系型数据库,要求更新过的数据能被后续的访问都能看到,这是强一致性。比如小明更新V0到V1,那么小华读取的时候也应该是V1。
(2)弱一致性
如果能容忍后续的部分或者全部访问不到,则是弱一致性。比如小明更新VO到V1,可以容忍那么小华读取的时候是V0。
可用性
可用性指服务一直可用,而且是正常响应时间。就好比刚刚的N1和N2节点,不管什么时候访问,都可以正常的获取数据值。而不会出现问题。好的可用性主要是指系统能够很好的为用户服务,不出现用户操作失败或者访问超时等用户体验不好的情况。
对于可用性来说就比较好理解了。
分区容错性
分区容错性指在遇到某节点或网络分区故障的时候,仍然能够对外提供满足一致性和可用性的服务。就好比是N1节点和N2节点出现故障,但是依然可以很好地对外提供服务。
这个分区容错性也是很好理解。
在经过上面的分析中,在理想情况下,没有出现任何错误的时候,这三条应该都是满足的。但是天有不测风云。系统总是会出现各种各样的问题。下面来分析一下为什么说CAP理论只能满足两条。
验证CAP理论
既然系统总是会有错误,那我们就来看看可能会出现什么错误。
N1节点更新了V0到V1,想在也想把这个消息通过M操作告诉N1节点,却发生了网络故障。这时候小明和小华都要同时访问这个数据,怎么办呢?现在我们依然想要我们的系统具有CAP三个特性,我们分析一下会发生什么。
(1)系统网络发生了故障,但是系统依然可以访问,因此具有容错性。
(2)小明在访问节点N1的时候更改了V0到V1,想要小华访问节点N2的V数据库的时候是V1,因此需要等网络故障恢复,将N2节点的数据库进行更新才可以。
(3)在网络故障恢复的这段时间内,想要系统满足可用性,是不可能的。因为可用性要求随时随地访问系统都是正确有效的。这就出现了矛盾。
正是这个矛盾所以CAP三个特性肯定不能同时满足。既然不能满足,那我们就进行取舍。
有两种选择:
(1)牺牲数据一致性,也就是小明看到的衣服数量是10,买了一件应该是9了。但是小华看到的依然是10。
(2)牺牲可用性,也就是小明看到的衣服数量是10,买了一件应该是9了。但是小华想要获取的最新的数据的话,那就一直等待阻塞,一直到网络故障恢复。
现在你可以看到了CAP三个特性肯定是不能同时满足的,但是可以满足其中两个。
CAP特性的取舍
我们分析一下既然可以满足两个,那么舍弃哪一个比较好呢?
(1)满足CA舍弃P,也就是满足一致性和可用性,舍弃容错性。但是这也就意味着你的系统不是分布式的了,因为涉及分布式的想法就是把功能分开,部署到不同的机器上。
(2)满足CP舍弃A,也就是满足一致性和容错性,舍弃可用性。如果你的系统允许有段时间的访问失效等问题,这个是可以满足的。就好比多个人并发买票,后台网络出现故障,你买的时候系统就崩溃了。
(3)满足AP舍弃C,也就是满足可用性和容错性,舍弃一致性。这也就是意味着你的系统在并发访问的时候可能会出现数据不一致的情况。
实时证明,大多数都是牺牲了一致性。像12306还有淘宝网,就好比是你买火车票,本来你看到的是还有一张票,其实在这个时刻已经被买走了,你填好了信息准备买的时候发现系统提示你没票了。这就是牺牲了一致性。
但是不是说牺牲一致性一定是最好的。就好比mysql中的事务机制,张三给李四转了100块钱,这时候必须保证张三的账户上少了100,李四的账户多了100。因此需要数据的一致性,而且什么时候转钱都可以,也需要可用性。但是可以转钱失败是可以允许的。
扩展服务的方案
数据分区: uid % 16
数据镜像:让多有的服务器都有相同的数据,提供相当的服务(冗余存储,一般3份为好)
两种方案的事务问题
A向B汇钱,两个用户不在一个服务器上
镜像:在不同的服务器上对同一数据的写操作如何保证一致性。
解决一致性事务问题的技术
Master -Slave
读写请求由Master负责
写请求写到Master后,由Master同步到Slave上
由Master push or Slave pull
通常是由Slave 周期性来pull,所以是最终一致性
问题: 若在 pull 周期内(不是期间?),master挂掉,那么会导致这个时间片内的数据丢失
若不想让数据丢掉,Slave 只能成为 ReadOnly方式等Master恢复
若容忍数据丢失,可以让 Slave代替Master工作
如何保证强一致性?
Master 写操作,写完成功后,再写 Slave,两者成功后返回成功。若 Slave失败,两种方法
标记 Slave 不可用报错,并继续服务(等恢复后,再同步Master的数据,多个Slave少了一个而已)
回滚自己并返回失败
Master-Master
数据同步一般是通过 Master 间的异步完成,所以是最终一致
好处: 一台Master挂掉,另外一台照样可以提供读写服务。当数据没有被赋值到别的Master上时,数据会丢失。
对同一数据的处理问题:Dynamo的Vector Clock的设计(记录数据的版本号和修改者),当数据发生冲突时,要开发者自己来处理
两阶段提交 Two Phase Commit (2PC)
第一阶段:针对准备工作
协调者问所有节点是否可以执行提交
参与者开始事务,执行准备工作:锁定资源(获取锁操作)
参与者响应协调者,如果事务的准备工作成功,则回应"可以提交",否则,拒绝提交
第二阶段:
若都响应可以提交,则协调者项多有参与者发送正式提交的命令(更新值),参与者完成正式提交,释放资源,回应完成。协调者收到所有节点的完成响应后结束这个全局事务.。若参与者回应拒绝提交,则协调者向所有的参与者发送回滚操作,并释放资源,当收到全部节点的回滚回应后,取消全局事务
存在的问题:若一个没提交,就会进行回滚
第一阶段:若消息的传递未接收到,则需要协调者作超时处理,要么当做失败,要么重载
第二阶段:若参与者的回应超时,要么重试,要么把那个参与者即为问题节点,提出整个集群
在第二阶段中,参与者未收到协调者的指示(也许协调者挂掉),则所有参与者会进入“不知所措” 的状态(但是已经锁定了资源),所以引入了三段提交
三段提交:把二段提交的第一阶段 break 成了两段
询问
锁定资源(获取锁)
提交
核心理念:在询问的时候并不锁定资源,除非所有人都同意了,才开始锁定
好处:当发生了失败或超时时,三段提交可以继续把状态变为Commit 状态,而二段提交则不知所措?
Raxos 算法(少数服从多数)
解决的问题:在一个可能发生异常的分布式系统中如何就某个值达成一致,让整个集群的节点对某个值的变更达成一致
任何一个节点都可以提出要修改某个数据的提案,是否通过这个提案取决于这个集群中是否有超过半数的节点同意(所以节点数总是单数)—— 版本标记。虽然一致性,但是只能对一个操作进行操作啊??
当一个Server接收到比当前版本号小的提案时,则拒绝。当收到比当前大的版本号的提案时,则锁定资源,进行修改,返回OK. 也就是说收到超过一半的最大版本的提案才算成功。
核心思想:
在抢占式访问权的基础上引入多个acceptor,也就是说当一个版本号更大的提案可以剥夺版本号已经获取的锁。
后者认同前者的原则:
在肯定旧epoch 无法生成确定性取值时,新的 epoch 会提交自己的valu
一旦 旧epoch形成确定性取值,新的 epoch肯定可以获取到此取值,并且会认同此取值,不会被破坏。
步骤
- P1 请求Acceptor的 #1,Acceptor 这时并没有其他线程获取到锁,所以把锁交给 P1,并返回这时 #1 的值为null
- P1 请求Acceptor的 #1,Acceptor 这时并没有其他线程获取到锁,所以把锁交给 P1,并返回这时 #1 的值为null
- 然后 P1 向 第一个 Acceptor 提交 #1 的值,Acceptor 接受并返回 OK
- 然后 P1 向 第一个 Acceptor 提交 #1 的值,Acceptor 接受并返回 OK
- 这个时候,P2向Acceptor请求#1上的锁,因为版本号更大,所以直接抢占了 P1 的锁。这时 Acceptor 返回了 OK并且返回了 #1 的值
- 这个时候,P2向Acceptor请求#1上的锁,因为版本号更大,所以直接抢占了 P1 的锁。这时 Acceptor 返回了 OK并且返回了 #1 的值
- 这时 P1 P向 后面两个 Acceptor 提交 #1 的值,但是由于中间的那个Acceptor 版本号已经更改为 2 了,所以拒绝P1。第三个 Acceptor 接受了,并且返回了 OK
- 这时 P1 P向 后面两个 Acceptor 提交 #1 的值,但是由于中间的那个Acceptor 版本号已经更改为 2 了,所以拒绝P1。第三个 Acceptor 接受了,并且返回了 OK
- 由于后者认同前者的原则,这时 P1 已经形成确定性取值了 V1 了,这时新的 P2 会认同此取值,而不是提交自己的取值。所以,P2会选择最新的那个取值 也就是V1 进行提交。这时Acceptor 返回 OK
- 由于后者认同前者的原则,这时 P1 已经形成确定性取值了 V1 了,这时新的 P2 会认同此取值,而不是提交自己的取值。所以,P2会选择最新的那个取值 也就是V1 进行提交。这时Acceptor 返回 OK
6.ZAB 协议
ZAB 协议 ( Zookeeper Atomic Broadcast) 原子广播协议:保证了发给各副本的消息顺序相同
定义:原子广播协议 ZAB 是一致性协议,Zookeeper 把其作为数据一致性的算法。ZAB 是在 Paxos 算法基础上进行扩展而来的。Zookeeper 使用单一主进程 Leader用于处理客户端所有事务请求,采用 ZAB 协议将服务器状态以事务形式广播到所有 Follower 上,由于事务间可能存在着依赖关系,ZAB协议保证 Leader 广播的变更序列被顺序的处理,一个状态被处理那么它所依赖的状态也已经提前被处理
核心思想:保证任意时刻只有一个节点是Leader,所有更新事务由Leader发起去更新所有副本 Follower,更新时用的是 两段提交协议,只要多数节点 prepare 成功,就通知他们commit。各个follower 要按当初 leader 让他们 prepare 的顺序来 apply 事务
协议状态
- Looking:系统刚启动时 或者 Leader 崩溃后正处于选举状态
- Following:Follower 节点所处的状态,Follower与 Leader处于数据同步状态
- Leading:Leader 所处状态,当前集群中有一个 Leader 为主进程
ZooKeeper启动时所有节点初始状态为Looking,这时集群会尝试选举出一个Leader节点,选举出的Leader节点切换为Leading状态;当节点发现集群中已经选举出Leader则该节点会切换到Following状态,然后和Leader节点保持同步;当Follower节点与Leader失去联系时Follower节点则会切换到Looking状态,开始新一轮选举;在ZooKeeper的整个生命周期中每个节点都会在Looking、Following、Leading状态间不断转换。
选举出Leader节点后 ZAB 进入原子广播阶段,这时Leader为和自己同步每个节点 Follower 创建一个操作序列,一个时期一个 Follower 只能和一个Leader保持同步
阶段
Election: 在 Looking状态中选举出 Leader节点,Leader的LastZXID总是最新的(只有lastZXID的节点才有资格成为Leade,这种情况下选举出来的Leader总有最新的事务日志)。在选举的过程中会对每个Follower节点的ZXID进行对比只有highestZXID的Follower才可能当选Leader
每个Follower都向其他节点发送选自身为Leader的Vote投票请求,等待回复;
Follower接受到的Vote如果比自身的大(ZXID更新)时则投票,并更新自身的Vote,否则拒绝投票;
每个Follower中维护着一个投票记录表,当某个节点收到过半的投票时,结束投票并把该Follower选为Leader,投票结束;
Discovery:Follower 节点向准 Leader推送 FollwerInfo,该信息包含了上一周期的epoch,接受准 Leader 的 NEWLEADER 指令
Sync:将 Follower 与 Leader的数据进行同步,由Leader发起同步指令,最终保持数据的一致性
Broadcast:Leader广播 Proposal 与 Commit,Follower 接受 Proposal 与 commit。因为一个时刻只有一个Leader节点,若是更新请求,只能由Leader节点执行(若连到的是 Follower 节点,则需转发到Leader节点执行;读请求可以从Follower 上读取,若是要最新的数据,则还是需要在 Leader上读取)
消息广播使用了TCP协议进行通讯所有保证了接受和发送事务的顺序性。广播消息时Leader节点为每个事务Proposal分配一个全局递增的ZXID(事务ID),每个事务Proposal都按照ZXID顺序来处理(Paxos 保证不了)
Leader节点为每一个Follower节点分配一个队列按事务ZXID顺序放入到队列中,且根据队列的规则FIFO来进行事务的发送。
Recovery :根据Leader的事务日志对Follower 节点数据进行同步更新
同步策略:
Follower将所有事务都同步完成后Leader会把该节点添加到可用Follower列表中;
Follower接收Leader的NEWLEADER指令,如果该指令中epoch比当前Follower的epoch小那么Follower转到Election阶段
SNAP :如果Follower数据太老,Leader将发送快照SNAP指令给Follower同步数据;
DIFF :Leader发送从Follolwer.lastZXID到Leader.lastZXID议案的DIFF指令给Follower同步数据;
TRUNC :当Follower.lastZXID比Leader.lastZXID大时,Leader发送从Leader.lastZXID到Follower.lastZXID的TRUNC指令让Follower丢弃该段数据;(当老Leader在Commit前挂掉,但是已提交到本地)
7. Raft 算法
Raft 算法也是一种少数服从多数的算法,在任何时候一个服务器可以扮演以下角色之一:
Leader:负责 Client 交互 和 log 复制,同一时刻系统中最多存在一个
Follower:被动响应请求 RPC,从不主动发起请求 RPC
Candidate : 由Follower 向Leader转换的中间状态
在选举Leader的过程中,是有时间限制的,raft 将时间分为一个个 Term,可以认为是“逻辑时间”:
每个 Term中至多存在1个 Leader
某些 Term由于不止一个得到的票数一样,就会选举失败,不存在Leader。则会出现 Split Vote ,再由候选者发出邀票
每个 Server 本地维护 currentTerm
选举过程:
获得超过半数的Server的投票,转换为 Leader,广播 HeatBeat
接收到 合法 Leader 的 AppendEnties RPC,转换为Follower
选举超时,没有 Server选举成功,自增 currentTerm ,重新选举
自增 CurrentTerm,由Follower 转换为 Candidate,设置 votedFor 为自身,并行发起 RequestVote RPC,不断重试,直至满足下列条件之一为止:
当Candidate 在等待投票结果的过程中,可能会接收到来自其他Leader的 AppendEntries RPC ,如果该 Leader 的 Term 不小于本地的 Current Term,则认可该Leader身份的合法性,主动降级为Follower,反之,则维持 candida 身份继续等待投票结果
Candidate 既没有选举成功,也没有收到其他 Leader 的 RPC (多个节点同时发起选举,最终每个 Candidate都将超时),为了减少冲突,采取随机退让策略,每个 Candidate 重启选举定时器
- 日志更新问题:
如果在日志复制过程中,发生了网络分区或者网络通信故障,使得Leader不能访问大多数Follwers了,那么Leader只能正常更新它能访问的那些Follower服务器,而大多数的服务器Follower因为没有了Leader,他们重新选举一个候选者作为Leader,然后这个Leader作为代表于外界打交道,如果外界要求其添加新的日志,这个新的Leader就按上述步骤通知大多数Followers,如果这时网络故障修复了,那么原先的Leader就变成Follower,在失联阶段这个老Leader的任何更新都不能算commit,都回滚,接受新的Leader的新的更新。 - 流程:
解决办法:Client 赋予每个 Command唯一标识,Leader在接收 command 之前首先检查本地log
Client 发送command 命令给 Leader
Leader追加日志项,等待 commit 更新本地状态机,最终响应 Client
若 Client超时,则不断重试,直到收到响应为止(重发 command,可能被执行多次,在被执行但是由于网络通信问题未收到响应)
8. paxos 算法与 raft 算法的差异
raft强调是唯一leader的协议,此leader至高无上
raft:新选举出来的leader拥有全部提交的日志,而 paxos 需要额外的流程从其他节点获取已经被提交的日志,它允许日志有空洞
相同点:得到大多数的赞成,这个 entries 就会定下来,最终所有节点都会赞成
NWR模型
N: N个备份
W:要写入至少 w 份才认为成功
R : 至少读取 R 个备份
W+ R > N ——> R > N - W(未更新成功的) ,代表每次读取,都至少读取到一个最新的版本(更新成功的),从而不会读到一份旧数据
问题:并非强一致性,会出现一些节点上的数据并不是最新版本,但却进行了最新的操作
版本冲突问题:矢量钟 Vector Clock : 谁更新的我,我的版本号是什么(对于同一个操作者的同一操作,版本号递增)