引申
拜占庭将军问题
著名的拜占庭将军问题
很久以前,拜占庭是东罗马帝国的首都。那个时候罗马帝国国土辽阔,为了防御目的,因此每个军队都分隔很远,将军与将军之间只能靠信使传递消息,在打仗的时候,拜占庭军队内所有将军必需达成一致的共识,且只有当半数以上的将军共同发起进攻时才能取得胜利.才能更好地赢得胜利。但是,在军队内有可能存有叛徒,可以任意篡改消息,扰乱将军们的决定。
在已知有成员不可靠的情况下,其余忠诚的将军需要在不受叛徒或间谍的影响下达成一致的协议。拜占庭将军问题和分布式场景下异步系统及不可靠的协议中达到一致性类似
一、分布式系统背景及面临的问题
常见lamp单体架构
分布式系统架构设计变得复杂,尤其是分布式的事务
- 部署变得复杂,一次可能需要部署多个服务
- 随着系统的吞吐量变大,响应时间会变长,组件越多、网络跳数越多,延迟时间便会更高
- 技术可以多样性,服务变多,运维复杂度会变得复杂
- 测试和查错的复杂度增大,学习曲线也增大
- 数据的一致性问题
大量的机器故障:宕机、重启、关机,数据可能丢失、陈旧、出错,如何让系统容纳这些问题,对外保证数据的正确性
- 网络和通信故障、以及网络通信延迟等
消息丢失,节点之间彼此完全无法通信、发送超时、消息乱序,TCP协议只能保证同一个TCP 链接内的网络消息不乱序,TCP 链接之间的网络消息顺序则无法保证
1.CAP理论
- Consistency (一致性)
一致性:对于客户端的每次读操作,要么读到的是最新的数据,要么读取失败。强调的是副本节点的强一致性
- Availiablity(可用性)
指系统在出现异常时也可以提供服务,任何客户端的请求都能得到响应数据,不会出现响应错误。不会给你返回错误,但不保证数据最新,强调的是不出错。
- Partition tolerance(分区容忍性)
由于分布式系统通过网络进行通信,网络是不可靠的,当任意数量的消息丢失或延迟到达时系统仍会继续提供服务,不会挂掉
对于一个分布式系统而言,P是前提,必须保证,因为只要有网络交互就一定会有延迟和数据丢失,这种状况我们必须接受,必须保证系统不能挂掉。
CAP理论说的就是:一个分布式系统,不可能同时做到这三点。
一致性和可用性,为什么不可能同时成立?答案很简单,因为可能通信失败(即出现分区容错)。
根据业务场景选择 CP or AP
Base理论
数据可能会迟到,但是最终不会缺席!
- Basically Available(基本可用) 它是要求分布式系统中所有节点要做到高可用。
- Soft State(软状态),它定义数据可以有一个中间状态,比如服务A更新数据,读取B服务时,允许存在一定的延迟,客户端读取的为中间状态。
- Eventually Consistent(最终一致性),它允许数据存在中间状态,做不到强一致性,但要做到最终一致性,比如消息列表多刷几次,总归能刷出来。比如实现一致性使用消息队列
互联网中对可用性要求非常高,但对一致性要求要少点,比如发一个消息给用户,用户不用立即收到,晚个一两秒也OK的。所以可以牺牲CAP中C,BASE理论大部分是对AP的补充和权衡
二、分布式一致性和共识算法
- 什么是共识机制?
百科解释:所谓"共识机制",是通过特殊节点的投票,在很短的时间内完成对交易的验证
和确认;对一笔交易,如果利益不相干的若干个节点能够达成共识,我们就可以认为全网
对此也能够达成共识。事物参与方对统一事件达到相同意见的机制
- 生活中的一些例子:
团队内小组聚餐,大家投票决定今晚吃啥 、社区选村长、前几年比较热门的美国执政党大
选拜登-川普之争, 都要美国大众中先选出一些候选人,从候选人中PK, 候选人且有任期
- 有哪些一致性和共识算法 ?
POW:Proof of Work,工作量证明
POS:Proof of Stake,股权证明
Raft、Paxos、ZAB算法
Gossip、分布式一致性hash ....
区块链的大脑-PoW和PoS机制
- PoW 工作量证明机制- 挖矿
为了获取记账权和BTC激励,矿工必须经过一定量的工作量进行数学运算,谁最快最准算出答案就获得记账权和激励,以时间和资源为担保,确保记账的真实性和有效性,按劳分配,算力越高挖矿时间越长,获得的数字货币就越多
优点:算法简单-找随机数,安全系数高
缺点:耗费大量算力,效率低下
- PoS- 股权证明机制
通过持币产生利息,持有币的天数越长,持有币的数量越多获取记账权和激励的概率就越大,PoS缩短共识的时间
2.P2P网络核心技术:Gossip 一致性协议
Gossip 过程是由种子节点发起,当一个种子节点有状态需要更新到网络中的其他节点时,它会随机的选择周围几个节点散播消息,收到消息的节点也会重复该过程,直至最终网络中所有的节点都收到了消息。这个过程可能需要一定的时间,由于不能保证某个时刻所有节点都收到消息,但是理论上最终所有节点都会收到消息,因此它是一个最终一致性协议。Gossip是一种去中心化、容错而又最终一致性的算法。
- Gossip 是周期性的散播消息,把周期限定为 1 秒
- 被感染节点随机选择 k 个邻接节点(fan-out)散播消息,这里把 fan-out 设置为 3,每次最多往 3 个节点散播。
- 每次散播消息都选择尚未发送过的节点进行散播
- 收到消息的节点不再往发送节点散播,比如 A -> B,那么 B 进行散播的时候,不再发给 A。
注意:Gossip 过程是异步的,也就是说发消息的节点不会关注对方是否收到,即不等待响应;不管对方有没有收到,它都会每隔 1 秒向周围节点发消息。异步是它的优点,而消息冗余则是它的缺点。
这里一共有 16 个节点,节点 1 为初始被感染节点,通过 Gossip 过程,最终所有节点都被感染
三、Raft协议实现逻辑介绍
Leader选举
node节点的几种角色转换
- Candidate:候选人状态,新一轮选举开始
-
- Candidate节点是由Follower节点转换而来的,当Follower节点长时间没有收到Leader节点发送的心跳消息时,则该节点的选举计时器就会过期,同时会将自身状态转换成Candidate,发起新一轮选举。
- Follower:跟随者状态
-
- 每当节点切换到这个状态时,意味着选举结束。不过每个follow节点其实都有点野心,只是碍于 Leader 的心跳广播不敢造反
- Follower节点不会发送任何请求,它们只是简单地响应来自Leader或者Candidate的请求,Follower节点也不处理Client的请求,而是将请求重定向给集群的Leader节点进行处理。
- Leader节点
-
- 接收到客户端的写入请求,Leader节点会在本地追加一条相应的日志,追加日志信息发送到集群中其他的Follower节点。
- 向Follower节点发送心跳消息,这主要是为了防止集群中的其他Follower节点的选举计时器超时而触发新一轮选举
- preCandidate(预候选人状态) raft协议优化后的一种角色
preCandidate是在真正参与选举之前,参选者会向集群中的所有结点先发送要开始选举的请求,如果这时候有半数以上节点回应,则该节点就成为Candidate,否则变回follower,处理分布式集群网络分区的问题
几个重要的概念
- Leader Election(领导人选举):简称选举,就是从候选人中选出领袖;
- Term(任期):它其实是个单独递增的连续数字,每一次任期就会重新发起一次领导人选举,每次开始时选举任期+1;
- Election Timeout(选举超时):就是一个超时时间,当群众超时未收到领袖的心跳时,会重新进行选举。每次随机150-300ms随机时间
- Heartbeat Timeout(心跳超时时间)Leader节点向集群中其他Follower节点发送心跳消息的时间间隔。心跳消息用于维持 Leader 的领导地位,并告知其他节点当前的 Leader 是谁。领袖广播心跳的周期必须要短于"选举定时器"的超时时间,否则群众会频繁成为候选者,也就会出现频繁发生选举,切换Leader的情况。
几种RPC请求
Raft 集群中的节点通过远端过程调用( RPC )来进行通信
I. RequestVote RPC 用于候选人向其他节点请求投票的消息。候选人使用RequestVote RPC向其他节点发送选举请求,并请求其他节点投票给自己成为新的 Leader, 传输的数据包含候选人的任期号(term),候选人的最后日志条目的索引和任期号
其他节点拒绝投票的几种情况:
1.如果候选人的任期号小于其他节点的任期号,则其他节点会拒绝投票。
2.如果其他节点已经投票给其他候选人或当前节点已经成为Leader,则其他节点会拒绝投票。
3.如果候选人的最后日志条目的索引和任期号不大于当前节点的最后日志条目,则当前节点会拒绝投票。
II. AppendEntries RPC 是领导人触发的,目的是向其他节点复制日志条目和发送心跳
其他节点拒绝追加日志的几种情况:
1.如果 Leader 的任期号小于当前节点的任期号,当前节点会拒绝追加日志条目。因为当前节点认为自己已经处于一个更高的任期,所以拒绝接受来自较低任期的 Leader 的日志条目
2.日志条目已经存在:如果当前节点在相同的索引处已经有一个相同任期号的日志条目,它会认为该日志条目已经存在并拒绝追加日志条目。这是为了避免重复追加相同的日志条目,确保日志的唯一性。
3.日志条目追加失败:如果当前节点的日志追加操作失败,例如磁盘写入错误或存储空间不足等问题,当前节点会拒绝追加日志条目并返回失败的响应
Leader 会根据拒绝响应来调整自己的日志复制策略,例如减少日志追加的速率或与其他节点重新同步。
选举过程
选举节点状态流转图
- 最开始,term=0,每个节点都是follow状态,等待谁最先超时,最先超时的成为候选人节点
- 假如节点A成为候选节点后,先给自己投一票,并会向集群中其他节点发送选举请求(Request Vote)以获取其选票,此时其他节点还都是处于Term=0的任期之中,且都是Follower状态,均未投出Term=1任期中的选票,接收到节点A的选举请求后会将选票投给节点A,其他节点在收到节点A的选举请求的同时会将选举定时器重置,节点A收到的票数最多成为leader
⭐️ 如果leader节点挂掉咋办?重新选
运行一段时间后,leader因为故障挂掉了,不再有心跳发给其他follow节点了,一段时间后,会有一个Follower节点的选举计时器最先超时假定为节点D,当节点B和节点C收到节点D的选举请求后,会将其选票投给节点D,由于节点A已经宕机,没有参加此次选举,也就无法进行投票,但是在此轮选举中,节点D依然获得了半数以上的选票,当节点A恢复之后,由于其他节点的任期大于节点A,所有节点A会降为follow
⭐️如果出现多个候选者?重新选
当两个follow节点同时选举时间超时,出现多个候选者时,两个候选者会同时发起投票,如果票数不同,最先得到大部分投票的节点会成为领袖;如果获取的票数相同,会重新发起新一轮的投票。
日志复制
复制流程
- 集群中只有Leader节点才能处理客户端的更新操作,这里假设客户端直接将请求发给了Leader节点。当收到客户端的请求时,Leader节点会将该更新操作记录到本地的Log中
- Leader节点会向其他节点发送Append Entries消息,其中记录了Leader节点最近接收到的请求日志,集群中其他Follower节点收到该Append Entries消息之后,会将该操作记录到本地的Log中
- 当Leader节点收到半数以上的响应消息之后,会认为集群中有半数以上的节点已经记录了该更新操作,Leader节点会将该更新操作对应的日志记录设置为已提交(committed),并应用到自身的状态机中。同时Leader节点还会对客户端的请求做出响应,同时,Leader节点也会向集群中的其他Follower节点发送消息,通知它们该更新操作已经被提交,Follower节点收到该消息之后,才会将该更新操作应用到自己的状态机中
如何解决日志不一致问题?
当一个新的 Leader 被选举来时,它的日志与其他的 Follower 的日志可能不一样,需要一个机制来保证日志是一致的
新Leader 产生时集群可能的一个状态图, index 10,nextlndex 的初始值是 11
- a-b 表示跟随者的日志项落后于当前领导者;
- c-d 表示跟随者有些日志项没有被提交;
- e-f 情况稍微有点复杂,以上两种情况它们都存在。
复现一下出现的原因
start->e(leader)term:1->e崩溃->f(leader)term:2->给其他节点追加rpc日志过程中中崩溃->f(leader) term:3 再次当选leader ->给其他节点追加rpc日志过程中中再次崩溃->e(leader)term:4->成功复制追加了一部分日志给follower节点,还有一部分没有成功追加到大多数跟随者->e再次崩掉->可能a(leader) term:5->同样复制了一部分崩掉->c(leader) term:6 还未全部将本地日志复制到其他跟随者之前又崩溃了->d(leader)term:7写入了若干日志项之后,在追加 RPC 请求中崩溃了,造成此图情况
日志索引信息
为了让 Follower 的日志同自己的保持一致,Leader 需要找到Follower与它的日志条目不一致的位置,然后让Follower连续删除该位置之后(包括该位置)所有的日志条目,并且将自己在该位置(包括该位置)之后的日志条目发送给Follower节点
如何定位不一样的位置?
- 每个节点都会维护commitIndex和lastApplied两个值,它们是本地Log的索引值,commitIndex表示的是当前节点已知的、最大的、已提交的日志索引值、lastApplied表示的是当前节点最后一条被应用到状态机中的日志索引值
- Leader 为每一个 Follower 维护了一个 nextlndex ,它表示领导人将要发送给该追随者的下一条日志条目的索引,当一个 Leader 得选举时,它会假设每个Follower 上的日志都与自己的保持-致,于是先将 nextlndex 初始化为它最新的日志条目索引,上图 Leader 最新的日志条目index 10,所以 nextlndex 的初始值是 11
- Leader向Follower 发送 ApendEntries RPC 时,它携带了(term_id,next-Index-1)二元组信息,Follower节点接收到AppendEntries RPC 消息后,进行一致性检查,搜索自己的日志文件中是否存在这样的日志条目,follow节点当前自己的任期和索引位置, 如果不存在,就向 Leader返回失败,拒绝添加,leader收到失败之后将 nextlndex 递减(next--),带着数据重试ApendEntries RPC直到成功,这个时候nextlndex 位置的日志条目中领导人与追随者的保持一致。然后Follower nextlndex 置之前的日志条目将全部保留,在此之后(与 Leader 有冲突)的日志条目将被 Follower 全部删除,该位置起追加 Leader 上在 nextlndex 位置之后的所有日志条目
3.网络分区-脑裂现象
- 网络出现故障,AB节点无法和CDE节点通信,CDE节点感受不到节点A的存活会重新发起选择选举
- 当网络分区恢复时,集群中存在新旧两个Leader节点(A和E),其中节点E的Term值较高,会成为整个集群中的Leader节点
》 问题1: 网络分区两个大脑如何和客户端交互?
如图当节点A、B与节点C、D、E之间发生网络分区之后,客户端发往节点A的请求将会超时,这主要是因为节点A无法将请求发送到集群中超过半数的节点上,该请求相应的日志记录也就无法提交,从而导致无法给客户端返回相应的响应
》问题2: 出现脑裂后,节点较少的一方没有leader节点,会导致不断选择出现任期数过高的问题么?
如果网络分区时,Leader节点划分到节点较多的分区中,此时节点较少的分区中,会有节点的选举计时器超时,切换成Candidate状态并发起新一轮的选举。但是由于该分区中节点数不足半数,所以无法选举出新的Leader节点。待一段时间之后,该分区中又会出现某个节点的选举计时器超时,会再次发起新一轮的选举,循环往复,从而导致不断发起选举,Term号不断增长该咋办?
在Raft协议中对这种情况有一个优化,当某个节点要发起选举之前,需要先进入一个叫作PreVote的状态,在该状态下,节点会先尝试连接集群中的其他节点,如果能够成功连接到半数以上的节点,才能真正发起新一轮的选举
4.安全
安全性的原则是一个term只有一个leader,被提交至状态机的数据不能发生更改。保证安全性主要通过限制leader的选举来保证
Candidate在拉票时需要携带本地已持久化的最新的日志信息,如果投票节点发现本地的日志信息比Candidate更新,则拒绝投票。
只允许Leader提交当前Term的日志。
拥有最新的已提交的log entry的Follower才有资格成为Leader。
四、分布式协议Raft的一些应用
1.etcd整体架构
2.etcd-raft实现
3.watch +blotdb
- 哨兵Redis Sentinel
- Etcd-Raft、Consule
- 开源分布式数据库Tidb....
Raft选举演示