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].term==currentTerm,设置commitIndex=N。(只有当前term有日志提交,才更新commitIndex)
4 安全性
4.1 选举安全
在给定的期限内,最多只能选举一位leader。成为leader需要获得大多数选票,且在一轮选举中,每个节点只能投一票。
4.2 只能leader添加日志
日志只能由leader添加,且只能从leader流向follower。leader永远不会覆盖或删除其日志中的条目.
4.3 日志匹配
- 如果不同节点的日志条目有相同的index和term,那么它们存储相同的指令。
- 如果不同节点的日志条目有相同的index和term,这两个日志中该索引之前的条目都是相同的。
4.4 leader完整性
如果某一任期内一条日志被提交,则该条目将出现在之后任期leader中。
leader提交了某一条日志,说明该日志被复制到大多说节点。然后leader宕机,其它节点发起选举。没有复制这条日志的节点的log比大多数节点旧,所以不可能获得大多数选票,不能成为leader .只有复制了上一任leader提交的最新的日志的节点才能成分下一任leader.
4.5 状态机安全性
如果服务器已在其状态机应用给定索引日志条目,则其他任何服务器都不会在该索引处应用其他内容的日志条目 。
应用的日志一定是提交了的,只有leader能提交,然后告诉follower,哪些日志被提交了,follower才可以修改commitindex,然后应用日志到状态机。follower修改commitindex,必须保证日志和leader匹配。所以,所有应用到状态机的日志,一定都是和leader匹配,且leader不会删除或修改自己的日志。因此,可以保证上述状态机安全性。
4.6 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继续保持它的状态直到有以下一种情况发生:
- 它赢得了选举
- 另一个服务器成为了leader
- 没有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投
请求对象:
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),返回结果给客户端。
- 一旦leader创建一个新的条目并提交,这还将提交leader日志中的所有先前条目,包括之前leader创建的条目(上一任leader创建一条日志,只复制到了少数节点,然后宕机;少数节点中一个节点当选leader,包含上一任leader创建的未提交的日志)。
- leader要在AErpc中包含已提交的最高索引(m_commiteindex),告知follower 。follower得知日志已被提交,便将其应用到本地状态机。
- 只要follower的日志和leader不一致,如follower宕机或运行缓慢或丢包,leader重复发送AE Rpc,直到follower的日志和leader保持一致。
6.3 日志机制
用于维护不同服务器上日志的高度一致性。简化了系统的行为并使其更容易预测,且是确保安全性的重要组成部分。
- 如果不同节点的日志条目有相同的index和term,那么它们存储相同的指令。
- 如果不同节点的日志条目有相同的index和term,这两个日志中该索引之前的条目都是相同的。
基于以下事实:
- 在一个term内,只会产生一个leader 。term相同,说明日志来自同一个leader。对同一个index, leader只能创建一条日志。并且,leader永远不会删除或覆盖自己日志中的条目。follower的日志是从leader复制的。
- AppendEntries一致性检查保证日志的一致性。初始时,leader和follower都没有日志,此时,日志是一致的。后续有日志添加,leader在AErpc中包含新日志的前一个日志的term和index (prelogterm 和 perlogindex)。如果follower在其日志中找不到相同term和index的日志,则拒绝AErpc ,表示follower的日志滞后或与leader冲突,leader前移对应follower的nextindex。如果能找到,则表明该日志与leader相同。类似归纳总结,可以保证前面的日志都与leader相同。 简单来说,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,如果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
下一步,
日志的三种情况:
- args->prevlogindex() > getLastLogIndex(),AE太新了,prelogindex > 当前节点最大index,失败,需要更早的日志, 要求nextindex更新为当前节点日志最大index+1
- args->prevlogindex() < m_lastSnapshotIncludeIndex, AE太旧了,prelogindex < 当前节点快照的最大index,失败,要求nextindex更新为当前节点快照的下一个index
- 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节点状态信息 和 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的开销,能够大幅提升吞吐,一定程度降低读的时延。大致流程:
- leader收到读请求,记录当前commitindex,称之为read index;
- leader向follower发起一次心跳,确保领导权,避免网络分区时,少数派分区leader仍处理请求;
- 等待状态机至少应用到read index;
- 执行读请求,返回结果给客户端。
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。
10.5 raft可能会多次执行某一条指令
比如,leadr在执行完命令,响应rpc之前,宕机了。开始选举,新leader当选,其日志中一定包含该指令。但客户端由于没有收到回复,会重新发送指令,导致指令被重复执行。
解决办法:
<clientId, commandId>
客户端为每条指令分配唯一递增的序列号,kvserver记录每个客户端最新的指令的序列号。kvserver如果收到已经执行的指令,立即响应且无需执行。
11 集群成员变更
12 rpc设计
13 序列化
将要持久化的数据序列化为string然后写入磁盘;
rpc消息也序列化:格式?
优化
序列化类
boost库了解
14 优化
第六篇:辅助功能 lockQueue defer boost序列化
leader可以初始化一个线程池,不用每次都创建新线程
增加日志系统