背景与介绍
在当今大规模分布式系统的背景下,需要可靠、高可用性的分布式数据存储系统。
传统的集中式数据库在面对大规模数据和高并发访问时可能面临单点故障和性能瓶颈的问题。
为了解决这些问题,本项目致力于构建一种基于Raft一致性算法的分布式键值存储数据库,以确保数据的一致性、可用性和分区容错性。
解决的问题
一致性: 通过Raft算法确保数据的强一致性,使得系统在正常和异常情况下都能够提供一致的数据视图。
可用性: 通过分布式节点的复制和自动故障转移,实现高可用性,即使在部分节点故障的情况下,系统依然能够提供服务。
分区容错: 处理网络分区的情况,确保系统在分区恢复后能够自动合并数据一致性。
什么是共识?
在一个分布式系统中,多个服务器(或节点)通常共享相同的资源或数据状态。共识协议的目标就是确保这些节点在面对不可靠的网络环境或节点故障时,依然能够就系统的某个状态或值达成一致,即共识。
对于本项目中的分布式键值存储系统,共识问题具体表现为:如何确保每个服务器节点对同一个键值对(key-value pair)的状态达成一致。系统中可能有多个请求同时尝试修改同一个键的值,而每个节点的响应时间可能不同。共识协议需要确保,无论外部条件如何,所有节点对最终的数据状态具有一致的看法。
共识的关键特性
一致性(Consistency):
当所有非故障节点在某个时刻达成共识时,它们的决策是相同的。任何已经通过共识的值,即使某个节点故障或重启,也不会被覆盖或修改。Raft算法通过日志复制机制保证这一点。
终止性(Termination):
所有非故障节点在有限的时间内最终会做出决策。这意味着,即使存在网络延迟或部分节点失效,系统仍能够继续运行,并最终做出一个全局一致的决定。
安全性(Safety):
一旦某个值或状态被确认(即通过共识),它就不能被改变,甚至在有新的请求或节点恢复后,这一决策仍然保持不变。Raft协议通过选举机制确保只有合法的领导者才能在集群中提出修改请求,避免并发冲突。
容错性(Fault tolerance):
共识协议在容错分布式系统中的一个主要功能就是能够在部分节点失效的情况下仍然保持数据的一致性。这意味着即便有部分节点崩溃,系统仍然能够通过剩余节点达成共识,保障系统的高可用性。
在Raft协议中的共识
Raft协议通过以下机制来确保共识的达成:
领导者选举:集群中的所有节点会通过选举选出一个领导者(Leader),只有该领导者能够提议状态变更(如对键值的修改)。通过这种方式避免了多个节点同时进行修改的冲突。
日志复制:领导者将修改请求记录在其日志中,并将该日志条目复制到所有跟随者(Follower)节点,确保每个节点拥有相同的操作历史。
一致性检查:一旦日志条目在大多数节点上被复制,领导者会进行一致性检查,确保所有节点都达成共识。之后,修改才被应用到状态机(即数据库)上,从而确保一致性和安全性。
总结
在容错分布式系统中,共识是确保多个节点在不可靠网络条件下达成一致的关键。通过共识机制,系统能够确保所有节点对状态机的状态保持一致,不会因为节点失效或网络分区导致数据不一致或错误的修改。Raft协议通过领导者选举、日志复制和一致性检查来有效地解决共识问题,从而确保了分布式系统的安全性和容错性。
一致性算法的多数派原则
在分布式系统中,集群的容错能力是基于多数派原则的,即需要大多数(n/2 + 1)节点达成共识来保持系统的正常运行。这种设计有以下几个关键点:
- 大多数派原则:系统能够容忍最多小于一半的节点失效而不影响服务。换句话说,集群中只要保持大多数节点是健康的,系统就可以继续达成共识并处理请求。
- 故障容忍:在集群中,如果超过一半的节点失效(例如在5个节点的集群中,3个或更多节点故障),那么系统将无法再达成共识,无法继续处理新的写请求。虽然集群停止对外提供服务,但它仍然不会返回不正确的结果。这就是一致性算法中的安全性保证:在不能保证一致性的情况下,系统宁愿停止工作,也不会返回错误的状态或数据。
- Raft、Paxos等一致性算法都基于多数派原则进行共识决策。即使在网络分区或部分节点失效的情况下,只要大多数节点能够通信,系统就能够继续运行并保持一致性。领导者(Leader)通过多数派投票的方式选举产生,并负责向其他节点同步日志条目。
概念
心跳、日志同步:leader向follower发送心跳(AppendEntryRPC)用于告诉follower自己的存在以及通过心跳来携带日志以同步
首先掌握日志的概念,Raft算法可以让多个节点的上层状态机保持一致的关键是让 **各个节点的日志 保持一致,**日志中保存客户端发送来的命令,上层的状态机根据日志执行命令,那么日志一致,自然上层的状态机就是一致的。
所以raft的目的就是保证各个节点的日志是相同的。
Leader :集群内最多只会有一个 leader,负责发起心跳,响应客户端,创建日志,同步日志。
Candidate leader 选举过程中的临时角色,由 follower 转化而来,发起投票参与竞选。
Follower :接受 leader 的心跳和日志同步数据,投票给 candidate。
Raft是一个强Leader 模型,可以粗暴理解成Leader负责统领follower,如果Leader出现故障,那么整个集群都会对外停止服务,直到选举出下一个Leader。如果follower出现故障(数量占少部分),整个集群依然可以运行。
Term|任期:
Raft将Term作为内部的逻辑时钟,使用Term的对比来比较日志、身份、心跳的新旧而不是用绝对时间。Term与Leader的身份绑定,即某个节点是Leader更严谨一点的说法是集群某个Term的Leader。Term用连续的数字进行表示。Term会在follower发起选举(成为Candidate从而试图成为Leader )的时候加1,对于一次选举可能存在两种结果:
1.胜利当选:胜利的条件是超过半数的节点认为当前Candidate有资格成为Leader,即超过半数的节点给当前Candidate投了选票。
2.失败:如果没有任何Candidate(一个Term的Leader只有一位,但是如果多个节点同时发起选举,那么某个Term
的Candidate可能有多位)获得超半数的选票,那么选举超时之后又会开始另一个Term(Term递增)的选举。
Raft中的重要过程
领导人选举相关
代码分析
Raft的主要流程:领导选举(sendRequestVote RequestVote ) 日志同步、心跳(sendAppendEntries AppendEntries )
定时器的维护:主要包括raft向状态机定时写入(applierTicker )、心跳维护定时器(leaderHearBeatTicker )、选举超时定时器(electionTimeOutTicker )。
持久化相关:包括哪些内容需要持久化,什么时候需要持久化(persist)
Raft::init()函数
这段代码实现了 Raft 协议节点的初始化,包括设置初始状态、恢复持久化数据、初始化 I/O 管理器以及启动处理心跳、选举超时和日志应用的线程或协程。代码使用了现代 C++ 特性,如 std::unique_ptr、std::vector 和 std::thread,并通过 std::lock_guard 管理互斥量的锁定,确保线程安全。
void Raft::init(std::vector<std::shared_ptr<RaftRpcUtil>> peers, int me, std::shared_ptr<Persister> persister,
std::shared_ptr<LockQueue<ApplyMsg>> applyCh) {
m_peers = peers;
m_persister = persister;
m_me = me;
// Your initialization code here (2A, 2B, 2C).
m_mtx.lock();
// applier
this->applyChan = applyCh;
// rf.ApplyMsgQueue = make(chan ApplyMsg)
m_currentTerm = 0;
m_status = Follower;
m_commitIndex = 0;
m_lastApplied = 0;
m_logs.clear();
for (int i = 0; i < m_peers.size(); i++) {
m_matchIndex.push_back(0);
m_nextIndex.push_back(0);
}
m_votedFor = -1;
m_lastSnapshotIncludeIndex = 0;
m_lastSnapshotIncludeTerm = 0;
m_lastResetElectionTime = now();
m_lastResetHearBeatTime = now();
// initialize from state persisted before a crash
readPersist(m_persister->ReadRaftState());
if (m_lastSnapshotIncludeIndex > 0) {
m_lastApplied = m_lastSnapshotIncludeIndex;
// rf.commitIndex = rf.lastSnapshotIncludeIndex todo :崩溃恢复为何不能读取commitIndex
}
DPrintf("[Init&ReInit] Sever %d, term %d, lastSnapshotIncludeIndex {%d} , lastSnapshotIncludeTerm {%d}", m_me,
m_currentTerm, m_lastSnapshotIncludeIndex, m_lastSnapshotIncludeTerm);
m_mtx.unlock();
m_ioManager = std::make_unique<monsoon::IOManager>(FIBER_THREAD_NUM, FIBER_USE_CALLER_THREAD);
// start ticker fiber to start elections
// 启动三个循环定时器
// todo:原来是启动了三个线程,现在是直接使用了协程,三个函数中leaderHearBeatTicker
// 、electionTimeOutTicker执行时间是恒定的,applierTicker时间受到数据库响应延迟和两次apply之间请求数量的影响,这个随着数据量增多可能不太合理,最好其还是启用一个线程。
m_ioManager->scheduler([this]() -> void { this->leaderHearBeatTicker(); });
m_ioManager->scheduler([this]() -> void { this->electionTimeOutTicker(); });
std::thread t3(&Raft::applierTicker, this);
t3.detach();
// std::thread t(&Raft::leaderHearBeatTicker, this);
// t.detach();
//
// std::thread t2(&Raft::electionTimeOutTicker, this);
// t2.detach();
//
// std::thread t3(&Raft::applierTicker, this);
// t3.detach();
}
这个 init 函数负责 Raft 节点的初始化工作,包括:
设置节点的基本信息和状态。
从持久化层恢复状态。
初始化快照相关的变量。
启动处理心跳、选举超时和日志应用的线程或协程。
每一步都为 Raft 协议的正常运行奠定了基础。
在 Raft::init 函数中,两个协程和一个线程分别用于处理 Raft 协议中的三个核心任务:领导者心跳、选举超时和日志应用。每一个任务都对应 Raft 算法的重要机制,下面详细解释这两个协程和一个线程的作用。
m_ioManager->scheduler([this]() -> void { this->leaderHearBeatTicker(); });
作用: 这个协程负责在当前节点是领导者时,周期性地向其他 Raft 节点发送心跳消息,以维持其领导者身份并告知其他节点它仍在工作。
Raft 协议中的意义:
在 Raft 中,领导者必须定期向所有跟随者发送心跳,通常是通过发送空的 AppendEntries RPC。心跳的主要目的是防止跟随者发起选举。
如果跟随者在心跳超时时间内没有收到领导者的心跳消息,它们会认为领导者失效,并尝试发起新的选举。
具体工作:
leaderHearBeatTicker 协程周期性运行,检查当前节点的状态是否是领导者。如果是领导者,它就会向每个节点发送心跳(空的 AppendEntries),确保保持领导者身份。
如果当前节点不是领导者,这个协程将不会发送心跳。
m_ioManager->scheduler([this]() -> void { this->electionTimeOutTicker(); });
作用: 这个协程负责处理选举超时。如果在一段时间内没有收到来自领导者的心跳(即领导者可能宕机),该协程会触发新一轮的选举过程。
aft 协议中的意义:
在 Raft 中,如果跟随者在选举超时时间内没有收到领导者的心跳,它会认为领导者已经失效,于是它会提升为候选者并发起选举。
选举超时是随机的(在一定范围内),以减少多个节点同时发起选举导致冲突的可能性。
具体工作:
electionTimeOutTicker 协程会定期检查当前时间是否超过了选举超时时间。
如果超时且当前节点是跟随者或候选人,那么该节点将提升为候选人,增加 currentTerm,并发送请求投票(RequestVote)消息,启动新的选举。
std::thread t3(&Raft::applierTicker, this);
t3.detach();
作用: 这个线程负责将已经提交的日志条目应用到状态机中。
Raft 协议中的意义:
日志条目是 Raft 协议用于记录客户端请求的核心。日志条目先由领导者提议,然后在多数节点上复制后才能被提交。
一旦日志条目提交,它们需要被应用到状态机上以实现客户端的请求,并产生实际的效果。
具体工作:
applierTicker 线程会定期检查已提交的日志条目(m_commitIndex)。
如果 m_commitIndex 超过了 m_lastApplied,则它会按顺序将尚未应用的日志条目应用到状态机,并更新 m_lastApplied。
这个线程独立运行,不与心跳或选举相关联,因此即使心跳和选举过程中有延迟,日志应用仍会按时进行。
leaderHearBeatTicker 协程:负责领导者定期发送心跳消息,防止重新选举。
electionTimeOutTicker 协程:负责检测选举超时,当没有心跳时触发选举。
applierTicker 线程:负责将提交的日志应用到状态机,处理客户端的请求。
这三个定时器的配合保证了 Raft 算法的正常运行:保持领导者身份、处理故障时重新选举、确保日志顺序性应用。
我的叙述:
这是raft节点的初始化函数,首先根据参数获得了其他raft节点的通信接口、持久化层