leadership transfer可以把raft group中的leader身份转给其中一个follower。这个功能可以用来做负载均衡,比如可以把leader放在性能更好的机器或者离客户端更近的机器上。
对于一个大规模分布式系统来说,负载均衡非常重要。然而raft本身在选主方面必须要求新主包含所有的意境committed的log,从这点上看,在选主阶段,不能加入自定义的选主逻辑。而paxos协议不太一样,paxos对选主没有要求,任何一个成员都可以成为主,选主协议可以自己实现。paxos leader当选后,从其他成员把commit的log拉过来即可。所以为了这个feature,raft作者提出了一个方案作为raft的扩展。
大概原理就是保证transferee(transfer的目标follower)拥有和原leader有一样新的日志,期间需要停写,然后给transferee发送一个特殊的消息,让这个follower可以马上进行选主,而不用等到election timeout,正常情况下,这个follower的term最大,当选,原来的leader变为备。
还是一样看看etcd实现的raft library怎么做,省略无关代码
首先应用通过如下函数来启动leader transfer,其中lead是当前的leader,transferee是目标leader,在任意一个成员上调用即可。
func (n *node) TransferLeadership(ctx context.Context, lead, transferee uint64) { select { // manually set 'from' and 'to', so that leader can voluntarily transfers its leadership case n.recvc <- pb.Message{Type: pb.MsgTransferLeader, From: transferee, To: lead}: case <-n.done: case <-ctx.Done(): }}
跑raft的goroutine从recvc中拿出message,首先做各种各样的检查,比如是否已经有transfer leader正在进行中,如果正在进行,目标是谁,然后做相应的处理。如果没有,则调用一下代码:
r.leadTransferee = leadTransfereeif pr.Match == r.raftLog.lastIndex() { r.sendTimeoutNow(leadTransferee) r.logger.Infof("%x sends MsgTimeoutNow to %x immediately as %x already has up-to-date log", r.id, leadTransferee, leadTransferee)} else { r.sendAppend(leadTransferee)}
首先将目标leader保存在leadTransferee中,标示着有transfer正在进行,后续如果有请求propose进来,会检查:
if r.leadTransferee != None { r.logger.Debugf("%x [term %d] transfer leadership to %x is in progress; dropping proposal", r.id, r.Term, r.leadTransferee) return}
这里相当于停写。
回到上面:
- 如果transferee和leader的log一样新,则给transferee发送MsgTimeoutNow类型的消息,告诉transferee可以立即选主,不需要等到election timeout。transferee端:
r.campaign(campaignTransfer)
raft为了防止出现网络分区的情况下,candidate频繁增加term从而导致term爆炸,在选主的时候新增加了一个PreVote阶段,通过了这个阶段才会真正开始Vote,这里,由于transferee明确知道是transfer,就没有必要采用这种两阶段的选主,所以传入的参数是campaignTransfer
- 如果leader发现transferee的日志落后,则给transferee append日志,leader在收到响应MsgAppResp后,会检查:
// Transfer leadership is in progress.if m.From == r.leadTransferee && pr.Match == r.raftLog.lastIndex() { r.logger.Infof("%x sent MsgTimeoutNow to %x after received MsgAppResp", r.id, m.From) r.sendTimeoutNow(m.From)}
如果发现transferee已经日志最新,则同样,给transferee发送MsgTimeoutNow
最后,看看etcd如何调用:
func (s *EtcdServer) transferLeadership(ctx context.Context, lead, transferee uint64) error { now := time.Now() interval := time.Duration(s.Cfg.TickMs) * time.Millisecond plog.Infof("%s starts leadership transfer from %s to %s", s.ID(), types.ID(lead), types.ID(transferee)) s.r.TransferLeadership(ctx, lead, transferee) for s.Lead() != transferee { select { case <-ctx.Done(): // time out return ErrTimeoutLeaderTransfer case <-time.After(interval): } } // TODO: drain all requests, or drop all messages to the old leader plog.Infof("%s finished leadership transfer from %s to %s (took %v)", s.ID(), types.ID(lead), types.ID(transferee), time.Since(now)) return nil}
调用TransferLeadership后,每隔一段时间检查是否transfer成功,要么超时,直接返回。