ETCD 源码学习--节点数据传输 Transport 源码

在 ETCD 源码学习过程,不会讲解太多的源码知识,只讲解相关的实现机制,需要关注源码细节的朋友可以自行根据文章中的提示,找到相关源码进行学习。

节点添加

/etcdserver/server.go 中,NewServer 会调用如下 AddRemote 和 AddPeer 添加节点

//添加远程节点
func (t *Transport) AddRemote(id types.ID, us []string) {
	t.mu.Lock()
	defer t.mu.Unlock()
	if t.remotes == nil {
		return
	}
	if _, ok := t.peers[id]; ok {
		return
	}
	if _, ok := t.remotes[id]; ok {
		return
	}
	urls, err := types.NewURLs(us)
	...
	t.remotes[id] = startRemote(t, urls, id)
	...
}

//添加伙伴节点
func (t *Transport) AddPeer(id types.ID, us []string) {
	t.mu.Lock()
	defer t.mu.Unlock()
	if t.peers == nil {
		panic("transport stopped")
	}
	if _, ok := t.peers[id]; ok {
		return
	}
	urls, err := types.NewURLs(us)
	...
	fs := t.LeaderStats.Follower(id.String())
	t.peers[id] = startPeer(t, urls, id, fs)
	...
}

remote 实例

type remote struct {
	lg       *zap.Logger
	localID  types.ID
	id       types.ID
	status   *peerStatus
	pipeline *pipeline //通过 pipeline 发送消息
}
//启动远程节点实例
func startRemote(tr *Transport, urls types.URLs, id types.ID) *remote {
	...
	pipeline := &pipeline{
		peerID: id,
		tr:     tr,
		picker: picker,
		status: status,
		raft:   tr.Raft,
		errorc: tr.ErrorC,
	}
	pipeline.start()
	...
}

//remote 发送消息时,会将消息发送的 pipeline.msgc 中,等待 pipeline 消费
func (g *remote) send(m raftpb.Message) {
	select {
	case g.pipeline.msgc <- m:
	default:
		...
	}
}

peer 实例

func startPeer(t *Transport, urls types.URLs, peerID types.ID, fs *stats.FollowerStats) *peer {
	...
	status := newPeerStatus(t.Logger, t.ID, peerID)
	picker := newURLPicker(urls)
	errorc := t.ErrorC
	r := t.Raft      
	//创建 pipeline 实例      
	pipeline := &pipeline{ 
		...
	}
	pipeline.start() //启动 pipeline
	//创建 peer 实例
	p := &peer{ 
		...
	}
	//处理 node 产生的消息
	go func() { 
		for {
			select {
			case mm := <-p.recvc:
				if err := r.Process(ctx, mm); err != nil {
				}
			case <-p.stopc:
				return
			}
		}
	}()
}

//发送消息
func (p *peer) send(m raftpb.Message) {
	p.mu.Lock()
	paused := p.paused
	p.mu.Unlock()

	if paused {
		return
	}
	//需要根据不同的消息类型,来选择是发送给 pipeline 管道还是 stream 管道
	writec, name := p.pick(m)
	select {
	case writec <- m:
	default:
		...
	}
}

//发送快照消息
func (p *peer) sendSnap(m snap.Message) {
	go p.snapSender.send(m)
}

//消息管道选择
func (p *peer) pick(m raftpb.Message) (writec chan<- raftpb.Message, picked string) {
	var ok bool
	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
}

/*
 该函数的作用是,当 HTTServer 接收到新请求,会调用该函数,将请求的套接字 conn 绑定到 stream 中,stream 实例对 conn 进行读写
*/

func (p *peer) attachOutgoingConn(conn *outgoingConn) {
	var ok bool
	switch conn.t {
	case streamTypeMsgAppV2:
		ok = p.msgAppV2Writer.attach(conn)
	case streamTypeMessage:
		ok = p.writer.attach(conn)
		...
	}
	...
}

pipeline 实例

func (p *pipeline) start() {
	p.stopc = make(chan struct{})
	p.msgc = make(chan raftpb.Message, pipelineBufSize)
	p.wg.Add(connPerPipeline)
	//开启多个 goroutine 处理消息
	for i := 0; i < connPerPipeline; i++ {
		go p.handle()
	}
}


func (p *pipeline) handle() { //发送消息,并报告raft底层,发送状态
	defer p.wg.Done()
	for {
		select {
		case m := <-p.msgc: //读取管道消息进行处理
			err := p.post(pbutil.MustMarshal(&m)) //序列化之后,通过 post 发送消息
			...
		case <-p.stopc:
			return
		}
	}
}

func (p *pipeline) post(data []byte) (err error) {
	u := p.picker.pick()
	req := createPostRequest(p.tr.Logger, u, RaftPrefix, bytes.NewBuffer(data), "application/protobuf", p.tr.URLs, p.tr.ID, p.tr.ClusterID)
	...
	/*
		发送 http 请求,
		这里的 pipelineRt,其实就是我们最开始说的 etcd 覆写的 Transport
	*/
	resp, err := p.tr.pipelineRt.RoundTrip(req)
	...

	return nil
}

streamWriter 实例

func startStreamWriter(lg *zap.Logger, local, id types.ID, status *peerStatus, fs *stats.FollowerStats, r Raft) *streamWriter {
	w := &streamWriter{
		...
	}
	go w.run()
	return w
}

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

	for {
		select {
		/*
			定时发送心跳消息,该心跳与 raft 中的 MsgHeartbeat 不同,而是为了防止底层网络连接超时。主要目的是防止连接长时间不用而断开,同时将积压的消息Flush出去
		*/
		case <-heartbeatc:
			err := enc.encode(&linkHeartbeatMessage)
			unflushed += linkHeartbeatMessage.Size()
			if err == nil {
				flusher.Flush()
				batched = 0
				sentBytes.WithLabelValues(cw.peerID.String()).Add(float64(unflushed))
				unflushed = 0
				continue
			}
			...

		case m := <-msgc:
			err := enc.encode(&m)
			if err == nil {
				unflushed += m.Size()

				/*
					在这里,消息是不会马上发送,会先积压在网络缓冲区,等到积累到一定的量,一起发送,以提高效率
					下面两种情况会将积压的消息真正发送出去
					一是消息积累到一定的阈值,比如下面的 if 条件
					二是超过一定的时间,比如上面的 heartbeatc 
				*/
				if len(msgc) == 0 || batched > streamBufSize/2 {
					flusher.Flush()
					sentBytes.WithLabelValues(cw.peerID.String()).Add(float64(unflushed))
					unflushed = 0
					batched = 0
				} else {
					batched++
				}

				continue
			}
			...
		/*
			其他节点主动跟当前节点创建连接,该连接实例会写入对应的 peer.writer.connc 通道,
			在 streamWriter.run 中,会通过该通道获取该连接的实例然后才能进行读写操作
		*/
		case conn := <-cw.connc:
			cw.mu.Lock()
			closed := cw.closeUnlocked()
			t = conn.t
			//根据不同的消息类型,实例化不同的 encoder/decoder 
			switch conn.t {
			case streamTypeMsgAppV2: 
				enc = newMsgAppV2Encoder(conn.Writer, cw.fs)
			case streamTypeMessage:
				enc = &messageEncoder{w: conn.Writer}
...
			}
			...
			flusher = conn.Flusher
			unflushed = 0
			cw.status.activate()
			cw.closer = conn.Closer
			cw.working = true
			cw.mu.Unlock()
			...
			heartbeatc, msgc = tickc.C, cw.msgc
			...
		}
	}
}

streamReader 实例

func (cr *streamReader) start() {
	...
	go cr.run()
}

func (cr *streamReader) run() {
	...
	for {
		rc, err := cr.dial(t) //对目标节点发送一个 http 请求
		if err != nil {
			...
		} else {
			...
			//读取对端消息,并将消息写入 StreamReader.recvc 通道中
			err = cr.decodeLoop(rc, t) 
			...
			switch {
			// all data is read out
			case err == io.EOF:
			// connection is closed by the remote
			case transport.IsClosedConnError(err):
			default:
				cr.status.deactivate(failureType{source: t.String(), action: "read"}, err.Error())
			}
		}
		...
	}
}

func (cr *streamReader) decodeLoop(rc io.ReadCloser, t streamType) error {
	var dec decoder
	cr.mu.Lock()
	//根据不同消息类型,实例化不同的消息解析器
	switch t {
	case streamTypeMsgAppV2:
		dec = newMsgAppV2Decoder(rc, cr.tr.ID, cr.peerID)
	case streamTypeMessage:
		dec = &messageDecoder{r: rc}
	...
	}
	...

	for {
		//从 conn 中读取消息,并进行解析
		m, err := dec.decode()
		if err != nil {
			cr.mu.Lock()
			cr.close()
			cr.mu.Unlock()
			return err
		}
		...
		//心跳消息
		if isLinkHeartbeatMessage(&m) {
			continue
		}
		//根据不同的消息类型,选择正确的管道
		recvc := cr.recvc
		if m.Type == raftpb.MsgProp {
			recvc = cr.propc
		}

		select {
		case recvc <- m:
		...
		}
	}
}

总结

本文主要介绍了 remote 节点、peer 节点 和 leader 节点之间消息传输的主要实现,传输方式会根据不同的消息类型从 pipeline 和 stream 中进行选择,pipeline 主要用于传输大数据,短链接,而 stream 主要用于传输小数据,是一个长连接。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值