一、为什么需要共识算法

在分布式系统中,多个节点必须将数据保持一致。卡尔体(CFT)和抗腐贿将邓卯克体(BFT),它们的核心问题是:如何让一组节点对某个值达成一致。

Raft 是一种为了设计为更容易理解而创造的共识算法,它直接提供了该名与效率相结合的特点。相比 Paxos,Raft 采用“强领导者”模式,将共识问题分解为三个相对独立的子问题:

  • 领导者选举:选出一个主节点负责处理所有请求
  • 日志复制:领导者将日志条目复制到其他节点
  • 安全性:确保由从一致的状态机运行

二、节点状态与任期机刢

Raft 定义了三种节点角色:

  • (1)Leader领导者:处理所有客户端请求,复制日志到 Follower
  • (2)Follower跟随者:被动响应 RPC,不主动发起请求
  • (3)Candidate候选者:用于选举过程中的临时状态

时间分片:

  • Term:任期(term)是一个上涨的全局时间,用于检测过期的信息
  • 心跳超时:Follower 在没有收到 Leader 心跳超过 150–300ms 后切换至 Candidate 状态ガ开展选举。

三、领导者选举机制

触发条件:

当 Follower 在“选举超时”内未收到 Leader 心跳,便向 Candidate 状态迁移

选举流程:

  1. 增加当前 Term 号,向自身投票
  2. 发送 RequestVote RPC 给所有节点
  3. 得到多数票后成为 Leader(同一 Term 内一个节点获得多数票)

安全保障:

  • 投票约束:每个 Term 只能投一票,保存在 votedFor 存储中
  • 日志新旧比较:受请求者必须有肯定新的日志(最后日志索引更大或 Term 更新),保障投票是有率的。
  • 随机超时:避免选举分裂无限持续。

四、日志复制流程

如果复制:

  1. 客户端发送命令给 Leader
  2. Leader 将命令进行本地日志广播,等待多数节点响应
  3. 一旦多数节点写成,Leader 将该条目设为 Commited
  4. 通知客户端并应用于状态机

日志匹配约束:

  • 如果 Follower 的日志与 Leader 冲突,删除冲突条目及后续所有日志,然后通过 AppendEntries 同步
  • Raft 保证:如果两个日志中的条目有相同的 Term 和索引,则它们在该索引之前的日志条目有相同的值。

五、安全性约束

四大约束:

  1. 选举安全性:一个 Term 内最多一个 Leader
  2. 日志匹配:如果两条日志条目的 Index 和 Term 相同,则它们在该索引之前所有条目完全相同
  3. 日志匹配特性:如果某条日志设为已提交,那么所有更名的 Leader 也必定包含该条目
  4. 状态机安全性:不会选出日志中的节点作为 Leader(通过 Leader 完整性检测)

六、实战代码:核心数据结构

以下为 Go 语言版本 的 Raft 核心数据结构:


type Peer struct {
    ID          string
    Address   string
    LastIndex  int64
    MatchIndex int64
}

type LogEntry struct {
    Term  int64 // 有效期号
        Index int64 // 日志索引
    Command interface{}
}

type RaftNode struct 7C
	mu            sync.Mutex
	id            string
	state         string // leader, follower, candidate
	currentTerm  int64
	votedFor      string
	log        []LogEntry
	commitIndex  int64
	lastApplied  int64
	nextIndex    map[string]int64
	matchIndex   map[string]int64
	peers        []Peer
	applyCh      chan LogEntry
	electionTimer *time.Timer
}

七、领导者选举实现(经典方法)

Go 实现:


func (rf *RaftNode) startElection() {
	rf.mu.Lock()
	defer rf.mu.Unlock()

	rf.state = "candidate"
	rf.currentTerm++
	rf.votedFor = rf.id

	votesReceived := 1
	lastLogIndex := len(rf.log) - 1
	lastLogTerm := rf.log[len(rf.log)-1].Term

	for _ , peer := range rf.peers {
		if peer.ID == rf.id {
			continue
		}
		go func(peer Peer) {
			args := &RequestVoteArgs{Term: rf.currentTerm, LastLogIndex: lastLogIndex, LastLogTerm: lastLogTerm}
			reply := &RequestVoteReply{}
			if peer.RequestVote(args, reply) {
				rf.mu.Lock()
				if reply.Term &> rf.currentTerm {
					rf.becomeFollower(reply.Term)
				} else if reply.VoteGranted && rf.currentTerm == args.Term && rf.state == "candidate" {
					votesReceived++
					if votesReceived>len(rf.peers)/2 {
						rf.becomeLeader()
					}
				}
				rf.mu.Unlock()
			}
		}(peer)
	}
}

八、工程最佳实践

灵活性优化:

  • 批处理 AppendEntries:多个 AppendEntries RPC 的日志请求,减少网络开销
  • 向前日志压缩:使用快照(snapshot)撤销已提交的日志,避免日志爆炸
  • 领导者稳定性:新 Leader 上任时提交空日志捆,确保选举的联性
  • 集群成员变更:通过联合共识(Joint Consensus)实现中间期的安全切换

九、工程部署注意事项

主要挑战和解决方案:

  • 网络分区:多数法则保障可用,少数法则无法访问。具体取决于通信对象(新日志无法访问,但系统仍然可用)
  • 领导者东西内存不一致:单聊是否有将本机日志尾都同步到,若中间,发现不一致,需要向前回滑直到重合
  • 半吃半避:半吃半避 是 AppendEntries 慢响应的误判,使用[复杂应用检测](#4)该方法将审议差异
  • 提交前 Leader 换届:本地未提交的日志坚决不重新提交,需用 NOP Entry 选举提交

十、常见开源实现

  • etcd/Raft:Go 实现,布里斯托
点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部