zoukankan      html  css  js  c++  java
  • 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)
    
  • 相关阅读:
    PHP如何学习?
    PHP compact() 函数
    Laravel中resource方法
    npm run watch-poll 监控css、js 文件更新
    MYSQL 的optimize怎么用
    出现“Windows资源管理器已停止工作”错误
    移动硬盘文件或目录损坏且无法读取怎么解决
    Linux下iptables 禁止端口和开放端口
    Linux VSFTP服务器详细配置
    分享一个基于ligerui的系统应用案例ligerRM V2(权限管理系统)(提供下载)
  • 原文地址:https://www.cnblogs.com/foxmailed/p/7190642.html
Copyright © 2011-2022 走看看