Nodo de la implementación de la balsa de etcd

1. Introducción

Antes de leer este artículo, espero leer el "registro de realización de la balsa de etcd" y el "rastreador y quórum de realización de la balsa de etcd". Sin el conocimiento previo de los dos primeros artículos, algunos conceptos mencionados en este artículo pueden ser difíciles de entender. Por supuesto, si el lector ya tiene algún conocimiento relevante, puede omitirlo directamente.

La primera impresión del nodo debería ser la realización de la función del nodo de la balsa, pero no es muy precisa. En mi opinión, el nodo debería ser una clase de interfaz de balsa. Si considera balsa como una biblioteca / paquete, entonces la interfaz que utiliza este paquete es nodo. Pero debe tenerse en cuenta que todos los nodos del clúster que utilizan el sistema empresarial de balsa (como, por ejemplo, etcd) deben llamar a este nodo de balsa. Creo que esta es la razón por la que la clase de interfaz balsa se define como nodo.

Al igual que los dos artículos anteriores, ahora explique algunos términos clave en este artículo:

  1. Proponer: Como sugiere el nombre, iniciar una agenda y más de la mitad de los parlamentarios la apoyan aunque se apruebe. Si parece entenderse literalmente que una propuesta es un asunto muy complicado, ¿cómo definir la agenda? ¿Qué cuenta como apoyo? Cómo aprobarlo se ha explicado en el artículo que presenta el quórum. De hecho, la agenda de implementación en la balsa de etcd es una entrada, y un nodo puede recibir este registro incluso si el nodo lo admite. La propuesta se vuelve muy simple: encapsularla en un log y difundirla a todos los nodos, si más de la mitad de ellos no se niegan, incluso serán aprobados.

2. Análisis

2.1nodo

Dado que el autor piensa que el nodo es una clase de interfaz de balsa, existe una cierta base, primero echemos un vistazo a la definición de una interfaz:

// 代码源自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.2 Listo

Con la definición de interfaz de Node, es necesario tener una comprensión completa del tipo de interfaz Ready, porque este tipo contiene mucha información, y comprender Ready es muy útil para comprender la balsa.

// 代码源自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 () es como una función de devolución de llamada, que envía datos de devolución de llamada a través de <-chan Ready.

2.3 Estado

La razón por la que se explica el tipo de estado es porque hay una función de estado () en la interfaz de nodo, que devuelve el tipo de estado.

// 代码源自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 (implementación de Node)

La definición de interfaz de Node se introdujo anteriormente. De acuerdo con las regulaciones no escritas de golang, definitivamente habrá un tipo de implementación de nodo. Echemos un vistazo a la definición de nodo:

// 代码源自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
}

De acuerdo con el nombre y el tipo de la variable, probablemente pueda ver para qué se usa la variable, y es básicamente del tipo chan, lo que significa que la llamada a la interfaz y la realización de la función son a través de chan como medio, y debe haber una rutina responsable del otro extremo de chan. Antes de explicar la implementación de la función de interfaz de nodo, echemos un vistazo a la implementación de la llamada de interfaz:

// 代码源自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}}})
}

Desde la perspectiva de la implementación de la interfaz, los conceptos básicos son todo tipo de operaciones de canal. Cada interfaz tiene un canal dedicado y algunas llamadas de interfaz se convierten en mensajes, lo que permite que el nodo se trate como una especie de mensaje. Esta forma de implementación se debe a que todas las funciones de node se implementan a través de una rutina run (). Golang recomienda chan para la interacción entre rutinas, y node básicamente usa chan al extremo. Antes de iniciar la función run (), ahora complete los pozos excavados arriba, que son las funciones step () y stepWait () De hecho, node implementa una serie de funciones step ().

// 代码源自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
}

Es tan complicado, ¿no puedes usar una llamada de función, tienes que usar chan para hacerlo tan "alto"? En teoría, es posible utilizar funciones, pero es necesario utilizar sync.Mutex para lograr la sincronización necesaria. Después de analizar la función run (), explique por qué el diseñador eligió esta solución en el capítulo de resumen.

Comencemos el viaje al núcleo del nodo ahora y estemos preparados mentalmente para analizar el código fuente de una función relativamente grande ~

// 代码源自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
}

para resumir

Node es una clase de interfaz completamente en balsa que proporciona varias funciones de interfaz y tipos de interfaz para convertir las solicitudes de los usuarios en solicitudes en balsa. El diseñador diseñó el nodo como un tipo con rutina. Supongo que lo siguiente:

  1. Todas las solicitudes se implementan a través de chan, por lo que no hay necesidad de considerar problemas de sincronización, evitar el uso de bloqueos y simplificar mucho el código.
  2. A través de los datos obtenidos por node.Ready (), el usuario puede procesar otras solicitudes de canal mientras el usuario está procesando los datos, de modo que se pueda lograr la concurrencia hasta que el usuario llame a Advance ().

Este capítulo presenta el tipo de Ready, que nos brinda una comprensión actualizada de la lógica de procesamiento de troncos de Raft:

  1. Los usuarios almacenan y utilizan los registros en lotes, por lo que la eficiencia del diseño es relativamente alta;
  2. El almacenamiento y la recepción de registros se realizan de forma síncrona, y los registros recibidos se almacenarán en caché en la memoria para esperar la siguiente ronda de Listo;
  3. Mientras se almacenan y reciben registros, el índice de envío también se actualizará. El índice de envío es el valor del índice de límite superior que el líder le indica al nodo del clúster que envíe el registro al usuario a través de Ready.CommittedEntries. Puede cubrir una parte de lo inestable Iniciar sesión. Debido a que el nodo es un objeto de rutina única, la salida del registro no confiable a través de Ready, el registro de confirmación y el valor del índice de confirmación en el estado duro están todos en el mismo momento. Siempre que el usuario esté diseñado para procesar los datos Ready de manera confiable, incluso si se envía el registro. Está bien que parte de él sea inestable. Debido a la complejidad de la lógica de procesamiento de balsa, el método de diseño de rutina única hace que el código de balsa sea más fácil de implementar; de lo contrario, será un dolor de cabeza para varios problemas de bloqueo.

Supongo que te gusta

Origin blog.csdn.net/weixin_42663840/article/details/101039942
Recomendado
Clasificación