EtcdRaft源碼分析(配置變更)

今天我們用配置變更來結(jié)束整個EtcdRaft源碼分析系列恒界。橫向擴展能力是衡量分布式系統(tǒng)優(yōu)劣的決定性指標十酣,而能否輕松,快捷耸采,有效虾宇,及時的變更集群成員是其中的關(guān)鍵。下面我們一起來看看EtcdRaft是怎么實現(xiàn)的好唯。

接口

type Node interface {
    ...
   // ProposeConfChange proposes config change.
   // At most one ConfChange can be in the process of going through consensus.
   // Application needs to call ApplyConfChange when applying EntryConfChange type entry.
   ProposeConfChange(ctx context.Context, cc pb.ConfChange) error
    ...
   // ApplyConfChange applies config change to the local node.
   // Returns an opaque ConfState protobuf which must be recorded
   // in snapshots. Will never return nil; it returns a pointer only
   // to match MemoryStorage.Compact.
   ApplyConfChange(cc pb.ConfChange) *pb.ConfState
    ...
}

可以看到有兩個方法跟配置變更相關(guān)燥翅,看過前面的知道,外部跟Raft打交道的方式靶端。先提案(propose), 然后等內(nèi)部達成一致,再落地(Apply)杨名。

struct

type ConfChange struct {
   ID               uint64         `protobuf:"varint,1,opt,name=ID" json:"ID"`
   Type             ConfChangeType `protobuf:"varint,2,opt,name=Type,enum=raftpb.ConfChangeType" json:"Type"`
   NodeID           uint64         `protobuf:"varint,3,opt,name=NodeID" json:"NodeID"`
   Context          []byte         `protobuf:"bytes,4,opt,name=Context" json:"Context,omitempty"`
   XXX_unrecognized []byte         `json:"-"`
}


const (
    ConfChangeAddNode        ConfChangeType = 0
    ConfChangeRemoveNode     ConfChangeType = 1
    ConfChangeUpdateNode     ConfChangeType = 2
    ConfChangeAddLearnerNode ConfChangeType = 3
)

以上是提案內(nèi)容台谍,很清晰,但有個地方需要注意趁蕊,一次只能變更一個節(jié)點。至于為什么是己,有興趣的可以去看論文哈任柜。

Propose

func (n *node) ProposeConfChange(ctx context.Context, cc pb.ConfChange) error {
   data, err := cc.Marshal()
   if err != nil {
      return err
   }
   return n.Step(ctx, pb.Message{Type: pb.MsgProp, Entries: []pb.Entry{{Type: pb.EntryConfChange, Data: data}}})
}
  • 基本就是走的提交數(shù)據(jù)的流程,唯一需要注意的是這里用pb.EntryConfChange將它與其他提案區(qū)別開來摔认。
  • 接下來宅粥,我們再走一遍數(shù)據(jù)提交的流程

Leader

...
for i, e := range m.Entries {
   if e.Type == pb.EntryConfChange {
      if r.pendingConfIndex > r.raftLog.applied {
         r.logger.Infof("propose conf %s ignored since pending unapplied configuration [index %d, applied %d]",
            e.String(), r.pendingConfIndex, r.raftLog.applied)
         m.Entries[i] = pb.Entry{Type: pb.EntryNormal}
      } else {
         r.pendingConfIndex = r.raftLog.lastIndex() + uint64(i) + 1
      }
   }
}
...
if !r.appendEntry(m.Entries...) {
    return ErrProposalDropped
}
r.bcastAppend()
  • 如果當前還有配置更新沒有處理完粹胯,那么這次新的變更將丟棄,用一個空的entry來替換它
  • 如果都處理完了风纠,那么記下這個配置變更的位置到pendingConfIndex
  • 后面就一樣了竹观,累加到本地,而且群發(fā)給其他人臭增。
  • 問題來了,配置變更都同步給成員了列牺,怎么確認都收到了拗窃,可以開始apply了呢泌辫?我想也猜得到九默,會通過Ready的committedIndex來通知應(yīng)用層驼修。

apply

應(yīng)用層

case raftpb.EntryConfChange:
   var cc raftpb.ConfChange
   if err := cc.Unmarshal(ents[i].Data); err != nil {
      c.logger.Warnf("Failed to unmarshal ConfChange data: %s", err)
      continue
   }

   c.confState = *c.Node.ApplyConfChange(cc)

   switch cc.Type {
   case raftpb.ConfChangeAddNode:
      c.logger.Infof("Applied config change to add node %d, current nodes in channel: %+v", cc.NodeID, c.confState.Nodes)
   case raftpb.ConfChangeRemoveNode:
      c.logger.Infof("Applied config change to remove node %d, current nodes in channel: %+v", cc.NodeID, c.confState.Nodes)
   default:
      c.logger.Panic("Programming error, encountered unsupported raft config change")
   }

   // This ConfChange was introduced by a previously committed config block,
   // we can now unblock submitC to accept envelopes.
   if c.confChangeInProgress != nil &&
      c.confChangeInProgress.NodeID == cc.NodeID &&
      c.confChangeInProgress.Type == cc.Type {

      if err := c.configureComm(); err != nil {
         c.logger.Panicf("Failed to configure communication: %s", err)
      }

      c.confChangeInProgress = nil
      c.configInflight = false
      // report the new cluster size
      c.Metrics.ClusterSize.Set(float64(len(c.opts.RaftMetadata.Consenters)))
   }

   if cc.Type == raftpb.ConfChangeRemoveNode && cc.NodeID == c.raftID {
      c.logger.Infof("Current node removed from replica set for channel %s", c.channelID)
      // calling goroutine, since otherwise it will be blocked
      // trying to write into haltC
      go c.Halt()
   }
}
  • 這里乙各,我舉的是Fabric的例子,只關(guān)注關(guān)鍵流程就好
  • 收到Raft的ConfChange觅丰,第一件事妇萄,我們就要Node.ApplyConfChange(cc)
  • Raft的通訊層是需要應(yīng)用層托管的咬荷,所以不是Raft那邊做完配置變更,就可以收工了懦底。
    • Fabric要根據(jù)最新的集群成員數(shù)據(jù)遂唧,去做grpc的連接
    • 如果有刪除的節(jié)點辫继,還要去停掉這個成員杆查,這個后面會講臀蛛。

Raft

ApplyConfChange

case cc := <-n.confc:
            if cc.NodeID == None {
                select {
                case n.confstatec <- pb.ConfState{
                    Nodes:    r.nodes(),
                    Learners: r.learnerNodes()}:
                case <-n.done:
                }
                break
            }
            switch cc.Type {
            case pb.ConfChangeAddNode:
                r.addNode(cc.NodeID)
            case pb.ConfChangeAddLearnerNode:
                r.addLearner(cc.NodeID)
            case pb.ConfChangeRemoveNode:
                // block incoming proposal when local node is
                // removed
                if cc.NodeID == r.id {
                    propc = nil
                }
                r.removeNode(cc.NodeID)
            case pb.ConfChangeUpdateNode:
            default:
                panic("unexpected conf type")
            }
            select {
            case n.confstatec <- pb.ConfState{
                Nodes:    r.nodes(),
                Learners: r.learnerNodes()}:
            case <-n.done:
            }
  • 首先浊仆,這里有個調(diào)用技巧,如果調(diào)用的時候傳入的NodeID為None舔琅,那么會返回當前Raft的成員
  • 下面我們具體看下這幾種變更類型具體是在干嘛

addNode&addLearner

func (r *raft) addNodeOrLearnerNode(id uint64, isLearner bool) {
   pr := r.getProgress(id)
   if pr == nil {
      r.setProgress(id, 0, r.raftLog.lastIndex()+1, isLearner)
   } else {
      if isLearner && !pr.IsLearner {
         // can only change Learner to Voter
         r.logger.Infof("%x ignored addLearner: do not support changing %x from raft peer to learner.", r.id, id)
         return
      }

      if isLearner == pr.IsLearner {
         // Ignore any redundant addNode calls (which can happen because the
         // initial bootstrapping entries are applied twice).
         return
      }

      // change Learner to Voter, use origin Learner progress
      delete(r.learnerPrs, id)
      pr.IsLearner = false
      r.prs[id] = pr
   }

   if r.id == id {
      r.isLearner = isLearner
   }

   // When a node is first added, we should mark it as recently active.
   // Otherwise, CheckQuorum may cause us to step down if it is invoked
   // before the added node has a chance to communicate with us.
   pr = r.getProgress(id)
   pr.RecentActive = true
}
  • 如果是全新的節(jié)點搏明,初始化Progress,這里match=0星著,next=r.raftLog.lastIndex()+1
  • 如果之前是learner虚循,那么從learner里面轉(zhuǎn)移到正常節(jié)點里面

removeNode

func (r *raft) removeNode(id uint64) {
   r.delProgress(id)

   // do not try to commit or abort transferring if there is no nodes in the cluster.
   if len(r.prs) == 0 && len(r.learnerPrs) == 0 {
      return
   }

   // The quorum size is now smaller, so see if any pending entries can
   // be committed.
   if r.maybeCommit() {
      r.bcastAppend()
   }
   // If the removed node is the leadTransferee, then abort the leadership transferring.
   if r.state == StateLeader && r.leadTransferee == id {
      r.abortLeaderTransfer()
   }
}
  • 真的刪除Progress就夠了么?想象下整個系統(tǒng)運轉(zhuǎn)是靠什么铺遂?
  • 還記得應(yīng)用層會tick么茎刚?Leader靠這個發(fā)心跳,非Leader靠這個選舉超時粮坞。
  • 我們再回顧下應(yīng)用層還做了些什么

應(yīng)用層

if cc.Type == raftpb.ConfChangeRemoveNode && cc.NodeID == c.raftID {
    // calling goroutine, since otherwise it will be blocked
    // trying to write into haltC
    go c.Halt()
}

case <-n.chain.haltC:
   ticker.Stop()
   n.Stop()
   n.storage.Close()
   n.logger.Infof("Raft node stopped")
   close(n.chain.doneC) // close after all the artifacts are closed
   return
}
  • 可以看到如果是刪除當前節(jié)點的消息初狰,會最終會讓該節(jié)點的ticker.Stop。這也導(dǎo)致該節(jié)點最終會被Raft拋棄筝闹。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末腥光,一起剝皮案震驚了整個濱河市解寝,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌嘹履,老刑警劉巖焕刮,帶你破解...
    沈念sama閱讀 217,907評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件邑闲,死亡現(xiàn)場離奇詭異偷霉,居然都是意外死亡,警方通過查閱死者的電腦和手機泣侮,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,987評論 3 395
  • 文/潘曉璐 我一進店門稠屠,熙熙樓的掌柜王于貴愁眉苦臉地迎上來龙屉,“玉大人枢步,你說我怎么就攤上這事货葬。” “怎么了?”我有些...
    開封第一講書人閱讀 164,298評論 0 354
  • 文/不壞的土叔 我叫張陵寝衫,是天一觀的道長拐邪。 經(jīng)常有香客問我,道長汹胃,這世上最難降的妖魔是什么东臀? 我笑而不...
    開封第一講書人閱讀 58,586評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮宰掉,結(jié)果婚禮上赁濒,老公的妹妹穿的比我還像新娘。我一直安慰自己挪拟,他們只是感情好击你,可當我...
    茶點故事閱讀 67,633評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著球切,像睡著了一般绒障。 火紅的嫁衣襯著肌膚如雪捍歪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,488評論 1 302
  • 那天庐镐,我揣著相機與錄音变逃,去河邊找鬼。 笑死名眉,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的陌粹。 我是一名探鬼主播福压,決...
    沈念sama閱讀 40,275評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼蒙幻!你這毒婦竟也來了胆筒?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,176評論 0 276
  • 序言:老撾萬榮一對情侶失蹤决乎,失蹤者是張志新(化名)和其女友劉穎派桩,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體范嘱,經(jīng)...
    沈念sama閱讀 45,619評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡员魏,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,819評論 3 336
  • 正文 我和宋清朗相戀三年撕阎,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片虏束。...
    茶點故事閱讀 39,932評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡镇匀,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出汗侵,到底是詐尸還是另有隱情,我是刑警寧澤发乔,帶...
    沈念sama閱讀 35,655評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站滑蚯,受9級特大地震影響抵栈,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜斥赋,卻給世界環(huán)境...
    茶點故事閱讀 41,265評論 3 329
  • 文/蒙蒙 一产艾、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧隘膘,春花似錦杠览、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,871評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至佛点,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間腺办,已是汗流浹背糟描。 一陣腳步聲響...
    開封第一講書人閱讀 32,994評論 1 269
  • 我被黑心中介騙來泰國打工船响, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留躬拢,地道東北人见间。 一個月前我還...
    沈念sama閱讀 48,095評論 3 370
  • 正文 我出身青樓米诉,卻偏偏與公主長得像,于是被迫代替她去往敵國和親拴泌。 傳聞我的和親對象是個殘疾皇子惊橱,可洞房花燭夜當晚...
    茶點故事閱讀 44,884評論 2 354

推薦閱讀更多精彩內(nèi)容

  • 背景 從區(qū)塊鏈的角度來說,kafka的方式是違背初衷的回季,試問中心化的kafka部署在哪里合適正林,云?第三方機構(gòu)觅廓?可以...
    Pillar_Zhong閱讀 2,580評論 1 51
  • ?fabric 在 1.4.1 版本正式引入 Raft 共識算法哪亿,用于替代現(xiàn)有的 Kafka 共識。fabric ...
    小蝸牛爬樓梯閱讀 1,481評論 3 1
  • ■文|米粒 暑假讨阻,我學會了煮面篡殷。我為什么要學煮面呢?第一板辽,因為煮面簡單。第二耳标,我喜歡吃面邑跪。 煮面首先要準備好面呼猪、鍋...
    印記_成長閱讀 569評論 2 4
  • 今天早上砸琅,媽媽一大早就去上班了,把我和爸爸扔在了家里谚赎,我突然想到媽媽去上班了,那我的午餐就沒了著落沸版。我問爸爸...
    吳吳志昊閱讀 204評論 0 0
  • 連續(xù)的陰天兴蒸,就像心情,冷風吹過蕾殴,抱緊衣袖岛啸,不再寒冷。 那天晚上坐地鐵去友人學校坚踩,耳機里放著陪我十幾年的音樂,兩人大...