etcd raft如何實現成員變更


成員變更在一致性協議里稍復雜一些,由於不同的成員不可能在同一時刻從舊成員組切換至新成員組,所以可能出現兩個不相交的majority,從而導致同一個term出現兩個leader,進而導致同一個index的日志不一致,違反一致性協議。下圖是個例子:

raft作者提出了一種比較簡單的方法,一次只增加或減少一個成員,這樣能夠保證任何時刻,都不可能出現兩個不相交的majority,所以,可以從舊成員組直接切到新成員組。如下圖:

切換的時機是把成員變更日志寫盤的時候,不管是否commit。這個切換時機帶來的問題是如果這條成員變更日志最終沒有commit,在發生leader切換的時候,成員組就需要回滾到舊的成員組。

etcd raft為了實現簡單,將切換成員組的實機選在apply成員變更日志的時候。

下面看看etcd raft library如何實現的:

應用調用

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}}})
}

可以看出,ConfChange是和普通的log entry一樣封裝在MsgProp消息中,進入propc,

跑raft算法的goroutine從propc中拿到消息后,會做如下判斷:

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
			}
}

檢查已經有成員變更正在做,就忽略新的成員變更。然后將pendingConf置為true,意味着目前有成員變更正在做了,從這里可以看出,多個成員變更不能同時進行。follower接收端的處理和普通log entry一樣。

如果成員變更日志達成了一致,則會被封裝在Ready中,應用拿到后,做如下處理:

if entry.Type == raftpb.EntryConfChange {
          var cc raftpb.ConfChange
          cc.Unmarshal(entry.Data)
          s.Node.ApplyConfChange(cc)
}

ApplyConfChange:

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
}

講ConfChange放入confc,然后阻塞在confstatec上,跑raft協議的goroutine從confc中拿出ConfChange,做相應的增加/刪除節點操作,然后將成員組放入confstatec。

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:
}

增加/刪除節點操作都只是更新prs,map的每個元素保存一個peer的狀態,其中最重要的狀態莫過於

Match, Next uint64

看過raft小論文的人一看變量名就很明確意義,Match代表最大的已經落盤的log index,Next代表下一條需要發給這個peer的log index。然后將pendingConf置為false,代表成員變更結束。

重啟如何恢復成員組:

hs, cs, err := c.Storage.InitialState()

Storage接口中:

// InitialState returns the saved HardState and ConfState information.
	InitialState() (pb.HardState, pb.ConfState, error)

Storage是個接口,其中InitialState()用於恢復成員組,需要應用自己實現,通常將ConfState記在最后一次Snapshot的Metadata中:

message SnapshotMetadata {
	optional ConfState conf_state = 1 [(gogoproto.nullable) = false];
	optional uint64    index      = 2 [(gogoproto.nullable) = false];
	optional uint64    term       = 3 [(gogoproto.nullable) = false];
}

ConfState:

message ConfState {
	repeated uint64 nodes = 1;
}

拿到ConfState后就可以初始化上面提到的prs,snapshot后續的已經commit的log entry一樣,通過Ready封裝,應用進行apply,如果其中有ConfChange,則調用

s.Node.ApplyConfChange(cc)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM