etcd-raft 2.3.7 raft peer间的交互通信流程

在写etcd-raft的leader选举的那篇文章时,每次牵扯到消息接收和发送都用”接收到消息”、“把消息发送”出去这样的字眼给代替了,感觉有那么一点的别扭,这节主要描述下etcd-raft的消息发送和接收。


先附上一副关键struct类成员关系图,先对这个交互复杂度在心里有个数^_^

这里写图片描述

在介绍信息交互流程之前先介绍几个比较关键的结构体类型以及结构体里面关键嵌入成员:
EtcdServer :

// etcd-2.3.7/etcdserver/server.go

type EtcdServer struct {
    // etcdserver中用于与raft模块交互的接口
    r raftNode

    // ...
}

raftNode:

// etcd-2.3.7/etcdserver/raft.go

type raftNode struct {
    //...

    // 指向raft.node,用于用于raft协议相关消息的处理和生成
    raft.Node
    // ...

    // transport specifies the transport to send and receive msgs to members.
    // 最终指向rafthttp.Transport
    transport rafthttp.Transporter
    // ...
}

raft.node:

// etcd-2.3.7/raft/node.go

// node is the canonical implementation of the Node interface
type node struct {
    // 从peer端收到的消息会通过recvc传给raft算法处理
    // 从客户端收到的写请求会通过propc传给raft算法处理
    propc      chan pb.Message
    recvc      chan pb.Message
    // ...
    // 通过readyc channel把已经被raft处理完毕待发送的消息传给raftNode,raftNode在start会select到r.ready()上
    readyc     chan Ready
    // ...
}

rafthttp.Transport:

// etcd-2.3.7/rafthttp/transport.go
type Transport struct {
    // 当前peer连接通过streamReader连接其他peer的超时时间
    DialTimeout time.Duration     // maximum duration before timing out dial of the request
    // ...
    // 当前raft节点的id,以及url
    ID          types.ID   // local member ID
    URLs        types.URLs // local peer URLs

    // ...
    // 指向EtcdServer
    Raft        Raft       // raft state machine, to which the Transport forwards received messages and reports status

    // ...

    // 每个peer代表一个集群中的raft节点,peer之间通过streamReader以及streamWriter相互通信,最终直到一个peer对象
    peers   map[types.ID]Peer    // peers map
    // ...
}

peer:

// etcd-2.3.7/rafthttp/peer.go

type peer struct {
    // id of the remote raft peer node
    id     types.ID
    // 指向EtcdServer,用于在收到消息时把消息内容传给EtcdServer的Process函数
    r      Raft
    // ...
    // msgAppV2Writer与writer等待peer的连接,用于用于发送消息
    msgAppV2Writer *streamWriter
    writer         *streamWriter
    // 发送snap时,文件内容比较多时才会用到
    pipeline       *pipeline
    // ...

    sendc chan raftpb.Message
    // streamReader从该peer的连接中读到非写请求消息时会通过recvc发送给当前peer对象的宿主goroutine,然后通过peer.r.Process传给EtcdServer
    recvc chan raftpb.Message
    // streamReader从该peer的连接中读到slave转发给master的写请求
    propc chan raftpb.Message
    // ...
}

streamReader:

// etcd-2.3.7/rafthttp/stream.go

type streamReader struct {
    // ...
    // recvc以及propc与peer中同名变量指向同一个对象,是peer在startPeer中创建streamReader传入的
    recvc         chan<- raftpb.Message
    propc         chan<- raftpb.Message

    // ...
}

streamWriter:

// streamWriter writes messages to the attached outgoingConn.
type streamWriter struct {
    // ...
    // 指向EtcdServer
    r      Raft

    // ...
    // 对外提供接口函数用于让其他类往msgc中写入需要发送给该peer的消息,然后streamWriter从msgc中接收消息并发送到peer
    msgc  chan raftpb.Message
    // 与peer建立连接的conn
    connc chan *outgoingConn
    // ...
}

与peer通信相关的基本类主要有以上几个,下面分别介绍peer交互的各个步骤:消息的发送,消息的接收。

消息的发送

raft算法模块发送消息的入口是raft.send,相关代码如下:

// etcd-2.3.7/raft/raft.go

func (r *raft) send(m pb.Message) {
    m.From = r.id
    // do not attach term to MsgProp
    // proposals are a way to forward to the leader and
    // should be treated as local message.
    if m.Type != pb.MsgProp {
        m.Term = r.Term
    }
    // 把消息追加msgs消息队列中   
    r.msgs = append(r.msgs, m)
}

raft.send中处理完毕需要发送的消息都放到了msgs消息队列中了,消息的读取主要是在raft.node.run中,读取到消息构造成ready包,然后通过自己的readyc管道发送出去:

// etcd-2.3.7/raft/node.go

func (n *node) run(r *raft) {
    var propc chan pb.Message
    var readyc chan Ready
    var advancec chan struct{}
    var prevLastUnstablei, prevLastUnstablet uint64
    var havePrevLastUnstablei bool
    var prevSnapi uint64
    var rd Ready

    lead := None
    prevSoftSt := r.softState()
    prevHardSt := emptyState

    for {
        if advancec != nil {
            readyc = nil
        } else {
            // 如果msgs队列中有消息就构造ready请求
            rd = newReady(r, prevSoftSt, prevHardSt)
            if rd.containsUpdates() {
                readyc = n.readyc
            } else {
                readyc = nil
            }
        }
        // ...
        select {
        // ...
        case readyc <- rd:
            if rd.SoftState != nil {
                prevSoftSt = rd.SoftState
            }
            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
            }
            r.msgs = nil
            advancec = n.advancec
        case //...
        } // select
    } // for
}

node在run函数里面把消息的传到了自己readyc中,由于raft.node是在raftNode中创建,因此从readyc中读取消息是在raftNode中,raftNode的goroutine相关代码在raftNode.start里面,由于ready消息里面不仅有待发送的消息,还有从其他peer接收的消息,比如slave从master接收的写请求,毕竟etcd的kv存储最终需要EtcdServer去管理的、master上已经被大多数节点写入的请求,需要在本地commit、待发送的消息等等,这些都会通过ready传回,这里我们只保留msg部分:

// etcd-2.3.7/etcdserver/raft.go

func (r *raftNode) start(s *EtcdServer) {
    r.s = s
    // ...

    go func() {
        var syncC <-chan time.Time

        defer r.onStop()
        islead := false

        for {
            select {
            case <-r.ticker:
                r.Tick()
            case rd := <-r.Ready():
                // ...
                if islead {
                    //最终会调用EtcdServer的send函数把消息发出去
                    r.s.send(rd.Messages)
                }

                // ...

                if !islead {
                    //最终会调用EtcdServer的send函数把消息发出去
                    r.s.send(rd.Messages)
                }
                raftDone <- struct{}{}
                r.Advance()
            case <-syncC:
                r.s.sync(r.s.cfg.ReqTimeout())
            case <-r.stopped:
                return
            }
        }
    }()
}

通过上面的代码可知最终会调用EtcdServer的send函数把消息发出去,EtcdServer.send的代码如下:

// etcd-2.3.7/etcdserver/server.go

func (s *EtcdServer) send(ms []raftpb.Message) {
    // ...
    s.r.transport.Send(ms)
}

通过分析EtcdServer的send函数我们可知,该函数有进一步调用了Transport.Send函数,毕竟用于peer间相互交互的对象都是在Transport的管理之下,Transport.Send会循环的发送消息,没法送一条消息是都会通过消息的目标peer id找到关联的peer对象,然后借助于peer.send继续把待发送消息向下传递:

// etcd-2.3.7/rafthttp/transport.go

unc (t *Transport) Send(msgs []raftpb.Message) {
    for _, m := range msgs {
        if m.To == 0 {
            // ignore intentionally dropped message
            continue
        }
        to := types.ID(m.To)

        t.mu.RLock()
        p, ok := t.peers[to]
        t.mu.RUnlock()

        if ok {
            if m.Type == raftpb.MsgApp {
                t.ServerStats.SendAppendReq(m.Size())
            }
            p.send(m)
            continue
        }
        // ...
    }
}

peer.send的代码如下:

// etcd-2.3.7/rafthttp/peer.go

func (p *peer) send(m raftpb.Message) {
    // ...
    // 通过pick取出消息应该给那个streamWriter,或则发送snap时通过pipeline
    writec, name := p.pick(m)
    select {
    case writec <- m:
    // ...
    }
}

func (p *peer) pick(m raftpb.Message) (writec chan<- raftpb.Message, picked string) {
    var ok bool
    // Considering MsgSnap may have a big size, e.g., 1G, and will block
    // stream for a long time, only use one of the N pipelines to send MsgSnap.
    if isMsgSnap(m) {
        return p.pipeline.msgc, pipelineMsg
    } else if writec, ok = p.msgAppV2Writer.writec(); ok && isMsgApp(m) {
        return writec, streamAppV2
    } else if writec, ok = p.writer.writec(); ok {
        return writec, streamMsg
    }
    return p.pipeline.msgc, pipelineMsg
}

streamWriter的writec()函数最终返回的是streamWriter.msgc管道,然后streamWriter通过在发送goroutine中循环的读取msgc管道,拿到待发送的消息,最终通过encoder把消息发送到对端 ,streamHandler何时被设置为peer请求的Handler可以参考etcd 2.3.7 启动流程分析

// etcd-2.3.7/rafthttp/stream.go

func (cw *streamWriter) run() {
    var (
        msgc       chan raftpb.Message
        heartbeatc <-chan time.Time
        t          streamType
        enc        encoder
        flusher    http.Flusher
        batched    int
    )
    tickc := time.Tick(ConnReadTimeout / 3)

    for {
        select {
        // ...
        case m := <-msgc:
        start := time.Now()
        err := enc.encode(m)
        if err == nil {
            if len(msgc) == 0 || batched > streamBufSize/2 {
                flusher.Flush()
                batched = 0
            } else {
                batched++
            }

            reportSentDuration(string(t), m, time.Since(start))
            continue
        }

        reportSentFailure(string(t), m)
        cw.status.deactivate(failureType{source: t.String(), action: "write"}, err.Error())
        cw.close()
        heartbeatc, msgc = nil, nil
        cw.r.ReportUnreachable(m.To)

    // encoder的建立主要是基于原始Tcp Conn,通过connc管道传送到streamWriter goroutine,该连接实在streamHandler.ServeHTTP里面被建立。
    case conn := <-cw.connc:
        cw.close()
        t = conn.t
        switch conn.t {
        case streamTypeMsgAppV2:
            enc = newMsgAppV2Encoder(conn.Writer, cw.fs)
        case streamTypeMessage:
            enc = &messageEncoder{w: conn.Writer}
        default:
            plog.Panicf("unhandled stream type %s", conn.t)
        }
        flusher = conn.Flusher
        cw.mu.Lock()
        cw.status.activate()
        cw.closer = conn.Closer
        cw.working = true
        cw.mu.Unlock()
        heartbeatc, msgc = tickc, cw.msgc
    case <-cw.stopc:
        cw.close()
        close(cw.done)
        return
    }
}

消息接收

上面在进行关键类的介绍时说过streamReader用于与peer建立连接并从连接上读取消息,因此从streamReader的goroutine里面开始:

// etcd-2.3.7/rafthttp/stream.go

func (cr *streamReader) run() {
    for {
        t := cr.t
        // 向peer发起TCP三次握手
        rc, err := cr.dial(t)
        if err != nil {
            // ...
        } else {
            cr.status.activate()
            // 真正的执行IO读取的loop,直到连接被关闭
            err := cr.decodeLoop(rc, t)
            switch {
            // all data is read out
            case err == io.EOF:
            // connection is closed by the remote
            case isClosedConnectionError(err):
            default:
                cr.status.deactivate(failureType{source: t.String(), action: "read"}, err.Error())
            }
        }
        // 如果一次没有连接成功尝试多次连接,特别是系统刚刚启动的时候,很难确定那个raft节点先启动,因此尝试多次连接很有必要。
        select {
        // Wait 100ms to create a new stream, so it doesn't bring too much
        // overhead when retry.
        case <-time.After(100 * time.Millisecond):
        case <-cr.stopc:
            close(cr.done)
            return
        }
    }   
}

func (cr *streamReader) decodeLoop(rc io.ReadCloser, t streamType) error {
    var dec decoder
    cr.mu.Lock()
    switch t {
    // 通过Tcp conn建立一个解码器
    case streamTypeMsgAppV2:
        dec = newMsgAppV2Decoder(rc, cr.local, cr.remote)
    case streamTypeMessage:
        dec = &messageDecoder{r: rc}
    default:
        plog.Panicf("unhandled stream type %s", t)
    }
    cr.closer = rc
    cr.mu.Unlock()
    // 循环的从通过解码器从TCP数据流返回一个个消息
    for {
        m, err := dec.decode()
        // ...
        // 如果是写请求通过propc向消息下游转发,否则使用recvc
        recvc := cr.recvc
        if m.Type == raftpb.MsgProp {
            recvc = cr.propc
        }

        select {
        case recvc <- m:
        default:
            if cr.status.isActive() {
                plog.MergeWarningf("dropped internal raft message from %s since receiving buffer is full (overloaded network)", types.ID(m.From))
            }
            plog.Debugf("dropped %s from %s since receiving buffer is full", m.Type, types.ID(m.From))
        }
    }
}

在streamReader中主要完成从TCP数据流中解码消息,并把消息通过recvc或propc传递到消息处理的下游,在上面介绍streamReader类时,介绍过recvc以及propc与peer的recvc以及propc指向同一个channel,接收消息并处理消息的流程主要在Peer.startPeer中,在startPeer中你不仅能看到peer从recvc以及propc中接收消息的过程还能看到streamReader以及streamWriter的创建流程,这里只分析消息的传递流程,stream***创建主要是参数的传递,感兴趣的可以追下参数:

// etcd-2.3.7/rafthttp/peer.go

func startPeer(transport *Transport, urls types.URLs, local, to, cid types.ID, r Raft, fs *stats.FollowerStats, errorc chan error, v3demo bool) *peer { 
    status := newPeerStatus(to)
    picker := newURLPicker(urls)
    p := &peer{
        id:             to,
        r:              r,
        v3demo:         v3demo,
        status:         status,
        picker:         picker,
        msgAppV2Writer: startStreamWriter(to, status, fs, r), 
        writer:         startStreamWriter(to, status, fs, r), 
        pipeline:       newPipeline(transport, picker, local, to, cid, status, fs, r, errorc),
        snapSender:     newSnapshotSender(transport, picker, local, to, cid, status, r, errorc),
        sendc:          make(chan raftpb.Message),
        recvc:          make(chan raftpb.Message, recvBufSize),
        propc:          make(chan raftpb.Message, maxPendingProposals),
        stopc:          make(chan struct{}),
    }   

    ctx, cancel := context.WithCancel(context.Background())
    p.cancel = cancel
    // 下面启动了两个goroutine分别从recvc以及propc中读取消息,无论从那个channel读取消息,都最终把消息传递给了EtcdServer.Process
    go func() {
        for {
            select {
            case mm := <-p.recvc:
                if err := r.Process(ctx, mm); err != nil {
                    plog.Warningf("failed to process raft message (%v)", err)
                }
            case <-p.stopc:
                return
            }
        }
    }()
    // r.Process might block for processing proposal when there is no leader.
    // Thus propc must be put into a separate routine with recvc to avoid blocking
    // processing other raft messages.
    go func() {
        for {
            select {
            case mm := <-p.propc:
                if err := r.Process(ctx, mm); err != nil {
                    plog.Warningf("failed to process raft message (%v)", err)
                }
            case <-p.stopc:
                return
            }
        }
    }()

    p.msgAppV2Reader = startStreamReader(transport, picker, streamTypeMsgAppV2, local, to, cid, status, p.recvc, p.propc, errorc)
    p.msgAppReader = startStreamReader(transport, picker, streamTypeMessage, local, to, cid, status, p.recvc, p.propc, errorc)

    return p
}

EtcdServer的Process方法主要代码如下:

// etcd-2.3.7/etcdserver/server.go

func (s *EtcdServer) Process(ctx context.Context, m raftpb.Message) error {
    // ...  
    return s.r.Step(ctx, m)
}

通过分析Process的代码,消息最终又被传递给了raftNode的Step方法,虽然raftNode没有直接实现Step,但是raftNode嵌入了接口raft.Node,并且这个接口最终指向一个raft.node,因此我们直接跳到raft.node的Step方法:

// etcd-2.3.7/raft/node.go

func (n *node) Step(ctx context.Context, m pb.Message) error {
    // ignore unexpected local messages receiving over network
    if IsLocalMsg(m) {
        // TODO: return an error?
        return nil
    }
    return n.step(ctx, m)
}

func (n *node) step(ctx context.Context, m pb.Message) error {
    ch := n.recvc
    if m.Type == pb.MsgProp {
        ch = n.propc
    }

    select {
    case ch <- m:
        return nil
    case <-ctx.Done():
        return ctx.Err()
    case <-n.done:
        return ErrStopped
    }
}

通过上面代码实现我们可以看到消息被发送到node的recvc和propc(不过这里要区别与peer的recvc以及propc,不同),消息传入channel后在raft.node的run方法中被接收:

func (n *node) run(r *raft) {
// ...

    for {
       // ...

        select {
        // ...
        case m := <-propc:
            m.From = r.id
            r.Step(m)
        case m := <-n.recvc:
            // filter out response message from unknown From.
            if _, ok := r.prs[m.From]; ok || !IsResponseMsg(m) {
                r.Step(m) // raft never returns an error
            }
       // ...
    }
}

通过上面的代码可以知道,无论消息从propc还是recvc收到都最终传入raft.raft.Step,交给你raft算法去处理了,最终会把消息传入raft.raft.step里面的一个step函数变量,这个变量在当前节点在集群中扮演的角色不同指向的函数函数不同,step指向的函数有三个:stepLeader、stepCandidate以及stepFollower,经过这三个中的任何一个函数处理完之后发送出去(发送的流程向上翻)。

猜你喜欢

转载自blog.csdn.net/u010154685/article/details/80855825