• etcd raft library设计原理和使用


    早在2013年11月份,在raft论文还只能在网上下载到草稿版时,我曾经写过一篇blog对其进行简要分析。4年过去了,各种raft协议的讲解铺天盖地,raft也确实得到了广泛的应用。其中最知名的应用莫过于etcd。etcd将raft协议本身实现为一个library,位于https://github.com/coreos/etcd/tree/master/raft,然后本身作为一个应用使用它。

    本文不讲解raft协议核心内容,而是站在一个etcd raft library使用者的角度,讲解要用上这个library需要了解的东西。

    这个library使用起来相对来说还是有点麻烦。官方有一个使用示例在 https://github.com/coreos/etcd/tree/master/contrib/raftexample。整体来说,这个库实现了raft协议核心的内容,比如append log的逻辑,选主逻辑,snapshot,成员变更等逻辑。需要明确的是:library没有实现消息的网络传输和接收,库只会把一些待发送的消息保存在内存中,用户自定义的网络传输层取出消息并发送出去,并且在网络接收端,需要调一个library的函数,用于将收到的消息传入library,后面会详细说明。同时,library定义了一个Storage接口,需要library的使用者自行实现。

    Storage接口如下:

    // Storage is an interface that may be implemented by the application
    // to retrieve log entries from storage.
    //
    // If any Storage method returns an error, the raft instance will
    // become inoperable and refuse to participate in elections; the
    // application is responsible for cleanup and recovery in this case.
    type Storage interface {
    	// InitialState returns the saved HardState and ConfState information.
    	InitialState() (pb.HardState, pb.ConfState, error)
    	// Entries returns a slice of log entries in the range [lo,hi).
    	// MaxSize limits the total size of the log entries returned, but
    	// Entries returns at least one entry if any.
    	Entries(lo, hi, maxSize uint64) ([]pb.Entry, error)
    	// Term returns the term of entry i, which must be in the range
    	// [FirstIndex()-1, LastIndex()]. The term of the entry before
    	// FirstIndex is retained for matching purposes even though the
    	// rest of that entry may not be available.
    	Term(i uint64) (uint64, error)
    	// LastIndex returns the index of the last entry in the log.
    	LastIndex() (uint64, error)
    	// FirstIndex returns the index of the first log entry that is
    	// possibly available via Entries (older entries have been incorporated
    	// into the latest Snapshot; if storage only contains the dummy entry the
    	// first log entry is not available).
    	FirstIndex() (uint64, error)
    	// Snapshot returns the most recent snapshot.
    	// If snapshot is temporarily unavailable, it should return ErrSnapshotTemporarilyUnavailable,
    	// so raft state machine could know that Storage needs some time to prepare
    	// snapshot and call Snapshot later.
    	Snapshot() (pb.Snapshot, error)
    }
    

    这些接口在library中会被用到。熟悉raft协议的人不难理解。上面提到的官方示例https://github.com/coreos/etcd/tree/master/contrib/raftexample中使用了library自带的MemoryStorage,和etcd的wal和snap包做持久化,重启的时候从wal和snap中获取日志恢复MemoryStorage。

    要提供这种IO/网络密集型的东西,提高吞吐最好的手段就是batch加批处理了。etcd raft library正是这么做的。

    下面看一下为了做这事,etcd提供的核心抽象Ready结构体:

    // Ready encapsulates the entries and messages that are ready to read,
    // be saved to stable storage, committed or sent to other peers.
    // All fields in Ready are read-only.
    type Ready struct {
    	// The current volatile state of a Node.
    	// SoftState will be nil if there is no update.
    	// It is not required to consume or store SoftState.
    	*SoftState
    
    	// The current state of a Node to be saved to stable storage BEFORE
    	// Messages are sent.
    	// HardState will be equal to empty state if there is no update.
    	pb.HardState
    
    	// ReadStates can be used for node to serve linearizable read requests locally
    	// when its applied index is greater than the index in ReadState.
    	// Note that the readState will be returned when raft receives msgReadIndex.
    	// The returned is only valid for the request that requested to read.
    	ReadStates []ReadState
    
    	// Entries specifies entries to be saved to stable storage BEFORE
    	// Messages are sent.
    	Entries []pb.Entry
    
    	// Snapshot specifies the snapshot to be saved to stable storage.
    	Snapshot pb.Snapshot
    
    	// CommittedEntries specifies entries to be committed to a
    	// store/state-machine. These have previously been committed to stable
    	// store.
    	CommittedEntries []pb.Entry
    
    	// Messages specifies outbound messages to be sent AFTER Entries are
    	// committed to stable storage.
    	// If it contains a MsgSnap message, the application MUST report back to raft
    	// when the snapshot has been received or has failed by calling ReportSnapshot.
    	Messages []pb.Message
    
    	// MustSync indicates whether the HardState and Entries must be synchronously
    	// written to disk or if an asynchronous write is permissible.
    	MustSync bool
    }
    

    可以说,这个Ready结构体封装了一批更新,这些更新包括:

    • pb.HardState: 包含当前节点见过的最大的term,以及在这个term给谁投过票,已经当前节点知道的commit index
    • Messages: 需要广播给所有peers的消息
    • CommittedEntries:已经commit了,还没有apply到状态机的日志
    • Snapshot:需要持久化的快照

    库的使用者从node结构体提供的一个ready channel中不断的pop出一个个的Ready进行处理,库使用者通过如下方法拿到Ready channel:

    func (n *node) Ready() <-chan Ready { return n.readyc }
    

    应用需要对Ready的处理包括:

    1. 将HardState, Entries, Snapshot持久化到storage。
    2. 将Messages(上文提到的msgs)非阻塞的广播给其他peers
    3. 将CommittedEntries(已经commit还没有apply)应用到状态机。
    4. 如果发现CommittedEntries中有成员变更类型的entry,调用node的ApplyConfChange()方法让node知道(这里和raft论文不一样,论文中只要节点收到了成员变更日志就应用)
    5. 调用Node.Advance()告诉raft node,这批状态更新处理完了,状态已经演进了,可以给我下一批Ready让我处理。

    应用通过raft.StartNode()来启动raft中的一个副本,函数内部通过启动一个goroutine运行

    func (n *node) run(r *raft)
    

    来启动服务。

    应用通过调用

    func (n *node) Propose(ctx context.Context, data []byte) error
    

    来Propose一个请求给raft,被raft开始处理后返回。

    增删节点通过调用

    func (n *node) ProposeConfChange(ctx context.Context, cc pb.ConfChange) error
    

    node结构体包含几个重要的channel:

    // node is the canonical implementation of the Node interface
    type node struct {
    	propc      chan pb.Message
    	recvc      chan pb.Message
    	confc      chan pb.ConfChange
    	confstatec chan pb.ConfState
    	readyc     chan Ready
    	advancec   chan struct{}
    	tickc      chan struct{}
    	done       chan struct{}
    	stop       chan struct{}
    	status     chan chan Status
    
    	logger Logger
    }
    
    • propc: propc是一个没有buffer的channel,应用通过Propose接口写入的请求被封装成Message被push到propc中,node的run方法从propc中pop出Message,append自己的raft log中,并且将Message放入mailbox中(raft结构体中的msgs []pb.Message),这个msgs会被封装在Ready中,被应用从readyc中取出来,然后通过应用自定义的transport发送出去。

    • recvc: 应用自定义的transport在收到Message后需要调用

      func (n *node) Step(ctx context.Context, m pb.Message) error
      

      来把Message放入recvc中,经过一些处理后,同样,会把需要发送的Message放入到对应peers的mailbox中。后续通过自定义transport发送出去。

    • readyc/advancec: readyc和advancec都是没有buffer的channel,node.run()内部把相关的一些状态更新打包成Ready结构体(其中一种状态就是上面提到的msgs)放入readyc中。应用从readyc中pop出Ready中,对相应的状态进行处理,处理完成后,调用

      rc.node.Advance()
      

      往advancec中push一个空结构体告诉raft,已经对这批Ready包含的状态进行了相应的处理,node.run()内部从advancec中得到通知后,对内部一些状态进行处理,比如把已经持久化到storage中的entries从内存(对应type unstable struct)中删除等。

    • tickc:应用定期往tickc中push空结构体,node.run()会调用tick()函数,对于leader来说,tick()会给其他peers发心跳,对于follower来说,会检查是否需要发起选主操作。

    • confc/confstatec:应用从Ready中拿出CommittedEntries,检查其如果含有成员变更类型的日志,则需要调用

      func (n *node) ApplyConfChange(cc pb.ConfChange) *pb.ConfState
      

      这个函数会push ConfChange到confc中,confc同样是个无buffer的channel,node.run()内部会从confc中拿出ConfChange,然后进行真正的增减peers操作,之后将最新的成员组push到confstatec中,而ApplyConfChange函数从confstatec pop出最新的成员组返回给应用。

    可以说,要想用上etcd的raft library还是需要了解不少东西的。

  • 相关阅读:
    html5页面资源预加载(Link prefetch)
    html5页面资源预加载(Link prefetch)
    纯CSS制作的图形效果
    echarts 较全面的参数设置分析
    设置css样式背景色透明 字体颜色的不透明 设置select 箭头样式
    this.refs['hh']获取dom对象,this.refs['hh'].value获取dom对象的值
    浏览器运行的时候 事件打印不出来,提示 此页面出现代码禁用了反向和正向缓存(由于默认事件导致的)
    react 点击事件以及原始event与react封装好的事件点击区别
    react中 props与forEach的用法
    基于webpack的react的环境项目搭建
  • 原文地址:https://www.cnblogs.com/foxmailed/p/7137431.html
Copyright © 2020-2023  润新知