Etcd源码分析-网络模型进阶篇

起初本篇打算介绍raft相关,但是后来发现,还是有必要再深入介绍一下网络模型。
一、基础网络模型

        Etcd采用http(https)协议作为应用层协议,关于http协议介绍不是本篇范畴。大家都知道http一般情况下是无状态协议,且网络是为请求+应答模式,当收到应答http session就结束了(tcp可能不结束)。但是在etcd中可能就不是这样子了。下面是抓取的http报文:

红色框,符合上面经典模型,请求+应答。
蓝色框,只有请求,没有应答。
对于上述蓝色框中请求,为什么没有应答呢?难道不会超时吗?

二、消息流程

2.1 http handler

      上一篇介绍到,在使用net/http模块需要用户自定义http handler(相当于http路由),针对不同http请求,定义不同handler。那么对于etcd中和peer相关的handler有哪些呢?
        在文件rafthttp/transport.go中:

func (t *Transport) Handler() http.Handler {
	pipelineHandler := newPipelineHandler(t, t.Raft, t.ClusterID)
	streamHandler := newStreamHandler(t, t, t.Raft, t.ID, t.ClusterID)
	snapHandler := newSnapshotHandler(t, t.Raft, t.Snapshotter, t.ClusterID)
	mux := http.NewServeMux() //http 请求路由
	mux.Handle(RaftPrefix, pipelineHandler) /* /raft */
	mux.Handle(RaftStreamPrefix+"/", streamHandler)  /* /raft/stream/ */
	mux.Handle(RaftSnapshotPrefix, snapHandler)      /* /raft/snapshot */
	mux.Handle(ProbingPrefix, probing.NewHandler())  /* /raft/probing */
	return mux
}

上面所有罗列出的handler并不是所有,只把相关介绍一下,我们只要知道,不同的url会有与之对应handler即可。

2.2 会话建立

    对于第一节中,我们以GET /raft/stream/message/8a840eaa4b694be1进行说明,因为这个是最复杂的。

2.2.1 报文

首先来看一下,发送http报文,本端Ip为192.63.63.1,远端Ip为192.63.63.30

名称

含义

Host

服务端ip地址以及端口

X-Etcd-Cluster-Id

集群id,每个etcd节点都会随机生成

X-Min-Cluster-Version

集群要求最低版本

X-Peerurls

告诉对端etcd节点,我(本端)监听的peer地址是什么

X-Raft-To

远端etcd节点id

X-Server-Form

本端etcd节点id,用于标识唯一etcd节点。与url后面数字一致

 

2.2.2 会话建立

上面的报文,在哪里构造出来?在哪里发出去呢?流程图如下:

 

rafthttp/http.go,会发现是ServeHTTP方法,这个方法在上一篇已经介绍!

func (h *streamHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	if r.Method != "GET" {
		w.Header().Set("Allow", "GET")
		http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
		return
	}
/*
* 忽略部分代码,这部分代码主要使用构造http头部信息
*/

/* 这个地方需要注意一下,此处并没有包把应答报文发出去,但是具体处理逻辑需要参考net/http中Flush */
	w.WriteHeader(http.StatusOK)
	w.(http.Flusher).Flush()

/* 构造conn对象 */
	c := newCloseNotifier()
	conn := &outgoingConn{
		t:       t,   /* 连接类型 */
		Writer:  w,   /* reponse writer */
		Flusher: w.(http.Flusher),  /* reponse flusher */
		Closer:  c,  /* 连接close channel对象 */
	}
	p.attachOutgoingConn(conn) /* 会发streamWriter run中connc操作 用于*/
	<-c.closeNotify() /* 等待close channel,若一直没数据可读则阻塞 */
}

        通过attach方法,可知会把conn对象写到channel cw.connc中,channel另外一端就在run方法中,下面为run的部分代码片段:

case conn := <-cw.connc: /* 从channel读取conn对象,表示会话已经建立 */
	cw.mu.Lock()
	closed := cw.closeUnlocked()
	t = conn.t
	switch conn.t { /* 根据StreamType生成对应的解析器 */
	    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 /* 用于send消息 等待接收消息 */
	unflushed = 0
	cw.status.activate()
	cw.closer = conn.Closer
	cw.working = true
	cw.mu.Unlock()

	if closed {
		plog.Warningf("closed an existing TCP streaming connection with peer %s (%s writer)", cw.peerID, t)
	}
	plog.Infof("established a TCP streaming connection with peer %s (%s writer)", cw.peerID, t)
	heartbeatc, msgc = tickc.C, cw.msgc //保存心跳和message的通道

 

2.3 消息发送

        发消息的接口为rafthttp/transport.gotransport.send方法,在介绍raft协议时会介绍如何调用此方法,目前只需要知道此方法用于发送消息即可。

func (t *Transport) Send(msgs []raftpb.Message) {
    for _, m := range msgs {
        if m.To == 0 {
            // ignore intentionally dropped message
            continue
        }
        to := types.ID(m.To) /* 将m.To转成type.ID格式 */
/* 以to作为key在map中查找peer对象 */
        t.mu.RLock()
        p, pok := t.peers[to]
        g, rok := t.remotes[to]
        t.mu.RUnlock()

        //存在peer则不去检查remote
        if pok {
            if m.Type == raftpb.MsgApp {
                t.ServerStats.SendAppendReq(m.Size())
            }
            p.send(m)  /* 调用peer.go  (p *peer) send */
            continue
        }

        if rok {
            g.send(m)
            continue
        }

        plog.Debugf("ignored message %s (sent to unknown peer %s)", m.Type, to)
    }
}

func (p *peer) send(m raftpb.Message) {
    p.mu.Lock()
    paused := p.paused
    p.mu.Unlock()

    if paused {
        return
    }
    // 如果消息类型是snapshot则返回pipeline,如果是MsgApp则返回msgAppV2Writer,否则返回wirter
    // wirtec创建是在
    writec, name := p.pick(m) 
    select {
    /* 将消息写入channel中 
* 接收端的channel位于stream.go streamWriter.run msgc 
*/
    case writec <- m: //写入channel
    default:
        p.r.ReportUnreachable(m.To)
        if isMsgSnap(m) {
            p.r.ReportSnapshot(m.To, raft.SnapshotFailure)
        }
        if p.status.isActive() {
            plog.MergeWarningf("dropped internal raft message to %s since %s's sending buffer is full 
                    (bad/overloaded network)", p.id, name)
        }
        plog.Debugf("dropped %s to %s since %s's sending buffer is full", m.Type, p.id, name)
    }
}

    假设返回的writec为streamWriter类型,则上面writec定义在stream.go func (cw *streamWriter)run() ,到了这里会发现在2.2.2节中介绍的会话建立流程也是在这个方法中。
    发送消息具体代码如下:
   //etcd大部分消息是通过http协议 此处使用的http通道   

case m := <-msgc:
    err := enc.encode(&m) /* 格式化消息,如选举消息 */
    if err == nil {
        unflushed += m.Size()
        if len(msgc) == 0 || batched > streamBufSize/2 {/*batched批处理 streamBufSize全局变量 4096 */
            flusher.Flush() /* 刷新缓冲区,发送到对端。Flush代码为net/http模块 */
            sentBytes.WithLabelValues(cw.peerID.String()).Add(float64(unflushed))
            unflushed = 0
            batched = 0
        } else {
            batched++
        }
        continue  /* 发送完成就返回上层 并没有结束会话 */
    }

    cw.status.deactivate(failureType{source: t.String(), action: "write"}, err.Error())
    cw.close() /* 表示本次收发消息结束 即http会话结束 */
    plog.Warningf("lost the TCP streaming connection with peer %s (%s writer)", cw.peerID, t)
    heartbeatc, msgc = nil, nil
    cw.r.ReportUnreachable(m.To)
    sentFailures.WithLabelValues(cw.peerID.String()).Inc()

        上述代码,有一个很关键的代码--continue。这段代码并不是像我们之前理解http请求一样,收到request之后,做处理并且响应一个reponse,最后关闭http会话。然而这里的做法是,发送一个消息后直接continue,并没有结束会话。难道说就是利用http通道(建立的socket),进行长连接操作吗?(c/s模式)。后来通过抓包,验证了我的想法:

发现一些数据在通过2380这端口发送数据(上图中tcp数据长度是59字节),具体内容wireshark无法解析。
至此,发送流程介绍完毕,下面来看一下接收流程。

2.4 消息接收

        在上一篇其实已经介绍了,接收流程,这里再深入介绍一下。etcd中有两个对象:streamReader和streamWriter,通过名字可知,用于读写网络流的。上一小节其实操作就是streamWriter,那么关于接收流程肯定和streamReader相关,流程图如下:

 

上一篇介绍到在rafthttp/stream.go中的run方法,cr.dial用于建立http会话(对应上述报文中没有响应的http请求),cr.decodeLoop循环等待对端的消息,代码如下:

func (cr *streamReader) decodeLoop(rc io.ReadCloser, t streamType) error {
    var dec decoder
    cr.mu.Lock()
    //根据stream类型,创建不同解码器
    switch t {
    case streamTypeMsgAppV2:
        dec = newMsgAppV2Decoder(rc, cr.tr.ID, cr.peerID)
    case streamTypeMessage:
        dec = &messageDecoder{r: rc}
    default:
        plog.Panicf("unhandled stream type %s", t)
    }
    select {
    case <-cr.stopc:
        cr.mu.Unlock()
        if err := rc.Close(); err != nil {
            return err
        }
        return io.EOF
    default:
        cr.closer = rc
    }
    cr.mu.Unlock()
  //死循环 等待消息
    for {
        m, err := dec.decode() //阻塞等待消息
        if err != nil {
            cr.mu.Lock()
            cr.close()
            cr.mu.Unlock()
            return err
        }
        receivedBytes.WithLabelValues(types.ID(m.From).String()).Add(float64(m.Size()))
        cr.mu.Lock()
        paused := cr.paused
        cr.mu.Unlock()
        if paused {
            continue
        }
        if isLinkHeartbeatMessage(&m) {
            // raft is not interested in link layer
            // heartbeat message, so we should ignore
            // it.
            continue
        }
        recvc := cr.recvc
        if m.Type == raftpb.MsgProp {
            recvc = cr.propc
        }
        select {
        case recvc <- m: /* 将消息写到channel中 channel另外一段是rafthttp/peer.go startPeer*/
        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))
            recvFailures.WithLabelValues(types.ID(m.From).String()).Inc()
        }
    }
}

结合流程和相关代码,基本上可以梳理清楚。流图中最后一个方法则进入raft相关处理,对于raft相关内容,后面会有介绍。

三、疑问解答

        经过上一节介绍,如下两个问题就有答案了:
为什么没有应答?etcd使用http作为通道,说明白点就是使用socket通道,传输数据,并没有完全遵守http协议流程。
难道不会超时吗?首先反问一句,超时不超时是由谁决定?由客户端决定!!客户端在发出请求一段时间内没有收到响应则认为超时,进行超时处理逻辑。但若客户端没有超时处理逻辑呢?那永远都不会超时,所以超时并不是由协议决定而是由业务逻辑决定。
        至此所有关于网络模型相关的内容,介绍到这里就算完全结束了,下一篇介绍核心重点之一Raft协议。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值