基于raft的kvDB

0 CAP理论

0.1 指标

0.1.1 一致性(consistency)

客户端每次读操作,不管访问哪个节点,要么读到最新写入的数据,要么读取失败。一致性强调数据正确。

0.1.2 可用性(availability)

不管访问哪个非故障节点,都能得到响应数据,但不保证是相同的最新数据。可用性强调服务可用,但不保证数据正确。

0.1.3 分区容错性(partition tolerance)

节点间通信出了问题,就会出现分区。分区容忍性强调,出现分区问题,系统仍能继续运行。

0.2  CAP不可能三角

对一个分布式系统而言,一致性、可用性、分区容错性只能在三个指标中选择两个。

但选择的指标并不是一成不变的。当出现分区,一致性和可用性只能选择其一;但不存在网络分区的情况下,一致性和可用性能够同时保证。 

C和A之间的选择可以是局部性的,不同的子系统可以选择不同的指标。比如,一套支付系统中,账户余额必须是强一致性的,选C;但用户的支付设置不必考虑强一致性,可以选A。

1 raft共识算法

1.1 raft特点

Raft是管理日志复制共识算法,效果和效率和Paxos一样,但结构不同,raft更简单更容易理解。raft把算法中各个部分分离开来,如选举、日志复制、安全性等。

  • raft是强leader模型,通过选举leader来实现一致性leader拥有完全的能力来管理复制日志
  • log只从leader流向其他服务器,leader告诉follower什么时候应用日志到状态机是安全的。
  • 如果一个leader宕机或者失联了,一个新的leader就会被选举。

Raft把一致性问题分解为三个独立的子问题:

  • leader选举。当现有的leader失败后,新的leader必须被选举产生。
  • 日志复制。leader必须接收客户端的日志条目然后通过集群复制,强制其他服务器的日志和leader的一样。
  • 状态机安全性。如果一个服务器应用了一条日志,那么其他的服务器应用的相同条目的日志内容就是一样的。  (这样的话,follower已经应用的日志和leader相同,follower的snapshot也不会和leader冲突)

1.2 复制状态机

复制状态机的结构

共识算法的任务就是保证复制日志的一致性。一个服务器的共识模型接受client的指令并添加到日志中,然后和其它服务器的共识模型交流,保证每一个服务器以相同的顺序包含相同的指令,即使某些服务器宕机。

指令被复制到所有的服务器中,每个服务器状态机都会按照日志中的顺序执行,然后返回结果给client.

用于实际系统的共识算法通常有如下特性:

  • 安全性:不会返回错误的结果。非拜占庭条件(没有恶意节点的情况)
  • 可靠性:可以容许部分节点故障。
  • 不依赖时间保证日志的一致性。
  • 少数慢服务器不影响整体性能,只要大多数节点做出响应。

2 State

2.1 所有节点都维护的需要持久化的状态

int m_me;                                     // raft节点的标识
int m_currentTerm;                            // 当前trem(任期)
int m_votedFor;                               // 记录当前任期给谁投了票
std::vector<raftRpcProctoc::LogEntry> m_logs; // 日志条目数组(日志:index,term,指令)

2.2 所有节点都维护的易失性状态

都初始化为0

int m_commitIndex; // 已经被提交的最新的log的index
int m_lastApplied; // 已经应用给状态机(kvsrver)的最新的log的index

2.3 只有leader需要维护的易失状态

// 对于每一个节点,leader(当前节点)下次应该从哪个日志开始发送;初始化为leader的log的最大index+1
std::vector<int> m_nextIndex;  
// 对于每一个节点,在哪个日志和leader(当前节点)匹配;初始化为0
std::vector<int> m_matchIndex; 

3 服务器规则

3.1 所有的服务器

  • 如果commitIndex > lastApplied:增加lastApplied,应用log[lastApplied]到状态机
  • 如果RPC的请求或者回复中的term T > currentTerm:设置currentTerm=T,转为follower

3.2 Follower

  • 回复来自candidate和leader的RPC
  • 如果经过了选举超时(election timeout)还没有收到当前leader的AppendEntries(心跳/日志复制)或者candidate的投票请求转为candidate,发起选举

3.3 candidate

  • 转为candidate之后,开始选举
    • 给所有的服务器发送RequestVote RPC
    • 重设选举定时器
    • 为自己投票
    • 增加currentTerm
  • 如果收到大多数服务器的选票:成为leader
  • 如果收到新leader的AppendEntries RPC转为follower
  • 如果经过了选举超时(选举定时器到达了):开始一个新的选举

3.4 leader

  • 定时发送空的AppendEntries RPC(心跳)给所有的服务器
  • 如果收到了客户端指令:将新的条目添加到本地日志中,在应用到状态机之后返回结果。当前项目中kvserver负责与客户端通信。
  • 如果最大的日志索引 >= follower的nextIndex:发送从包含nextIndex开始的日志条目的AppendEntries RPC:
    • 如果因为日志的不一致导致失败:leader减小nextIndex然后重试
    • 如果成功:更新follower的nextIndex(下一个要发送的)和matchIndex(匹配的)
  • 如果存在一个N,N > commitIndex,大部分的matchIndex[i] >= N(说明log[N已经被复制到大多数节点])。而且log[N].term==currentTerm,设置commitIndex=N。(只有当前term有日志提交,才更新commitIndex

4 raft的保证

4.1 选举安全

在给定的期限内,最多只能选举一位leader。成为leader需要获得大多数选票,且在一轮选举中,每个节点只能投一票。

4.2 只能leader添加日志

日志只能由leader添加,且只能从leader流向followerleader永远不会覆盖或删除其日志中的条目.

4.3 日志匹配

  • 如果不同节点的日志条目有相同的index和term,那么它们存储相同的指令
  • 如果不同节点的日志条目有相同的index和term,这两个日志中该索引之前的条目都是相同的

4.4 状态机安全性

如果服务器已在其状态机应用给定索引日志条目,那么其他服务器应用的相同的条目的日志内容是一样的。

理解:(还是不太理解......2024/7/29)

想要应用某条日志,必须保证该日志已经被提交,即被leader成功复制到大多数节点。

follower想要复制leader的日志,必须保证它前面的日志也和leader完全相同。follower复制了日志,就保证follower的日志和leader是一致的。

并且,leader不会修改自身的日志。

这样,就保证了follower应用的日志一定是和leader完全相同的。

这样,就保证了状态机的安全性。

但是,这样,还不能保证线性一致性。

4.5 安全性限制

4.5.1 选举限制——leader完整性

如果某一任期内一条日志被提交,则该条目将出现在之后任期leader中。

leader提交了某一条日志,说明该日志被复制到大多说节点。然后leader宕机,其它节点发起选举。没有复制这条日志的节点的log比大多数节点旧,所以不可能获得大多数选票,不能成为leader .只有复制了上一任leader提交的最新的日志的节点才能成分下一任leader.

4.5.2 Leader不允许提交之前任期的日志

Leader不允许提交之前任期的日志leader只有在当前term有日志提交的时候才更新commitIndex(commitindex更新,意味着被提交,日志可以被应用到状态机)

详见6.5.3.1

5 选举

leader向所有follower定时发送心跳信号(不携带日志条目的AE RPC),以维护其领导地位。只要服务器收到来自leader或者candidate有效RPC,就会保持follower状态。如果follower在称为选举超时(election timeout)的时间段内没有任何通信,则它假定没有可行的leader,并开始竞选新leader。

5.1 开始选举

转为candidate之后,开始选举

  • 给所有的服务器发送RequestVote RPC
  • 重设选举定时器
  • 为自己投票
  • 增加currentTerm

5.2 选举结果

一个candidate继续保持它的状态直到有以下一种情况发生:

  1. 它赢得了选举
  2. 另一个服务器成为了leader
  3. 没有leader产生

5.2.1 赢得了选举

在一个任期内(一次选举过程,也是一个term),如果收到大多数服务器投票,candidate就赢得了选举。

每个服务器在一个任期中最多给一个candidate投票,而且是基于先来先服务原则(限制:投票要求candidate的日志不比自己旧)。

这个大多数规则保证了最多一个candidate可以赢得某一任期的选举(选举安全性)。

一旦一个candidate赢得了选举,它就成为了leader。它给集群中所有的服务器发送心跳以建立它的地位,同时也阻止了新的选举发生

5.2.2 另一个服务器成为了leader

当等待投票的时候,candidate可能收到另一个服务器声明已经成为leader的AppendEntries RPC。

如果这个leader的任期不低于这个candidate的任期,这个candidate就承认leader的正当性然后成为follower。

如果这个RPC中的任期比candidate的任期小,这个candidate就就拒绝这个RPC,然后继续保持candidate状态。(可能是旧的leader重连)

5.2.3 没有leader产生

没有candidate赢得或者输掉这个选举。原因就是多个follower同时成为candidate,投票会分散给各个candidate,从而没有candidate获得了大多数的票。

当这种情况发生,每个candidate会等到超时然后增加它的任期号开始新一轮的选举。然而,没有额外的措施的话,分裂投票的情况还会出现。

Raft使用随机的选举超时(randomized election timeout)来解决分裂投票。选举超时在一定的范围(150-300ms)内随机获得。这会把所有的服务器分散开来,因此在大多数情况下,只有一个服务器会经历完选举超时。 它在其他服务器到达选举超时之前赢得选举和发送心跳。 每个candidate在选举开始时都会重置选举定时器,并等待该定时结束后再开始下一次选举。

5.3 选举流程

5.3.1 electionTimeOutTicker

负责查看是否该发起选举,如果该发起选举就执行doElection发起选举。

5.3.2 doElection

改变状态为candidate,发起选举,构造需要发送的rpc对象,调用sendRequestVote发送rpc并处理rpc响应结果。发送给所有节点每个节点对应一个线程

当前节点状态变化:

m_status = Candidate;
m_currentTerm += 1;
m_votedFor = m_me;          // 自己给自己投,也避免candidate给同辈的candidate投

设置rpc请求对象: 

requestVoteArgs->set_term(m_currentTerm);
requestVoteArgs->set_candidateid(m_me);
requestVoteArgs->set_lastlogindex(lastLogIndex);   //candidate的最高日志条目index
requestVoteArgs->set_lastlogterm(lastLogTerm);     //candidate的最高日志条目term

tips:

  • 发起选举后,会重置选举定时器。如果选举定时器超时就必须重新选举,不然没有选票就会一直卡住;
  • 重竞选超时导致重新选举term也会增加

5.3.3 sendRequestVote

发送rpc并处理rpc响应结果。

// rpc远程调用:将投票请求通过socket连接发送给raft节点,并获取结果
bool ok = m_peers[server]->RequestVote(args.get(), reply.get());

调用完成后,查看reply

比较term:

  • reply->term() > m_currentTerm,当前结点变为follower
  • reply->term() < m_currentTerm,不会出现这种情况,如果对方term比自己小,会自动变为和当前节点相等
  • reply->term() == m_currentTerm

查看是否获得投票:

  • reply->votegranted() == false,没有获得投票

  • reply->votegranted() == true,获得投票

获得半数以上投票后,当选leader:

  • 更新m_nextIndex和m_matchIndex
  • doHeartBeat,马上向其他节点宣告自己就是leader

5.3.4 RequestVote

接收别人发来的选举请求,主要检验是否要给对方投票。

比较term:

  • args->term() < m_currentTerm,candidate过时,拒绝投票
  • args->term() > m_currentTerm,更新term,并变成follower
  • args->term() == m_currentTerm

term相同,比较日志:

  • candidate的最高日志index较小,说明其日志比较旧。拒绝投票。

日志新旧比较:先比较term,term大的更新;term相等,index大的更新

证明

 基于先来先服务原则,进行投票,每轮只能投一次票。

6 日志复制

6.1 日志生成

一旦一个leader产生,就会开始处理客户端请求。leader将请求中的指令封装为一个log添加到日志数组中,并通过AppendEntries RPC发送给其它所有服务器。

日志条目指令,leader收到指令的term, index表示在日志数组中的位置

6.2 日志提交

当log被大多数节点成功复制,表示该log被提交,然后,leader将该log应用到状态机(发送给kvserver,应用到kvdb),返回结果给客户端。

  1. 一旦leader创建一个新的条目并提交,这还将提交leader日志中的所有先前条目,包括之前leader创建的条目上一任leader创建一条日志,只复制到了少数节点,然后宕机;少数节点中一个节点当选leader,包含上一任leader创建的未提交的日志)。
  2. leader要在AErpc中包含已提交的最高索引(m_commiteindex),告知follower 。follower得知日志已被提交,便将其应用到本地状态机。
  3. 只要follower的日志和leader不一致,如follower宕机或运行缓慢或丢包,leader会重复发送AE Rpc,直到follower的日志和leader保持一致。

6.3 日志机制

用于维护不同服务器上日志的高度一致性。简化了系统的行为并使其更容易预测,且是确保安全性的重要组成部分。

  • 如果不同节点的日志条目有相同的index和term,那么它们存储相同的指令
  • 如果不同节点的日志条目有相同的index和term,这两个日志中该索引之前的条目都是相同的

基于以下事实:

  1. 在一个term内,只会产生一个leader 。term相同,说明日志来自同一个leader。leader永远不会删除或覆盖自己日志中的条目,对同一个index, leader只能创建一条日志。follower的日志是从leader复制的。
  2. AppendEntries一致性检查保证日志的一致性。初始时,leader和follower都没有日志,此时,日志是一致的。后续有日志添加,leader在AErpc中包含新日志的前一个日志的term和index (prelogterm 和 perlogindex)。如果follower在其日志中找不到相同term和index的日志,则拒绝AErpc ,表示follower的日志滞后或与leader冲突,leader前移对应follower的nextindex。如果能找到,则表明该日志与leader相同。类似归纳总结,可以保证前面的日志都与leader相同。 简单来说,follower复制leader的日志的时候需要确保前一条日志相同,如果不同,拒绝复制;归纳得知follower和leader的数据是相同的。

6.4 日志不一致

正常情况下,follower的日志与leader保持一致。

但是,leader宕机在没有完全复制新的日志条目之前宕机)或follower宕机,会导致日志不一致。follower可能没有leader的某些条目,或leader没有follower的某些条目,或两者都有

Raft中,leader强制follower复制leader的日志解决不一致问题,follower中冲突的日志会被覆盖。

leader为每个follower维护一个nextindex, 表示将发送给该follower的下一个日志的index,当选leader后,将其初始化为最高index+1 。如果follower与leader的日志不一致,AErpc失败,leader递减nextindex并重试,最终找到匹配的点,这将删除follower中不匹配的日志。

减少拒绝的AErpc的数量

follower拒绝AErpc请求,在回复中包含冲突日志所在term和该term下第一个日志的index,leader递减nextindex避开冲突任期中的所有冲突条目。这样,每个有冲突的任期一个rpc,而不是每条冲突的日志一个rpc 。当然,这并不是说,某条日志冲突,其所在任期内所有日志都是冲突的,这样做只是为了减少rpc.

6.5 日志复制流程

日志复制/心跳

6.5.1 leaderHearBeatTicker

负责查看是否该发送心跳了,如果该发起就执行doHeartBeat。

6.5.2 doHeartBeat

实际发送心跳,声明领导者的存在。

如果需要发送日志:

  • 需要发送的日志已经被快照,发送快照 leaderSendSnapShot
  • 需要发送的日志已经没有被快照,发送日志 sendAppendEntries

发送快照:

raftRpcProctoc::InstallSnapshotRequest args;
args.set_leaderid(m_me);
args.set_term(m_currentTerm);
args.set_lastsnapshotincludeindex(m_lastSnapshotIncludeIndex);
args.set_lastsnapshotincludeterm(m_lastSnapshotIncludeTerm);
args.set_data(m_persister->ReadSnapshot()); // 数据为kvserver的snapshot

 发送日志/心跳:

心跳不包含日志,其余相同

std::shared_ptr<raftRpcProctoc::AppendEntriesArgs> appendEntriesArgs = std::make_shared<raftRpcProctoc::AppendEntriesArgs>();
appendEntriesArgs->set_term(m_currentTerm);
appendEntriesArgs->set_leaderid(m_me);
appendEntriesArgs->set_prevlogindex(preLogIndex);
appendEntriesArgs->set_prevlogterm(PrevLogTerm);
appendEntriesArgs->set_leadercommit(m_commitIndex); // leader已经提交的最新的log的index
appendEntriesArgs->clear_entries();
//下面要添加日志

6.5.3 sendAppendEntries

负责发AppendEntries RPC,在发送完rpc后还需要负责接收并处理对端发送回来的响应。

调用follower的服务,让follower复制日志:

m_peers[server]->AppendEntries(args.get(), reply.get());

收到回应后,

比较term:

  • reply->term() > m_currentTerm,当前结点变为follower
  • reply->term() < m_currentTerm,不会出现这种情况,如果对方term比自己小,会自动变为和当前节点相等
  • reply->term() == m_currentTerm

判断reply->success:

  • reply->success == false:follower添加日志失败,根据follower的返回值,回缩nextindex或后移nextindex
  • reply->success == true: follower添加日志成功,更新matchindex和nextindex; 如果大多数节点复制了日志,且当前term有日志提交,更新commitindex
6.5.3.1 Leader不允许提交之前任期的日志

Leader不允许提交之前任期的日志leader只有在当前term有日志提交的时候才更新commitIndex(commitindex更新,意味着被提交,日志可以被应用到状态机) 

当选leader后,之前任期的日志可以发送给其它节点,但即使被大多数节点复制,也不允许提交(更新commitindex)。

论文fig8

论文fig8,如果S1提交了之前任期的日志(c),然后宕机,所提交的日志可能被新的leader覆盖掉(d),那么同一位置的index会提交两次,这是绝不允许的。如果S1在宕机之前把当前term的日志复制给大多数服务器,那么,S5无法赢得选举(e),之前任期的日志也会被提交。

为了处理这种情况,raft规定,不会通过计算副本数目(复制到多少个节点)的方式提交之前任期的日志。只有leader当前term的日志可以通过计算副本数提交。一旦当前任期的日志被提交,那么由于日志匹配的特性,之前任期的日志也会被提交。

但是这样的话,如果当前任期一直没有新的日志产生,之前任期的日志也不会被提交

因此,Raft引入no-op的概念(该日志只包含index和term,不包含指令),当选leader后,立即写入一条no-op日志。当这条日志被复制到大多数节点后,之前任期的日志也会被提交。

6.5.4 AppendEntries 

接收leader发来的AErpc,主要检验用于检查当前日志是否匹配并同步leader的日志到本机。

比较term:

  • args->term() < m_currentTerm,leader过时,拒绝
  • args->term() > m_currentTerm,更新term,并变成follower
  • args->term() == m_currentTerm

下一步,

日志的三种情况:

  1. args->prevlogindex() >  getLastLogIndex(),AE太新了,prelogindex > 当前节点最大index,失败,需要更早的日志, 要求nextindex更新为当前节点日志最大index+1
  2. args->prevlogindex() < m_lastSnapshotIncludeIndex, AE太旧了,prelogindex < 当前节点快照的最大index,失败,要求nextindex更新为当前节点快照的下一个index
  3. m_lastSnapshotIncludeIndex <= args->prevlogindex() <= getLastLogIndex(),尝试复制

第三种情况,尝试复制日志:

首先判断args->prevlogindex()的日志是否和当前节点匹配:

已知 “如果不同节点的日志条目有相同的index和term,那么它们存储相同的指令”,所以判断index和term是否相等;若相等,则可知前面的日志也都匹配。

1.匹配:

复制日志,若follower中已存在的日志和leader不同,用leader的日志覆盖原来的日志

更新commitindex,

 if (args->leadercommit() > m_commitIndex)
{
    // 可能leader提交的日志,本节点还没有
    m_commitIndex = std::min(args->leadercommit(), getLastLogIndex()); 
}

2.不匹配:

要求nextindex回缩。

优化:在当前节点快照之后的范围遍历,从后往前寻找矛盾的term的第一个元素,索引为idx,要求nextindex更新为idx。

6.5.5 leaderSendSnapShot

负责发送快照的RPC,在发送完rpc后还需要负责接收并处理对端发送回来的响应。

调用follower的服务,让follower安装快照:

m_peers[server]->InstallSnapshot(&args, &reply);

比较term:

  • reply->term() > m_currentTerm,当前结点变为follower
  • reply->term() == m_currentTerm

更新commitindex和matchindex:

// 匹配的index为快照的last index
m_matchIndex[server] = args.lastsnapshotincludeindex(); 
m_nextIndex[server] = m_matchIndex[server] + 1;

6.5.6 InstallSnapshot

接收leader发来的快照请求,同步快照到本机。

比较term:

  • args->term() < m_currentTerm, 直接返回
  • args->term() > m_currentTerm, 更新term
  • args->term() == m_currentTerm

应用快照:

  • 当前节点的快照比leader快照更新,直接返回。
  • 当前节点日志比leader的快照新,把快照部分日志清除;
  • 当前节点日志比leader的快照旧,清除当前节点所有日志,应用leader的快照。

更新m_commitIndex,m_lastApplied,m_lastSnapshotIncludeIndex,m_lastSnapshotIncludeTerm

7 applierTicker

定期向kvserver写入日志:向applyChan写入已经提交但还未应用的logs

kvserver从applyChan读取消息,判断是command还是snapshot:

        command:执行

        snapshot:使用快照恢复kvserver的状态信息和kvdb

8 快照

8.1 什么时候执行快照

KvServer::GetCommandFromRaft中,每次kvserver执行完一条指令后,会判断raftstat文件大小是否大于阈值,需要快照,执行Raft::Snapshot。

Raft每次发生变化,执行persist(),会将raft节点持久化到raftstat文件中。

8.2 Raft::Snapshot

丢弃已经被应用到kvdb的logs ,然后,持久化raft节点snapshot

8.2.1 snapshot

snapshot是kvserver的序列化数据,包含kvserver的状态信息和kvdb的序列化数据8.2.2

8.2.2 持久化

  • 写入raftstatePersisti.txt:   Raft节点状态信息(state/term/votedfor)  和  m_logs中所有logs  
  • 写入snapshotPersisti.txt:  snapshot.

每次写入之前都先清空文件

8.2.3 m_logs + kvdb

kvdb保存log执行的结果,所以执行到kvdb的logs可以用kvdb来表示。

因此,所有日志 = m_logs + kvdb的序列化snapshot。

Raft执行Snapshot,会持久化raft节点和snapshot。结束后,m_logs中的日志都是没有应用到kvdb的。

Raft执行persist,持久化raft节点。可能m_logs的部分日志已经应用到kvdb,但是m_logs中数据还在,没有被丢弃。此时,仍然满足所有日志 = m_logs + kvdb的序列化snapshot

8.2.4

每台机器独立进行快照,自己判断是否需要快照。

Leader会给follower发送快照,follower收到后要进行处理。

8.3 什么时候Raft向applyChan写入

Command类型:定时写入已提交未应用的日志(封装成ApplyMsg),kvserver取出后应用

Snapshot类型:Follower收到leader的InstallSnapshot;follower raft将快照封装为ApplyMsg,写入applychan,传递给kvserver,kvserver使用snapshot恢复kvdb

8.4 什么时候raft节点读取快照,恢复自身

Raft::init():从raftStateFile(raftstatePersisti.txt)中读取节点的序列化状态数据,反序列化,并以此初始化raft节点.

节点崩溃恢复是不是也需要?项目中暂时没涉及

8.5 什么时候kvserver节点读取快照,恢复自身

1.构造函数中,读取snapshotFile,反序列化数据,恢复kvserver和kvdb的状态

2.kvserver收到快照消息:follower raft节点收到leader的InstallSnapshot,会将快照封装为ApplyMsg发送给kvserver。Kvserver收到快照,根据snapshot恢复kvserver.  Follower Raft会丢弃快照中的日志。

9 时间和可用性

  • broadcastTime:集群中节点与其它节点并行发送rpc并接收响应的平均时间,底层系统属性,一般0.5毫秒到20毫秒不等
  • electionTimeout:选举超时时间
  • MTBF:单个服务器两次故障之间的平均时间,一般是几个月

要求:broadcastTime<<electionTimeout<<MTBF

electionTimeout比broadcastTime大一个数量级,保证有足够的时间来完成心跳;

electionTimeout应比MTBF小几个数量级,以使系统稳定运行。当leader崩溃时,该系统将在大约electionTimeout时间内不可用;

electionTimeout设置为10毫秒到500毫秒之间

10 kvserver 与 线性一致性

10.1 kvserver

 kvserver负责接收和响应外部请求;沟通raft节点和kvdb。

//kvServer和raft节点的通信管道
std::shared_ptr<LockQueue<ApplyMsg> > applyChan; 

//kvDB,作为kvserver类的成员
std::unordered_map<std::string, std::string> m_kvDB; 

10.2 一致性

一致性(线性一致性):保证写操作完成后任何后续访问都能读到更新后的值

一致性:写操作完成后,不能保证后续的访问都能读到更新后的值。

最终一致性:保证如果对某个对象没有新的写操作了,最终所有后续访问都能读到相同的最近更新的值。

10.2.1  线性一致性

一次rpc请求是一个过程,请求的指令在这个过程的某一个瞬间恰好执行一次。这一个瞬间可以是请求过程的任何位置。

client C读取的一定是2,

client D读取的可能是1,也可能是2.

可以推出的是:只要读操作发生在写操作完成之后,那么读到的一定是写操作完成之后的结果。

10.3 raft怎样实现线性一致性读

 leader收到client的请求,封装成log,复制到所有节点中,然后提交、应用。

raft一致性算法可以保证不同节点的log数组是一致的,但后面的状态机(kvdb)的一致性,raft算法并没有做详细规定,用户可自由实现。

所有写命令都要交给leader处理,真正的关键点在于读操作的处理方式。

10.3.1 写主读从缺陷分析

假设读操作简单地向follower发起,由于raft的Quorum机制(大部分节点成功即可),针对某一个指令在某一时间,集群可能有以下两种状态:

  • 某次写操作的日志尚未被复制到少部分follower,但leader已经将其commit;
  • 某次写操作的日志已经被同步到所有follower,但leader将其commit后,心跳尚未通知到follower,因此follower没有应用该写操作。

这都可能读到过时的数据,不满足线性一致性。

10.3.2 写主读主缺陷分析

问题1

一旦一个log被commit,就响应客户端,并没有限定log应用到状态机后再响应客户端。所以,从客户端的视角,一个写操作执行成功后,下一次读操作可能还会读到旧值。

解决方案:

保证log应用到状态机后再响应客户端。当leader收到读命令使,记录当前commitindex,当applyindex追上commitindex后,再响应客户端。

问题2

发生网络分区,旧leader位于少数派分区中,还未发现自己失去领导权。当多数派分区选出新的leader并执行了写操作,连接旧leader进行读,就会读到旧数据。

解决方案:

响应之前先确认自己的leader地位,可以向其它节点发送心跳。

10.3.3 Log Read 

一个简单的、合理的办法。

为确保leader处理读操作时仍拥有领导权,将读命令也封装为一个log,走一遍raft复制日志的流程

依次应用log,将应用的结果返回给客户端。

log在每个节点中的顺序一致,那么各个节点应用每个log的顺序也自然是相同的。

为什么这种方案满足线性一致?

这个方法根据commitindex对所有请求进行排序使每个请求都能反映出状态机执行完前一个请求的状态,必然符合线性一致性。该方法简称Log Read,很明显,性能较差。

一方面,读写操作开销几乎相同。

另一方面,所有操作都线性化,无法并发读状态机。(读操作之间不能并行,必须等前一个读操作完成之后才能进行下一个读操作)

本项目使用这种方法。

10.4 raft读性能优化

10.4.1 Read Index

与Read Log相比,Read Index省掉了读操作同步log的开销,能够大幅提升吞吐,一定程度降低读的时延。大致流程:

  1. leader收到读请求,记录当前commitindex,称之为read index;
  2. leader向follower发起一次心跳,确保领导权,避免网络分区时,少数派分区leader仍处理请求;
  3. 等待状态机至少应用到read index
  4. 执行读请求,返回结果给客户端。

简而言之,需要等待读请求到来时,所有已经提交的请求应用到状态机后,就能执行读请求。

能保证线性一致性吗?

        一个写操作在读操作之前,执行读的时候,写操作未完成。好像也违反线性一致性。

10.4.2 Lease Read

设置一个比选举超时更短的时间作为租期,在租期内可以相信其它节点一定没有发起选举,集群也就不存在脑裂。

所以这个时间段内可以直接读,超出这个时间段执行Read Index流程,Read Index的心跳包也会更新租期。

10.4.3 Follower Read

前两种优化方案,核心思想有两点:

  • 保证读操作到来时最新的commitindex对应的日志已经被应用
  • 保证读取时leader仍拥有领导权

读操作最终还是由leader来承载的。

一个可行的读follower方案

follower收到读请求时,向leader询问当前最新的commit index,

由于leader中所有日志最终都会被同步到follower,所以follower只需要等待自身的该日志被commit并apply到状态机,然后响应客户端。

为了保证线性一致性读,仍然要依赖leader。

简而言之,需要等待读请求到来时,leader所有已经提交的请求,在follower中被应用到状态机后,就能执行读请求。

10.5 raft可能会多次执行某一条指令

比如,leadr在执行完命令,响应rpc之前,宕机了。开始选举,新leader当选,其日志中一定包含该指令。但客户端由于没有收到回复,会重新发送指令,导致指令被重复执行。

解决办法:

<clientId, commandId>

客户端为每条指令分配唯一递增的序列号kvserver记录每个客户端最新的指令的序列号。kvserver如果收到已经执行的指令,立即响应且无需执行。

11 集群成员变更

12 优化

        第六篇:辅助功能 lockQueue defer boost序列化

        leader可以初始化一个线程池,不用每次都创建新线程

        增加日志系统        

        rpc优化

        follower read

  • 10
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值