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
}