6000字详解基于事件驱动以及两段rpc实现raft

前言

不了解raft论文可以去看我的博客:万字解析raft论文

背景

一直很好奇分布式是怎么个事,去年尝试了一波6.824劝退了,理由是raft状态太过复杂,涂涂改改自己都受不了自己的代码。今年重新挑战,这次我一开始就考虑用事件驱动的方式实现,本文提供了在go中实现事件驱动风格代码的思路。

这里推荐一个UP主的视频,讲的很好: 解读共识算法Raft(6)总结与拓展_哔哩哔哩_bilibili

此外推荐试试Raft Scope, 这是个很好的raft模拟器: Raft Scope

事件驱动简要模型

简而言之事件驱动就是一个大的for循环不断Poll(拉取)事件, 每获取一个事件就执行相关逻辑,更新状态信息,完成状态转移。简单的事件驱动是这样的:

poller := NewPoller()
state := State{}

for {
    ev := poller.PollEvent()
    switch ev {
        case "...":
            // do something
        case "...":
            // do something
    }
}

Raft的状态机思路

下面是我的Raft基本骨架, 看个思路就够了:

type Raft struct {
	// 对等端, 可以向它们同步信息
	peers []*labrpc.ClientEnd // RPC end points of all peers

	// 相当于一个wal, 能持久化数据, 能保证要么成功要么失败, 不会发生半写
	persister *Persister

	// 自己在peer的索引
	me int

	// 自己用channel做退出
	reqDead   chan struct{}
	reqDeadOK chan struct{}

	// 异步获取当前状态, 我写的代码是状态机, 最好不用锁, 不侵入状态机的状态, 通过请求打入状态机器循环
	reqGetState  chan struct{}
	getStateChan chan GetStateInfo

	// 发送命令的Channel, 以及反馈结果的管道
	sendCmdChan chan SendCmdChanInfo

	// 外部消息, 进入总线
	messagePipeLine chan Message

	// apply相关的设施
	applyCh    chan ApplyMsg

	// 只有一个timer
	timer <-chan time.Time

	// 状态
	state RaftState
}

func (rf *Raft) GetState() (int, bool) {
	rf.reqGetState <- struct{}{}
	s := <-rf.getStateChan

	return s.Term, s.Isleader
}

func (rf *Raft) Kill() {
	rf.reqDead <- struct{}{}
	<-rf.reqDeadOK
}

type Empty struct{}

type RequestVoteRequest struct {
	Term         int
	CandidateID  int
	LastLogIndex int
	LastLogTerm  int
}

type RequestVoteReply struct {
	Term        int
	VoteGranted bool
}

// 心跳: AppendEntries RPCs that carry no log entries is heartbeat
type AppendEntriesRequest struct {
	Term         int
	LeaderID     int
	PrevLogIndex int
	PreLogTerm   int
	// may send more than one for efficiency
	// empty for heartbeat
	Entries      [][]byte
	LeaderCommit int
}

type AppendEntriesReply struct {
	ID             int
	Term           int
	PreIndex       int
	Success        bool
	NLogsInRequest int
	// 实现简化版本的快速回退
	ConflictIndex int
}

// 外部调用, 会进入这里来
func (rf *Raft) RequestVoteReq(args *RequestVoteRequest, reply *Empty) {
	rf.messagePipeLine <- Message{
		Term: args.Term,
		Msg:  args,
	}
}

func (rf *Raft) RequestVoteReply(args *RequestVoteReply, reply *Empty) {
	rf.messagePipeLine <- Message{
		Term: args.Term,
		Msg:  args,
	}
}

func (rf *Raft) AppendEntries(args *AppendEntriesRequest, reply *Empty) {
	rf.messagePipeLine <- Message{
		Term: args.Term,
		Msg:  args,
	}
}

func (rf *Raft) AppendEntriesReply(args *AppendEntriesReply, reply *Empty) {
	rf.messagePipeLine <- Message{
		Term: args.Term,
		Msg:  args,
	}
}

func Make(peers []*labrpc.ClientEnd, me int,
	persister *Persister, applyCh chan ApplyMsg) *Raft {
	rf := &Raft{}
	rf.peers = peers
	rf.persister = persister
	rf.me = me
	rf.reqDead = make(chan struct{})
	rf.reqGetState = make(chan struct{})
	rf.getStateChan = make(chan GetStateInfo)
	rf.messagePipeLine = make(chan Message)
	rf.sendCmdChan = make(chan SendCmdChanInfo)
	rf.applyCh = applyCh
	rf.reqDeadOK = make(chan struct{})

	go StateMachine(rf)

	return rf
}

func (rf *Raft) Start(command interface{}) (idx int, term int, isLeader bool) {
	c := make(chan SendCmdRespInfo)
	rf.sendCmdChan <- SendCmdChanInfo{
		Command: command,
		Resp:    c,
	}
	s := <-c
	return s.Index, s.Term, s.IsLeader
}


func StateMachine(rf *Raft) {
        // 读入磁盘的persist信息放入rf.State相关变量里面
	rf.readPersist(rf.persister.ReadRaftState())
	rf.timer = timer.After(time.Millisecond*50)

	for {
		select {
		case <-rf.reqDead:
			rf.Debug("dead")
			rf.reqDeadOK <- struct{}{}
			return
		case <-rf.reqGetState:
			rf.Debug("rf.reqGetState")
			go func() {
				rf.getStateChan <- GetStateInfo{Term: rf.state.PersistInfo.Term, Isleader: rf.state.State == "leader"}
			}()
		// 选举超时timer
		case <-rf.timer:
			switch rf.state.State {
			// 如果是follower超时, 那么进入candidate状态, 并且为自己加一票
			case "follower":
                        // 省略代码
			// 说明candidate超时了, 加term继续
			case "candidate":
                        // 省略代码
			// leader超时是定时器超时, 只需要发送心跳维统治即可
			case "leader":
                        // 省略代码
			}
		// 统一的外部事件总线, 从messagePipe进入
		case command := <-rf.sendCmdChan:
			switch rf.state.State {
			case "follower", "candidate":
				go func(t int) {
					command.Resp <- SendCmdRespInfo{
						Term:     t,
						Index:    -1,
						IsLeader: false,
					}
				}(rf.state.Term)
			case "leader":
				// 省略代码
			}
		case input := <-rf.messagePipeLine:
			if rf.state.PersistInfo.Term < input.Term {
                            // 相关逻辑 
			}

			switch val := input.Msg.(type) {
			case *RequestVoteReply:
                        // 省略代码
			case *RequestVoteRequest:
                        // 省略代码
			case *AppendEntriesRequest:
                        // 省略代码
			case *AppendEntriesReply:
                        // 省略代码
			}
		}
	}
}

传统的做法估计就是做一个ticker当作定时器周期地time.Sleep加锁后执行对应的动作, 但是在我的事件驱动的实现中用channel实现定时器才是正确的做法。

此外, 外部的事件通过channel打入事件循环, 同样事件循环用channel将响应信息递交给请求者。比如外界调用Start尝试开始提交一个日志, 这个信息会通过sendCmdChan打入事件循环, 然后事件循环会通过对应的channel传递响应。

为什么事件驱动?

一句话, 事件驱动控制力超级强, 传统用多个goroutine担任多个角色, 例如ticker和applier看起来很美好,实际上中间加锁共享状态的代码很难写。而是事件驱动的方法很容易推进,只需要分析好状态转移就能很好地coding。更爽的是不用加锁了,岂不美哉?

为什么我把论文中的一个RPC分为了两个?

可以看到,我定义了一个Empty结构体, 且把论文里提到的RPC分成了两段RPC。例如选举:

type Empty struct{}

type RequestVoteRequest struct {
	Term         int
	CandidateID  int
	LastLogIndex int
	LastLogTerm  int
}

type RequestVoteReply struct {
	Term        int
	VoteGranted bool
}

理由是受Raft Scope图景的影响, 它的请求和响应是一个个独立的小球,感觉很适合这样做? 此外把它们分来能够很好地适配事件驱动的写法,并能够很简单地实现Pipeline优化, 不用等待RPC响应, 加大并发度。

所谓pipeline优化, 就是不用等待对端的响应, 能够并发地发送rpc, 如果采用这种"分割"的思路就很容易实现了。

我们知道写界面或者reactor框架中eventloop一般是不能包含长时间的阻塞的代码的, 这里也是同理, 比如在follower超时后状态变为candidate, 此时可以开多个goroutine并发向所有服务器发送`RequestVoteRequest`, 不用等待响应。后续的响应信息将由另外一个RPC打入事件循环, 再根据信息转移状态就行了。

我认为分隔它们是极佳的优化策略,且和raft scope的图景很像不是么? 后续甚至可以用udp来做优化, 少了tcp的握手流程想必能获得更大的性能提升。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值