lab2A


title: lab2A
date: 2020-12-01 16:33:37
tags:
category:

  • 公开课
  • mit_6.824

实验完成leaderElection部分

每个raft节点需要持有的状态

在所有机器需要持久化的状态

在rpc响应之前,需要更新稳定的存储介质

    state       State    //服务器所处的角色(Paper中不存在)
    currentTerm int      //服务器最后知道的Term
    voteFor     int      //当前任期内收到本机投票的候选人id
    log         []Log   //日志条目


每个Raft节点上的易变状态

    commitIndex  int    //已知的被提交的最大日志条目的索引值 从 0 开始递增
    lastApplied  int    //已被状态机执行的最大日志条目的索引值, 从 0 开始递增

在Leader上的易变状态

    nextIndex   []int   //index 为每台server的标号,元素为下条发送到该sever的日志索引(初始化为Leader的最新一条日志的index + 1)
    matchIndex  []int   //已经复制到该服务器的的最新索引, 从0 开始递增

为了方便编程所添加的一些数据

    voteCh chan bool    //通知有投票请求到达
    appendEntryCh chan bool //通知有添加日志请求到达

Raft的状态转换

考虑的问题:

  1. 状态的转换需要加锁吗,因为状态转换的时候这段代码会被包裹起来?
func (rf *Raft) beCandidate() {
    rf.mu.Lock()
    rf.state = Candidate
    rf.currentTerm++
    rf.votedFor = rf.me
    rf.mu.Unlock()
    go  rf.startElection()
}

func (rf *Raft) beFollower(term int) {
    rf.state = Follower
    rf.votedFor = NULL
    rf.currentTerm = term
}

func (rf *Raft) beLeader() {
    if rf.state != Candidate {
        return
    }
    rf.state = Leader
    rf.nextIndex = make([]int, len(rf.peers))
    rf.matchIndex = make([]int, len(rf.peers))
    for i := 0; i < len(rf.nextIndex); i++ {
        rf.nextIndex[i] = rf.getLastLogIdx() + 1
    }
}

告知有rpc请求到达

func send(ch chan bool) {
    select {
    case<-ch:
    default:
    }
    ch <- true
}

实现投票

应对投票请求

应对投票请求的逻辑

  1. 如果candidate的Term大于自身,立马转换为follower(集群中)
  2. agrg.Term < rf.currentTerm, 拒绝投票
  3. 已经投过票,拒绝投票
  4. candidate的log不是最新的,拒绝投票
  5. 同意投票,立马转换自身角色,并通知voteCh

一点思考,如果args.Term == rf.Term 会产生什么情况呢

func (rf *Raft) RequestVote(args *RequestVoteArgs, reply *RequestVoteReply) {
    // Your code here (2A, 2B).
    rf.mu.Lock()
    defer rf.mu.Unlock()
    if args.Term > rf.currentTerm {
        rf.beFollower(args.Term)
        send(rf.voteCh)
    }
    success := false
    if args.Term < rf.currentTerm{

    }else if rf.votedFor != NULL && rf.votedFor != args.CandidateId{

    }else if args.LastLogTerm < rf.getLastLogTerm() {

    }else if args.LastLogTerm == rf.getLastLogTerm() && args.LastLogIndex < rf.getLastLogIdx() {

    }else {
        rf.votedFor = args.CandidateId
        success = true
        rf.state = Follower
        send(rf.voteCh)
    }
    reply.Term = rf.currentTerm
    reply.VoteGranted = success
}


candidate开始选举

收到rpc回复之后的处理逻辑

  1. 集群中有peer的Term比自己大,立马切换状态为Follower
  2. 收到有效票,判断是否得到了大多数的选票,如果是,立马转换为Leader

一些思考,

  1. 如果已经得到了多数票,那么是否需要其他的请求投票协程停下来
  2. 变成Follower之后,是否可能因为脑裂的问题,再次统计票数成为了Leader(比如集群中有一个Follower领先太多)

func (rf *Raft) startElection() {
    rf.mu.Lock()
    //DPrintf("[%v] start Election at Term %d", )
    args := &RequestVoteArgs{
        Term : rf.currentTerm,
        CandidateId: rf.me,
        LastLogIndex : rf.getLastLogIdx(),
        LastLogTerm : rf.getLastLogTerm(),
    }
    var votes int32 = 1;
    for i := 0 ; i < len(rf.peers); i++ {
        if i == rf.me {
            continue
    }
        go func(peer int){
            reply := &RequestVoteReply{}
            if rf.sendRequestVote(peer, args, reply){
                rf.mu.Lock()
                defer rf.mu.Unlock()
                if reply.Term > rf.currentTerm {
                    rf.beFollower(reply.Term)
                    send(rf.voteCh)
                    return
                }
                if rf.state != Candidate {
                    return
                }
                if reply.VoteGranted {
                    atomic.AddInt32(&votes, 1)
                }
                if atomic.LoadInt32(&votes) > int32(len(rf.peers)) / 2{
                    rf.beLeader()
                    send(rf.voteCh)
                }
            }
        }(i)
    }
}


实现日志追加(心跳算是特殊的日志)

请求参数和返回参数如下

type AppendEntriesArgs struct {
	Term         int
	LeaderId     int
	PrevLogIndex int
	PreLogIterm  int
	Entries      []Log
	LeaderCommit int
}
type AppendEntriesReply struct {
	Term          int
	Success       bool
	ConflictIndex int
	ConflictTerm  int
}

AppendEntriesRPCHandler

  1. 如果Leader的任期小于自己的任期则返回false
  2. 如果索引不存在索引、任期和preLogIndex、preLogItem匹配的日志返回false
  3. 如果存在一条日志索引和preLogIndex相等,但是任期和preLogItern不相同的日志,需要删除这条日志以及后继日志
  4. 如果Leader复制的日志本地没有,则直接追加
  5. 如果LeaderCommit>commitIndex,则更新本地commitIndex为以上两者中较小的一个

2A只包含选主,所以展示不用做日志添加的处理以及更新commitIndex的操作

rf.mu.Lock()
	defer rf.mu.Unlock()
	defer send(rf.entryCh)
	if args.Term > rf.currtenTerm {
		rf.beFollower(args.Term)
	}
	reply.Success = false
	reply.Term = rf.currtenTerm

	//limit 1
	if args.Term < rf.currtenTerm {
		return
		//limit 2, 3
	} else if args.PrevLogIndex > rf.getLastIndex() {
		return
	} else if rf.logs[args.PrevLogIndex].Term != args.PreLogIterm {
		return
	}
	reply.Success = true

Leader发送心跳

处理Rpc回复的基本逻辑:

  1. 如果reply的Tem > rf.current,则立马变更自己的角色为Folliwer
  2. 如果reply为false(由于日志不一致而失败)减少 nextIndex 并重试(暂时不需要实现)
  3. 如果成功,更新该参与者的 nextIndex 和 matchIndex。(暂时没有更新日志的操作,先不用管)
    选主过程中

代码:

func (rf *Raft) startAppendEntries() {
	for i := 0; i < len(rf.peers); i++ {
		if i == rf.me {
			continue
		}
		go func(peer int) {
				rf.mu.Lock()
				args := &AppendEntriesArgs{
					Term:         rf.currtenTerm,
					LeaderId:     rf.me,
					PrevLogIndex: rf.getPrevLogIndex(peer),
					PreLogIterm:  rf.getPrevLogTerm(peer),
					Entries:      append(make([]Log, 0), rf.logs[rf.nextIndex[peer]:]...),
				}

				rf.mu.Unlock()
				reply := &AppendEntriesReply{}
				ret := rf.sendAppendEntries(peer, args, reply)
				rf.mu.Lock()
				if !ret {
					rf.mu.Unlock()
					return
				}
				if reply.Term > rf.currtenTerm {
					rf.beFollower(reply.Term)
					rf.mu.Unlock()
					return
				}
			}
		}(i)
}

Debug过程

按照指南,先看有没有产生Datarace,再进行test,多线程的编程,只能用日志打印一步步看。

产生了dataRace

先是肉眼debug, 看看加锁的地方哪里不对,遵循lab:

  1. 在所有用到共享数据的地方加锁
  2. 不要再阻塞的地方加锁
  3. 尽量是把大锁,包裹一个连续的处理共享数据的过程

没有产生一个Leader

在这里插入图片描述
这个测试点:在三个节点的集群中,当一个节点宕机后,依然能选出一个Leader来。
逻辑分析发现是应该将voteCh和appendCh设置为一个有缓冲为1的通道,否则投票人会阻塞在该通道上。

角标越界

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值