Node of etcd's raft implementation

1 Introduction

Before reading this article, I hope to read the author's "etcd's raft realization log" and "etcd's raft realization tracker&quorum". Without the background knowledge of the first two articles, some concepts mentioned in this article may be difficult to understand. Of course, if the reader already has some relevant knowledge, you can skip it directly.

The first impression of node should be the realization of the function of the raft node, but it is not very accurate. In my opinion, node should be an interface class of raft. If you regard raft as a library/package, then the interface that uses this package is node. But it should be noted that every node in the cluster that uses raft's business system (such as etcd) must call this node of raft. I think this is the reason why the raft interface class is defined as node.

Like the previous two articles, now explain a few key terms in this article:

  1. Propose: As the name suggests, initiate an agenda, and more than half of the parliamentarians support it even if it is approved. If it seems to be understood literally that a proposal is a very complicated matter, how to define the agenda? What counts as support? How to pass it has been explained in the article introducing quorum. In fact, the implementation agenda in etcd's raft is an entry, and this log can be received by a node even if the node supports it. The proposal becomes very simple: encapsulate it into a log and broadcast it to all nodes. If more than half of them do not refuse, they will even be approved.

2. Analysis

2.1Node

Since the author thinks that node is an interface class of raft, there is a certain basis, let’s take a look at the definition of an interface first:

// 代码源自go.etcd.io/etcd/raft/node.go
type Node interface {
    // raft内部有两个计时:心跳和选举。raft内部没有设计定时器,计时就是由这个接口驱动的,每调用一次
    // 内部计数一次,这就是raft的计时原理。所以raft的计时粒度取决于调用者,这样的设计使得raft的
    // 适配能力更强。
    Tick()
    // 源码注释:使Node转换到候选状态并开始竞选成为leader。在后面接口实现代码中读者就会看到,这个
    // 函数的调用会触发节点发起选举,那源码注释中为什么会说“开始竞选成为leader”呢?这是因为在所有
    // 节点日志状态相同的情况下,谁先发起选举谁就大概率成为leader。
    Campaign(ctx context.Context) error
    // 这个是raft对外提供的非常核心的接口,前面关于提议的定义可知,该接口把使用者的data通过日志广播
    // 到所有节点,当然这个广播操作需要Leader执行。如果当前节点不是Leader,那么会把日志转发给Leader。
    // 需要注意:该函数虽然返回错误代码,但是返回nil不代表data已经被超过半数节点接收了。因为提议
    // 是一个异步的过程,该接口的返回值只能表示提议这个操作是否被允许,比如在没有选出Leader的情况下
    // 是无法提议的。而提议的数据被超过半数的节点接受是通过下面的Ready()获取的。
    Propose(ctx context.Context, data []byte) error
    // 与Propose功能类似,数据不是使用者的业务数据,而是与raft配置相关的数据。配置数据对于raft
    // 需要保证所有节点是相同的,所以raft也采用了提议的方法保障配置数据的一致性。
    ProposeConfChange(ctx context.Context, cc pb.ConfChange) error
    // raft本身作为一种算法的实现并没有网络传输相关的实现,这些需要使用者自己实现。这也让raft
    // 本身的适配能力更强。在以前的文章中,笔者提到了大量的“消息”,消息就是raft节点间通信的协议,
    // 所以使用者在实现网络部分会收到其他节点发来的消息,所以把其他节点发来的消息需要通过Step()函数
    // 送到raft内部处理。此处多说一句,笔者喜欢etcd实现的raft原因之一就是不实现定时器,不实现网络
    // 传输,只专注与raft算法实现本身。
    Step(ctx context.Context, msg pb.Message) error
    // 关于Ready笔者后面会有更加详细的说明,此处只举一个例子来说明Ready是个什么?在关于log文章
    // 中笔者提到过日志会从提交状态转到应用状态,这个转换过程就是通过Ready()函数实现的,使用者
    // 通过该函数获取chan然后从chan获取Ready数据,使用者再把数据应用到系统中。Ready中的数据
    // 不只有已提交的日志,还有其他内容,后面再详细说明。熟悉C++的同学可以把这个理解为回调,使用者
    // 通过Propose()提议的数据通过回调的方式在通知使用者,当然,回调的内容不只这一点点,在介绍
    // Ready类型的时候会详细说明。
    Ready() <-chan Ready
    // Advance()函数是和Ready()函数配合使用的,当使用者处理完Ready数据后调用Advance()接口
    // 告知raft,这样raft才能向chan中推新的Ready数据。也就是使用者通过<-chan Ready获取的数据
    // 处理完后必须调用Advance()接口,否则将会阻塞在<-chan Ready上,因为raft此时也通过另一个
    // chan等待Advance()的信号。
    Advance()
    
    // ProposeConfChange()提议修改配置,当这个提议产生的日志被使用者应用的时候就需要通过调用
    // 这个接口把配置应用到本地节点。常规日志被应用都修改使用者的系统状态,而配置修改日志更新的是
    // raft自己的状态。
    ApplyConfChange(cc pb.ConfChange) *pb.ConfState

    // 试图把leader转移到transferee指定的peer,当使用者需要指定某个节点为Leader时会调用这个
    // 接口
    TransferLeadership(ctx context.Context, lead, transferee uint64)

    // 在了解该接口之前,读者需要了解linearizable read的概念,就是读请求需要读到最新的已经
    // commit的数据。我们知道etcd是可以在任何节点读取数据的,如果该节点不是leader,那么数据
    // 很有可能不是最新的,会造成stale read。即便是leader也有可能网络分区,leader还自认为
    // 自己是leader,这里就会涉及到一致性模型,感兴趣的读者可以阅读笔者的《一致性模型》。
    // 书归正传,ReadIndex就是使用者获取当前集群的最大的提交索引,此时使用者只要应用的最大
    // 索引大于该值,使用者就可以实现读取是最新的数据。说白了就是读之前获取集群的最大提交索引,
    // 然后等节点应用到该索引时再把系统中对应的值返回给用户。其中rctx唯一的标识了此次读操作,
    // 索引会通过Ready()返回给使用者,获取最大提交索引是一个异步过程。
    ReadIndex(ctx context.Context, rctx []byte) error

    // 获取raft的状态,关键在于Status的定义,后面有
    Status() Status
    // 报告raft指定的节点上次发送没有成功.
    ReportUnreachable(id uint64)
    // 源码注释还是比较详细的,笔者总结为:Leader向其他节点发送的快照如果使用者发送失败了就需要通过
    // 这个接口汇报给raft。在tracker的文章中可知,节点的Progress处于快照状态是暂停的,直到节点回复
    // 快照被应用才能触发状态更新,如果此时节点异常接收快照失败,那么Leader会以为该节点的Progress
    // 一直处于暂停状态而不再向该节点发送任何日志。笔者此处有一个疑问:作为使用者只能检测到快照是否
    // 发送成功,但是快照如果还在unstable中时节点崩溃,那么是不是还会出现刚刚提到的僵局?如果还能
    // 出现,那要怎么解?
    ReportSnapshot(id uint64, status SnapshotStatus)
    
    // 停止raft,相当于close的概念。
    Stop()
}

2.2Ready

With Node's interface definition, it is necessary to have a full understanding of the interface type Ready, because this type contains a lot of information, and understanding Ready is very helpful for understanding raft.

// 代码源自go.etcd.io/etcd/raft/node.go
type Ready struct {
    // Ready引入的第一个概念就是“软状态”,主要是节点运行状态,包括Leader是谁,自己是什么角色。
    // 关于角色的定义,笔者会在下一篇文章解释,此处只要了解软状态主要跟当前节点有关即可。该参数为
    // nil代表软状态没有变化
    *SoftState

    // Ready引入的第二个概念局势“硬状态”,主要是集群运行状态,包括届值、提交索引和Leader。如何
    // 更好理解软、硬状态?一句话,硬状态需要使用者持久化,而软状态不需要,就好像一个是内存
    // 值一个是硬盘值,这样就比较好理解了。
    pb.HardState

    // ReadState是包含索引和rctx(就是ReadIndex()函数的参数)的结构,意义是某一时刻的集群
    // 最大提交索引。至于这个时刻使用者用于实现linearizable read就是另一回事了,其中rctx就是
    // 某一时刻的唯一标识。一句话概括:这个参数就是Node.ReadIndex()的结果回调。
    ReadStates []ReadState

    // 需要存入可靠存储的日志,还记得log那片文章里面提到的unstable么,这些日志就是从哪里获取的。
    Entries []pb.Entry

    // 需要存入可靠存储的快照,它也是来自unstable
    Snapshot pb.Snapshot

    // 已经提交的日志,用于使用者应用这些日志,需要注意的是,CommittedEntries可能与Entries有
    // 重叠的日志,这是因为Leader确认一半以上的节点接收就可以提交。而节点接收到新的提交索引的消息
    // 的时候,一些日志可能还存储在unstable中。
    CommittedEntries []pb.Entry

    // Messages是需要发送给其他节点的消息,raft负责封装消息但不负责发送消息,消息发送需要使用者
    // 来实现。
    Messages []pb.Message

    // MustSync指示了硬状态和不可靠日志是否必须同步的写入磁盘还是异步写入,也就是使用者必须把
    // 数据同步到磁盘后才能调用Advance()
    MustSync bool
}

Ready() is like a callback function, sending callback data through <-chan Ready.

2.3Status

The reason why the Status type is explained is because there is a Status() function in the Node interface, which returns the Status type.

// 代码源自go.etcd.io/etcd/raft/status.go
type Status struct {
    // 节点ID,raft中每个节点都有一个64位无符号整型唯一ID。
    ID uint64
    // 软硬状态不需要解释了。
    pb.HardState
    SoftState
    // 应用索引,其实这个使用者自己也知道,因为Ready的回调里提交日志被应用都会有日志的索引。
    Applied  uint64
    // 如果是Leader,还有其他节点的进度。
    Progress map[uint64]tracker.Progress
    // Leader转移ID,如果正处于Leader转移期间。
    LeadTransferee uint64
}

2.2node (implementation of Node)

The interface definition of Node was introduced earlier. According to the unwritten regulations of golang, there will definitely be an implementation type of node. Let’s take a look at the definition of node:

// 代码源自go.etcd.io/etcd/raft/node.go
type node struct {
    // 用于实现Propose()接口
    propc      chan msgWithResult
    // 用于实现Step()接口
    recvc      chan pb.Message
    // 这两个chan用于实现ApplyConfChange()接口
    confc      chan pb.ConfChange
    confstatec chan pb.ConfState
    // 用于实现Ready()接口
    readyc     chan Ready
    // 用于实现Advance()接口
    advancec   chan struct{}
    // 用于实现Tick()接口,这个需要注意一下,创建node时tickc是有缓冲的,设计者的解释是当node
    // 忙的时候可能一个操作会超过tick的周期,这样会使得计时不准,有了缓冲就可以避免这个问题。
    tickc      chan struct{}
    // 在处理中避免不了各种chan操作,此时如果Stop()被调用了,相应的阻塞就应该被激活,否则可能
    // 面临死锁以后长时间退出后者永远无法退出。
    done       chan struct{}
    // 为Stop接口实现的,应该还好理解
    stop       chan struct{}
    // 一看就是为实现Status()用的,但是chan chan Status这个类型有点意思,后面分析实现函数
    // 看看如何实现的
    status     chan chan Status
    // 用来写运行日志的
    logger Logger
}

According to the name and type of the variable, you can probably see what the variable is used for, and it is basically of the chan type, which means that the interface call and function realization are through chan as the medium, and there must be a routine responsible for the other end of chan. Before explaining the implementation of the node interface function, let's take a look at the implementation of the interface call:

// 代码源自go.etcd.io/etcd/raft/node.go
// Tick()接口实现
func (n *node) Tick() {
    // 向tickc发数据同时用done避免死锁,符合预期
    select {
    case n.tickc <- struct{}{}:
    case <-n.done:
    default:
        n.logger.Warningf("A tick missed to fire. Node blocks too long!")
    }
}
// Campaign()接口实现
func (n *node) Campaign(ctx context.Context) error { 
    // 封装成pb.MsgHup消息然后再处理,step()后面会详细说明
    return n.step(ctx, pb.Message{Type: pb.MsgHup}) 
}
// Propose()接口实现
func (n *node) Propose(ctx context.Context, data []byte) error {
    // 封装成pb.MsgProp消息后再处理,stepWait()后面会详细说明
    return n.stepWait(ctx, pb.Message{Type: pb.MsgProp, Entries: []pb.Entry{
   
   {Data: data}}})
}
// Step()接口实现
func (n *node) Step(ctx context.Context, m pb.Message) error {
    // 本地消息就不用处理了,至于本地消息的判断读者自己可以看看
    if IsLocalMsg(m.Type) {
        return nil
    }
    return n.step(ctx, m)
}
// ProposeConfChange()接口实现
func (n *node) ProposeConfChange(ctx context.Context, cc pb.ConfChange) error {
    // 序列化pb.ConfChange
    data, err := cc.Marshal()
    if err != nil {
        return err
    }
    // 再把序列化后的配置封装成pb.MsgProp消息再处理。
    return n.Step(ctx, pb.Message{Type: pb.MsgProp, Entries: []pb.Entry{
   
   {Type: pb.EntryConfChange, Data: data}}})
}
// Ready()接口实现,非常简单了,不需要再解释了
func (n *node) Ready() <-chan Ready { return n.readyc }

// Advance()接口实现
func (n *node) Advance() {
    // 向advancec发送数据,利用done避免死锁
    select {
    case n.advancec <- struct{}{}:
    case <-n.done:
    }
}

// ApplyConfChange()接口实现
func (n *node) ApplyConfChange(cc pb.ConfChange) *pb.ConfState {
    var cs pb.ConfState
    // 把配置调整发送到confc
    select {
    case n.confc <- cc:
    case <-n.done:
    }
    // 再通过confstatec把调整后的结果读出来
    select {
    case cs = <-n.confstatec:
    case <-n.done:
    }
    return &cs
}

// Status()接口实现
func (n *node) Status() Status {
    // 创建一个Status的chan
    c := make(chan Status)
    select {
    // 通过status把c送给node,让node通过c把Status输出
    case n.status <- c:
        // 此时再从c中把Status读出来
        return <-c
    case <-n.done:
        return Status{}
    }
}

// 实现ReportUnreachable()接口
func (n *node) ReportUnreachable(id uint64) {
    select {
    // 封装成pb.MsgUnreachable消息,然后再统一走消息处理流程
    case n.recvc <- pb.Message{Type: pb.MsgUnreachable, From: id}:
    case <-n.done:
    }
}

// 实现ReportSnapshot()接口
func (n *node) ReportSnapshot(id uint64, status SnapshotStatus) {
    rej := status == SnapshotFailure
    // 和大部分接口一样,就是封装成消息再处理
    select {
    case n.recvc <- pb.Message{Type: pb.MsgSnapStatus, From: id, Reject: rej}:
    case <-n.done:
    }
}

// 实现TransferLeadership()接口
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():
    }
}

// 实现ReadIndex()接口
func (n *node) ReadIndex(ctx context.Context, rctx []byte) error {
    return n.step(ctx, pb.Message{Type: pb.MsgReadIndex, Entries: []pb.Entry{
   
   {Data: rctx}}})
}

From the perspective of interface implementation, the basics are all kinds of chan operations. Each interface has a dedicated chan, and some interface calls are converted into messages, letting node be treated as a kind of message. This way of implementation is because all the functions of node are implemented through a run() routine. Golang recommends chan for the interaction between routines, and node basically uses chan to the extreme. Before starting the run() function, now fill in the pits dug above, which are the step() and stepWait() functions. In fact, node implements a series of step() functions.

// 代码源自go.etcd.io/etcd/raft/node.go
// step()和stepWait()都是调用stepWithWaitOption()函数,唯一差别就是是否等待标记,这个这两个
// 函数名字上也能看出来
func (n *node) step(ctx context.Context, m pb.Message) error {
    return n.stepWithWaitOption(ctx, m, false)
}
func (n *node) stepWait(ctx context.Context, m pb.Message) error {
    return n.stepWithWaitOption(ctx, m, true)
}
// stepWithWaitOption才是核心
func (n *node) stepWithWaitOption(ctx context.Context, m pb.Message, wait bool) error {
    // 所有的非pb.MsgProp消息通过recvc送给node处理,此时是否wait根本不关心,因为通过recvc
    // 提交给node处理的消息可以理解为没有返回值的调用。
    if m.Type != pb.MsgProp {
    select {
    case n.recvc <- m:
        return nil
    case <-ctx.Done():
        return ctx.Err()
    case <-n.done:
        return ErrStopped
    }
  }
    
    // 只有pb.MsgProp的消息才会通过propc送给node处理,而且送到chan的类型是msgWithResult,
    // msgWithResult从名字可以看出来是消息+结果的组合,而结果是chan error类型的变量,需要
    // node通过这个chan把error返回,所以通过propc提交给node处理的消息可以理解为error返回值
    // 的调用
    ch := n.propc
    pm := msgWithResult{m: m}
    // 此处用到了wait,这个参数就是为了告诉函数调用者是否关心返回值,这个很像一个有返回值的函数
    // 但是调用者可以在乎返回值,也可以忽视返回值。如果在乎返回值,那么就要创建一个chan来获取返
    // 回的错误。这里有个细节需要注意,result的这个chan的缓冲量为1,为什么?请看下面的代码。
    if wait {
        pm.result = make(chan error, 1)
    }
    
    // 向node提交请求
    select {
    case ch <- pm:
        // 如果不在乎返回值直接返回
        if !wait {
            return nil
        }
    case <-ctx.Done():
        return ctx.Err()
    case <-n.done:
        return ErrStopped
    }
    
    // 获取返回值 
    select {
    // 这里就是为什么result的有缓冲的原因,如果因为ctx.Done而退出,那么node就会因为result没有
    // 缓冲而阻塞致死
    case err := <-pm.result:
        if err != nil {
            return err
        }
    case <-ctx.Done():
        return ctx.Err()
    case <-n.done:
        return ErrStopped
    }
    return nil
}

It's so complicated, can't you use a function call, you have to use chan to make it so "tall"? Theoretically speaking, it is possible to use functions, but it is necessary to use sync.Mutex to achieve the necessary synchronization. After we analyze the run() function, explain why the designer chose this solution in the summary chapter.

Let’s start the journey to the core of node now, and be mentally prepared to parse the source code of a relatively large function~

// 代码源自go.etcd.io/etcd/raft/node.go
// 该函数需要raft参数,因为node是raft的接口类,所以需要把接口调用最终转换为raft的调用
func (n *node) run(r *raft) {
    var propc chan msgWithResult
    var readyc chan Ready
    var advancec chan struct{}
    var prevLastUnstablei, prevLastUnstablet uint64
    var havePrevLastUnstablei bool
    var prevSnapi uint64
    var applyingToI uint64
    var rd Ready

    // 初始状态不知道谁是leader,需要通过Ready获取
    lead := None
    // 因为run()函数是一个串行函数,所以每次循环都可以用当前状态和上一次状态做比较,如果有任何变化
    // 都可以通过Ready输出给使用者,用局部变量记录上一次状态是一个非常简单有效的方法。此处需要剧透
    // 一些下一篇文章的一些内容:类型raft(就是变量r的类型)是raft的最核心实现,但是raft对象需要
    // node的run函数驱动,所以raft本身也不需要关心线程安全的问题,因为他的唯一使用者就是node,而
    // node又设计成了单routine形式。
    prevSoftSt := r.softState()
    prevHardSt := emptyState

    for {
        // 前面的介绍知道Ready()和Advance()是一个串行的工作,使用者调用Ready()从node获取数据
        // 处理完后再调用Advance()通知node处理完成准备处理下一轮Ready的数据。在同一时刻只能处理
        // 一个chan的,所以advancec和reaydc不可能同时非nil。读者需要注意:advancec != nil
        // 的时候,是使用者处理Ready数据的时候,这时候node的run()函数还是可以处理其他chan的
        // 请求的,这是一种并行的方法,毕竟使用者把日志持久化是一个比较耗时的事情。
        if advancec != nil {
            readyc = nil
        } else {
            // 从传入的参数可以看出来,Ready数据通过上一次的软、硬状态计算这两个状态的变化,其他
            // 的数据都是来源于raft。关于newReady()下面有函数注释。
            rd = newReady(r, prevSoftSt, prevHardSt)
            // 如果有任何更新就要通过readyc把数据输出。其实Ready的设计挺好的,通过一个数据结构
            // 实现了多种数据的回调。
            if rd.containsUpdates() {
                readyc = n.readyc
            } else {
                readyc = nil
            }
        }

        // leader发生了变化,因为初始化lead=None,所以这个变化也可能是刚刚获取leader ID。
        if lead != r.lead {
            // 都是一些日志的东西,核心内容就是知道leader是谁后就可以通过propc读取数据,否则
            // 不处理。这也说明propc里的数据都需要通过leader发起提议才能处理的,前面总结了
            // proc里的数据都是需要有返回值的,这两点一结合,可以总结出:通过proc输入给node
            // 处理的数据都是需要同步到整个集群的(自己不是leader就把数据转发给leader实现),
            // 而这类的数据处理必须有返回值告知用户是否成功,其他的数据通过recvc输入到node,
            // 这类数据只需要节点自己处理,不是集群性的,此时再看这两个chan的命名感觉很深刻了。
            if r.hasLeader() {
                if lead == None {
                    r.logger.Infof("raft.node: %x elected leader %x at term %d", r.id, r.lead, r.Term)
                } else {
                    r.logger.Infof("raft.node: %x changed leader from %x to %x at term %d", r.id, lead, r.lead, r.Term)
                }
                propc = n.propc
            } else {
                r.logger.Infof("raft.node: %x lost leader %x at term %d", r.id, lead, r.Term)
                propc = nil
            }
            lead = r.lead
        }

        select {
        // 处理proc中的数据,当然是通过raft处理的,然后把raft的返回值在返回给调用者
        case pm := <-propc:
            m := pm.m
            m.From = r.id
            err := r.Step(m)
            if pm.result != nil {
                pm.result <- err
                close(pm.result)
            }
        // 处理recvc中的数据
        case m := <-n.recvc:
            // 相比于propc中的数据处理,多了一些判断,来看看这些判断的目的是什么?
            // 1.要么是是合法节节点发来的,这个挺好理解的;
            // 2.关键的是第二点,要么是非响应消息,为什么处理非法节点发来的非响应消息呢?这个笔者
            // 也比较困惑~
            if pr := r.prs.Progress[m.From]; pr != nil || !IsResponseMsg(m.Type) {
                r.Step(m)
            }
        // 配置修改处理
        case cc := <-n.confc:
            // 如果NodeID是None,就变成了获取节点信息的操作~这我相信是后加的功能,放在这里毫无
            // 违和感
            if cc.NodeID == None {
                select {
                case n.confstatec <- pb.ConfState{
                    Nodes:    r.prs.VoterNodes(),
                    Learners: r.prs.LearnerNodes()}:
                case <-n.done:
                }
                break
            }
            // 根据配置修改的类型做不同的处理,但实际的处理都是交给raft实现的
            switch cc.Type {
            // 添加节点
            case pb.ConfChangeAddNode:
                r.addNode(cc.NodeID)
            // 添加learner
            case pb.ConfChangeAddLearnerNode:
                r.addLearner(cc.NodeID)
            // 删除节点
            case pb.ConfChangeRemoveNode:
                // 如果删除的节点是自己,那么就把propc设置为空,避免下一循环接收任何数据
                if cc.NodeID == r.id {
                    propc = nil
                }
                r.removeNode(cc.NodeID)
            case pb.ConfChangeUpdateNode:
            default:
                panic("unexpected conf type")
            }
            // 返回配置状态
            select {
            case n.confstatec <- pb.ConfState{
                Nodes:    r.prs.VoterNodes(),
                Learners: r.prs.LearnerNodes()}:
            case <-n.done:
            }
        // 时钟tick处理
        case <-n.tickc:
            r.tick()
        // Ready输出给使用者后的处理,也就是使用者在调用Advance()之前,此处可以在使用者
        // 处理Ready数据的同时node在做下面的处理,算是一种并行吧。
        case readyc <- rd:
            // 此时的软状态已经成为了上一次软状态
            if rd.SoftState != nil {
                prevSoftSt = rd.SoftState
            }
            // 如果有不可靠日志输出,那么就要记录最后一个不可靠日志的索引和届,在使用者调用Advance()
            // 用来调整log的可靠索引值,下面的代码会有涉及。           
            if len(rd.Entries) > 0 {
                prevLastUnstablei = rd.Entries[len(rd.Entries)-1].Index
                prevLastUnstablet = rd.Entries[len(rd.Entries)-1].Term
                havePrevLastUnstablei = true
            }
            // 如果有硬状态,就要把这个硬状态设置为上一次硬状态
            if !IsEmptyHardState(rd.HardState) {
                prevHardSt = rd.HardState
            }
            // 如果有快照,把快照的索引记为上一次快照的日志
            if !IsEmptySnap(rd.Snapshot) {
                prevSnapi = rd.Snapshot.Metadata.Index
            }
            // 这个处理就是计算Ready里提交日志的最后一个索引,提交日志被使用者应用完调用Advance()
            // 时需要调整log的应用索引。
            if index := rd.appliedCursor(); index != 0 {
                applyingToI = index
            }
            r.msgs = nil
            r.readStates = nil
            // 此处需要解释一下,在raft中统计了未提交日志的总量(单位是字节),用来避免大量日志
            // 造成的大量内存开销,在超过一定量的情况下就不允许提议了。有的读者会问Leader更新
            // 提交索引后才需要减少未提交日志的总量,为什么在这里调用呢?这个思路是对的,Leader
            // 通过quorum获得了最新的提交索引后广播给其他节点,但是读者需要知道,这只是告诉所有
            // 节点日志可以提交到这个索引,但是具体提交动作是需要各自的节点自己完成的。通过Ready
            // 就是把日志提交给了使用者,那自然就在此处把这部分提交日志的大小从未提交总量中减去。
            r.reduceUncommittedSize(rd.CommittedEntries)
            advancec = n.advancec
        // 使用者处理完Ready数据后,调用了Advance()
        case <-advancec:
            // 更新log的应用索引值,这个值就在上面的case中获得的。
            if applyingToI != 0 {
                r.raftLog.appliedTo(applyingToI)
                applyingToI = 0
            }
            // 更新可靠索引值,这个值也是在上面的case中获得的。
            if havePrevLastUnstablei {
                r.raftLog.stableTo(prevLastUnstablei, prevLastUnstablet)
                havePrevLastUnstablei = false
            }
            // 更新快照索引,这个值同样是在上面的case中获得的。
            r.raftLog.stableSnapTo(prevSnapi)
            advancec = nil
        // 获取状态
        case c := <-n.status:
            c <- getStatus(r)
        // 关闭node
        case <-n.stop:
            close(n.done)
            return
        }
    }
}

// 一下是run()函数中引用到的函数
// 创建新的Ready数据,这个函数其实非常重要,因为笔者认为Ready是etcd的raft实现的精髓之一。
func newReady(r *raft, prevSoftSt *SoftState, prevHardSt pb.HardState) Ready {
    // Entries和CommittedEntries这两个值在介绍log的文章中已经说明了,分别代表不可靠日志和
    // 提交日志。不可靠日志需要使用者将其存储可靠存储,提交日志需要使用者应用到系统中。Message
    // 是封装好需要通过网络发送都其他节点的消息,这个笔者会在raft实现的文章做详细说明。此处需要
    // 读者体会一下Ready(准备好的)这个类型的用意,意思就是raft已经准备好需要使用者处理的数据,
    // 这个类型的命名充分的体现了它存在的目的。
    rd := Ready{
        Entries:          r.raftLog.unstableEntries(),
        CommittedEntries: r.raftLog.nextEnts(),
        Messages:         r.msgs,
    }
    // 软状态有变化就要输出,软状态是通过raft对象获取的。
    if softSt := r.softState(); !softSt.equal(prevSoftSt) {
        rd.SoftState = softSt
    }
    // 硬状态有变化就要输出,硬状态是通过raft对象获取的。
    if hardSt := r.hardState(); !isHardStateEqual(hardSt, prevHardSt) {
        rd.HardState = hardSt
    }
    // 如果有新快照需要存储,那就通过Ready输出给使用者
    if r.raftLog.unstable.snapshot != nil {
        rd.Snapshot = *r.raftLog.unstable.snapshot
    }
    // 如果有ReadIndex()的结果,那就通过Ready输出。
    if len(r.readStates) != 0 {
        rd.ReadStates = r.readStates
    }
    // 设置必须同步标记,见下面有函数的解析,其实在etcd中并没有用Ready.MustSync标记,
    // 而是用了MustSync()函数。
    rd.MustSync = MustSync(r.hardState(), prevHardSt, len(rd.Entries))
    return rd
}
// 设置是否必须同步
func MustSync(st, prevst pb.HardState, entsnum int) bool {
    // 有不可靠日志、leader更换以及换届选举都需要设置同步标记,也就是说当有不可靠日志或者
    // 新一轮选举发生时必须等到这些数据同步到可靠存储后才能继续执行,这还算是比较好理解,毕竟
    // 这些状态是全局性的,需要leader统计超过半数可靠可靠以后确认为可靠的数据。如果此时采用
    // 异步实现,就会出现不一致的可能性。
    return entsnum != 0 || st.Vote != prevst.Vote || st.Term != prevst.Term
}

to sum up

Node is an interface class that is completely raft. It provides various interface functions and interface types to convert user requests into raft requests. The designer designed node as a type with routine. I guess as follows:

  1. All requests are implemented through chan, so that there is no need to consider synchronization issues, avoid the use of locks, and simplify the code a lot.
  2. Through the data obtained by node.Ready(), the user can process other chan requests while the user is processing the data, so that concurrency can be achieved until the user calls Advance().

This chapter introduces the type of Ready, which gives us an updated understanding of raft's log processing logic:

  1. Logs are stored and used by users in batches, so the design efficiency is relatively high;
  2. Storing logs and receiving logs are performed synchronously, and the received logs will be cached in the memory to wait for the next round of Ready;
  3. While storing and receiving logs, the submission index will also be updated. The submission index is the upper limit index value that the leader instructs the cluster node to submit the log to the user through Ready.CommittedEntries. It may cover a part of the unstable log. Because node is a single-routine object, the unreliable log output through Ready, the commit log, and the commit index value in the hard state are all at the same point in time. As long as the user is designed to process the Ready data reliably, even if the log is submitted It's okay to have part of it in unstable. Because of the complexity of raft's processing logic, the single-routine design method makes the code of raft easier to implement, otherwise it will be a headache for various locking problems.

Guess you like

Origin blog.csdn.net/weixin_42663840/article/details/101039942