日志一致性协议Raft

前言

       数据一致性是分布式服务的一大难题。假设我们能无成本的保证数据一致性,在分布式环境中就可以使用无限扩容来分发流量。那么数据一致性,何为数据 ?往往日志log是一应用的可靠性的保障,例如 mysql的binlog,redis 的aof日志。通过分析日志从机子可以达到和本机子相同的状态。因此数据一致性的问题,化简为日志一致性问题。Raft则是一个用于管理日志一致性的协议

Raft简介

Raft又称 木筏协议,一种共识算法,旨在替代PaxOS,相比容易理解许多。斯坦福大学的两个博士Diego Ongaro和John Ousterhout两个人以易懂为目标设计的一致性算法,2013以论文方式发布。由于易懂,不从论文发布到不到两年的时间,即出现大量的Raft开源实现。为简化PaxOS的晦涩,它采用分而治之的办法,将算法分为三个子问题:选举(Leader Election)、日志复制(Log Replication)、安全性(Safety)和集群成员动态变更(Cluster Membership Changes)。

Raft选举

Raft 中node 的 3 种角色/状态
在这里插入图片描述

  1. Follower:完全被动,不能发送任何请求,只接受并响应来自 Leader 和 Candidate 的 Message,每个节点启动后的初始状态一定是 Follower;
  2. Leader:处理所有来自客户端的请求,以及复制 Log 到所有 Followers;
  3. Candidate:用来竞选一个新 Leader (Candidate 由 Follower 触发超时而来)。
Follower --> Candidate
  1. Follower没收到心跳的时间 > Heartbeat Timeout
Candidate --> Candidate (选举超时)
  1. 处于 Candidate 状态的时间 > Election Timeout
Candidate(选举)
  1. 先建个term并投票给自己
  2. Candidate 向其它节点 node发投票请求
  1. Candidate.term小于 node. term,失败并降级为Follower
  2. Candidate.term大于 node. term,成功如果node也是Candidate则降级,并更新term
  3. Candidate.term等于node. term,如果node也是Candidate失败,否则4
  4. 比较上一条日志的term,如果node大则失败,小则成功 ,等于到5
  5. 比较上一条日志的index,如果node大则失败,否则成功
Candidate --> Follower
  1. Candidate选举时,发现其它节点term > 自己term.
Candidate --> Leader
  1. Candidate选举结果 > 总节点数/2

获取Leader结点特性
1.有最大的Term;
2. 如果Term相同,则有最大的Index;
3. 如果Term相同,Index也相同,就看谁最先发起;

Leader(维持)
  1. 向所有结点发送心跳和日志
Leader --> Follower
  1. 发现其它节点term > 自己term.
Raft日志复制

日志复制是分布式一致性算法的核心,所谓的一致性,就是集群多节点的数据一致性。

日志的数据结构
  1. 创建日志时的任期号term(用来检查节点日志是否出现不一致的情况)
  2. 状态机需要执行的指令(真正的内容)
  3. 索引index:整数索引表示日志条目在日志中位置

Raft 把每条日志都附加了任期号和下标来保证日志的唯一性。如果 2 个日志的相同的索引位置的日志条目的任期号相同,那么 Raft 就认为这个日志从头到这个索引之间全部相同。

日志的复制过程

在这里插入图片描述

  1. 客户端的向服务端发送请求(指令)。
  2. follower接收到请求转发给leader
  3. leader 把这个指令作为一条新的日志条目添加到日志中,然后并行发起 RPC 给其他的服务器,让他们复制这条信息。
  4. 假如这条日志被安全的复制 > 总结点/2,leader就提交这条日志并返回给客户端。
  5. 关于日志复制异常的Follower,Raft异步会通过强制 follower 直接复制 leader 的日志解决
日志冲突及解决

下图,所有的 follower 都和 leader 的日志冲突了,leader 的最后一条日志的任期是 6, 下标是 10 ,而其他 follower 的日志都与 leader 不匹配。
在这里插入图片描述
一致性操作:

  1. a follower 不需要删除任何条目,
  2. b 也不需要删除
  3. c follower 需要删除最后一个条目
  4. d follower 需要删除最后 2 个任期为 7 的条目,
  5. e 需要删除最后 2 个任期为 4 的条目,
  6. f 则比较厉害,需要删除 下标为 3 之后的所有条目。

具体思路:

  1. Leader 为每一个 follower 维护一个下标,称之为 nextIndex,表示下一个需要发送给 follower 的日志条目的索引。
  2. 当一个新 leader 刚获得权力的时候,他将自己的最后一条日志的 index + 1,也就是上面提的 nextIndex 值,如果一个 follower 的日志和 leader 不一致,那么在下一次 RPC 附加日志请求中,一致性检查就会失败(不会插入数据)。
  3. 如果检查失败,leader 就会把 nextIndex 递减进行重试,直到遇到匹配到正确的日志。当匹配成功之后,follower 就会把冲突的日志全部删除,此时,follower 和 leader 的日志就达成一致。
安全性

Raft在非拜占庭错误情况下,包括网络延迟、分区、丢包、冗余和乱序等错误都可以保证正确,不会返回错误结果,这就是安全性保证。

  • Leader选举之后,如果Follower与Leader日志有冲突该如何处理?
    :通过强制 follower直接复制 leader 的日志解决,详情见上文。
  • 如果在一个Follower宕机的时候Leader提交了若干的日志条目,然后这个Follower上线后可能会被选举为Leader并且覆盖这些日志条目,如此就会产生不一致?
    :candidate发送出去的投票请求消息必须带上其最后一条日志条目的Index与Term;接收者需要判断该Index与Term至少与本地日志的最后一条日志条目一样新,否则不给投票。因为前一个Leader提交日志条目的条件是日志复制给集群中的过半成员,选举为Leader的条件也是需要过半成员的投票。

在这里插入图片描述

  • a) S1是Leader,并且部分地复制了index-2;
  • b) S1宕机,S5得到S3、S4、S5的投票当选为新的Leader(S2不会选择S5,因为S2的日志较S5新),并且在index-2写入到一个新的条目,此时是term=3(注:之所以是term=3,是因为在term-2的选举中,S3、S4、S5至少有一个参与投票,也就是至少有一个知道term-2,虽然他们没有term-2的日志);
  • c) S5宕机,S1恢复并被选举为Leader,并且开始继续复制日志(也就是将来自term-2的index-2复制给了S3),此时term-2,index-2已经复制给了多数的服务器,但是还没有提交;
  • d) S1再次宕机,并且S5恢复又被选举为Leader(通过S2、S3、S4投票,因为S2、S3、S4的term=4<5,且日志条目(为term=2,index=2)并没有S5的日志条目新,所以可以选举成功),然后覆盖Follower中的index-2为来自term-3的index-2;(注:此时出现了,term-2中的index-2已经复制到三台服务器,还是被覆盖掉);
  • e) 然而,如果S1在宕机之前已经将其当前任期(term-4)的条目都复制出去,然后该条目被提交(那么S5将不能赢得选举,因为S1、S2、S3的日志term=4比S5都新)。此时所有在前的条目都会被很好地提交。
  • 如果上述情况©中,term=2,index=2的日志条目被复制到大多数后,如果此时当选的S1提交了该日志条目,则后续产生的term=3,index=2会覆盖它,此时就可能会在同一个index位置先后提交一个不同的日志,这就违反了状态机安全性,产生不一致。
    :为了消除上述场景就规定Leader可以复制前面任期的日志,但是不会主动提交前面任期的日志。而是通过提交当前任期的日志,而间接地提交前面任期的日志。


  • 网络分区会不会产生不一致的状况
    :机子是2n+1的个数。如产生分区,机子小于等于n的分区,将不会产生日志数据以及term不再增加。如果分区合并,n分区的机子板本落后也不可能成为Leader

线性一致性

所谓线性一致性,就是在 t1 的时间我们写入了一个值,那么在 t1 之后,我们的读一定能读到这个值,不可能读到 t1 之前的值。

在这里插入图片描述

Mysql 主从异步复制,你t1时间向主库插入数据x,t2时间去从库读数据x,是有可能读不到的。而线性一致性系统则是必须读到,即CAP理论中的C.

为什么Raft中没有线性一致性

Raft中有一个状态机,专门用来执行日志,相当于Mysql的解析执行器。状态机执行是要花时间的,Follower没这么快。

怎么实现线性一致性
  • Raft log
    • 任何的读请求走一次Leader
    • Leader 将读请求也记到log里面(像写请求一样)
    • 等这个 log 提交之后,在 apply 的时候从状态机里面读取值

点评:非常方便的实现线性 read,但每次read 都需要走 Raft流程,全部依赖Leader, Follower在浪费。

  • ReadIndex Read
    • 读请进来,将Leader commit index 记录到一个 local 变量 ReadIndex
    • 向其他节点发起一次 heartbeat,如果大多数节点返回了对应的 heartbeat response,那么 leader 就能够确定现在自己仍然是 leader。(防止网络分区了,自己不知道)
    • 将Leader commit index (Follower通过网络获取)记录到一个 local 变量 ReadIndex
    • 等待自己的状态机执行,直到 apply index 超过了 ReadIndex

点评:Follower可通过网络获取ReadIndex。Follower帮助分担了Learder的部份压力,向其他节点发起一次 heartbeat仍是Learder的负担

Lease Read
- 读请进来,将Leader commit index 记录到一个 local 变量 ReadIndex
- heartbeat + election timeout内默认自己是leader。超过时间 向其他节点发起一次 heartbeat并刷新heartbeat
- 将Leader commit index (Follower通过网络获取)记录到一个 local 变量 ReadIndex
- 等待自己的状态机执行,直到 apply index 超过了 ReadIndex

点评:优化了ReadIndex Read ,进一步节省了heartbeat对Leader的负担

Raft存储及日志压缩

在这里插入图片描述
在实际的系统中,不能让日志无限增长,否则系统重启时需要花很长的时间进行回放,从而影响可用性。Raft采用对整个系统进行snapshot来解决,snapshot之前的日志都可以丢弃。

集群成员动态变更

集群配额从 3 台机器变成了 5 台,可能存在这样的一个时间点,两个不同的领导者在同一个任期里都可以被选举成功(双主问题),一个是通过旧的配置,一个通过新的配置。
在这里插入图片描述

解决方法

Raft解决方法是每次成员变更只允许增加或删除一个成员(如果要变更多个成员,连续变更多次)。
在这里插入图片描述

Paxos算法

在Paxos base协议中存在3种角色: acceptor, proposer, learner.

  • Proposer向Acceptor提交提案(proposal),提案中含有决议(value)。
  • Acceptor审批提案。
  • Learner执行通过审批的提案中含有的决议。
Client   Proposer      Acceptor     Learner
   |         |          |  |  |       |  |
   X-------->|          |  |  |       |  |  Request
   |         X--------->|->|->|       |  |  Prepare(1)
   |         |<---------X--X--X       |  |  Promise(1,{Va,Vb,Vc})
   |         X--------->|->|->|       |  |  Accept!(1,V)
   |         |<---------X--X--X------>|->|  Accepted(1,V)
   |<---------------------------------X--X  Response
   |         |          |  |  |       |  |
  1. Request, 客户端发送请求
  2. Prepare,Proposer提交提案编号为n;
  3. Promise,Acceptor审核提案,如果Acceptor当前提案内容大于等于n,则不接受,小于则成功。
  4. Promise sucess, Proposer结果成功数是多数,进行Accept
  5. Promise fail, Proposer结果成功数是少数,提案编号n+1跳回Prepare
  6. Accept, Proposer将编号为n的提案内容发过去
  7. Accepted,如果Acceptor中最大编号没超过n,则写入提案及Learner做备份。否则重新到Prepare
  8. Response,返回结果。

点评:

  1. 当Proposer高并发的时候,可能出现活锁的现象,谁也提交不了?当发生冲突的时候,进行编号小的进行休眼,迟点发起。影响效率。
  2. 每次请求都要经2轮RPC通信,影响效率。

因为上述的问题,对Paxos base进行优化变成 Multi Paxos

  1. 项目启动时,通上Paxos base流程,先对Proposer选出Leader。
  2. 之前client语法只通过Leader来提案,流程如下
Client     P(leader)   Acceptor     Learner
   |         |          |  |  |       |  |
   X-------->|          |  |  |       |  |  Request
   |         X--------->|->|->|       |  |  Accept!(1,V)
   |         |<---------X--X--X------>|->|  Accepted(1,V)
   |<---------------------------------X--X  Response
   |         |          |  |  |       |  |

点评:
Proposer,Acceptor,Learner可以合到一个Server里面,然后通过状态来区分他们。这样Multi Paxos 和Raft模式已经很接近了

ZAB算法

zab同raft一样,也是由Multi Paxos变型过来的。也有三个状态LOOKING/FOLLOWING/LEADING。

  • LOOKING: 节点正处于选主状态,不对外提供服务,直至选主结束;
  • FOLLOWING: 作为系统的从节点,接受主节点的更新并写入本地日志;
  • LEADING: 作为系统主节点,接受客户端更新,写入本地日志并复制到从节点

选主过程:

  1. LOOKING向其它机子发送(sid,zxid)

sid:机子编号。不同机子不同的编号
zxid:高32为epoch 轮数,相当于Raft中的term;低32位表示自增ID由0开始,相当于Raft中的Index.每次选出新的Leader,epoch会递增,同时zxid的低32位清0。

  1. 集群其他节点,这些节点会为发起选主的节点进行投票

zxid(A) > zxid(B)
sid(A) > sid(B)

点评:
ZAB和Raft在选主上面不是极其相似的。估计日志复制的细节上面有所差异

我的RaftDemo

/**
 *
 * 1.electionTime最终时间短的会选为主
 * 2.日志存储和读取
 */
public class RaftTest {

    public static void main(String[] args) {

        List<String> list = Lists.newArrayList("localhost:8771", "localhost:8772", "localhost:8773", "localhost:8774", "localhost:8775");

        List<RpcServer> ss = Lists.newArrayList();

        for (int i = 0; i < list.size(); i++) {

            String url = list.get(i);

            RpcServer server = new RpcServer(url);

            ss.add(server);
            if (i == 0) {
                server.setElectionTime(500l);
            }
            server.start();
        }

        ThreadUtil.sleep(3000);

        //结果大概率是8771
        for (int i = 0; i < ss.size(); i++) {

            if(i == ss.size() -1) {
                i = 0;
                ThreadUtil.sleep(2000);
            }
            RpcServer server = ss.get(i);
            if(server.isLeader()){
                System.out.println("leader node is " + server.getUrl());
                break;
            }

        }

        //让node结点接收一波心跳新
        ThreadUtil.sleep(2000);

        RpcClient rpcClient = new RpcClient();
        ClientKVReq putReq = ClientKVReq.newBuilder()
                .type(0)
                .key("yoyo")
                .value("eee")
                .build();
        ClientKVAck putRes = rpcClient.sendMassage(putReq);
        System.out.println(putRes.getResult());


        //让node结点接收一波心跳新
        //ThreadUtil.sleep(20000);


        ClientKVReq getReq = ClientKVReq.newBuilder()
                .type(1)
                .key("yoyo")
                .build();
        ClientKVAck getRes = rpcClient.sendMassage(getReq);
        System.out.println(getRes.getResult());

    }
}

运行结果:

[localhost:8771] electionTime:500
[localhost:8774] electionTime:6785
[localhost:8772] electionTime:4948
[localhost:8775] electionTime:3759
[localhost:8773] electionTime:4643
[localhost:8772] stepDown become FOLLOWER
[localhost:8774] stepDown become FOLLOWER
[localhost:8774] agree [localhost:8771]
[localhost:8775] stepDown become FOLLOWER
[localhost:8775] agree [localhost:8771]
[localhost:8772] agree [localhost:8771]
[localhost:8773] stepDown become FOLLOWER
[localhost:8773] agree [localhost:8771]
[localhost:8771] become leader
leader node is localhost:8771
LEADER[localhost:8771]  write [{index=0, term=2, command=Command(key=yoyo, value=eee)}]
FOLLOWER[localhost:8772]  write [{index=0, term=2, command=Command(key=yoyo, value=eee)}]
FOLLOWER[localhost:8773]  write [{index=0, term=2, command=Command(key=yoyo, value=eee)}]
FOLLOWER[localhost:8774]  write [{index=0, term=2, command=Command(key=yoyo, value=eee)}]
FOLLOWER[localhost:8775]  write [{index=0, term=2, command=Command(key=yoyo, value=eee)}]
LEADER[localhost:8771]  apply [{index=0, term=2, command=Command(key=yoyo, value=eee)}]
ok
FOLLOWER[localhost:8772]  apply [{index=0, term=2, command=Command(key=yoyo, value=eee)}]
FOLLOWER[localhost:8774]  apply [{index=0, term=2, command=Command(key=yoyo, value=eee)}]
FOLLOWER[localhost:8773]  apply [{index=0, term=2, command=Command(key=yoyo, value=eee)}]
FOLLOWER[localhost:8775]  apply [{index=0, term=2, command=Command(key=yoyo, value=eee)}]
FOLLOWER[localhost:8772]  ReadIndex to [0] with value:eee
eee

资源地址:https://download.csdn.net/download/y3over/12577334

Raft进阶-SOFAJRaft

SOFAJRaft 是一个基于 Raft 一致性算法的生产级高性能 Java 实现,是从百度的 braft 移植而来,做了一些优化和改进。
SOFAStack 开源 SOFAJRaft:生产级 Java Raft 算法库
SOFAJRaft 选举机制剖析 | SOFAJRaft 实现原理
SOFAJRaft 线性一致读实现剖析 | SOFAJRaft 实现原理
SOFAJRaft 实现原理 - 生产级 Raft 算法库存储模块剖析
SOFAJRaft 实现原理 - SOFAJRaft-RheaKV 是如何使用 Raft 的
SOFAJRaft—初次使用

主要参考

Raft 日志复制 Log replication
编写你的第一个 Java 版 Raft 分布式 KV 存储
Raft协议安全性保证
TiKV 功能介绍 - Lease Read
RAFT算法详解
《从Paxos到Zookeeper:分布式一致性原理与实践》

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值