etcd系列-----集大成者etcdserver 模塊

1、raftNode 相關

raftNode是充當raft模塊與上層模塊之間交互的橋樑
    term ( uint64類型): 當前節點己應用Entry記錄的最大任期號,term、 index和lead三個宇段的讀寫都是原子操作。
    index ( uint64類型): 當前節點中己應用Entry記錄的最大索引值。
    lead ( uint64類型):記錄當前集羣中Leader節點的ID值。
    msgSnapC ( chan raftpb. Message類型):在前面分析中提到, raft模塊通過返回Ready實例與上層模塊進行交互,其中Ready.Message字段記錄了待發送的消息,其中可能會包含MsgSnap類型的消息,該類型消息中封裝了需要發送到其他節點的快照數據。當raftNode收到MsgSnap消息之後, 會將其寫入msgSnapC通道中,並等待上層模塊進行發送。
    applyc ( chan apply類型): 在etcd-raft模塊返回的Ready實例中, 除了封裝了待持久化的Entry記錄和待持久化的快照數據, 還封裝了待應用的Entry記錄。raftNode會將待應用的記錄和快照數據封裝成apply實例之後寫入applyc通道等待上層模塊處理。
    readStateC ( chan raft.ReadState類型):Readyc.ReadStates中封裝了只讀請求相關的
    ReadState實例,其中的最後一項將會被寫入readStateC通道中等待上層模塊處理。
    ticker ( *time.Ticker類型):該定時器就是邏輯時鐘,每觸發一次就會推進一次底層的選舉計時器和心跳計時器。
    raftStorage ( *raft. MemoryStorage 類型): 與前面介紹的raftLog.storage 字段指向的
    MemoryStorage爲同一實例,主要用來保存持久化的Entry記錄和快照數據。
    storage ( etcdserver.Storage類型):注意該字段的類型,在raft模塊中有一個與之同名的接口(raft.Storage接口),  MemoryStorage就是raft.Storage接口的實現之一。

type Storage interface { 
    //Save ()方法負責將Entry記錄和HardState狀態信息保存到底層的持久化存儲上, 該方法可能會阻塞,Storage接口的實現是通過WAL模塊將上述數據持久化到WAL日誌文件中的
    Save(st raftpb.HardState,  ents []raftpb.Entry)  error 
    //SaveSnap ()方法負責將快照數據持久到底層的持久化存儲上,該方法也可能會阻塞, Storage接口的實現是使用之前介紹的Snapshotter將快照數據保存到快照文件中的
    SaveSnap(snap raftpb.Snapshot)  error 
}

通過raftNode.start()方法啓動相關服務, 在該方法中會啓動一個獨立的後臺goroutine, 在該後臺goroutine中完成了絕大部分與底層raft模塊交互的功能

func (r *raftNode) start(rh *raftReadyHandler) {
	internalTimeout := time.Second

	go func() {
		defer r.onStop()
		islead := false

		for {
			select {
			case <-r.ticker.C://計時器到期被觸發,調用Tick()方法才住進選舉計時器和心跳計時器
				r.tick()
			case rd := <-r.Ready():
			//Ready實例的處理,處理完調用raft.node.Advance()方法,通知raft模塊此次Ready處理完成,raft模塊更新相應信息(例如,己應用Entry的最大索引值)之後,可以繼續返回Ready實例
			

				r.Advance()
			case <-r.stopped:
				return
			}
		}
	}()
}

raftNode對Ready實例中各個字段的處理:
softstate:1、更新raftNode.lead字段。2、根據leader節點的變化情況調用updateLeadership()回調函數
readStates:readStateC通道
CommittedEntries:封裝成apply實例,送入applyc通道
Snapshot:1、封裝成apply實例,送入applyc通道。2、將快照數據保存到本地盤。3、保存到MemoryStorage中
Messages:1、目標節點不存在的,踢除。2、如果有多條msgAppresp消息,只保留最後一條。3、如果有msgSnap消息,送入raftNode.msgSnapC中。
Entries:保存到MemoryStorage中

SoftState處理:

//在SoftState中封裝了當前集羣的Leader信息和當前節點角色
if rd.SoftState != nil {
    //檢測集羣的Leader節點是否發生變化,並記錄相關監控信息
	newLeader := rd.SoftState.Lead != raft.None && atomic.LoadUint64(&r.lead) != rd.SoftState.Lead
	if newLeader {
		leaderChanges.Inc()
	}

	if rd.SoftState.Lead == raft.None {
		hasLeader.Set(0)
	} else {
		hasLeader.Set(1)
	}
    //更新raftNode.lead字段,將其更新爲新的Leader節點ID
	atomic.StoreUint64(&r.lead, rd.SoftState.Lead)
	islead = rd.RaftState == raft.StateLeader
	if islead {
		isLeader.Set(1)
	} else {
		isLeader.Set(0)
	}
	rh.updateLeadership(newLeader)//調用raftReadyHandler中的updateLeadership()回調
	r.td.Reset()
}

待應用的Entry記錄處理:

notifyc := make(chan struct{}, 1)
ap := apply{    //將Ready實例中的待應用Entry記錄以及快照數據封裝成apply實例,其中封裝了notifyc通道, 該通道用來協調當前goroutine和EtcdServer啓動的後臺goroutine的執行
	entries:  rd.CommittedEntries,//已提交、待應用的Entry記錄
	snapshot: rd.Snapshot,//待持久化的快照數據
	notifyc:  notifyc,
}

updateCommittedIndex(&ap, rh)//更新EtcdServer中記錄的己提交位置(EtcdServer. committedindex字段)

select {
case r.applyc <- ap://將apply實例寫入applyc通道中,等待上層應用讀取並進行處理
case <-r.stopped:
	return
}

如果當前節點處於Leader狀態,則raftNode.start()方法會先調用raftNode.processMessages() 方法對待發送的消息進行過濾, 然後調用rafNode.transport.Send()方法完成消息的發送

if islead { 
    r.transport.Send{r.processMessages(rd.Messages)) 
}

raftNode.processMessages() 方法處理待發送消息的邏輯比較清晰,它首先會對消息進行過濾,去除目標節點己被移出集羣的消息,然後分別過濾MsgAppResp消息、MsgSnap消息和MsgHeartbeat消息

func (r *raftNode) processMessages(ms []raftpb.Message) []raftpb.Message {
	sentAppResp := false
	for i := len(ms) - 1; i >= 0; i-- {//從後向前邊歷全部待發送的消息
		if r.isIDRemoved(ms[i].To) {//消息的目標節點已從集羣中移除,將消息的目標節點ID設立爲0
			ms[i].To = 0
		}
        //只會發送最後一條MsgAppResp消息,通過前面對raft模塊的分析可知,沒有必妥同時發送多條MsgAppResp消息
		if ms[i].Type == raftpb.MsgAppResp {
			if sentAppResp {
				ms[i].To = 0
			} else {
				sentAppResp = true
			}
		}

		if ms[i].Type == raftpb.MsgSnap {//對MsgSnap消息的處理
			// current store snapshot and KV snapshot.
			select {
			case r.msgSnapC <- ms[i]://將MsgSnap消息寫入msgSnapC遠遠中
			default://如採msgSηape通道的緩衝區滿了, 則放棄此次快照的發送
				// drop msgSnap if the inflight chan if full.
			}
			ms[i].To = 0//將目標節點設豆爲0,如l rafNode. transport後續不會發送該消息
		}
		if ms[i].Type == raftpb.MsgHeartbeat {//對MsgHeartbeat類型的消息
			ok, exceed := r.td.Observe(ms[i].To)
			if !ok {
				// TODO: limit request rate.
				plog.Warningf("failed to send out heartbeat on time (exceeded the %v timeout for %v)", r.heartbeat, exceed)
				plog.Warningf("server is likely overloaded")
				heartbeatSendFailures.Inc()
			}
		}
	}
	return ms
}

raftNode對Ready中待持久化的Entry記錄,以及快照數據的處理,相關代碼

//通過raftNode. storage將Ready實例中攜帶的HardState信息和待持久化的Entry記錄寫入WAL日誌文件中
if err := r.storage.Save(rd.HardState, rd.Entries); err != nil {
	plog.Fatalf("raft save state and entries error: %v", err)
}
if !raft.IsEmptyHardState(rd.HardState) {//根據HardState信息, 記錄相關的監控信息
	proposalsCommitted.Set(float64(rd.HardState.Commit))
}
// gofail: var raftAfterSave struct{}

if !raft.IsEmptySnap(rd.Snapshot) {
	// gofail: var raftBeforeSaveSnap struct{}
	if err := r.storage.SaveSnap(rd.Snapshot); err != nil {//通過raftNode.storage將Ready實例中攜帶的快照數據保存到磁盤中
		plog.Fatalf("raft save snapshot error: %v", err)
	}
	// 在後面介紹的EtcdServer中會啓動後臺goroutine讀取前面介紹的applye遙遙,並處理apply中封裝快照數據。 這裏使用notifyc迢迢通知該後臺goroutine,該apply實例中的快照數據已經被持久化到磁盤,後臺goroutine可以開始應用該快照數據了
	notifyc <- struct{}{}

	// 將快照數據保存到MemoryStorage中
	r.raftStorage.ApplySnapshot(rd.Snapshot)
	plog.Infof("raft applied incoming snapshot at index %d", rd.Snapshot.Metadata.Index)
	// gofail: var raftAfterApplySnap struct{}
}
r.raftStorage.Append(rd.Entries)//將待持久化的Entry記錄寫入MemoryStorage中

if !islead {//與Leader節點的處理邏輯類似
	// finish processing incoming messages before we signal raftdone chan
	msgs := r.processMessages(rd.Messages)

	// 處理Ready實例的過程基本結束,這裏會通知EtcdServer啓動的後臺goroutine,檢測是否生成快照
	notifyc <- struct{}{}

	waitApply := false
	for _, ent := range rd.CommittedEntries {
		if ent.Type == raftpb.EntryConfChange {
			waitApply = true
			break
		}
	}
	if waitApply {
		// blocks until 'applyAll' calls 'applyWait.Trigger'
		// to be in sync with scheduled config-change job
		// (assume notifyc has cap of 1)
		select {
		case notifyc <- struct{}{}:
		case <-r.stopped:
			return
		}
	}

	// gofail: var raftBeforeFollowerSend struct{}
	r.transport.Send(msgs)//發送消息
} else {
	// 處理Ready實例的過程基本結束,這裏會通知EtcdServer啓動的後臺goroutine,檢測是否生成快照
	notifyc <- struct{}{}
}

Storage接口的具體實現,etcdserver模塊提供了一個storage結構體,其中內嵌了前面介紹的WAL和Snapshotter

type storage struct { 
    *wal.WAL 
    *snap.Snapshotter
}

HardState及待持久化Entry記錄時調用的storage.Save()方法,實際就是前面介紹的WAL.Save()方法。 storage中重新實現了SaveSnap()方法

func (st *storage) SaveSnap(snap raftpb.Snapshot) error {
	walsnap := walpb.Snapshot{
		Index: snap.Metadata.Index,
		Term:  snap.Metadata.Term,
	}
	//將walpb.Snapshot實例封裝成Record記錄寫入WAL日誌文件中
	err := st.WAL.SaveSnapshot(walsnap)
	if err != nil {
		return err
	}
	//通過Snapshotter將快照數據寫入到破盤
	err = st.Snapshotter.SaveSnap(snap)
	if err != nil {
		return err
	}
	//根據WAL日誌文件的名稱及快照的元數據,將放快照之前的WAL日誌文件句柄
	return st.WAL.ReleaseLockTo(snap.Metadata.Index)
}

2、etcdServer 相關

type Server interface {
    Start()  //讀取自己豆文件,啓動當前Server實例
    Stop()  //關閉當前Server實例
    ID()  types.ID //獲取當前Server實例的ID
    Leader() types.ID //獲取當前集羣中的Leader的ID
    Do (ctx context. Context,  r  pb. Request) (Response,  error) //處理Client請求
    Process(ctx context.Context,  m raftpb.Message) error //處理Raft請求
    AddMember (ctx co口text. Context,  memb membership.Member) ( []*membership. Member,  error) //向當前etcd集羣中添加一個節點
    RemoveMember (ctx context. Context,  id uint64)  ( []*membership .Member,  error) //從當前etcd集羣中刪除一個節點
    UpdateMember(ctx context.Context,  updateMemb membership.Member) ( []*membership. Member,  error) //修改集羣成員屬性,如採成員ID不存在則返回錯誤
}

結構體EtcdServer 中核心字段的含義
    appliedlndex ( uint64類型): 當前節點己應用的Entry記錄的最大索引值。
    committedlndex ( uint64類型): 當前己提交的Entry記錄的索引值
    readych ( chan struct{}類型): 當前節點將自身的信息推送到集羣中其他節點之後,會將該通道關閉,也作爲當前EtcdServer實例,可以對外提供服務的一個信號。
    r(raftNode 類型): 即前面介紹的etcdserver.raftNode,它是EtcdServer 實例與底層raft模塊通信的橋樑。
    snapCount( uint64類型):當前EtcdServer實例每應用snapCount條數的Entry記錄,就會觸發一次生成快照的操作
    id  ( types.I D類型):記錄當前節點的ID。
    cluster ( *membership.RaftCluster 類型):記錄當前集羣中全部節點的信息。
    store ( store.Store類型):前面介紹的etcdv2版本存儲
    applyV2 ( Applierv2類型): Applierv2接口主要功能是應用v2版本的Entry記錄
    applyV3、 applyV3Base( applierv3類型): applierV3接口主要功能是應用v3版本的Entry記錄
    be ( backend. Backend類型): v3版本的後端存儲
    kv ( mvcc.ConsistentWatchableKV類型): etcd v3版本的存儲
    compactor ( *compactor. Periodic類型):Leader節點會對存儲進行定期壓縮,該字段用於控制定期壓縮的頻率。

2.1 初始化

    (1)定義初始化過程中使用的變量,創建當前節點使用的目錄
    (2)根據配置項初始化etcd-ra企模塊使用到的相關組件,例如,檢測當前wal 目錄下是否存在WAL日誌文件、初始化v2 存儲、查找BoltDB 數據庫文件、創建Backend 實例、創建RoundTripper實例等
    (3)根據前面對WAL日誌文件的查找結果及當前節點啓動時的配置信息,初始化raft模塊中的Node實例
    (4)創建EtcdServer實例,並初始化其各個字段

2.2 啓動

func (s *EtcdServer) Start() {
	s.start()//其中會啓動一個後臺goroutine, 執行EtcdServer.run( )方法
	s.goAttach(func() { s.adjustTicks() })//啓動一個後臺goroutine,將當前節點的相關信息發送到集羣其他節點
	s.goAttach(func() { s.publish(s.Cfg.ReqTimeout()) })//啓動一個後臺goroutine,定義清理WAL日誌文件和快照文件
	s.goAttach(s.purgeFile)
	s.goAttach(func() { monitorFileDescriptor(s.stopping) })
	s.goAttach(s.monitorVersions)
	s.goAttach(s.linearizableReadLoop)
	s.goAttach(s.monitorKVHash)
}

start

run()方法是EtcdServer啓動的核心,其中會啓動前面介紹的etcdserver.raftNode實例,然後處理raft模塊返回的Ready實例.

func (s *EtcdServer) run() {
	sn, err := s.r.raftStorage.Snapshot()
	if err != nil {
		plog.Panicf("get snapshot from raft storage error: %v", err)
	}

	// asynchronously accept apply packets, dispatch progress in-order
	sched := schedule.NewFIFOScheduler()

	var (
		smu   sync.RWMutex
		syncC <-chan time.Time
	)
	setSyncC := func(ch <-chan time.Time) {
		smu.Lock()
		syncC = ch
		smu.Unlock()
	}
	getSyncC := func() (ch <-chan time.Time) {
		smu.RLock()
		ch = syncC
		smu.RUnlock()
		return
	}
	rh := &raftReadyHandler{
	//raftNode在處理raft模塊返回的Ready.SoftState字段時,會調用raftReadyHandler.updateLeadership()回調函數, 其中會根據當前節點的狀態和Leader節點是否發生變化完成一些相應的操作
		updateLeadership: func(newLeader bool) {
			if !s.isLeader() {
				if s.lessor != nil {
					s.lessor.Demote()
				}
				if s.compactor != nil {
					s.compactor.Pause()//非Leader節點暫停自動壓縮
				}
				setSyncC(nil)//非Leader節點不會發送SYNC消息
			} else {
				if newLeader {//如果發生Leader節點的切換,且當前節點成爲Leader節點,則初始化leadElectedTime字段,該字段記錄了當前節點最近一次成爲Leader節點的時間
					t := time.Now()
					s.leadTimeMu.Lock()
					s.leadElectedTime = t
					s.leadTimeMu.Unlock()
				}
				setSyncC(s.SyncTicker.C)//Leader節點會定期發送SYNC消息,恢復該定時器
				if s.compactor != nil {
					s.compactor.Resume()//重啓自動壓縮的功能
				}
			}

			// TODO: remove the nil checking
			// current test utility does not provide the stats
			if s.stats != nil {
				s.stats.BecomeLeader()
			}
		},
		//在raftNode處理apply實例時會調用updateCommittedindex()函數,該函數會根據apply實例中封裝的待應用Entry記錄和快照數據確定當前的committedindex值, 然後調用raftReadyHandler中的同名回調函數更新EtcdServer.committedindex字段位
		updateCommittedIndex: func(ci uint64) {
			cci := s.getCommittedIndex()
			if ci > cci {
				s.setCommittedIndex(ci)
			}
		},
	}
	//啓動raftNode,其中會啓動後臺goroutine處理raft模塊返回的Ready實例,前面已經介紹
	s.r.start(rh)

	//記錄當前快照相關的元數據信息和己應用Entry記錄的位置信息
	ep := etcdProgress{
		confState: sn.Metadata.ConfState,
		snapi:     sn.Metadata.Index,
		appliedt:  sn.Metadata.Term,
		appliedi:  sn.Metadata.Index,
	}

	defer func() {
		s.wgMu.Lock() // block concurrent waitgroup adds in goAttach while stopping
		close(s.stopping)
		s.wgMu.Unlock()
		s.cancel()

		sched.Stop()

		// wait for gouroutines before closing raft so wal stays open
		s.wg.Wait()

		s.SyncTicker.Stop()

		// must stop raft after scheduler-- etcdserver can leak rafthttp pipelines
		// by adding a peer after raft stops the transport
		s.r.stop()

		// kv, lessor and backend can be nil if running without v3 enabled
		// or running unit tests.
		if s.lessor != nil {
			s.lessor.Stop()
		}
		if s.kv != nil {
			s.kv.Close()
		}
		if s.authStore != nil {
			s.authStore.Close()
		}
		if s.be != nil {
			s.be.Close()
		}
		if s.compactor != nil {
			s.compactor.Stop()
		}
		close(s.done)
	}()

	var expiredLeaseC <-chan []*lease.Lease
	if s.lessor != nil {
		expiredLeaseC = s.lessor.ExpiredLeasesC()
	}

	for {
		select {
		case ap := <-s.r.apply()://讀取raftNode.applyc通過中的apply實例並進行處理
			f := func(context.Context) { s.applyAll(&ep, &ap) }
			sched.Schedule(f)
		case leases := <-expiredLeaseC:
			s.goAttach(func() {
				// Increases throughput of expired leases deletion process through parallelization
				c := make(chan struct{}, maxPendingRevokes)
				for _, lease := range leases {
					select {
					case c <- struct{}{}:
					case <-s.stopping:
						return
					}
					lid := lease.ID
					s.goAttach(func() {
						ctx := s.authStore.WithRoot(s.ctx)
						_, lerr := s.LeaseRevoke(ctx, &pb.LeaseRevokeRequest{ID: int64(lid)})
						if lerr == nil {
							leaseExpired.Inc()
						} else {
							plog.Warningf("failed to revoke %016x (%q)", lid, lerr.Error())
						}

						<-c
					})
				}
			})
		case err := <-s.errorc:
			plog.Errorf("%s", err)
			plog.Infof("the data-dir used by this member must be removed.")
			return
		case <-getSyncC()://定時發送SYNC消息
			if s.store.HasTTLKeys() {
				s.sync(s.Cfg.ReqTimeout())
			}
		case <-s.stop:
			return
		}
	}
}

purgeFile

在EtcdServer.Start()方法中會啓動兩個後臺goroutine,其中一個後臺goroutine負責定期清理WAL日誌文件,另一個後臺goroutine負責定期清理快照文件,相應的邏輯位於EtcdServer.purgeFile()方法中,具體實現如下:

func (s *EtcdServer) purgeFile() {
	var dberrc, serrc, werrc <-chan error
	if s.Cfg.MaxSnapFiles > 0 {
	   //這裏會啓動後臺goroutine,定期清理快照文件(默認purgeFileinterval的值爲30s)
		dberrc = fileutil.PurgeFile(s.Cfg.SnapDir(), "snap.db", s.Cfg.MaxSnapFiles, purgeFileInterval, s.done)
		serrc = fileutil.PurgeFile(s.Cfg.SnapDir(), "snap", s.Cfg.MaxSnapFiles, purgeFileInterval, s.done)
	}
	if s.Cfg.MaxWALFiles > 0 {
	    //啓動一個後臺goroutine,定期清理WAL日誌文件(默認purgeFileinterval的位爲30s)
		werrc = fileutil.PurgeFile(s.Cfg.WALDir(), "wal", s.Cfg.MaxWALFiles, purgeFileInterval, s.done)
	}
	select {
	case e := <-dberrc:
		plog.Fatalf("failed to purge snap db file %v", e)
	case e := <-serrc:
		plog.Fatalf("failed to purge snap file %v", e)
	case e := <-werrc:
		plog.Fatalf("failed to purge wal file %v", e)
	case <-s.stopping:
		return
	}
}

apply

rungoroutine會監聽raftNode.applyc通道,並調用EtcdServer.applyAll()方法處理從中讀取到的apply實例。在apply實例中封裝了待應用的Entry記錄、待應用的快照數據和notifyc通道.在EtcdServer.applyAll()方法中,首先會調用EtcdServer.applySnapshot()方法處理apply實例中的快照數據。 EtcdServer.applySnapshot()方法會先等待raftNode將快照數據持久化到磁盤中,之後根據快照元數據查找BoltDB數據庫文件並重建Backend實例, 最後根據重建後的存儲更新本地RaftCluster實例

func (s *EtcdServer) applySnapshot(ep *etcdProgress, apply *apply) {
	if raft.IsEmptySnap(apply.snapshot) {//檢測待應用的快煒、數據是否爲空, 如果爲空則直接返回
		return
	}

	plog.Infof("applying snapshot at index %d...", ep.snapi)
	defer plog.Infof("finished applying incoming snapshot at index %d", ep.snapi)

	if apply.snapshot.Metadata.Index <= ep.appliedi {//如採該快照中最後一條Entry的索引小於當前節點己應用Entry索引,則異常結束
		plog.Panicf("snapshot index [%d] should > appliedi[%d] + 1",
			apply.snapshot.Metadata.Index, ep.appliedi)
	}

	// raftNode在將快照數據寫入磁盤文件之後,會向notifyc通道中寫入一個空結構體作爲信號,這裏會阻塞等待該信號
	<-apply.notifyc
    //根據快照信息查找對應的BoltDB數據庫文件,並創建新的Backend實例
	newbe, err := openSnapshotBackend(s.Cfg, s.snapshotter, apply.snapshot)
	if err != nil {
		plog.Panic(err)
	}

	// 因爲在store.restore()方法中除了恢復內存索引,還會重新綁定鍵值對與對應的Lease,所以需先恢復EtcdServer.lessor,再恢復EtcdServer.kv字段
	if s.lessor != nil {
		plog.Info("recovering lessor...")
		s.lessor.Recover(newbe, func() lease.TxnDelete { return s.kv.Write() })
		plog.Info("finished recovering lessor")
	}

	plog.Info("restoring mvcc store...")

	if err := s.kv.Restore(newbe); err != nil {
		plog.Panicf("restore KV error: %v", err)
	}
	s.consistIndex.setConsistentIndex(s.kv.ConsistentIndex())// 重置EtcdServer.consistindex字段

	plog.Info("finished restoring mvcc store")

	// Closing old backend might block until all the txns
	// on the backend are finished.
	// We do not want to wait on closing the old backend.
	s.bemu.Lock()
	oldbe := s.be
	go func() {
		plog.Info("closing old backend...")
		defer plog.Info("finished closing old backend")
        //因爲此時可能還有事務在執行,關閉舊Backend實例可能會被阻塞,所以這裏啓動一個後臺goroutine用來關閉Backend實例
		if err := oldbe.Close(); err != nil {
			plog.Panicf("close backend error: %v", err)
		}
	}()

	s.be = newbe
	s.bemu.Unlock()

	plog.Info("recovering alarms...")
	if err := s.restoreAlarms(); err != nil {//恢復EtcdServer中的alarmStore和authStore,它們分別對應BoltDB中的alarm Bucket 和auth Bukcet
		plog.Panicf("restore alarms error: %v", err)
	}
	plog.Info("finished recovering alarms")

	if s.authStore != nil {
		plog.Info("recovering auth store...")
		s.authStore.Recover(newbe)
		plog.Info("finished recovering auth store")
	}

	plog.Info("recovering store v2...")
	if err := s.store.Recovery(apply.snapshot.Data); err != nil {
		plog.Panicf("recovery store error: %v", err)
	}
	plog.Info("finished recovering store v2")

	s.cluster.SetBackend(s.be)
	plog.Info("recovering cluster configuration...")
	s.cluster.Recover(api.UpdateCapability)
	plog.Info("finished recovering cluster configuration")

	plog.Info("removing old peers from network...")
	// recover raft transport
	s.r.transport.RemoveAllPeers()
	plog.Info("finished removing old peers from network")

	plog.Info("adding peers from new cluster configuration into network...")
	for _, m := range s.cluster.Members() {
		if m.ID == s.ID() {
			continue
		}
		s.r.transport.AddPeer(m.ID, m.PeerURLs)
	}
	plog.Info("finished adding peers from new cluster configuration into network...")
    //更新etcdProgress,其中涉及已應用Entry記錄的Term值、 Index值和快照相關信息
	ep.appliedt = apply.snapshot.Metadata.Term
	ep.appliedi = apply.snapshot.Metadata.Index
	ep.snapi = ep.appliedi
	ep.confState = apply.snapshot.Metadata.ConfState
}

應用完快照數據之後, run goroutine緊接着會調用EtcdServer.applyEntries()方法處理待應用的Entry記錄

func (s *EtcdServer) applyEntries(ep *etcdProgress, apply *apply) {
	if len(apply.entries) == 0 {//檢測是否存在待應用的Entry記錄,如果爲空直接返回
		return
	}
	firsti := apply.entries[0].Index
	if firsti > ep.appliedi+1 {//檢測待應用的第一條Entry記錄是否合法
		plog.Panicf("first index of committed entry[%d] should <= appliedi[%d] + 1", firsti, ep.appliedi)
	}
	var ents []raftpb.Entry
	if ep.appliedi+1-firsti < uint64(len(apply.entries)) {//忽略己應用的Entry記錄,只留未應用的Entry記錄
		ents = apply.entries[ep.appliedi+1-firsti:]
	}
	if len(ents) == 0 {
		return
	}
	var shouldstop bool
	//調用apply()方法應用ents中的Entry記錄
	if ep.appliedt, ep.appliedi, shouldstop = s.apply(ents, &ep.confState); shouldstop {
		go s.stopWithDelay(10*100*time.Millisecond, fmt.Errorf("the member has been permanently removed from the cluster"))
	}
}

在apply()方法中會遍歷ents 中的全部Entry記錄,並根據Entry的類型進行不同的處理。

func (s *EtcdServer) apply(es []raftpb.Entry, confState *raftpb.ConfState) (appliedt uint64, appliedi uint64, shouldStop bool) {
	for i := range es {//遙歷待應用的Entry記錄
		e := es[i]
		switch e.Type {//根據Entry記錄的不同類型,進行不同的處理
		case raftpb.EntryNormal:
			s.applyEntryNormal(&e)
		case raftpb.EntryConfChange:
			// set the consistent index of current executing entry
			if e.Index > s.consistIndex.ConsistentIndex() {
				s.consistIndex.setConsistentIndex(e.Index)
			}
			var cc raftpb.ConfChange
			pbutil.MustUnmarshal(&cc, e.Data)
			removedSelf, err := s.applyConfChange(cc, confState)
			s.setAppliedIndex(e.Index)
			shouldStop = shouldStop || removedSelf
			s.w.Trigger(cc.ID, &confChangeResponse{s.cluster.Members(), err})
		default:
			plog.Panicf("entry type should be either EntryNormal or EntryConfChange")
		}
		atomic.StoreUint64(&s.r.index, e.Index)
		atomic.StoreUint64(&s.r.term, e.Term)
		appliedt = e.Term
		appliedi = e.Index
	}
	return appliedt, appliedi, shouldStop
}

applyEntryNormal()方法處理EntryNormal 記錄的具體過程。applyEntryNormal()方法首先會嘗試將Entry.Data反序列化成IntemalRaftRequest實例, 如果失敗,則將其反序列化成etcdserverpb.Request實例,之後根據反序列化的結果調用EtcdSever的相應方法進行處理, 最後將處理結果寫入Entry對應的通道中

func (s *EtcdServer) applyEntryNormal(e *raftpb.Entry) {
	shouldApplyV3 := false
	if e.Index > s.consistIndex.ConsistentIndex() {
		// set the consistent index of current executing entry
		s.consistIndex.setConsistentIndex(e.Index)//更新EtcdServer. consistindex記錄的索引位
		shouldApplyV3 = true
	}
	defer s.setAppliedIndex(e.Index)//方法結束時更新EtcdServer.appliedindex字段記錄的索引值

	// raft state machine may generate noop entry when leader confirmation.
	// skip it in advance to avoid some potential bug in the future
	if len(e.Data) == 0 {//空的Entry記錄只會在Leader選舉結束時出現
		select {
		case s.forceVersionC <- struct{}{}:
		default:
		}
		// promote lessor when the local member is leader and finished
		// applying all entries from the last term.
		if s.isLeader() {//如果當前節點爲Leader,則晉升其lessor實例
			s.lessor.Promote(s.Cfg.electionTimeout())
		}
		return
	}

	var raftReq pb.InternalRaftRequest
	if !pbutil.MaybeUnmarshal(&raftReq, e.Data) { // 嘗試將Entry.Data反序列化成InternalRaftRequest實例, InternalRaftRequest中封裝了所有類型的Client請求
		var r pb.Request
		rp := &r
		//兼容性處理, 如採上述序列化失敗,則將Entry.Date反序列化成pb.Request
		pbutil.MustUnmarshal(rp, e.Data)
		s.w.Trigger(r.ID, s.applyV2Request((*RequestV2)(rp)))//調用EtcdServer.applyV2Request()方法進行處理
		return
	}
	if raftReq.V2 != nil {
		req := (*RequestV2)(raftReq.V2)
		s.w.Trigger(req.ID, s.applyV2Request(req))
		return
	}

	// do not re-apply applied entries.
	if !shouldApplyV3 {
		return
	}
    //下面是對v3版本請求的處理
	id := raftReq.ID
	if id == 0 {
		id = raftReq.Header.ID
	}

	var ar *applyResult
	needResult := s.w.IsRegistered(id)
	if needResult || !noSideEffect(&raftReq) {
		if !needResult && raftReq.Txn != nil {
			removeNeedlessRangeReqs(raftReq.Txn)
		}
		//調用applyV3.Apply ()方法處理該Entry,其中會根據請求的類型選擇不同的方法進行處理
		ar = s.applyV3.Apply(&raftReq)
	}

	if ar == nil {
		return
	}
//返回結采ar(applyResult類型)爲nil,直接返回,如採返回了ErrNoSpace錯誤,則表示底層的Backend已經沒有足夠的空間,如是第一次出現這種情
//況,則在後面立即啓動一個後臺goroutine,並調用EtcdServer.raftRequest()方法發送AlarmRequest請求,當前其他節點收到該請求時, 會停止後續的PUT操作
	if ar.err != ErrNoSpace || len(s.alarmStore.Get(pb.AlarmType_NOSPACE)) > 0 {
		s.w.Trigger(id, ar)//將上述處理結果寫入對應的通道中, 然後將對應通道關閉
		return
	}

	plog.Errorf("applying raft message exceeded backend quota")
	s.goAttach(func() {//第一次出現ErrNoSpace錯誤
		a := &pb.AlarmRequest{//創建AlarmRequest
			MemberID: uint64(s.ID()),
			Action:   pb.AlarmRequest_ACTIVATE,
			Alarm:    pb.AlarmType_NOSPACE,
		}
		s.raftRequest(s.ctx, pb.InternalRaftRequest{Alarm: a})//將AlarmRequest請求封裝成MsgProp消息,發送到集羣
		s.w.Trigger(id, ar)//將上述處理結果寫入對應的通道中,然後將對應通過關閉
	})
}

EtcdServer.applyAll()方法中首先調用applySnapshot()方法處理apply實例中記錄的快照數據,然後調用applyEntries()方法處理apply實例中的En町記錄,之後根據apply實例的處理結果檢測是否需要生成新的快照文件, 最後處理MsgSnap消息

func (s *EtcdServer) applyAll(ep *etcdProgress, apply *apply) {
	s.applySnapshot(ep, apply)//調用applySnapshot()方法處理apply實例中記錄的快照數據
	s.applyEntries(ep, apply)//調用applyEntries()方法處理apply實例中的Entry記錄

	proposalsApplied.Set(float64(ep.appliedi))
	s.applyWait.Trigger(ep.appliedi)//在前面的說到,etcdProgress.appliedi記錄了已應用Entry的索引值。 這裏通過調用WaitTirne.Trigger()方法, 將id小於etcdProgress.appliedi的Entry對應的通道全部關閉,這樣就可以通知其他監聽通道的goroutine
	// 回顧前面對raftNode的分析, 當Ready處理基本完成時, 會向notifyc通道中寫入一個信號,通知當前goroutine去檢測是否需要生成快照
	<-apply.notifyc

	s.triggerSnapshot(ep)//根據當前狀態決定是否觸發快照的生成
	select {
	// 在raftNode中處理Ready實例時,如果並沒有直接發送MsgSnap消息,而是將其寫入rnsgSnapC中,這裏會讀取rnsgSnapC通過,並完成快照數據的發送
	case m := <-s.r.msgSnapC:
	    //將v2存儲的快照數據和v3存儲的數據合併成完整的快照、數據
		merged := s.createMergedSnapshotMessage(m, ep.appliedt, ep.appliedi, ep.confState)
		s.sendMergedSnap(merged)//發送快照數據
	default:
	}
}

triggerSnapshot()方法對是否需要生成新快照文件的判定

func (s *EtcdServer) triggerSnapshot(ep *etcdProgress) {
	if ep.appliedi-ep.snapi <= s.Cfg.SnapCount {//連續應用一定量的Entry記錄,會觸發快照的生成(snapCount默認爲100000條)
		return
	}

	plog.Infof("start to snapshot (applied: %d, lastsnap: %d)", ep.appliedi, ep.snapi)
	s.snapshot(ep.appliedi, ep.confState)//創建新的快照文件
	ep.snapi = ep.appliedi//更新etcdProgress.snapi
}

snapshot()方法是真正生成快照文件的地方,其中會啓動一個單獨的後臺goroutine來完成新快照文件的生成,主要是序列化v2存儲中的數據並持久化到文件中,觸發相應的壓縮操作

func (s *EtcdServer) snapshot(snapi uint64, confState raftpb.ConfState) {
	clone := s.store.Clone()//複製v2存儲
	// commit kv to write metadata (for example: consistent index) to disk.
	// KV().commit() updates the consistent index in backend.
	// All operations that update consistent index must be called sequentially
	// from applyAll function.
	// So KV().Commit() cannot run in parallel with apply. It has to be called outside
	// the go routine created below.
	s.KV().Commit()//提交v3存儲中當前等待讀寫事務------即將數據寫入db文件

	s.goAttach(func() {
		d, err := clone.SaveNoCopy()//將v2存儲序列化成JSON數據
		if err != nil {
			plog.Panicf("store save should never fail: %v", err)
		}
		//將上述快照數據和元數據更新到raft模塊中的MemoryStorage中,並且返回Snapshot實例(即MemoryStorage.snapshot字段)
		snap, err := s.r.raftStorage.CreateSnapshot(snapi, &confState, d)
		if err != nil {
			// the snapshot was done asynchronously with the progress of raft.
			// raft might have already got a newer snapshot.
			if err == raft.ErrSnapOutOfDate {
				return
			}
			plog.Panicf("unexpected create snapshot error %v", err)
		}
		// 將v2存儲的快照數據記錄到磁盤中,該過程涉及在WAL日誌文件中記錄快照元數據及寫入snap文件等操作, 
		if err = s.r.storage.SaveSnap(snap); err != nil {
			plog.Fatalf("save snapshot error: %v", err)
		}
		plog.Infof("saved snapshot at index %d", snap.Metadata.Index)

		if atomic.LoadInt64(&s.inflightSnapshots) != 0 {
			plog.Infof("skip compaction since there is an inflight snapshot")
			return
		}

		// 爲了防止集羣中存在比較慢的Follower節點,保留5000條Entry記錄不壓縮
		compacti := uint64(1)
		if snapi > numberOfCatchUpEntries {
			compacti = snapi - numberOfCatchUpEntries
		}
		//壓縮MemoryStorage中的指定位置之前的全部Entry記錄
		err = s.r.raftStorage.Compact(compacti)
		if err != nil {
			// the compaction was done asynchronously with the progress of raft.
			// raft log might already been compact.
			if err == raft.ErrCompacted {
				return
			}
			plog.Panicf("unexpected compaction error %v", err)
		}
		plog.Infof("compacted raft log at %d", compacti)
	})
}

msgSnapC 通道

首先分析EtcdServer.createMergedSnapshotMessage()方法,該方法中會將v2版本存儲和v3版本存儲封裝成snap.Message實例

func (s *EtcdServer) createMergedSnapshotMessage(m raftpb.Message, snapt, snapi uint64, confState raftpb.ConfState) snap.Message {
	// get a snapshot of v2 store as []byte
	clone := s.store.Clone()//複製一份v2存儲的數據, 並轉換成JSON格式
	d, err := clone.SaveNoCopy()
	if err != nil {
		plog.Panicf("store save should never fail: %v", err)
	}

	// commit kv to write metadata(for example: consistent index).
	s.KV().Commit()//提交v3存儲中當前的讀寫事務
	dbsnap := s.be.Snapshot()//獲取v3存儲快照, 其實就是對BoltDB數據庫進行快照, 
	// get a snapshot of v3 KV as readCloser
	rc := newSnapshotReaderCloser(dbsnap)

	// put the []byte snapshot of store into raft snapshot and return the merged snapshot with
	// KV readCloser snapshot.
	snapshot := raftpb.Snapshot{
		Metadata: raftpb.SnapshotMetadata{
			Index:     snapi,
			Term:      snapt,
			ConfState: confState,
		},
		Data: d,
	}
	m.Snapshot = snapshot

	return *snap.NewMessage(m, rc, dbsnap.Size())//將消息MsgSnap消息和v3存儲中的數據封裝成snap.Message實例返回
}

創建完snap.Message 實例之後會調用EtcdServer.sendMergedSnap()方法將其發送到指定節點

func (s *EtcdServer) sendMergedSnap(merged snap.Message) {
	atomic.AddInt64(&s.inflightSnapshots, 1)//遞增inflightSnapshots字段,它表示已發送但未收到響應的快照消息個數

	s.r.transport.SendSnapshot(merged)//發送snap.Message消息, 底層會啓動羊獨的後臺goroutine, 通過snapshotSender完成發送
	s.goAttach(func() {//啓動一個後臺goroutine監聽該快照消息是否發送完成
		select {
		case ok := <-merged.CloseNotify():
			// delay releasing inflight snapshot for another 30 seconds to
			// block log compaction.
			// If the follower still fails to catch up, it is probably just too slow
			// to catch up. We cannot avoid the snapshot cycle anyway.
			if ok {
				select {
				case <-time.After(releaseDelayAfterSnapshot):
				case <-s.stopping:
				}
			}
			//snap.Message,肖息發送完成(或是超時)之後會遞減inflightSnapshots,當inflightSnapshots遞減到0時, 前面對MernoryStorage的壓縮才能執行
			atomic.AddInt64(&s.inflightSnapshots, -1)
		case <-s.stopping:
			return
		}
	})
}

2.3 註冊消息Handle

在執行完EtcdServer.NewServer()方法之後,Transport己經啓動,我們可以在其上註冊多個Handler 實例, 主要函數:func (e *Etcd) servePeers()中,這些Handler 實例主要用於集羣內部各節點之間的通信:

func NewPeerHandler(s etcdserver.ServerPeer) http.Handler {
    //s.RaftHandler() 參見前面rafthttp中的說明,這裏啓動pipelineHandler、streamHandler、snapHandler
	return newPeerHandler(s.Cluster(), s.RaftHandler(), s.LeaseHandler())
}

func newPeerHandler(cluster api.Cluster, raftHandler http.Handler, leaseHandler http.Handler) http.Handler {
	mh := &peerMembersHandler{
		cluster: cluster,
	}

	mux := http.NewServeMux()
	mux.HandleFunc("/", http.NotFound)//使用默認的Handler,直接返回404狀態碼
	
	//註冊Transport.Handler()方法返回的Handler
	mux.Handle(rafthttp.RaftPrefix, raftHandler)
	mux.Handle(rafthttp.RaftPrefix+"/", raftHandler)
	mux.Handle(peerMembersPrefix, mh)//將上述 peerMemberHandler 實例註冊到“/me巾ers”路徑上
	if leaseHandler != nil {
		mux.Handle(leasehttp.LeasePrefix, leaseHandler)
		mux.Handle(leasehttp.LeaseInternalPrefix, leaseHandler)
	}
	mux.HandleFunc(versionPath, versionHandler(cluster, serveVersion))
	return mux
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章