在HttpKVAPI中kvstore的集群增加一個(gè)節(jié)點(diǎn)請(qǐng)求處理如下:
case r.Method == "POST":
url, err := ioutil.ReadAll(r.Body)
if err != nil {
log.Printf("Failed to read on POST (%v)\n", err)
http.Error(w, "Failed on POST", http.StatusBadRequest)
return
}
nodeId, err := strconv.ParseUint(key[1:], 0, 64)
if err != nil {
log.Printf("Failed to convert ID for conf change (%v)\n", err)
http.Error(w, "Failed on POST", http.StatusBadRequest)
return
}
cc := raftpb.ConfChange{
Type: raftpb.ConfChangeAddNode,
NodeID: nodeId,
Context: url,
}
h.confChangeC <- cc
// As above, optimistic that raft will apply the conf change
w.WriteHeader(http.StatusNoContent)
處理邏輯是向confChangeC通道寫(xiě)入增加節(jié)點(diǎn)消息,下面看下raftNode的routine中對(duì)該通道事件的處理:
//集群變更事件
case cc, ok := <-rc.confChangeC:
if !ok {
rc.confChangeC = nil
} else {
confChangeCount += 1
cc.ID = confChangeCount
rc.node.ProposeConfChange(context.TODO(), cc)
}
}
調(diào)用了node的ProposeConfChange方法:
func (n *node) ProposeConfChange(ctx context.Context, cc pb.ConfChange) error {
data, err := cc.Marshal()
if err != nil {
return err
}
return n.Step(ctx, pb.Message{Type: pb.MsgProp, Entries: []pb.Entry{{Type: pb.EntryConfChange, Data: data}}})
}
調(diào)用了node的Step方法,然后調(diào)用step方法,觸發(fā)MsgProp事件:
func (n *node) step(ctx context.Context, m pb.Message) error {
ch := n.recvc
if m.Type == pb.MsgProp {
//當(dāng)是追加配置請(qǐng)求時(shí)ch為n.propc
ch = n.propc
}
select {
//當(dāng)是追加配置請(qǐng)求時(shí)會(huì)向ch(即n.propc)通道寫(xiě)入數(shù)據(jù)(消息類(lèi)型和數(shù)據(jù))
case ch <- m:
return nil
case <-ctx.Done():
return ctx.Err()
case <-n.done:
return ErrStopped
}
}
由此可見(jiàn)對(duì)于集群變更的處理是將集群變更信息寫(xiě)入n.propc通道,下面看下leader角色的node對(duì)于該通道數(shù)據(jù)的處理,在raft的stepLeader方法中:
case pb.MsgProp:
//配置追加請(qǐng)求
if len(m.Entries) == 0 {
r.logger.Panicf("%x stepped empty MsgProp", r.id)
}
if _, ok := r.prs[r.id]; !ok {
// If we are not currently a member of the range (i.e. this node
// was removed from the configuration while serving as leader),
// drop any new proposals.
return
}
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
}
for i, e := range m.Entries {
if e.Type == pb.EntryConfChange {
if r.pendingConf {
r.logger.Infof("propose conf %s ignored since pending unapplied configuration", e.String())
m.Entries[i] = pb.Entry{Type: pb.EntryNormal}
}
r.pendingConf = true
}
}
r.appendEntry(m.Entries...)
r.bcastAppend()
return
有以上代碼可知是將該集群變更作為日志追加到本地r.appendEntry(m.Entries...),然后向其他follower發(fā)送附加日志rpc:r.bcastAppend()。
當(dāng)該集群變更日志復(fù)制到過(guò)半數(shù)server后,raftNode提交日志的處理邏輯如下:
case raftpb.EntryConfChange:
var cc raftpb.ConfChange
cc.Unmarshal(ents[i].Data)
rc.confState = *rc.node.ApplyConfChange(cc)
switch cc.Type {
case raftpb.ConfChangeAddNode:
if len(cc.Context) > 0 {
rc.transport.AddPeer(types.ID(cc.NodeID), []string{string(cc.Context)})
}
case raftpb.ConfChangeRemoveNode:
if cc.NodeID == uint64(rc.id) {
log.Println("I've been removed from the cluster! Shutting down.")
return false
}
rc.transport.RemovePeer(types.ID(cc.NodeID))
}
}
對(duì)于添加一個(gè)節(jié)點(diǎn)的處理,首先是更新集群變更的狀態(tài)信息,如下:
func (n *node) ApplyConfChange(cc pb.ConfChange) *pb.ConfState {
var cs pb.ConfState
select {
case n.confc <- cc:
case <-n.done:
}
select {
case cs = <-n.confstatec:
case <-n.done:
}
return &cs
}
會(huì)向n.confc通道寫(xiě)入集群變更消息,下面看node的處理:
case cc := <-n.confc:
if cc.NodeID == None {
r.resetPendingConf()
select {
case n.confstatec <- pb.ConfState{Nodes: r.nodes()}:
case <-n.done:
}
break
}
switch cc.Type {
case pb.ConfChangeAddNode:
r.addNode(cc.NodeID)
case pb.ConfChangeRemoveNode:
// block incoming proposal when local node is
// removed
if cc.NodeID == r.id {
propc = nil
}
r.removeNode(cc.NodeID)
case pb.ConfChangeUpdateNode:
r.resetPendingConf()
default:
panic("unexpected conf type")
}
select {
case n.confstatec <- pb.ConfState{Nodes: r.nodes()}:
case <-n.done:
}
r.addNode的代碼如下:
func (r *raft) addNode(id uint64) {
r.pendingConf = false
if _, ok := r.prs[id]; ok {
// Ignore any redundant addNode calls (which can happen because the
// initial bootstrapping entries are applied twice).
return
}
r.setProgress(id, 0, r.raftLog.lastIndex()+1)
}
就是設(shè)置下該peer的發(fā)送日志進(jìn)度信息。再回到raftNode中對(duì)于可提交的集群變更日志的處理,在更新完集群信息后執(zhí)行了rc.transport.AddPeer(types.ID(cc.NodeID), []string{string(cc.Context)}),即把該節(jié)點(diǎn)加入到當(dāng)前集群,建立與該節(jié)點(diǎn)的通信,這塊代碼之前分析過(guò)了不再贅述。當(dāng)該日志在其他follower也提交時(shí),其他follower也會(huì)同樣處理把這個(gè)新節(jié)點(diǎn)加入集群。
因此,只有集群變更日志在當(dāng)前server被提交完成后,當(dāng)前server才建立與新節(jié)點(diǎn)的通信,才知道集群的最新規(guī)模,在復(fù)制集群變更日志的過(guò)程中他們依然不知道集群的最新規(guī)模。但對(duì)于新節(jié)點(diǎn)來(lái)說(shuō),在啟動(dòng)式會(huì)知道老集群的節(jié)點(diǎn)信息,因此新節(jié)點(diǎn)啟動(dòng)后就知道了集群的最新規(guī)模。
總結(jié)如下,在ectd的raft實(shí)現(xiàn)中,處理集群變更的方案是:每次變更只能添加或刪除一個(gè)節(jié)點(diǎn),不能一次變更多個(gè)節(jié)點(diǎn),因?yàn)槊看巫兏粋€(gè)節(jié)點(diǎn)能保證不會(huì)有多個(gè)leader同時(shí)產(chǎn)生,下面以下圖為例分析下原因。
最初節(jié)點(diǎn)個(gè)數(shù)為3,即server1、server2、server3,最初leader為server3,如果有2個(gè)節(jié)點(diǎn)要加入到集群,那么在原來(lái)的3個(gè)節(jié)點(diǎn)收到集群變更請(qǐng)求前認(rèn)為集群中有3個(gè)節(jié)點(diǎn),確切的說(shuō)是集群變更日志提交前,假如server3作為leader,把集群變更日志發(fā)送到了server4、server5,這樣可以提交該集群變更日志了,因此server3、server4、server5的集群變更日志提交后他們知道當(dāng)前集群有5個(gè)節(jié)點(diǎn)了。而server1和server2還沒(méi)收到集群變更日志或者收到了集群變更日志但沒(méi)有提交,那么server1和server2認(rèn)為集群中還是有3個(gè)節(jié)點(diǎn)。假設(shè)此時(shí)server3因?yàn)榫W(wǎng)絡(luò)原因重新發(fā)起選舉,server1也同時(shí)發(fā)起選舉,server1在收到server2的投票贊成響應(yīng)而成為leader,而server3可以通過(guò)server4和server5也可以成為leader,這時(shí)出現(xiàn)了兩個(gè)leader,是不安全且不允許的。
但如果每次只添加或減少1個(gè)節(jié)點(diǎn),假設(shè)最初有3個(gè)節(jié)點(diǎn),有1個(gè)節(jié)點(diǎn)要加入。最初leader為server1,在server1的集群變更日志提交前,server1、server2、server3認(rèn)為集群中有3個(gè)節(jié)點(diǎn),只有server4認(rèn)為集群中有4個(gè)節(jié)點(diǎn),如果leader在server1、server2、server3中產(chǎn)生,那么必然需要2個(gè)server,而server4只能收到server1、server2、server3中1個(gè)server的響應(yīng),是無(wú)法成為leader的,因?yàn)閟erver4認(rèn)為集群中有4個(gè)節(jié)點(diǎn),需要3個(gè)節(jié)點(diǎn)同意才能成為leader。如果在server1是leader時(shí)該集群變更日志提交了,那么集群中至少有2個(gè)server有該集群變更日志了,假如server1和server2都有該集群變更日志了,server3和server4還沒(méi)有,那么server3和server4不可能被選為leader,因?yàn)樗麄兊娜罩静粔蛐隆?duì)于server4來(lái)說(shuō)需要3個(gè)server同時(shí)同意才能成為leader,而server1和server2的日志比他新,不會(huì)同意他成為leader。對(duì)于server3來(lái)說(shuō),在集群變更日志提交前他認(rèn)為集群中只有3個(gè)server,因此只會(huì)把投票請(qǐng)求發(fā)送給server1和server2,而server1和server2因?yàn)槿罩颈人虏粫?huì)同意;如果server3的集群變更日志也提交了,那么他人為集群中有4個(gè)節(jié)點(diǎn),這時(shí)與server4一樣,需要3個(gè)server同時(shí)同意才能成為leader,如果server1通過(guò)server2成為leader了,那么server1和server2都不會(huì)參與投票了。
因此每次一個(gè)節(jié)點(diǎn)的加入不管在集群變更日志提交前、提交過(guò)程中還是提交后都不會(huì)出現(xiàn)兩個(gè)leader的情況。
提交前是因?yàn)樵瓉?lái)的節(jié)點(diǎn)不知道這個(gè)新的節(jié)點(diǎn),不會(huì)發(fā)送投票給他,也不會(huì)處理新節(jié)點(diǎn)的投票請(qǐng)求;
提交后是因?yàn)榇蠹叶贾兰旱淖钚乱?guī)模了,不會(huì)產(chǎn)生兩個(gè)大多數(shù)的投票;
提交過(guò)程中是因?yàn)闆](méi)有這條集群變更日志的server由于日志不夠新也不能成為leader,比如最初集群規(guī)模是2n+1,現(xiàn)在有1個(gè)新節(jié)點(diǎn)加入,如果集群變更日志復(fù)制到了過(guò)半數(shù)server,因?yàn)橹暗膌eader是老的集群的,因此過(guò)半數(shù)是n+1,假如這個(gè)n+1個(gè)server中產(chǎn)生了一個(gè)leader,那么對(duì)于新的節(jié)點(diǎn)來(lái)說(shuō),因?yàn)榧鹤兏罩具€沒(méi)有應(yīng)用到狀態(tài)機(jī)所以只有這個(gè)新節(jié)點(diǎn)認(rèn)為集群中有2n+2個(gè)server,因此需要n+2個(gè)server同意投票他才能成為leader,但這是不可能的,因?yàn)橐呀?jīng)有n+1個(gè)節(jié)點(diǎn)已經(jīng)投過(guò)票了,而對(duì)于老集群中的剩下的沒(méi)有投票的n個(gè)節(jié)點(diǎn)中,他們?nèi)魏我粋€(gè)server都需要n+1個(gè)server同意才能成為leader,而他們因?yàn)檫€沒(méi)有把集群變更日志真正提交即應(yīng)用到狀態(tài)機(jī),還不知道新節(jié)點(diǎn)的存在,也就不能收到n+1個(gè)server投票,最多只能收到n個(gè)節(jié)點(diǎn)的投票,因此也不能成為leader,保證了只能有一個(gè)leader被選出來(lái)。