etcd系列-----raft网络层实现之rafthttp模块

raftexample示例中, raftNode.serveRaft()方法中有一段使用的HTTP库的代码,实现如下:

func (rc *raftNode) serveRaft() {
	url, err := url.Parse(rc.peers[rc.id-1])
	if err != nil {
		log.Fatalf("raftexample: Failed parsing URL (%v)", err)
	}
//创建stoppableListener实例, stoppableListener内嵌了net.TCPListener接口,它会与http. Server配合实现对当前节点的URL地址进行监听
	ln, err := newStoppableListener(url.Host, rc.httpstopc)
	if err != nil {
		log.Fatalf("raftexample: Failed to listen rafthttp (%v)", err)
	}

	err = (&http.Server{Handler: rc.transport.Handler()}).Serve(ln)//创建http.Server实例
	select {
	case <-rc.httpstopc:
	default:
		log.Fatalf("raftexample: Failed to serve rafthttp (%v)", err)
	}
	close(rc.httpdonec)
}

这里创建了一个http.Server实例,它会通过传入的TCPListener实例监昕指定地址上的连接请求,当TCPListener实例通过Accept()方法监昕到新连接到来时,会创建对应的net.Conn实例(可以将其理解为一个单独的网络连接),  http.Server 会为每个连接启动一个独立的后台goroutine,处理该连接上的请求。每个请求的处理逻辑封装在http.Server.Handler 中(创建http.Server实例时传入),流程如下图:

处理消息使用的Handler实例是由rafthttp.Transporter.Handler()方法创建的, 其中为多个路径绑定了对应的Handler实例:

func (t *Transport) Handler() http.Handler {
//创建pipelineHandler、 streamHandler和snapshotHandler三个实例,这三个实例都实现了http.Server.Handler接口,具体实现在本章后面介绍rafthttp.Transport时详细分析
	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)
	:= http.NewServeMux()
	mux.Handle(RaftPrefix, pipelineHandler)
	mux.Handle(RaftStreamPrefix+"/", streamHandler)
	mux.Handle(RaftSnapshotPrefix, snapHandler)
	mux.Handle(ProbingPrefix, probing.NewHandler())
	return mux
}

 这些handle是如何被调用的呢?首先是http.Server.Serve()方法,其中通过net.Listener接收新连接,并为每个连接启动一个单独的后台goroutine来处理该连接上的请求.
在http.Server.Serve()中首先调用ηet.Listener的Accept()函数将将net.Conn封装成http.conn,然后单独起一个goroutine处理新连接。进入http.conn.Serve()方法,该方法会读取客户端的请求创建serverHandler实例,并调用其ServeHTTP()方法处理请求。处理单个请求的核心就是serverHandler.ServeHTTP()方法,该方法会将请求交给绑定的http.Server.Handler实例进行处理,需要注意的是这里serverHandler.srv字段指向的就是前面创建的http.Server实例

func  (sh serverHandler)  ServeHTTP(rw ResponseWr工ter, req *Request)  { 
    //http.Server.Handler字段指向的就是前面通过Transport.Handler()方法创建的http.ServeMux实例
    handler :=  sh.srv.Handler 
    if handler == nil {//如采http.Server未初始化Handler字段,则使用默认的ServeMux实例
        handler = DefaultServeMux
	}
       
    handler.ServeHTTP(rw,  req)  //调用http.ServeMux.ServeHTTP()方法进行处理
}

http.ServeMux.ServHttp()方法会根据请求的具体路径,选择合适的Handler 实例处理请求,该方法的具体实现如下:

func  (mux  *ServeMux)  ServeHTTP(w ResponseWr工ter, r  *Request)  { 

    h, := mux.Handler(r) // 根据请求路径选择对应的Handler
    h.ServeHTTP(w,  r)  // 将请求交给Handler进行处理
}

//下面是ServeMux. Handler()方法的具体实现

func  (mux  *ServeMux)  Handler (r *Request)  (h  Handler, pattern string)  { 
    //对CONNECT请求方法的处理(略)
    host : =  stripHostPort(r.Host) //从请求中获取host信息
    path := cleanPath(r.URL.Path) // 从请求中获取请求路径
    return mux.handler(host, r .URL.Path) // 根据host信息和请求路径查找对应的Handler实例
}

//下面是ServeMux.handler()方法的具体实现

func  (mux *ServeMux)  handler (host,  path string)  (h  Handler,  pattern string)  { 
    if mux.hosts {  //首先根据host和path的组合查找
        h,  pattern= mux.match(host +path) 
	}
    if h  == nil {  //只根据path查找
        h, pattern = mux.match(path) 
	}
    return 
}

//下面是ServeMux. match()方法的具体实现

func  (mux *ServeMux)  match (path string)  (h  Handler,  pattern string) { 
    v,  ok := mux.m[path] //如采存在精确匹配的Handler,则直接返回
    if ok {  return v.h,  v.pattern  } 
    for k,  v  := range mux.m  { //查找与path匹配程度最高的Handler实例
        //具体查找过程(略)
    }
    return 
}

rafthttp模块详解

通过前面介绍对raft模块的介绍我们知道,raft模块井未提供网络层的相关实现,而是将待发送的消息封装进Ready实例返回给上层模块,然后由上层模块决定如何将这些消息发送到集群中的其他节点。etcd 将网络层相关的实现独立成一个单独的模块,也就是要详细介绍的rafthttp模块。之所以独立出该模块,是为了降低raft模块与网络层实现之间的耦合,降低raft模块和rafthttp模块实现的成本,提高整个程序的可扩展性。在etcd中有多种消息类型,不同类型的消息所能携带的数据大小也不尽相同,其中快照相关消息的数据量就比较大,小至几k,大至几GB都是有可能的,而Leader节点到Follower,大至几GB都是有可能的,而Leader节点到Follower节点之间的心跳消息一般只有几十到几百个字节。因此,etcd的网络层会创建两个消息传输通道。

上面说的两个消息传输通道:Stream消息通道和Pipeline消息通道。这两种消息通道的主要区别在于: Stream消息通道维护的HTTP长连接,主要负责传输数据量较小、发送比较频繁的消息, 例如,前面介绍的MsgApp消息、MsgHeartbeat消息、MsgVote消息等;而Pipeline 消息通道在传输数据完成后会立即关闭连接,主要负责传输数据量较大、发送频率较低的消息,例如,MsgProp、MsgSnap消息等。

Stream消息通道是节点启动后,主动与集群中的其他节点建立的。每个Stream消息通道有2个关联的后台goroutine, 其中一个用于建立关联的HTTP连接,并从连接上读取数据,然后将这些读取到的数据反序列化成Message实例,传递到raft模块中进行处理。另外一个后台goroutine会读取raft模块返回的消息数据并将其序列化,最后写入Stream消息通道。前面对raftNode.serveRaft()方法的介绍,在启动http.Server 时会通过rafthttp.Transporter.Handler()方法为指定的URL路径添加相应的Handler实例,其中“/raft/stream/” 路径对应的Handler为此eamHandler类型,它负责处理Stream消息通道上的请求。相同的,pipelineHandler、snapshotHandler类型都有相应的路径。

1、rafthttp.Transporter接口

rafthttp.Transporter 接口,它是rafthttp包的核心接口之一,它定义了etcd网络层的核心功能,其具体定义如下:

type Transporter interface {
    //初始化操作
	Start() error
	//创建Handler实例,并关联到指定的URL上
	Handler() http.Handler
	//发送消息
	Send(m []raftpb.Message)
	//发送快照数据
	SendSnapshot(m snap.Message)
	//在集群中添加一个节点时,其他节点会通过该方法添加该新加入节点的信息
	AddRemote(id types.ID, urls []string)
	Peer接口是当前节点对集群中其他节点的抽象表示,而结构体peer则是Peer接口的一个具体实现下面几个方法就是对Peer的操作,通过名称即可了解其含义
	AddPeer(id types.ID, urls []string)
	RemovePeer(id types.ID)
	RemoveAllPeers()
	UpdatePeer(id types.ID, urls []string)
	ActiveSince(id types.ID) time.Time
	ActivePeers() int
	Stop()
}

还有另一个需要重点介绍的接口:Raft,在前面介绍raftexample示例时,也简单提到过该接口,该接口的定义如下:

type Raft interface {
    //将指定的消息实例传递到底层的etcd-raft模块进行处理
	Process(ctx context.Context, m raftpb.Message) error
	IsIDRemoved(id uint64) bool
	//通知底层etcd-raft模块, 当前节点与指定的节点无法遥远
	ReportUnreachable(id uint64)
	//通知底层的etcd-raft模块,快照数据是否发送成功
	ReportSnapshot(id uint64, status raft.SnapshotStatus)
}
rafthttp.Transport是rafthttp.Transporter接口具体实现,rafthttp.Transport中定义的关键字段:
    ID  (types.ID类型): 当前节点自己的ID。
    URLs ( types.URLs类型): 当前节点与集群中其他节点交互时使用的Url地址。
    ClusterlD ( types.ID类型):当前节点所在的集群的ID。
    Raft ( Raft类型): Raft是一个接口,其实现的底层封装了前面介绍的etcd-raft模块,当rafthttp.Transport收到消息之后,会将其交给Raft实例进行处理。
    Snapshotter ( *snap. Snapshotter类型):Snapshotter负责管理快照文件,后面会介绍其实现。
    streamRt ( http.RoundTripper类型): Stream消息通道中使用的http. RoundTripper实例。
    pipelineRt ( http.RoundTripper 类型):Pipeline 消息通道中使用的http.RoundTripper实例
    peers( map[types. ID]Peer类型):Peer接口是当前节点对集群中其他节点的抽象表示。对于当前节点来说,集群中其他节点在本地都会有一个Peer 实例与之对应,peers 字段维护了节点ID到对应Peer实例之间的映射关系。
    remotes ( map[types.ID]*remote类型): remote 中只封装了pipeline 实例,remote主要负责发送快照数据,帮助新加入的节点快速追赶上其他节点的数据。
    prober ( probing.Prober类型):用于探测Pipeline消息通道是否可用。
	
func (t *Transport) Start() error {
	var err error
	//创建Stream消息通道使用的http.RoundTripper实例,底层实际上是创建http.Transport实例,
    //这里需要读者注意一下几个参数的设置,分别是:创建连接的超时时间(根据配置指定)、读写请求的超时时间(默认Ss)和keepAl工ve时间(默认为30s)
	t.streamRt, err = newStreamRoundTripper(t.TLSInfo, t.DialTimeout)
	if err != nil {
		return err
	}
	//创建Pipeline消息通道用的http.RoundTripper实例与streamRt不同的是,读写请求的起时时间设置成了永不过期
	t.pipelineRt, err = NewRoundTripper(t.TLSInfo, t.DialTimeout)
	if err != nil {
		return err
	}
	t.remotes = make(map[types.ID]*remote)
	t.peers = make(map[types.ID]Peer)
	t.pipelineProber = probing.NewProber(t.pipelineRt)
	t.streamProber = probing.NewProber(t.streamRt)

	// If client didn't provide dial retry frequency, use the default
	// (100ms backoff between attempts to create a new stream),
	// so it doesn't bring too much overhead when retry.
	if t.DialRetryFrequency == 0 {
		t.DialRetryFrequency = rate.Every(100 * time.Millisecond)
	}
	return nil
}	

Transport.Handler()方法主要负责创建Steam消息通道和Pipeline 消息通道用到的Handler实例,并注册到相应的请求路径上, 其具体实现在前面介绍过了 。  
下面再来看一下Transport.AddPeer()方法,其主要工作就是创建井启动对应节点的Peer实例,具体实现如下:

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)
	if err != nil {
		plog.Panicf("newURLs %+v should never fail: %+v", us, err)
	}
	fs := t.LeaderStats.Follower(id.String())
	//创建指定节点对应的Peer实例,其中会相关的Stream消息通道和Pipeline消息通道
	t.peers[id] = startPeer(t, urls, id, fs)
	//每隔一段时间, prober会向该节点发送探测消息, 检测对端的健康状况
	addPeerToProber(t.pipelineProber, id.String(), us, RoundTripperNameSnapshot, rtts)
	addPeerToProber(t.streamProber, id.String(), us, RoundTripperNameRaftMessage, rtts)
	plog.Infof("added peer %s", id)
}	
Transport. Send()方法负责发送指定的ra位pb.Message消息,其中首先尝试使用目标节点对应的Peer实例发送消息,如果没有找到对应的Peer实例,则尝试使用对应的remote实例发送消息。 Transport.Send()方法的具体实现如下	
func (t *Transport) Send(msgs []raftpb.Message) {
	for _, m := range msgs {//遍历msgs切片中的全部J肖息
		if m.To == 0 {
			// ignore intentionally dropped message
			continue
		}
		//根据raftpb.Message.To字段,获取目标节点对应的Peer实例
		to := types.ID(m.To)

		t.mu.RLock()
		p, pok := t.peers[to]
		g, rok := t.remotes[to]
		t.mu.RUnlock()

		if pok {
			if m.Type == raftpb.MsgApp {
				t.ServerStats.SendAppendReq(m.Size())
			}
			p.send(m)//通过peer.send()方法完成消息的发送
			continue
		}

		if rok {
		//如采指定节点ID不存在对应的Peer实例,则尝试使用查找对应remote实例,通过remote. send()方法完成消息的发送
			g.send(m)
			continue
		}

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

Transport.SendSnapshot()方法负责发送指定的snap.Message 消息(其中封装了对应的MsgSnap消息实例及其他相关信息〉。该方法是通过调用peer.sendSnap()方法完成的。  

2、 Peer接口

type Peer interface {
    //发送单个消息, 该方法是非阻塞的,如采出现发送失败,则会将失败信息报告给底层的Raft接口
	send(m raftpb.Message)
    //发送snap.Message,其他行为与上面的send()方法类似
	sendSnap(m snap.Message)

	update(urls types.URLs)

	attachOutgoingConn(conn *outgoingConn)

	activeSince() time.Time

	stop()
}	
rafhttp.peer是Peer接口的具体实现,其中各字段的含义如下	
    id  ( types.ID类型):该peer实例对应的节点的ID。
    r  ( Raft类型): Raft接口,在Raft接口实现的底层封装了etcd-raft模块。
    picker ( *urlPicker类型):每个节点可能提供了多个URL供其他节点访问, 当其中一个访问失败时,我们应该可以尝试访问另一个。 而urlPicker提供的主要功能就是在这些URL之间进行切换。
    writer ( *streamWrite「类型): 负责向Stream消息通道写入消息。
    msgAppReader ( *streamReader类型): 负责从Stream消息通道读取消息。
    pipeline ( *pipeline类型):Pipeline消息通道。
    snapSender ( *snapshotSende「类型): 负责发送快照数据。
    recvc ( ehan raftpb.Message类型):从Stream消息通道中读取到消息之后, 会通过该通道将消息交给Raft接口,然后由它返回给底层etcd-raft模块进行处理。
    Prope( ehan raftpb. Message类型):从Stream消息通道中读取到MsgProp类型的消息之后,会通过该通道将MsgProp消息交给Raft接口,然后由它返回给底层etcd-raft模块进行处理。
    paused ( bool类型):是否暂停向对应节点发送消息。

 在rafthttp.peer.startPeer()方法中完成了初始化上述字段的工作,同时也启动了关联的后台goroutine。该方法的具体实现如下

func startPeer(transport *Transport, urls types.URLs, peerID types.ID, fs *stats.FollowerStats) *peer {
	plog.Infof("starting peer %s...", peerID)
	defer plog.Infof("started peer %s", peerID)

	status := newPeerStatus(peerID)
	picker := newURLPicker(urls)
	errorc := transport.ErrorC
	r := transport.Raft
	pipeline := &pipeline{//创建pipeine实例
		peerID:        peerID,
		tr:            transport,
		picker:        picker,
		status:        status,
		followerStats: fs,
		raft:          r,
		errorc:        errorc,
	}
	pipeline.start()//启动pipeline
    //创建Peer实例
	p := &peer{
		id:             peerID,
		r:              r,
		status:         status,
		picker:         picker,
		msgAppV2Writer: startStreamWriter(peerID, status, fs, r),
		writer:         startStreamWriter(peerID, status, fs, r),//创建并启动streamWriter
		pipeline:       pipeline,
		snapSender:     newSnapshotSender(transport, picker, peerID, status),
		recvc:          make(chan raftpb.Message, recvBufSize),//创建recvc远远,注意缓冲区大小
		propc:          make(chan raftpb.Message, maxPendingProposals),//创建propc通道,注意缓冲区大小
		stopc:          make(chan struct{}),
	}

	ctx, cancel := context.WithCancel(context.Background())
	p.cancel = cancel
	//启动单独的goroutine,它主妾负责将recvc通道中读取消息,该通道中的消息就是从对端节点发送过来的消息,然后将读取到的消息交给底层的Raft状态机进行处理
	go func() {
		for {
			select {
			case mm := <-p.recvc://从recvc远远中获取连接上读取到的Message,将Message交给底层Raft状态机处理
				if err := r.Process(ctx, mm); err != nil {
					plog.Warningf("failed to process raft message (%v)", err)
				}
			case <-p.stopc:
				return
			}
		}
	}()

	//在底层Raft状态机处理MsgProp类型的Message时,可能会阻塞,所以启动单独的goroutine来处理
	go func() {
		for {
			select {
			case mm := <-p.propc://从propc通道中获取MsgProp类型的Message,将Message交给底层Raft状态机处理
				if err := r.Process(ctx, mm); err != nil {
					plog.Warningf("failed to process raft message (%v)", err)
				}
			case <-p.stopc:
				return
			}
		}
	}()

	p.msgAppV2Reader = &streamReader{
		peerID: peerID,
		typ:    streamTypeMsgAppV2,
		tr:     transport,
		picker: picker,
		status: status,
		recvc:  p.recvc,
		propc:  p.propc,
		rl:     rate.NewLimiter(transport.DialRetryFrequency, 1),
	}
	//创建并启动streamReader实例, 它主要负责从Stream消息遥远上读取消息
	p.msgAppReader = &streamReader{
		peerID: peerID,
		typ:    streamTypeMessage,
		tr:     transport,
		picker: picker,
		status: status,
		recvc:  p.recvc,
		propc:  p.propc,
		rl:     rate.NewLimiter(transport.DialRetryFrequency, 1),
	}

	p.msgAppV2Reader.start()
	p.msgAppReader.start()

	return p
}

看看peer核心方法。首先是peer.send()方法,前面分析的Transport.Send()方法就是通过调用该方法实现消息发送功能的

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

	if paused {
		return
	}
    //根据消息的类型选择合适的消息通道,peer.pick ()方法下面会详细介绍
	writec, name := p.pick(m)
	select {
	case writec <- m://将Message写入writec通道中,发送
	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)
		sentFailures.WithLabelValues(types.ID(m.To).String()).Inc()
	}
}

在peer.pick()方法中,会根据消息的类型选择合适的消息通道并返回相应的通道供send()方法写入待发迭的消息,具体实现如下

func (p *peer) pick(m raftpb.Message) (writec chan<- raftpb.Message, picked string) {
	var ok bool
	//如果是MsgSnap类型的消息, 返回Pipeline消息通道对应的Channel,否则返回Stream消息通道对应的Channel,如果Stream消息通道不可用,则使用Pipeline消息通道发送所有类型的消息
	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
}

3、streamWriter

在peer.start()方法中会创建并启动了一个sreamWriter实例。从命名上可以猜出其主要功能就是向Stream 消息通道写入消息在peer.start()方法中是通过调用startStreamWriter()方法初始化并启动streamWriter 实例的,其中还启动了一个后台goroutine来执行streamWriter.run()方法。在streamWriter.run() 方法中,主要完成了下面三件事情:
(1)当其他节点主动与当前节点创建连接(即Stream消息通道底层使用的网络连接〉时,该连接实例会写入对应peer.writer.connc通道, 在streamWriter.run()方法中会通过该通道获取该连接实例井进行绑定,之后才能开始后续的消息发送。
(2)定时发送心跳消息,该心跳消息并不是前面介绍raft模块时提到的MsgHeatbeat消息,而是为了防止底层连接超时的消息。
(3)发送除心跳消息外的其他类型的消息。

func (cw *streamWriter) run() {
	var (
		msgc       chan raftpb.Message   //指向当前streamWriter. msgc字段
		heartbeatc <-chan time.Time  //定时器会定时向该通道发送信号, 触发心跳消息的发送,该心跳消息与后面介绍的Raft的心跳消息有所不同,该心跳消息的主要目的是为了防止连接长时间不用断升的
		t          streamType   //用来记录消息的版本信息
		enc        encoder  //编码器,负责将消息序列化并写入连接的缓冲区
		flusher    http.Flusher //负责刷新底层连接,将数据真正发送出去
		batched    int   //当前未Flush的消息个数
	)
	tickc := time.NewTicker(ConnReadTimeout / 3)//发送心跳消息的定时器
	defer tickc.Stop()
	unflushed := 0

	plog.Infof("started streaming with peer %s (writer)", cw.peerID)

	for {
		select {
		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
			}

			cw.status.deactivate(failureType{source: t.String(), action: "heartbeat"}, err.Error())

			sentFailures.WithLabelValues(cw.peerID.String()).Inc()
			cw.close()
			plog.Warningf("lost the TCP streaming connection with peer %s (%s writer)", cw.peerID, t)
			//将heartbeatc和msgc两个通道清空,后续就不会再发送心跳消息和其他类型的消息了
			heartbeatc, msgc = nil, nil

		case m := <-msgc:
		//peer向streamWriter.msgc写入待发送的消息
			err := enc.encode(&m)//将消息序列化并写入底层连接
			if err == nil {
				unflushed += m.Size()

				if len(msgc) == 0 || batched > streamBufSize/2 {//msgc通道中的消息全部发送完成或是未Flush的消息较多,则触发Flush,否则只是递增batched变量
					flusher.Flush()
					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()
			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()

		case conn := <-cw.connc:
		//获取与当前streamWriter实例绑定的底层连接
		//当其他节点主动与当前节点创建Stream消息通道时,会先通过StreamHandler的处理,
        //StreamHandler会通过attach()方法将连接写入对应peer.writer.connc通道,
        //而当前的goroutine会通过该通道获取连接,然后开始发送消息
			cw.mu.Lock()
			closed := cw.closeUnlocked()
			t = conn.t
			switch conn.t {
			case streamTypeMsgAppV2:
				enc = newMsgAppV2Encoder(conn.Writer, cw.fs)
			case streamTypeMessage:
			    //将http.ResponseWriter封装成messageEncoder,上层调用通过messageEncoder实例完成消息发送
				enc = &messageEncoder{w: conn.Writer}
			default:
				plog.Panicf("unhandled stream type %s", conn.t)
			}
			flusher = conn.Flusher    //记录底层连接对应的Flusher
			unflushed = 0
			cw.status.activate()     //peerStatus.activeit直为true
			cw.closer = conn.Closer //记录底层连接对应的Flusher
			cw.working = true        //标识当前streamWriter正在运行
			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  //更新heartbeatc和msgc两个远远,自此之后,才能发送消息
		case <-cw.stopc:
			if cw.close() {
				plog.Infof("closed the TCP streaming connection with peer %s (%s writer)", cw.peerID, t)
			}
			plog.Infof("stopped streaming with peer %s (writer)", cw.peerID)
			close(cw.done)
			return
		}
	}
}

stream Writer中另一个需要读者了解的方法是attach()方法,该方法会接收outgoingConn实例井写入streamWriter.connc通道中,peer.attachOutgoingConn()方法就是通过调用该方法实现的。在后面介绍的streamHandler中,也就是通过调用peer.attachOutgoingConn()方法将底层网络连接传递到streamWriter中的。 streamWriter.attach()方法和peer.attachOutgoingConn()方法的具体实现如下:

func (p *peer) attachOutgoingConn(conn *outgoingConn) {
	var ok bool
	switch conn.t {
	case streamTypeMsgAppV2:
		ok = p.msgAppV2Writer.attach(conn)
	case streamTypeMessage://这里只关注最新版本的实现,将conn实例交给streamWriter处理
		ok = p.writer.attach(conn)
	default:
		plog.Panicf("unhandled stream type %s", conn.t)
	}
	if !ok {
		conn.Close()
	}
}
func (cw *streamWriter) attach(conn *outgoingConn) bool {
	select {
	case cw.connc <- conn://将conn实例写入strearnWriter.connc通过中
		return true
	case <-cw.done:
		return false
	}
}

 4、streamReader

在peer.start()方法中,除了创建前面介绍的steamWriter,还创建井启动了一个sreamReader实例。 从该结构体的命名和前面对streamWriter 的介绍, 可以猜出其主要功能是从Stream通道中读取消息
    peerlD (types. ID类型):对应节点的ID。
    typ ( streamType类型):关联的底层连接使用的协议版本信息。
    tr (寸「anspo同类型): 关联的rafthttp.Transport实例。
    picker ( *urlPicker类型):用于获取对端节点的可用的URL。
    recvc ( ehan<- Message 类型):在前面介绍的peer.startPeer()方法中提到, 创建streamReader 实例时是使用peer.recvc 通道初始化该宇段的,其中还会启动一个后台goroutine 从peer.recvc 通道中读取消息。在下面分析中会看到,从对端节点发送来的非MsgProp类型的消息会首先由streamReader 写入recvc通道中, 然后由peer.start()启动的后台goroutine读取出来, 交由底层的raft模块进行处理。
    prope ( ehan<-raftpb.Message类型):该通道与上面介绍的recvc通道类似,只不过其中接收的是MsgProp类型的消息。
    paused ( bool类型):是否暂停读取数据。

在streamReader.start()方法中会启动一个单独的后台goroutine来执行streamReader.run()方法,与streamWriter类似,它也是streamReader的核心。

func (cr *streamReader) run() {
	t := cr.typ
	plog.Infof("started streaming with peer %s (%s reader)", cr.peerID, t)
	for {
		rc, err := cr.dial(t)//向对端节点发送一个GET请求, 主要负责与对端节点建立连接
		if err != nil {
			if err != errUnsupportedStreamType {
				cr.status.deactivate(failureType{source: t.String(), action: "dial"}, err.Error())
			}
		} else {
			cr.status.activate()
			plog.Infof("established a TCP streaming connection with peer %s (%s reader)", cr.peerID, cr.typ)
			err = cr.decodeLoop(rc, t)//如采未出现异常,则开始读取对端返回的消息,并将读取到的消息写入streamReader.recvc中
			plog.Warningf("lost the TCP streaming connection with peer %s (%s reader)", cr.peerID, cr.typ)
			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())
			}
		}
		// Wait for a while before new dial attempt
		err = cr.rl.Wait(cr.ctx)
		if cr.ctx.Err() != nil {
			plog.Infof("stopped streaming with peer %s (%s reader)", cr.peerID, t)
			close(cr.done)
			return
		}
		if err != nil {
			plog.Errorf("streaming with peer %s (%s reader) rate limiter error: %v", cr.peerID, t, err)
		}
	}
}

//decodeLoop()方法是streamReader中的核心方法,它会从底层的网络连接读取数据并进行反序列化, 之后将得到的消息实例写入recvc通道(或prope通道)中, 等待Peer进行处理, 其具体实现如下:
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}//messageDeeoder主要负责从连接中读取数据
	default:
		plog.Panicf("unhandled stream type %s", t)
	}
	select {
	case <-cr.ctx.Done():
		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()//从底层连接中读取数据,并反序列化成raftpb.Message实例
		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://将消息写入对应的远远中,之后会交给底层的Raft状态机进行处理
		default:    //recvc通过满了之后,只能丢弃消息,并打印日志(
			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()
		}
	}
}

 5、pipelineHandler实例

在前面介绍的ra的ttp.Transport.Handler()方法中, 会创建多个Handler实例并与指定的URL路径关联,我们重点关注其中的pipelineHandler和streamHandler两个Handler的具体实现

在前面分析的pipeline.handle()方法和pipeline.post()方法中,我们只看到了创建连接和发送请求的逻辑,这里介绍在对端节点的pipelineHandler 中是如何读取快照消息的。下面先来看一下pipelineHandler中各个字段的含义。
    tr ( Transporter类型): 当前pipeline实例关联的rafthttp.Transport实例。
    r ( Raft类型): 底层的Raft实例。
    cid (types.ID类型): 当前集群的ID。

正如前面介绍的那样, 在pipelineHandler中实现了http.Server.Handler接口的ServeHTTP()方法,也是其处理请求的核心方法。pipelineHandler.ServerHTTP()方法通过读取对端节点发来的请求得到相应的消息实例, 然后将其交给底层的raft模块进行处理, 该方法的具体大致实现如下:

正如前面介绍的那样, 在pipelineHandler中实现了http.Server.Handler接口的ServeHTTP()方法,也是其处理请求的核心方法。pipelineHandler.ServerHTTP()方法通过读取对端节点发来的请求得到相应的消息实例, 然后将其交给底层的raft模块进行处理, 该方法的具体大致实现如下:
func (h *pipelineHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    //首先进行一系列的检查,例如· 检查请求的Method是否为POST, 检测集群ID是否合法,等等(
	if r.Method != "POST" {
		w.Header().Set("Allow", "POST")
		http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
		return
	}

	w.Header().Set("X-Etcd-Cluster-ID", h.cid.String())

	if err := checkClusterCompatibilityFromHeader(r.Header, h.cid); err != nil {
		http.Error(w, err.Error(), http.StatusPreconditionFailed)
		return
	}

	addRemoteFromRequest(h.tr, r)

	//限制每次从底层连接读取的字节数上线,默认是64KB,因为快照数据可能非常大,为了防止读取超时,
    //只能每次读取一部分数据到缓冲区中,最后将全部数据拼接起来,得到完整的快照数据
	limitedr := pioutil.NewLimitedBufferReader(r.Body, connReadLimitByte)
	b, err := ioutil.ReadAll(limitedr)//读取HTTP请求的Body的全部内容
	if err != nil {
		plog.Errorf("failed to read raft message (%v)", err)
		http.Error(w, "error reading raft message", http.StatusBadRequest)
		recvFailures.WithLabelValues(r.RemoteAddr).Inc()
		return
	}
    //反序列化得到raftpb.Message实例
	var m raftpb.Message
	if err := m.Unmarshal(b); err != nil {
		plog.Errorf("failed to unmarshal raft message (%v)", err)
		http.Error(w, "error unmarshaling raft message", http.StatusBadRequest)
		recvFailures.WithLabelValues(r.RemoteAddr).Inc()
		return
	}

	receivedBytes.WithLabelValues(types.ID(m.From).String()).Add(float64(len(b)))
    //将读取到的消息实例交给成层的Raft状态机进行处理,
	if err := h.r.Process(context.TODO(), m); err != nil {
		switch v := err.(type) {
		case writerToResponse:
			v.WriteTo(w)
		default:
			plog.Warningf("failed to process raft message (%v)", err)
			http.Error(w, "error processing raft message", http.StatusInternalServerError)
			w.(http.Flusher).Flush()
			// disconnect the http stream
			panic(err)
		}
		return
	}

	// Write StatusNoContent header after the message has been processed by
	// raft, which facilitates the client to report MsgSnap status.
	w.WriteHeader(http.StatusNoContent)//向对端节点返回合适的状态码, 表示请求已经被处理
}

在前面分析streamWriter 时介绍了向对端消息发送的基本逻辑,在分析streamReader时,我们大致了解了建立连接和读取对端消息的过程。这里主要介绍streamHandler, 它主要负责在接收到对端的网络连接之后,将其与对应的streamWriter实例进行关联。这样, streamWriter 就可以开始向对端节点发送消息了。下面来分析streamHandler中各个字段的含义。
    tr ( *Transp。同类型): 关联的rafthttp.Transport实例。
    peerGetter ( peerGetter类型):peerGetter接口中的Get()方法会根据指定的节点ID获取对应的peer实例。
    r  ( Raft类型):底层的Raft实例。
    id  ( types.ID类型): 当前节点的ID。
    cid (types.ID类型): 当前集群的ID

6、streamHandler

在前面分析streamWriter 时介绍了向对端消息发送的基本逻辑,在分析streamReader时,我们大致了解了建立连接和读取对端消息的过程。这里主要介绍streamHandler, 它主要负责在接收到对端的网络连接之后,将其与对应的streamWriter实例进行关联。这样, streamWriter 就可以开始向对端节点发送消息了。下面来分析streamHandler中各个字段的含义。
    tr ( *Transp。同类型): 关联的rafthttp.Transport实例。
    peerGetter ( peerGetter类型):peerGetter接口中的Get()方法会根据指定的节点ID获取对应的peer实例。
    r  ( Raft类型):底层的Raft实例。
    id  ( types.ID类型): 当前节点的ID。
    cid (types.ID类型): 当前集群的ID。

//streamHandler的核心也是ServeHTTP()方法
func (h *streamHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    //检测请求Method是否为GET,检测集群的ID
	if r.Method != "GET" {
		w.Header().Set("Allow", "GET")
		http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
		return
	}

	w.Header().Set("X-Server-Version", version.Version)
	w.Header().Set("X-Etcd-Cluster-ID", h.cid.String())

	if err := checkClusterCompatibilityFromHeader(r.Header, h.cid); err != nil {
		http.Error(w, err.Error(), http.StatusPreconditionFailed)
		return
	}

	var t streamType
	switch path.Dir(r.URL.Path) {//确定使用的协议版
	case streamTypeMsgAppV2.endpoint():
		t = streamTypeMsgAppV2
	case streamTypeMessage.endpoint():
		t = streamTypeMessage   
	default:
		plog.Debugf("ignored unexpected streaming request path %s", r.URL.Path)
		http.Error(w, "invalid path", http.StatusNotFound)
		return
	}

	fromStr := path.Base(r.URL.Path)//获取对揣节点的ID,异常处理
	from, err := types.IDFromString(fromStr)
	if err != nil {
		plog.Errorf("failed to parse from %s into ID (%v)", fromStr, err)
		http.Error(w, "invalid from", http.StatusNotFound)
		return
	}
	if h.r.IsIDRemoved(uint64(from)) {
		plog.Warningf("rejected the stream from peer %s since it was removed", from)
		http.Error(w, "removed member", http.StatusGone)
		return
	}
	p := h.peerGetter.Get(from)//根据对端节点ID获取对应的Peer实例
	if p == nil {
		if urls := r.Header.Get("X-PeerURLs"); urls != "" {
			h.tr.AddRemote(from, strings.Split(urls, ","))
		}
		plog.Errorf("failed to find member %s in cluster %s", from, h.cid)
		http.Error(w, "error sender not found", http.StatusNotFound)
		return
	}

	wto := h.id.String()//获取当前节点的ID
	if gto := r.Header.Get("X-Raft-To"); gto != wto {
		plog.Errorf("streaming request ignored (ID mismatch got %s want %s)", gto, wto)
		http.Error(w, "to field mismatch", http.StatusPreconditionFailed)
		return
	}

	w.WriteHeader(http.StatusOK)//返回200状态码
	w.(http.Flusher).Flush()//调用Flush()方法将响应数据发送到对端节点

	c := newCloseNotifier()
	conn := &outgoingConn{ //创建outgoingConn实例
		t:       t,
		Writer:  w,
		Flusher: w.(http.Flusher),
		Closer:  c,
	}
	//将outgoingConn实例与对应的streamWriter实例绑定,peer.attachOutgoingConn ()方法在前面attach部分已经介绍过
	p.attachOutgoingConn(conn)
	<-c.closeNotify()
}
发布了48 篇原创文章 · 获赞 9 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/cyq6239075/article/details/105452133