MIT6824——lab2(实现一个Raft库)的一些实现,问题,和思考

news2024/11/19 6:14:47

MIT 6824 关于lab2的实现,由于开源许可的问题,代码暂时不开源,下面是自己在实现过程中的思路,遇到的问题,以及总结

1 总结

1.1 raft整个流程

  • 应用程序:kv数据库
  • 启动raft库,选举leader,对应领导人选举 RequestVote RPC
  • 客户端发送请求给kv数据库,kv数据库将请求给到leader Start(),由leader将其添加到自己的log,并通知raft节点添加log,对应日志复制 AppendEntires RPC
  • 如果leader收到了过半raft节点日志复制成功的消息,将会通知kv数据库执行该log指令。 applyCh管道
  • 当kv数据库累计执行了一定数量的log,会在一个快照点生成一个快照,然后通知leader将快照点之前的log都删除 Snapshot(),leader再通知其他raft节点安装快照 InstallSnapshot RPC,其他raft节点安装完快照后,就通知其上面的kv数据库执行快照 applyCh管道,kv数据库执行快照后,再通知这个raft节点调整log CondInstallSnapshot()

1.2 领导人选举

  • raft节点会有三种状态,Follower,Candidate,Leader

    const (
    	StateFollower      = 1
    	StateCandidate     = 2
    	StateLeader        = 3
    )
    
  • 什么时候会发生选举?

    • raft中有个随机定时器,当定时器超时,该节点还未收到leader发来的消息,就认为leader宕机,此时raft成为Candidate,并开始选举
  • 心跳包的时间和随机定时器

    • 这里心跳包设置的是100ms,每隔100ms,leader都会和其他raft节点进行通信。
    const (
    	HEART_BEAT_TIMEOUT = 100
    )
    
    • 随机定时器必须要>HEART_BEAT_TIMEOUT ,这样raft节点才是正常进入选举的时候,这里将其设置为[200ms, 300ms]
    // 随机选举定时器 心跳包时间*2 + rand(心跳包)  = [200, 300]
    electionTimeout := time.Duration(
    			HEART_BEAT_TIMEOUT*2+rand.Intn(HEART_BEAT_TIMEOUT),
    ) * time.Millisecond
    
  • Candidate如何进行选举?

    • 将自己的状态设置为Candidate,投自己一票,当前任期++
    if rf.state != StateLeader && time.Since(rf.lastHeartBeatTime) >= electionTimeout {
    			rf.ChangeState(StateCandidate)
    			rf.currentTerm += 1 // 当前任期号+1
    			rf.votedFor = rf.me // 投自己一票
    			rf.voteCount = 1
    			rf.lastHeartBeatTime = time.Now() // 刷新心跳包,每次开始选举,重新计时
    			...
    }
    
    • 通知其他raft节点,并统计投票,如果过半,就成为Leader。这里通知其他raft节点使用**ReqeustVote()**方法,我们使用了并行的方式进行,这样不会出现每次请求都需要等待其他raft节点的回复。

      // 给其他raft节点发送RequestVote RPC请求
      for peer := range rf.peers {
      	if peer == rf.me {
      		continue
      	}
      	go rf.sendRequestVote(peer)
      }
      
    • raft节点收到消息,需要判断是否给出投票,这里论文中给出了两条限制,必须要满足其一,才给出投票

      • 第一,Candidate最后一个日志条目的Term > Follower最后一个日志条目的Term
      • 第二,候选人最后一个日志条目的Term == Follower最后一个日志条目的Term && len(候选人log) ≥ len(Follower的log)
      // 判断Follower是否投票
      // 有两个限制
      func (rf *Raft) isLogUpToDate(candidateLastLogTerm int, candidateLastLogIdx int) bool {
      	followerLastLogTerm := rf.GetLogEntry(-1).Term
      	followerLastLogIdx := rf.GetLogEntry(-1).Index
      
      	if candidateLastLogTerm > followerLastLogTerm {
      		return true
      	} else if candidateLastLogTerm == followerLastLogTerm && candidateLastLogIdx >= followerLastLogIdx {
      		return true
      	}
      
      	return false
      }
      
      func (rf *Raft) RequestVote(args *RequestVoteArgs, reply *RequestVoteReply) {
      	// Your code here (2A, 2B).
      	rf.mu.Lock()
      	defer rf.mu.Unlock()
      	defer rf.persist()
      	//defer DPrintf("{Node %v}'s state is {state %v,term %v,commitIndex %v,lastApplied %v,firstLog %v,lastLog %v} before processing requestVoteRequest %v and reply requestVoteResponse %v", rf.me, rf.state, rf.currentTerm, rf.commitIndex, rf.lastApplied, rf.GetLogEntry(0), rf.GetLogEntry(-1), args, reply)
      
      	// 论文中的figure 2
      	// 1. 候选人的currentTerm < Follower的currentTerm,拒绝投票
      	if args.Term < rf.currentTerm {
      		reply.Term, reply.VoteGranted = rf.currentTerm, false
      		return
      	}
      
      	// 候选者的currentTerm > Follower的currentTerm,需要更新Follower的currentTerm
      	if args.Term > rf.currentTerm {
      		rf.ChangeState(StateFollower)
      		rf.currentTerm = args.Term
      		rf.votedFor = -1
      	}
      
      	// 选举限制,满足Follower才投票
      	// 1. Follower还没投过票 || 已经给该候选人投过票了
      	// +2. 候选人最后一个日志条目的Term > Follower最后一个日志条目的Term
      	// +3. 候选人最后一个日志条目的Term == Follower最后一个日志条目的Term && len(候选人log) >= len(Follower的log)
      	//DPrintf("rf.isLogUpToDate(args.LastLogTerm, args.LastLogIndex) == %v", rf.isLogUpToDate(args.LastLogTerm, args.LastLogIndex))
      	if (rf.votedFor == -1 || rf.votedFor == args.CandidatedId) &&
      		rf.isLogUpToDate(args.LastLogTerm, args.LastLogIndex) {
      
      		// 给候选人投票
      		rf.votedFor = args.CandidatedId
      
      		// 每次完成投完票,应该重新设置随机选举定时器
      		rf.lastHeartBeatTime = time.Now()
      		reply.Term, reply.VoteGranted = rf.currentTerm, true
      		return
      	}
      
      	reply.Term, reply.VoteGranted = rf.currentTerm, false
      	return
      }
      
    • Candidate统计投票,当有过半票同意当选,Candidate成为Leader,然后再通过心跳包通知其他raft节点。

      // 有一票
      		if voteGranted {
      			rf.voteCount += 1
      		}
      
      		// 过半票决
      		//DPrintf("投票:%v", grantedVotes)
      		if rf.voteCount >= len(rf.peers)/2+1 {
      			//DPrintf("{Node %v} receives majority votes in term %v", rf.me, rf.currentTerm)
      			rf.ChangeState(StateLeader)
      
      			// 成为Leader后,需要发送心跳包(AppendEntries)通知其他Follower
      			// rf.mu.Unlock()
      			// rf.BroadcastAppendEntries()
      
      			for i := 0; i < len(rf.peers); i++ {
      				rf.nextIndex[i] = rf.GetLogEntry(-1).Index + 1
      				rf.matchIndex[i] = 0
      			}
      			rf.mu.Unlock() // 解锁完,replicateRountine就可以发送添加请求了
      
      			for peer := range rf.peers {
      
      				if peer == rf.me {
      					continue
      				}
      				go rf.sendAppendEntries(peer)
      			}
      
      		}
      

1.2 日志复制

  • 日志复制的流程

    • 客户端→kv数据库→leader,leader需要在log中添加这个请求,并通知其他raft节点添加。
    func (rf *Raft) Start(command interface{}) (int, int, bool) {
    	rf.mu.Lock()
    	defer rf.mu.Unlock()
    
    	index := rf.GetLogEntry(-1).Index + 1
    	term := rf.currentTerm
    	isLeader := rf.state == StateLeader
    
    	// leader每增加一个日志,就需要发送一次AppendEntries请求
    	if isLeader {
    		rf.logs = append(rf.logs, LogEntry{
    			Command: command,
    			Term:    term,
    			Index:   index,
    		})
    
    		rf.matchIndex[rf.me] = index
    		rf.nextIndex[rf.me] = index + 1
    		rf.persist()
    
    		rf.mu.Unlock()
    
    		for peer := range rf.peers {
    			if peer == rf.me {
    				continue
    			}
    			go rf.sendAppendEntries(peer)
    		}
    		rf.mu.Lock()
    	}
    	
    	return index, term, isLeader
    }
    
  • Leader需要保证Follower和自己保持一样的log,这样kv数据库的状态才能保证一致性。

    • 这里有几个变量需要注意
      • nextIndex[peer]:leader应该和哪个peer的log槽位比
      • preLogIndex:nextIndex - 1
      • preLogTerm:preLogIndex对应的term
      • entries:leader的preLogIndex后面的log
      • commitedIndex:已经提交给kv数据库的log槽位号
    • Follower中,必须要保证preLogTerm == Follower在preLogIndex的term,因为RAFT规定,如果Leader.log[i].term == Follower.log[i].term,则i之前的log一定是一致的。因此,如果preLogTerm != Follower在preLogIndex的term,则可能导致之前的log不一致性。
    • 当满足上面的条件
      • Follower会将preLogIndex后面的log复制为entries。
      • 同时,根据commitedIndex,Follower会通知kv数据库执行log
    • 如果不满足以上条件,Follower会返回一些信息,加速日志恢复
      • ConflictTerm:Follower在preLogIndex的term,没有就是-1
      • ConflictIndex:Follower在preLogIndex的term,第一次出现的槽位号
      • Leader再根据这些信息进行调整,其实包含了三种情况
        • case1
          在这里插入图片描述
          在这里插入图片描述

        • case 2
          在这里插入图片描述
          在这里插入图片描述

        • case 3
          在这里插入图片描述
          在这里插入图片描述

  • Leader得到过半票的日志复制成功消息时,通知kv数据库执行自己term的log

1.3 持久化

  • RAFT规定了对三种数据的持久化

    • 每次对这些数据进行了改变都应当做持久化
    // 持久化存储
    currentTerm int        // 最新的任期服务器
    votedFor    int        // 已经为谁投过票
    logs        []LogEntry // 日志
    

1.4 快照

  • 长时间来看,日志要的存储量比kv数据的状态大。另外,每次宕机后,服务器都从头开始执行log,那效率一定很低。
  • 快照就是kv数据的状态,当kv数据库可以在一定时间确认一个快照点,生成一个快照,leader就可以在log中删除该快照点之前的log。
  • leader需要将快照发给follower,follower再把快照给kv数据库,如果kv数据库接受了这个快照,会通知follower调整log

2 问题

2.1 领导人选举

  • 为什么要用过半票决?
    • 防止脑裂问题,导致的不一致性
    • 当有一半的raft节点还在工作时,集群就可以使用,这是一种容错机制
    • 新leader和旧leader一定有交集的raft节点,出现网络分区时,新leader会通过这些交集的raft节点告诉旧leader他不在是leader了。
  • 这里为什么使用的是随机定时器?
    • 为了防止raft节点同时进行选举,每个节点都给自己投票,拒绝其他节点,投票分离,所有节点都不可能得到过半投票。
  • 如果选举失败会怎么办?
    • 会开始新的一轮选举,这个过程中,客户端的请求会无法得到响应。

2.2 日志复制

  • 如果日志复制过程中,Leader宕机了怎么办?如何保证Follower的log一致性?
    • 这个时候raft集群一定有节点的log已经不一致了。
    • 需要重新进行选举,新选举出来的Leader会保证过半节点的log一致性

2.3 持久化

  • 为什么要对这三种数据做持久化?
    • currentTerm:保证集群只有一个leader。需要知道自己当前的任期。这有一个反例
      在这里插入图片描述

    • votedFor:保证集群只有一个leader。如果r1已经给leader投过票,但之后r1宕机,之后r1又重启,r2此时也发来一个选举请求,如果此时没有读取votedFor,r1又会给r2投一票,此时集群可能就有两个leader.

    • logs:保存了kv数据库的状态,保存下来,下次重启才能恢复宕机时的状态

2.4 快照

  • Leader为什么还要给Follower发送快照?
    • 因为网络,Leader在发送AppendEntires的过程中出现宕机,导致Follower的log比较短(短于快照点),这样,如果leader此时删除了快照点之前的log,那Follower就再也无法得到少的那段log。因此为了保证一致性,Leader需要给Follower发送快照。

2.5 其他问题

  • raft宕机重启后加入集群,如何恢复自己的状态。
    • 此时raft会读取自己的log,然后leader会和它通信,通过AppendEntires,保证日志一致性
  • 当leader和副本都关机,重启后,如何恢复服务器状态
    • 都会立即读取log,但不执行。此时需要先选举leader,之后leader再和副本进行沟通,找到过半副本中最近的log点,然后leader从头执行,之后给副本发送commited号,副本再接着恢复。
  • RAFT是如何保证一致性的?
    • 日志复制保证日志的一致性

3 实验中遇到的困难

3.1 处理如何发数据的问题

  • 刚开始使用的时候,没考虑那么多,直接就是一整个业务逻辑放在一起,顺序执行。比如选举领导人需要给其他的节点的发送RequestVote请求,每次发完统计一次票数,当票数过半时,再确定Leader。这种方式很慢,后一次RPC请求都要等前一次请求返回。后面针对于每次发请求,我们都开一个协程来,这样并行的方式效率更高。

3.2 使用锁的一些问题

  • 因为项目中遇到了多线程,因此会涉及到对一些共享数据加锁。在实际的过程中,我习惯用defer去释放锁,这就导致每次发送RPC请求的时候,还拿着这把锁,这就导致其他的线程无法立即给其他的raft节点发送RPC请求,这样我们的并行就失效了,这就导致了超时。后面发现需要在发RPC请求之前就应该先把锁放掉。

3.2 针对很大的快照包,如何实现发送

  • Leader每次给raft的快照包可能比较大,很显然一次性传给Follower很慢。所以这里采用了分包的方式,然后开了3个线程去发送这些包,这里主要参考了TCP的分包的思路。。。。。后面再补充

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/537387.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

跳槽前,把自己逼成卷王...

前段时间席卷全互联网行业的内卷现象&#xff0c;想必有不少人都深陷其中。其实刚开始测试行业人才往往供不应求&#xff0c;而在发展了十几年后&#xff0c;很多人涌入这个行业开始面对存量竞争。红利期过去了&#xff0c;仅剩内部争夺。 即便如此&#xff0c;测试行业仍有许…

AspNetCore中的配置文件详解

1 配置文件 程序开发中&#xff0c;有些信息是要根据环境改变的&#xff0c;比如开发环境的数据库可能是本地数据&#xff0c;而生产环境下需要连接生产数据库&#xff0c;我们需要把这些信息放到程序外面&#xff0c;在程序运行时通过读取这些外部信息实现不改变程序代码适应…

计算机图形学-GAMES101-8

引言 着色是针对某一个点(片段)的应用&#xff0c;这里需要考虑着色的频率。  漫反射项代表光向四面八方均匀的反射出去&#xff0c;和观察方向无关。  Blinn-Phong反射模型结构如下&#xff1a; ) 一、Blinn-Phong模型 &#xff08;1&#xff09;Specular 什么时候才能看到…

SpringBoot实操篇1

一、工程打包与运行&#xff08;windows版&#xff09; 在浏览器中就可以访问到了&#xff0c;此时IDEA并没有启动。服务器就是命令行窗口。 跳过测试&#xff1a;可以看到多了很多数据&#xff0c;是因打包的时候将功能测试了一遍。在IDEA中可以关掉。 注意&#xff1a;必须…

nginx+php+mysql安装以及环境的搭建

目录 一、nginx的安装 二、php的下载安装 1.进入到/usr/local/下&#xff0c;下载php的安装包 2.解压 3.进入到php-8.2.6下&#xff0c;安装需要的依赖包 4.预编译php 5.编译 6.为php提供配置文件 7.为php-fpm提供配置文件 8.添加用户和用户组 9.修改php-fpm.conf配置…

JavaScript全解析-this指向

this指向&#xff08;掌握&#xff09; ●this 是一个关键字&#xff0c;是一个使用在作用域内的关键字 ●作用域分为全局作用域和局部作用域&#xff08;私有作用域或者函数作用域&#xff09; 全局作用域 ●全局作用域中this指向window 局部作用域 ●函数内的 this, 和 函…

OS之作业调度算法

目录 一、基本概念 二、先来先服务算法(FCFS) 三、短作业算法(SJF/SPF) 四、轮转调度算法(RR) 五、优先级调度算法 六、多级反馈队列调度算法 一、基本概念 T(周转)T(完成)-T(到达) 二、先来先服务算法(FCFS) 不利于短作业&#xff0c;非抢占式算法 算法思想&#xff…

Linux日志文件服务器搭建

文章目录 Linux日志文件服务器搭建节点规划案例实施(1)修改主机名(2)配置日志服务器(3)重新启动查看rsyslogd(4)配置客户端(5)测试 Linux日志文件服务器搭建 节点规划 IP主机名节点192.168.100.10serverlog日志服务器192.168.100.20clientlog日志客户端 必须两台机器可以ping…

IPv6之组播地址分类

本文目录 1、IPv6组播地址的结构2、特殊的预留地址和预留组播地址 1、IPv6组播地址的结构 IPv6组播地址是由固定的8bit地址前缀FF::/8&#xff0c;4bit的标志位&#xff0c;4bit组播范围和112bit多播组标识符&#xff08;组ID&#xff09;组成 FF::/8 IPv6的组播地址的最高8bi…

linux环境下设置python定时任务

linux环境下设置python定时任务 Linux 系统提供了使用者控制计划任务的命令 :crontab 命令 1、在linux环境执行命令,进入编辑界面 crontab -e2、按键盘 i 键&#xff0c;进入编辑模式&#xff0c;输入以下内容&#xff0c;设置2个定时任务 定时任务1&#xff1a;每隔10分钟执…

MindFusion.JavaScript Pack 2023.R1 Crack

图表控件添加了径向树布局和套索缩放工具。 2023年5月17日-10:53新版 特征 JavaScript图表中的新增功能 径向树布局-添加了新的类&#xff0c;它将树级别排列在围绕根的同心圆中。 套索缩放工具-控件现在支持使用套索工具进行缩放的几种方法&#xff1a; 可以将行为属性设置为…

单点登录协议

认证和授权 认证&#xff1a;确认该用户的身份是他所声明的那个人 授权&#xff1a;根据用户身份授予他访问特定资源的权限 当用户登录应用系统时&#xff0c;系统需要先认证用户身份&#xff0c;然后依据用户身份再进行授权。认证与授权需要联合使用&#xff0c;才能让用户真…

浏览器网络请求——HTTP详解

文章目录 HTTP 是什么HTTP 发展历程HTTP 1.0HTTP 1.1HTTP 2.0 常用方法头部信息 (Headers)请求头&#xff08;request Headers响应头&#xff08;response Headers&#xff09; 状态码HTTP无状态理解&#xff1a;cookie与session总结 HTTP 是什么 HTTP&#xff08;Hyper Text T…

【Linux】2.4 第一个小程序——进度条(C语言)

文章目录 character缓冲区的问题&#xff1a;ps. sleep 函数 倒计时进度条1.打印进度条2.让进度条“动起来”3.预留进度条的位置并用提示符显示进度条的状态4.颜色打印只用颜色来表示进度条 character 回车 与 换行 键盘上的 Enter 键&#xff1a; 换行的过程&#xff1a; …

程序员如何成为一名独立开发者?

这里有一个最简单粗暴的方式让你确定你是否能成为一个独立的开发者。 Lv.1 顺畅地完成一个独立外包项目 一个最低成本的试错方式就是去独立地完成一个外包项目&#xff0c;一般来说外包项目的难度较低&#xff0c;但也具有作为开发者必备的大多数流程&#xff0c;如果不确定自…

ChatGPT+小红书爆文,牛!

随着AI技术的不断发展&#xff0c;它已经逐渐渗透到了我们的生活之中&#xff0c;包括内容营销领域。 我们通过AI算法生成文本、优化搜索引擎排名、提高用户体验等&#xff0c;现在AI已逐渐在改变时代的进步&#xff0c;AI也将成为下一个十年的一个变革。我们每个创业者、内容…

Go的开发工具

Go的开发工具 1.VSCode 开源地址: GitHub - microsoft/vscode: Visual Studio Code 官网&#xff1a;https://code.visualstudio.com 好处是免费的&#xff0c;插件多&#xff01;&#xff01;&#xff01; 2.GoLand 收费&#xff0c;是和IDEA是类似的&#xff0c;非常强。…

零基础转行从事云计算运维工作,不得不掌握的几项技能

转行云计算运维已成为今年热门话题之一&#xff0c;面对内卷严重的Java领域&#xff0c;虽然高薪有前景&#xff0c;但是很多人都是望而止步&#xff0c;自己的实力不允许自己卷入这场“高薪职业争夺战”。于是新的IT热门转行职业云计算被重点关注&#xff0c;它会不会成为下一…

如何使用SCQA模型提高表达能力

SCQA架构是“结构化表达”工具。 一、什么是“SCQA架构”&#xff1f;‍ S&#xff08;Situation&#xff09;情景——由熟悉的情境或事实引入 C&#xff08;Complication&#xff09;冲突——指出实际面临的困境或冲突 Q&#xff08;Question&#xff09;疑问——你如何分析…

【开发日志】2023.05 NormalMap Back To Sphere

【开发日志】2023.03.04 ZENO----SimpleGeometry----CreateSphere_EndlessDaydream的博客-CSDN博客CreateSpherehttps://blog.csdn.net/Angelloveyatou/article/details/129178914(4条消息) 【开发日志】2023.04 ZENO----Composite----CompNormalMap_EndlessDaydream的博客-CSD…