6.824 lab2B raft 记录

news2024/9/28 8:22:30

Raft 2B Task

LOG

Implement the leader and follower code to append new log entries


  • 您的第一个目标应该是传递 TestBasicAgree3B()。 首先实现 Start(),然后编写代码 通过 AppendEntries RPC 发送和接收新的日志条目, 如图 2 所示。发送每个新提交的条目 在每个对等体上的 applyCh 上。
  • 您将需要实施选举 限制(论文第 5.4.1 节)。
  • 您的代码可能具有重复检查某些事件的循环。 不要有这些循环 从那时起,连续执行而不停顿 会减慢您的实现速度,以至于无法通过测试。 使用 Go 的条件变量, 或插入时间。睡眠(10 * 次。毫秒)在每次循环迭代中。
  • 为未来的实验室提供帮助,并编写(或重写)代码 这很清楚。如需想法,请重新访问我们的 “指南”页面,其中包含有关如何执行以下操作的提示 开发和调试代码。
  • 如果测试失败,请查看 test_test.goconfig.go 了解正在测试的内容。config.go 也 演示了测试人员如何使用 Raft API。

您需要一个单独的长期运行的 goroutine,在 applyCh 上按顺序发送已提交的日志条目。它必须是单独的,因为在 applyCh 上发送可能会阻塞;而且它必须是一个单独的 goroutine,否则可能难以确保按日志顺序发送日志条目。 推进 commitIndex 的代码需要启动 apply goroutine;为此,使用条件变量(Go 的 sync.Cond)可能是最简单的方法。

需要修改的函数

在这里插入图片描述

注意这里并没有快照,index直接为log的索引, 没有偏移量,为了以后考虑,直接设为0也可以

Start

使用 Raft 的服务(例如 k/v 服务器)想要就下一个要附加到 Raft 日志的命令启动协议。需要从这里获取entry,即command

如果此服务器不是领导者,则返回 false。否则启动协议并立即返回。

无法保证此命令一定会提交到 Raft 日志,因为领导者可能会失败或输掉选举。

即使 Raft 实例已被终止,此函数也应该正常返回。
第一个返回值是命令提交后将出现的索引。第二个返回值是当前任期。如果此服务器认为自己是领导者,则第三个返回值为 true。

  • 首先判断是否leader。是则继续,否则直接返回
  • 封装后加入log

[!NOTE]

只有这个消息提交时,才返回index

func (rf *Raft) Start(command interface{}) (index int, term int, isLeader bool) {
	rf.mu.Lock()
	defer rf.mu.Unlock()

	//首先判断leader
	if !rf.isLeader {
		return
	}
	isLeader = true

	//DPrintf("command:%v", command)

	检测是否已经提交
	//if command == rf.log[rf.committedIndex].Command {
	//	index = rf.committedIndex
	//	return
	//}

	rf.matchIndex[rf.me] = rf.nextIndex[rf.me]
	//			1					1
	//DPrintf("matchIndex:%v", rf.matchIndex)

	index = rf.nextIndex[rf.me]
	term = rf.currentTerm
	//			1
	//DPrintf("NextIndex:%v", rf.nextIndex)

	rf.nextIndex[rf.me]++
	//        2

	//DPrintf("%v", command)

	entry := Entries{
		Command: command,
		Index:   index,
		Term:    rf.currentTerm,
	}

	//将新的entry加入log
	rf.log = append(rf.log, entry)
	//DPrintf("leaderLog:%v", rf.log)
	// Your code here (3B).

	return
}

Make

applyCh 是测试人员或服务期望 Raft 发送 ApplyMsg 消息的通道。Make() 必须快速返回,因此它应该为任何长时间运行的工作启动 goroutines。

func Make(peers []*labrpc.ClientEnd, me int, persister *Persister,
	applyCh chan ApplyMsg) *Raft {

	nextIndex := make([]int, len(peers))
	for i, _ := range nextIndex {
		nextIndex[i] = 1
	}
	matchIndex := make([]int, len(peers))
	log := make([]Entries, 0)

	rf := &Raft{
		peers:          peers,
		persister:      persister,
		me:             me,
		votedFor:       -1,
		electedTimeOut: time.Millisecond * 400, //设定心跳超时时间,开始选举
		lastRevTime:    time.Now(),             //初始化为设置当前时间
		currentTerm:    0,
		isLeader:       false,
		nextIndex:      nextIndex,
		matchIndex:     matchIndex,
		committedIndex: 0,
		log:            log,
	}

	// Your initialization code here (3A, 3B, 3C).

	// initialize from state persisted before a crash
	rf.readPersist(persister.ReadRaftState())

	// start ticker goroutine to start elections

	go rf.ticker() //RequestVote RPC

	go rf.HeartBeats()

	go rf.apply(applyCh)

	return rf
}

applyMsg结构体

当每个 Raft 对等体意识到连续的日志条目被提交时,对等体应该通过传递给 Make() 的 applyCh 向同一服务器上的服务(或测试器)发送 ApplyMsg。将 CommandValid 设置为 true 以指示 ApplyMsg 包含新提交的日志条目。

所以说当有新的commited entry时,将其打包为applyMsg再发送到applyCh

因为follower也要对其服务进行提交,所以也要有这个长线服务,应该在make中新建一个go routine来完成这件事

rf.apply()

  • 需要检查自己的身份? 每个peer都要将commited的上传
    leader需要确认,follower需要吗?
    不对,leader确认有commited之后,告知follower,自己再应用
    只有leader需要确认,follower只要发现committed大于lastApplied就可以应用

    需要

  • 从matchindex中,如果大多数比自己的commitedIndex大,则将一个放入applyCh

如何检测新的commited entry?

matchIndex?

rf从哪里获取entry?

start函数

func (rf *Raft) apply(applyCh chan ApplyMsg) {
	for rf.killed() == false {

		//leader角度
		for rf.isLeader && rf.killed() == false {
			rf.mu.Lock()
			//me := rf.me
			committedIndex := rf.committedIndex
			matchIndex := rf.matchIndex
			th := len(rf.peers) / 2
			lastApplied := rf.lastApplied
			rf.mu.Unlock()

			counts := 0
			//DPrintf("id:%v,committedIndex:%v", me, committedIndex)
			for _, v := range matchIndex {
				if committedIndex != -1 && v > committedIndex {
					counts++
				}
			}

			applyMsg := ApplyMsg{
				CommandValid: true,
				//Command:      rf.log[committedIndex].Command,
				CommandIndex: lastApplied + 1,
			}
			if counts > th {
				rf.mu.Lock()
				rf.committedIndex++
				DPrintf("leader,id:%v,log:%v,committedIndex:%v", rf.me, rf.log, rf.committedIndex)
				initIndex := rf.log[0].Index
				applyMsg.Command = rf.log[committedIndex+1-initIndex].Command
				//DPrintf("id:%v,commandIndex:%v", rf.me, committedIndex+1)

				//DPrintf("state:%v,一条消息过半认同", rf.isLeader)
				applyCh <- applyMsg

				rf.lastApplied++
				rf.mu.Unlock()
			}
			time.Sleep(30 * time.Millisecond)
		}
		//follower角度
		for !rf.isLeader && rf.killed() == false {
			rf.mu.Lock()
			//DPrintf("follower:%v try apply", rf.me)
			committedIndex := rf.committedIndex
			lastApplied := rf.lastApplied
			log := rf.log

			if committedIndex > lastApplied {
				//1					0

				DPrintf("follower,id:%v,log:%v,committedIndex:%v", rf.me, rf.log, rf.committedIndex)
				iniIndex := 1
				//DPrintf("log:%v", log)
				if len(log) != 0 {
					iniIndex = rf.log[0].Index
				}
				//DPrintf("id:%v,commandIndex:%v", rf.me, committedIndex)

				//DPrintf("commitedIndex:%v", committedIndex)
				applyMsg := ApplyMsg{
					CommandValid: true,
					Command:      log[lastApplied+1-iniIndex].Command,
					CommandIndex: lastApplied + 1,
				}

				//DPrintf("state:%v,leader确认commited,apply", rf.isLeader)
				applyCh <- applyMsg

				rf.lastApplied++

			}
			rf.mu.Unlock()
			time.Sleep(30 * time.Millisecond)
		}
		time.Sleep(20 * time.Millisecond)
	}
}

HeartBeats

  • 需要收集append的结果

  • 没有成功的append需要针对性地在下一次重发

    超时的数据不变,下次重复发送

  • 不能卡在没有结果上,定时下次重传,修改sendAppendEntries

  • 根据不同的peer匹配不同的index等

    都从rf的nextIndex参数传,发现对不上,则减小一位

  • 给args传递正确的数据,并根据reply调整下次传递的参数

    type AppendEntriesArgs struct {
     Term         int
     LeaderId     int
     PrevLogIndex int
     PrevLogTerm  int
     Entries      []Entries
     LeaderCommit int
    }
    

    PrevLogIndex怎么获取?
    matchIndex-1

    entries怎么设置?
    commitedIndex和nextIndex进行对比

    raft中的nextIndex[]和matchIndex[]分别代表什么?

    nextIndex[] 记录了每个从节点下一个应该发送的日志条目的索引。换句话说,领导者知道对于每个从节点,应该从日志的哪个条目开始发送以进行同步。
    matchIndex[] 记录了每个从节点已经复制了的日志的最高索引值。这有助于领导者知道哪些日志条目已经被大多数从节点复制,从而可以安全地提交这些日志

func (rf *Raft) HeartBeats() {
	for rf.killed() == false {
		for rf.isLeader == true && rf.killed() == false {
			rf.mu.Lock()
			//nums := len(rf.peers)
			//resultCh := make(chan AppendEntriesResult, nums)
			peers := rf.peers
			me := rf.me
			DPrintf("me:%v", me)
			currentTerm := rf.currentTerm
			matchIndex := rf.matchIndex //0
			nextIndex := rf.nextIndex   //2

			//DPrintf("matchIndex:%v", matchIndex)

			//commitedIndex := rf.committedIndex
			entries := rf.log
			leaderCommit := rf.committedIndex
			rf.lastRevTime = time.Now()
			iniIndex := 0
			if len(entries) != 0 {
				iniIndex = entries[0].Index //1
			}
			rf.mu.Unlock()

			//DPrintf("entries:%v", entries)

			for i, _ := range peers {
				if i != me {
					go func(i int) {
						args := &AppendEntriesArgs{
							Term:         currentTerm,
							LeaderId:     me,
							PrevLogIndex: nextIndex[i] - 1, //1
							LeaderCommit: leaderCommit,
						}
						//DPrintf("matchIndex:%v", matchIndex)
						//DPrintf("nextIndex:%v", nextIndex)
						prevLogTerm := 0

						//如果有新消息,log一定存在 全是leader信息
						//if matchIndex[me] > commitedIndex && len(entries) != 0 {
						if nextIndex[i] < matchIndex[me]+1 && len(entries) != 0 { //有过start的leader

							//考虑消息累计,leader有多个还没发给follower
							//如何判断是别人的全场第一条:
							//nextIndex是否为1,已经判断过leader是否有内容
							if nextIndex[i] == 1 { //第一条
								args.Entries = entries
							} else if nextIndex[i] == 0 { //全覆盖
								args.Entries = entries
								prevLogTerm = -1
							} else { //不止一条的话传follower没有的
								//这里要使用nextIndex了
								DPrintf("id:%v,nextIndex:%v,initIndex:%v", i, nextIndex[i], iniIndex)
								args.Entries = entries[nextIndex[i]-iniIndex:] //2-1

								//如果新的leader,match清零怎么办
								prevLogTerm = entries[nextIndex[i]-iniIndex-1].Term
							}

							//if len(entries) == 1 { //只有一条的话肯定就是这一条
							//	//leader只有一条,则全场第一条
							//	args.Entries = entries
							//	//prevLogTerm = entries[0].Term
							//	//prevLogTerm = 0
							//
							//} else { //不止一条的话传follower没有的
							//	//这里要使用nextIndex了
							//	args.Entries = entries[nextIndex[i]-iniIndex:] //2-1
							//
							//	//如果新的leader,match清零怎么办
							//	prevLogTerm = entries[nextIndex[i]-iniIndex-1].Term
							//}
						}

						//即使没有新消息,也应该根据follower的nextIndex和自己的matchIndex来发送follower没有的日志
						//if nextIndex[i]<matchIndex[me]-1 {
						//
						//}
						if matchIndex[me] != 0 { //没有start过的新leader

						}
						//新继任的leader也应该同步

						args.PrevLogTerm = prevLogTerm
						reply := &AppendEntriesReply{}

						//DPrintf("prev:(%v,%v)", args.PrevLogIndex, args.PrevLogTerm)
						//DPrintf("args entry:%v", args.Entries)
						//ok := rf.sendAppendEntries(i, args, reply)
						rf.sendAppendEntries(i, args, reply)

						// 将 reply 的 success 状态发送到 channel 中
						//resultCh <- AppendEntriesResult{
						//	TimeOut: ok,
						//	Success: reply.Success,
						//	Number:  reply.Term,
						//}
					}(i)

				}
			}
			// 收集所有协程的结果
			//stop := false
			//
			//for j := 0; j < nums-1; j++ {
			//	result := <-resultCh
			//	if !result.Success && !result.TimeOut { //说明自己的term小于被请求对象
			//		stop = true
			//		//DPrintf("%v恢复follower,", rf.me)
			//		rf.mu.Lock()
			//		rf.isLeader = false
			//		rf.currentTerm = max(result.Number, rf.currentTerm)
			//		rf.lastRevTime = time.Now()
			//		rf.mu.Unlock()
			//		break
			//	}
			//}

			//if stop {
			//	break
			//}
			time.Sleep(40 * time.Millisecond)
		}
		time.Sleep(10 * time.Millisecond)
	}
}

sendAppendEntries

  • 等待10毫秒,没有结果的话返回false

  • 对返回值进行处理,更新matchIndex和nextIndex

func (rf *Raft) sendAppendEntries(server int, args *AppendEntriesArgs, reply *AppendEntriesReply) bool {
	/*	DPrintf("%v给%v发送AppendEntries", rf.me, server)

		tim := make(chan bool)

		go func() {
			time.Sleep(10 * time.Millisecond)
			tim <- false
		}()
		tim <- rf.peers[server].Call("Raft.AppendEntries", args, reply)

		return <-tim*/
	//DPrintf("%v给%v发送AppendEntries", rf.me, server)

	rf.mu.Lock()
	defer rf.mu.Unlock()

	// 创建一个结果通道
	resultChan := make(chan bool)

	// 启动一个Goroutine进行RPC调用
	go func() {
		ok := rf.peers[server].Call("Raft.AppendEntries", args, reply)
		resultChan <- ok
	}()

	select {
	case result := <-resultChan:
		DPrintf("%vsendAppendEntries有结果", server)
		//成功的话更新matchindex表
		if reply.Success {
			if len(args.Entries) != 0 {
				rf.matchIndex[server] = args.Entries[len(args.Entries)-1].Index
				rf.nextIndex[server] = args.Entries[len(args.Entries)-1].Index + 1
			}
		} else {
			if rf.currentTerm < reply.Term { //leader过时导致失败
				rf.isLeader = false

				DPrintf("id%v被发现超时,term由%v到%v", rf.me, rf.currentTerm, reply.Term)
				rf.currentTerm = reply.Term
				rf.log = rf.log[:rf.committedIndex]
			} else { //其他失败
				DPrintf("id:%v,减nextIndex", server)
				rf.nextIndex[server]--
			}
		}

		return result // RPC调用成功,返回结果
	case <-time.After(10 * time.Millisecond):
		DPrintf("%vsendAppendEntries超时", server)
		return false // 超时,返回false
	}
}

AppendEntries

需要的igure2的指导:

  • ### 1. 接收请求

  • Follower节点收到来自leader的

    AppendEntries RPC
    

    请求。该请求中包含了以下信息:

    • term:Leader的任期号。
    • leaderId:Leader的ID。
    • prevLogIndex:新的日志条目紧接在此之前的日志条目的索引。
    • prevLogTermprevLogIndex条目的任期号。
    • entries[]:需要被存储的日志条目(可能为空,用于心跳)。
    • leaderCommit:Leader的已知已提交日志的索引。
  • ### 2. 步骤A: 任期检查

  • 如果收到的term比当前节点的任期号小,说明这是一个旧的AppendEntries请求,因此拒绝这个请求并返回false

  • ### 3. 步骤B: 日志一致性检查

  • 检查本地日志在prevLogIndex位置处的条目是否与prevLogTerm匹配:

    • 如果不匹配,说明当前Follower的日志与Leader不一致,则拒绝该请求并返回false
    • 如果匹配,则进入下一步。
  • ### 4. 步骤C: 日志复制

  • 删除本地日志中从prevLogIndex+1位置开始的所有条目,然后将entries[]中的新日志条目附加到本地日志中。

  • 这样可以确保Follower与Leader的日志保持一致。

  • ### 5. 步骤D: 更新commitIndex

  • 如果leaderCommit大于当前Follower的commitIndex,那么Follower将其commitIndex更新为leaderCommit和新日志条目最后一个索引值中的较小值。

  • 这一步确保了Follower的提交进度与Leader保持一致。

  • ### 6. 返回结果

  • 如果以上所有检查和操作都成功执行,Follower返回true,表示该AppendEntries RPC请求处理成功。


func (rf *Raft) AppendEntries(args *AppendEntriesArgs, reply *AppendEntriesReply) {
	rf.mu.Lock()
	defer rf.mu.Unlock()
	reply.Success = true
	rf.lastRevTime = time.Now()

	//判断是否leader过时
	//if args.Term < rf.currentTerm && args.LeaderCommit < rf.committedIndex {
	if args.Term < rf.currentTerm {
		//DPrintf("Args Term:%v", args.Term)
		//DPrintf("currentTerm:%v", rf.currentTerm)
		DPrintf("ID:%v,leader过时消息护着index小,follower当前term:%v,candidate term:%v", args.LeaderId, rf.currentTerm, args.Term)
		reply.Success = false
		reply.Term = rf.currentTerm
		return
	}

	//entry不为空,说明是正式的leader发送的有消息的心跳,否则为上位宣称
	//或者二者刚刚同步,本次心跳没有新消息
	if len(args.Entries) != 0 {
		DPrintf("收到有效内容")

		//正常消息,非第一条
		if len(rf.log) != 0 && args.PrevLogTerm != -1 {

			iniIndex := rf.log[0].Index //1

			//推算prevLogIndex对应的entry的实际log位置
			order := args.PrevLogIndex - iniIndex
			//0			1			1

			//检查prevLogIndex
			//if order < 0 || order >= len(rf.log) || rf.log[order].Term != args.PrevLogTerm {
			if order < 0 || order >= len(rf.log) || rf.log[order].Term != args.PrevLogTerm { //不匹配问题,不是要减next的地方
				reply.Success = false
				return
			}

			//修剪log
			rf.log = append(rf.log[:order+1], args.Entries...)
			DPrintf("received1,id:%v", rf.me)

		} else { //全场第一条消息

			prevLogIndex为0,则合理,否错错误
			//if args.PrevLogIndex != 0 {
			//	reply.Success = false
			//	return
			//}
			//直接保留所有log
			rf.log = args.Entries
			DPrintf("received2,id:%v", rf.me)
		}

		//修改commitedIndex
		//如果没错,log已经有内容
		if args.LeaderCommit > rf.committedIndex {
			rf.committedIndex = min(args.LeaderCommit, rf.log[len(rf.log)-1].Index)
			DPrintf("follower,1committedIndex:%v", rf.committedIndex)
		}

	} else if args.LeaderCommit != -1 { //正常领导者的心跳
		if len(rf.log) != 0 {
			if args.LeaderCommit > rf.committedIndex && args.Term == rf.log[len(rf.log)-1].Term {
				rf.committedIndex = min(args.LeaderCommit, rf.log[len(rf.log)-1].Index)
				DPrintf("follower,2committedIndex:%v", rf.committedIndex)
			}
		} else if args.LeaderCommit > rf.committedIndex {
			rf.committedIndex = 0
			DPrintf("follower,3committedIndex:%v", rf.committedIndex)
		}
	}

	//同步term
	rf.currentTerm = args.Term
	rf.isLeader = false
	//重置心跳时间和是否投票
	rf.lastRevTime = time.Now()
	rf.votedFor = -1
	//DPrintf("%v成为follower", rf.me)
}

其他函数等,源码链接

这里不再进行赘述,放下[github源代码地址](kingsill/RPC (github.com)),需要者自取即可

博主这里是自己搞得,可能比别人写的垃圾很多,轻喷

index问题等一系列东西,做的过程中简单记录,大家来看应该无意义,附上其他人比较好的记录

别人帖子

MIT 6.824 Lab 2: Raft 实验 | Ray’s Blog (rayzhang.top)

leader:
nextIndex[] 用于记录送给各follower的index 初始为leader的最新的log的index+1
matchIndex[] 每个follower的最新的log的index 初始为0

all
lastApplied 给状态机的最后一条指令的index 初始0 ,单调增
committedIndex 已知的最高的committed的index 初始0

  1. leader收到第一条消息,用自己的nextIndex作为entey的index,在把自己的nextIndex+1
    同时把自己的matchIndex置为nextIndex,新消息检查

  2. leader在心跳中检查是否有新消息,通过自己的matchIndex和committedIndex进行对比,由于只在start中进行自己的nextIndex修改,所以只要自己的matchIndex大于commitedIndex即有新消息

  3. 在args填充信息后发送

    leader刚上任时和普通心跳没有新消息的区别|

    entries都为空

    刚上任的宣称消息中的leadercommited改为-1

    普通心跳包正常

    没有消息的心跳包的任务;

    1. 重置超时时间
    2. 同步commited信息
  4. AppendEntries处理

    即使arg没有entry,是否要检查prevIndex等

  5. leader的heartBeats(sendAppendEntries)再进行处理

    • 超时
      直接结束,不进行处理
    • 未超时
      • 成功
        • 有新消息
          matchIndex更新
          nextIndex更新
        • 无新消息
          不更新
      • 失败
        NextIndex-1

apply协程

  • follower
    1. command

新的leader,match清零,prevLogTerm怎么算?
prevLogIndex 也是应该由 nextIndex来算 ,next是下一条日志的index,所以prev就是next-1

没有新消息的时候是否也能更新nextIndnex

如何确保follower Apply的log是更新过的呢?

leader确认后会更新commit

从leader回到follower需要采取什么措施

分区导致问题,单一分区的leader收到消息后堆积在自己的日志内

删除不是committed的部分?


received1 修剪log

received2 覆盖log

testBackUp

创建5个服务器

在这里插入图片描述

在这里插入图片描述

append 失败可能性:

  1. 过时 term小 或者 committedIndex小

  2. 上位宣称:这个不会失败

    只需要更新term,自身isleader否,更新心跳,投票重置

  3. leader有,follower没有
    next - -

  4. 冲突,相同index,不同term,不删除,next - -

当leader发现某一个next为0后,直接发送自己的全部log


当前问题

401
第一次的leader,被断开旧follower,一直被关一直跟随,被断开
term小,1断开后自增 term,term大,7term,2
第一个10个随机数,index,11但是没有消息,index小,1index收第二次随机数,11
committed 1committed 1committed 11

如何处理不一致问题,4和1的消息不一致,4的未commit,1的commit大

nextIndex大于follower所有的

leader,id:4,log:[{4678004126722157505 1 1}],committedIndex:1
2024/08/29 16:25:18 follower,id:0,log:[{4678004126722157505 1 1}],committedIndex:1
2024/08/29 16:25:18 follower,id:1,log:[{4678004126722157505 1 1}],committedIndex:1
2024/08/29 16:25:18 follower,id:3,log:[{4678004126722157505 1 1}],committedIndex:1
2024/08/29 16:25:18 follower,id:2,log:[{4678004126722157505 1 1}],committedIndex:1

2024/08/29 16:25:18 断开id:1,2,3连接
2024/08/29 16:25:18 开始给leader4,发送10个随机数

2024/08/29 16:25:21 每一个都断开连接
2024/08/29 16:25:21 恢复连接:4 0 1
2024/08/29 16:25:21 发送10随机数

2024/08/29 16:25:20 leader,id:2,log:[{4678004126722157505 1 1} {8938097423806100375 2 2} {723196532827361542 2 3} {18442494331360046 2 4} {5649708825675347494 2 5} {5043821671661526099 2 6} {984076030453990503 2 7} {2555064003070238563 2 8} {4577031850588410304 2 9} {474870608042202811 2 10} {3407621702695960717 2 11}],committedIndex:11
2024/08/29 16:25:20 follower,id:3,log:[{4678004126722157505 1 1} {8938097423806100375 2 2} {723196532827361542 2 3} {18442494331360046 2 4} {5649708825675347494 2 5} {5043821671661526099 2 6} {984076030453990503 2 7} {2555064003070238563 2 8} {4577031850588410304 2 9} {474870608042202811 2 10} {3407621702695960717 2 11}],committedIndex:11
2024/08/29 16:25:20 follower,id:1,log:[{4678004126722157505 1 1} {8938097423806100375 2 2} {723196532827361542 2 3} {18442494331360046 2 4} {5649708825675347494 2 5} {5043821671661526099 2 6} {984076030453990503 2 7} {2555064003070238563 2 8} {4577031850588410304 2 9} {474870608042202811 2 10} {3407621702695960717 2 11}],committedIndex:11

 follower,id:0,log:[{4678004126722157505 1 1} {2858403243448308923 1 2} {5374456348256256886 1 3} {2411776181554754243 1 4} {2594279163070091109 1 5} {1446728681006973319 1 6} {4312261448858846746 1 7} {4585759772685965362 1 8} {6390925677967595987 1 9} {1637295433917407814 1 10} {8198173872564129616 1 11}],committedIndex:11

上传的未committed

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

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

相关文章

非整周期截取信号对FFT分析的影响

原文出自微信公众号【小小的电子之路】 自然界中的模拟信号大部分都是无限长的&#xff0c;或者说对计算机而言可以说是无限长的&#xff0c;而计算机只能处理有限长的信号&#xff0c;怎么办呢&#xff1f;以快速傅里叶变换为例&#xff0c;我们通常是截取目标信号中有限长的一…

<Rust>egui学习之小部件(八):如何在窗口中添加滑动条slider部件?

前言 本专栏是关于Rust的GUI库egui的部件讲解及应用实例分析&#xff0c;主要讲解egui的源代码、部件属性、如何应用。 环境配置 系统&#xff1a;windows 平台&#xff1a;visual studio code 语言&#xff1a;rust 库&#xff1a;egui、eframe 概述 本文是本专栏的第八篇博…

虚拟机Linux(Centos7)系统静态IP设置

文章目录 虚拟机Linux系统中通过DHCP获取IP地址的配置和静态IP设置1. 更改网络模式为NAT模式2. 设置虚拟机网络编辑器3. 配置网络文件3.1 修改网络配置文件3.2 添加静态IP、子网掩码和网关3.3 配置DNS 4. 重启网络服务5. 验证IP配置 虚拟机Linux系统中通过DHCP获取IP地址的配置…

LoRAMoE:缓解大模型的世界知识遗忘问题

人工智能咨询培训老师叶梓 转载标明出处 大模型&#xff08;LLMs&#xff09;在进行SFT时&#xff0c;通过增加指令数据量来提升其在多个下游任务中的性能或显著改善特定任务的表现。但研究者们发现&#xff0c;这种大规模的数据增加可能会导致模型遗忘其预训练阶段学习到的世…

加密与安全_前后端通过AES-CBC模式安全传输数据

文章目录 Pre概述前端加密是否有意义&#xff1f;环境准备加密方法、MODE和PADDING的选择前端后端应用&#xff1a;从传输到解密的全过程安全性增强动态生成密钥和初始向量1. 前端&#xff1a;动态生成密钥和IV2. 后端&#xff1a;解密动态密钥和IV 结语 Pre 加密与安全_解密A…

TMGM:欧元区通胀放缓将支持9月欧洲中央银行降息

八月份德国通胀率出乎意料的下降超过预期。欧洲中央银行可能会保持其放松的货币政策。美元/欧元矫正性下跌可能在本周结束前继续。 欧洲统计局将在周五公布八月份欧元区消费者价格(调和)指数(HICP)的预估数值&#xff0c;预期结果将支持9月份决策者降息的决定。 因为对经济增…

大数据-110 Flink 安装部署 下载解压配置 Standalone模式启动 打包依赖

点一下关注吧&#xff01;&#xff01;&#xff01;非常感谢&#xff01;&#xff01;持续更新&#xff01;&#xff01;&#xff01; 目前已经更新到了&#xff1a; Hadoop&#xff08;已更完&#xff09;HDFS&#xff08;已更完&#xff09;MapReduce&#xff08;已更完&am…

Python 如何进行声音处理(pydub, wave模块)

Python 是一种功能强大的编程语言&#xff0c;它提供了丰富的库和模块用于各种任务的执行&#xff0c;包括声音处理。对于声音处理&#xff0c;pydub 和 wave 模块是最常用的两个库。 一、Python中的声音处理基础知识 在深入探讨具体的模块之前&#xff0c;我们先了解一些声音…

深度学习基础--损失函数

前三章分别介绍了线性回归、浅层神经网络和深度神经网络。这些都属于函数家族&#xff0c;能够实现从输入到输出的映射&#xff0c;其具体的函数取决于模型参数 ϕ \phi ϕ。在训练这些模型时&#xff0c;我们的目标是找到能够为特定任务提供最优输入输出映射的参数。本章将详…

C语言 ——— 文件读取结束的判定

目录 判定文件读取结束的方式 被错误使用的feof函数 判定文件结束的正确使用 判定文件读取结束的方式 判断文本文件是否读取结束&#xff1a; 利用 fgetc 判断返回值是否为 EOF 利用 fgets 判断返回值是否为 NULL 判断二进制文件是否读取结束&#xff1a; 利用 fread 判…

00 Tkinter学习路线

Tkinter学习路线 此Tkinter以更新完毕&#xff0c;几乎涵盖了Tkinter所有知识点 此文章用于快速找到对应的知识点 01 Tkinter介绍 02 Tkinter窗口的管理与设置 03 Tkinter布局方式 04 Tkinter布局组件 05 Tkinter事件 06 Tkinter可变变量 07 Label 组件 08 Button 组件 09 Entr…

大模型技术 | 基于大模型构建本地知识库

前言 随着人工智能技术的发展&#xff0c;大模型已成为智能系统进步的关键力量。 模型以其庞大的数据容量和深度学习能力&#xff0c;为处理复杂任务提供了前所未有的可能性。但在特定应用场景下仍面临挑战&#xff0c;尤其是在需要快速、准确响应的情境中。为了克服这些限制…

.NET Razor类库-热加载 就是运行时编译

1.新建3个项目 1.1 一个.NET Standard2.1项目 IX.Sdk.SvnCICD4NuGet 1.2 一个.NET Razor类库项目 IX.Sdk.SvnCICD4NuGet.RazorWeb 1.3 一个.NET6 Web项目 IX.Sdk.SvnCICD4NuGet.Web 这3个项目的引用关系 Web引用 Razor类库 和 .NET Standard2.1 Razor类库引用.NET Standard2.1…

VBA学习(65):Excel VBA 凭证打印/SQL连接Eexcel文件/Listview控件/CommandButton命令按钮控件

本期内容信息量相当的大&#xff0c;内容涉及很多方面&#xff0c;请耐心阅读&#xff0c;肯定不会让你失望的&#xff01;建议收藏&#xff01; Excel中记账凭证的打印&#xff0c;几种思路 Excel表记账的缺点 最新的打印方法&#xff1a;勾选凭证列表&#xff0c;点打印即可…

OpenCV中使用金字塔LK光流法(下)

接下来通过一个demo来调用calcOpticalFlowPyrLK()实现光流计算,需要注意的是该方法适用于具有丰富特征的像素点的光流计算,平坦区域的像素点往往会得到误差较大的结果。所以我们需要先选取得到一些角点,demo中通过goodFeaturesToTrack()这个接口实现角点提取。 如下有两张图…

nvm切换node版本(windows版本)

如果是win系统&#xff0c;不能直接通过npm来安装nvm(npm install nvm不行&#xff01;)。需要手动去nvm官网下载安装包安装nvm github官网 先卸载本地的node版本 npm ls -g --depth0 // 查看全局安装中是否有早前安装的node 1.点击进去Github上往下滑会发现有一个download。进…

IP网络协议

目录 一、IP协议简介 二、IP协议报头 三、IP网段划分&#xff08;子网划分&#xff09; 四、特殊的IP地址 五、IP地址的数量限制 六、私有IP地址和公网IP地址 七、路由 八、分片与组装 一、IP协议简介 IP指网际互连协议&#xff0c;Internet Protocol的缩写&#xff0…

Vue+ElementUI+Electron环境搭建及程序打包

一.环境 Node.js Element-ui Electron 二.Node.js 1.下载并安装Node.js 2.安装完成后,新建目录”node_cache“ ”node_global“ 3.新建及修改环境变量 4.执行如下命令 npm config set prefix "D:\Source_Install\nodejs\node_global" npm config set cache &q…

设计模式-简单工厂模式工厂方法模式

1. 简单工厂模式定义 简单工厂模式&#xff08;Simple Factory Pattern&#xff09;是一种创建型设计模式&#xff0c;它通过专门定义一个类来负责创建其他类的实例&#xff0c;这个类通常被称为工厂类。简单工厂模式并不是一种正式的设计模式&#xff0c;但它确实是一种常用的…

认识Kubebuilder

认识Kubebuilder 一、什么是Kubebuilder?Kubebuilder&#xff0c;K8s operator创建框架controller-runtime和controller-tools库 二、Kubebuilder&#xff0c;举例来说开源项目kuik 三、使用 kubebuilder init 创建基础项目四、使用kubebuilder create api生成控制器CachedIma…