在 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 主要用于传输小数据,是一个长连接。