深入淺出etcd系列Part 1 – etcd架構和程式碼框架

雲容器大師發表於2018-12-12

1、緒論

etcd作為華為雲PaaS的核心部件,實現了PaaS大多陣列件的資料持久化、叢集選舉、狀態同步等功能。如此重要的一個部件,我們只有深入地理解其架構設計和內部工作機制,才能更好地學習華為雲Kubernetes容器技術,笑傲雲原生的“江湖”。本系列將從整體框架再細化到內部流程,對etcd的程式碼和設計進行全方位解讀。本文是《深入淺出etcd》系列的第一篇,重點解析etcd的架構和程式碼框架,下文所用到的程式碼均基於etcd v3.2.X版本。

另,由華為雲容器服務團隊傾情打造的《雲原生分散式儲存基石:etcd深入解析》一書已正式出版,各大平臺均有發售,購書可瞭解更多關於分散式key—value儲存和etcd的相關內容!

2、etcd簡介

etcd是一個分散式的key-value儲存系統,底層通過Raft協議進行leader選舉和資料備份,對外提供高可用的資料儲存,能有效應對網路問題和機器故障帶來的資料丟失問題。同時它還可以提供服務發現、分散式鎖、分散式資料佇列、分散式通知和協調、叢集選舉等功能。為什麼etcd如此重要?因為etcd是Kubernetes的後端唯一儲存實現,毫不誇張地說,etcd就是Kubernetes的“心臟”。

2.1 Raft協議

要理解etcd分散式協同的工作原理,必須提到Raft演算法。Raft演算法是史丹佛的Diego Ongaro、John Ousterhout兩人以易懂(Understandability)為目標設計的一致性共識演算法。在此之前,提到共識演算法(Consensus Algorithm)必然會提到Paxos,但是Paxos的實現和理解起來都非常複雜,以至於Raft演算法提出者的博士論文中,作者提到,他們用了將近一年時間研究這個演算法的各種解釋,但還是沒有完全理解這個演算法。Paxos的演算法原理和真正實現也有很大的距離,實現Paxos的系統,如Chubby,對Paxos進行了很多的改進有優化,但是細節卻是不為人所知的。 Raft協議採用分治的思想,把分散式協同的問題分為3個問題:

  • 選舉: 一個新的叢集啟動時,或者老的leader故障時,會選舉出一個新的leader;

  • 日誌同步: leader必須接受客戶端的日誌條目並且將他們同步到叢集的所有機器;

  • 安全: 保證任何節點只要在它的狀態機中生效了一條日誌條目,就不會在相同的key上生效另一條日誌條目。

 

一個Raft叢集一般包含數個節點,典型的是5個,這樣可以承受其中2個節點故障。每個節點實際上就是維護一個狀態機,節點在任何時候都處於以下三種狀態中的一個。

  • leader:負責日誌的同步管理,處理來自客戶端的請求,與Follower保持這heartBeat的聯絡;

  • follower:剛啟動時所有節點為Follower狀態,響應Leader的日誌同步請求,響應Candidate的請求,把請求到Follower的事務轉發給Leader;

  • candidate:負責選舉投票,Raft剛啟動時由一個節點從Follower轉為Candidate發起選舉,選舉出Leader後從Candidate轉為Leader狀態。

節點啟動以後,首先都是follower狀態,在follower狀態下,會有一個選舉超時時間的計時器(這個時間是在配置的超時時間基礎上加一個隨機的時間得來的)。如果在這個時間內沒有收到leader傳送的心跳包,則節點狀態會變成candidate狀態,也就是變成了候選人,候選人會迴圈廣播選舉請求,如果超過半數的節點同意選舉請求,則節點轉化為leader狀態。如果在選舉過程中,發現已經有了leader或者有更高的任期值的選舉資訊,則自動變成follower狀態。處於leader狀態的節點如果發現有更高任期值的leader存在,則也是自動變成follower狀態。

Raft把時間劃分為任期(Term)(如下圖所示),任期是一個遞增的整數,一個任期是從開始選舉leader到leader失效的這段時間。有點類似於一屆總統任期,只是它的時間是不一定的,也就是說只要leader工作狀態良好,它可能成為一個獨裁者,一直不下臺。

2.2 etcd的程式碼整體架構

etcd整體架構如下圖所示: 

從大體上可以將其劃分為以下4個模組

- http:負責對外提供http訪問介面和http client 

- raft 狀態機:根據接受的raft訊息進行狀態轉移,呼叫各狀態下的動作。 

- wal 日誌儲存:持久化儲存日誌條目。 

- kv資料儲存:kv資料的儲存引擎,v3支援不同的後端儲存,當前採用boltdb。通過boltdb支援事務操作。 

相對於v2,v3的主要改動點為:

1. 使用grpc進行peer之間和與客戶端之間通訊;

2. v2的store是在記憶體中的一棵樹,v3採用抽象了一個kvstore,支援不同的後端儲存資料庫。增強了事務能力。 

去除單元測試程式碼,etcd v2的程式碼行數約40k,v3的程式碼行數約70k。

2.3 典型內部處理流程

我們將上面架構圖的各個部分進行編號,以便下文的處理流程介紹中,對應找到每個流程處理的元件位置。

2.3.1 訊息入口

一個etcd節點執行以後,有3個通道接收外界訊息,以kv資料的增刪改查請求處理為例,介紹這3個通道的工作機制。 1. client的http呼叫:會通過註冊到http模組的keysHandler的ServeHTTP方法處理。解析好的訊息呼叫EtcdServer的Do()方法處理。(圖中2) 2. client的grpc呼叫:啟動時會向grpc server註冊quotaKVServer物件,quotaKVServer是以組合的方式增強了kvServer這個資料結構。grpc訊息解析完以後會呼叫kvServer的Range、Put、DeleteRange、Txn、Compact等方法。kvServer中包含有一個RaftKV的介面,由EtcdServer這個結構實現。所以最後就是呼叫到EtcdServer的Range、Put、DeleteRange、Txn、Compact等方法。(圖中1) 3. 節點之間的grpc訊息:每個EtcdServer中包含有Transport結構,Transport中會有一個peers的map,每個peer封裝了節點到其他某個節點的通訊方式。包括streamReader、streamWriter等,用於訊息的傳送和接收。streamReader中有recvc和propc佇列,streamReader處理完接收到的訊息會將訊息推到這連個佇列中。由peer去處理,peer呼叫raftNode的Process方法處理訊息。(圖中3、4)

2.3.2 EtcdServer訊息處理

對於客戶端訊息,呼叫到EtcdServer處理時,一般都是先註冊一個等待佇列,呼叫node的Propose方法,然後用等待佇列阻塞等待訊息處理完成。Propose方法會往propc佇列中推送一條MsgProp訊息。 對於節點間的訊息,raftNode的Process是直接呼叫node的step方法,將訊息推送到node的recvc或者propc佇列中。 可以看到,外界所有訊息這時候都到了node結構中的recvc佇列或者propc佇列中。(圖中5)

2.3.3 node處理訊息

node啟動時會啟動一個協程,處理node的各個佇列中的訊息,當然也包括recvc和propc佇列。從propc和recvc佇列中拿到訊息,會呼叫raft物件的Step方法,raft物件封裝了raft的協議資料和操作,其對外的Step方法是真正raft協議狀態機的步進方法。當接收到訊息以後,根據協議型別、Term欄位做相應的狀態改變處理,或者對選舉請求做相應處理。對於一般的kv增刪改查資料請求訊息,會呼叫內部的step方法。內部的step方法是一個可動態改變的方法,將隨狀態機的狀態變化而變化。當狀態機處於leader狀態時,該方法就是stepLeader;當狀態機處於follower狀態時,該方法就是stepFollower;當狀態機處於Candidate狀態時,該方法就是stepCandidate。leader狀態會直接處理MsgProp訊息。將訊息中的日誌條目存入本地快取。follower則會直接將MsgProp訊息轉發給leader,轉發的過程是將先將訊息推送到raft的msgs陣列中。 node處理完訊息以後,要麼生成了快取中的日誌條目,要麼生成了將要傳送出去的訊息。快取中的日誌條目需要進一步處理(比如同步和持久化),而訊息需要進一步處理髮送出去。處理過程還是在node的這個協程中,在迴圈開始會呼叫newReady,將需要進一步處理的日誌和需要傳送出去的訊息,以及狀態改變資訊,都封裝在一個Ready訊息中。Ready訊息會推行到readyc佇列中。(圖中5)

2.3.4 raftNode的處理

raftNode的start()方法另外啟動了一個協程,處理readyc佇列(圖中6)。取出需要傳送的message,呼叫transport的Send方法並將其傳送出去(圖中4)。呼叫storage的Save方法持久化儲存日誌條目或者快照(圖中9、10),更新kv快取。 另外需要將已經同步好的日誌應用到狀態機中,讓狀態機更新狀態和kv儲存,通知等待請求完成的客戶端。因此需要將已經確定同步好的日誌、快照等資訊封裝在一個apply訊息中推送到applyc佇列。

2.3.5 EtcdServer的apply處理

EtcdServer會處理這個applyc佇列,會將snapshot和entries都apply到kv儲存中去(圖中8)。最後呼叫applyWait的Trigger,喚醒客戶端請求的等待執行緒,返回客戶端的請求。

3、重要的資料結構

3.1 EtcdServer

type EtcdServer struct {

    // 當前正在傳送的snapshot數量

    inflightSnapshots int64 

    //已經apply的日誌index

    appliedIndex      uint64 

    //已經提交的日誌index,也就是leader確認多數成員已經同步了的日誌index

    committedIndex    uint64 

    //已經持久化到kvstore的index

    consistIndex consistentIndex 

    //配置項

    Cfg          *ServerConfig

    //啟動成功並註冊了自己到cluster,關閉這個通道。

    readych chan struct{}

    //重要的資料結果,儲存了raft的狀態機資訊。

    r       raftNode

    //滿多少條日誌需要進行snapshot

    snapCount uint64

    //為了同步呼叫情況下讓呼叫者阻塞等待呼叫結果的。

    w wait.Wait

    //下面3個結果都是為了實現linearizable 讀使用的

    readMu sync.RWMutex

    readwaitc chan struct{}

    readNotifier *notifier

    //停止通道

    stop chan struct{}

    //停止時關閉這個通道

    stopping chan struct{}

    //etcd的start函式中的迴圈退出,會關閉這個通道

    done chan struct{}

    //錯誤通道,用以傳入不可恢復的錯誤,關閉raft狀態機。

    errorc     chan error

    //etcd例項id

    id         types.ID

    //etcd例項屬性

    attributes membership.Attributes

    //叢集資訊

    cluster *membership.RaftCluster

    //v2的kv儲存

    store       store.Store

    //用以snapshot

    snapshotter *snap.Snapshotter

    //v2的applier,用於將commited index apply到raft狀態機

    applyV2 ApplierV2

    //v3的applier,用於將commited index apply到raft狀態機

    applyV3 applierV3

    //剝去了鑑權和配額功能的applyV3

    applyV3Base applierV3

    //apply的等待佇列,等待某個index的日誌apply完成

    applyWait   wait.WaitTime

    //v3用的kv儲存

    kv         mvcc.ConsistentWatchableKV

    //v3用,作用是實現過期時間

    lessor     lease.Lessor

    //守護後端儲存的鎖,改變後端儲存和獲取後端儲存是使用

    bemu       sync.Mutex

    //後端儲存

    be         backend.Backend

    //儲存鑑權資料

    authStore  auth.AuthStore

    //儲存告警資料

    alarmStore *alarm.AlarmStore

    //當前節點狀態

    stats  *stats.ServerStats

    //leader狀態

    lstats *stats.LeaderStats

    //v2用,實現ttl資料過期的

    SyncTicker *time.Ticker

    //壓縮資料的週期任務

    compactor *compactor.Periodic

    //用於傳送遠端請求

    peerRt   http.RoundTripper

    //用於生成請求id

    reqIDGen *idutil.Generator

    // forceVersionC is used to force the version monitor loop

    // to detect the cluster version immediately.

    forceVersionC chan struct{}

    // wgMu blocks concurrent waitgroup mutation while server stopping

    wgMu sync.RWMutex

    // wg is used to wait for the go routines that depends on the server state

    // to exit when stopping the server.

    wg sync.WaitGroup

    // ctx is used for etcd-initiated requests that may need to be canceled

    // on etcd server shutdown.

    ctx    context.Context

    cancel context.CancelFunc

    leadTimeMu      sync.RWMutex

    leadElectedTime time.Time

}

3.2 raftNode

raftNode是Raft節點,維護Raft狀態機的步進和狀態遷移。

type raftNode struct {

    // Cache of the latest raft index and raft term the server has seen.

    // These three unit64 fields must be the first elements to keep 64-bit

    // alignment for atomic access to the fields.

    //狀態機當前狀態,index代表當前已經apply到狀態機的日誌index,term是最新日誌條目的term,lead是當前的leader id

    index uint64

    term  uint64

    lead  uint64

    //包含了node、storage等重要資料結構

    raftNodeConfig

    // a chan to send/receive snapshot

    msgSnapC chan raftpb.Message

    // a chan to send out apply

    applyc chan apply

    // a chan to send out readState

    readStateC chan raft.ReadState

    // utility

    ticker *time.Ticker

    // contention detectors for raft heartbeat message

    td *contention.TimeoutDetector

    stopped chan struct{}

    done    chan struct{}

}

3.3 node

type node struct {

    //Propose佇列,呼叫raftNode的Propose即把Propose訊息塞到這個佇列裡

    propc      chan pb.Message

    //Message佇列,除Propose訊息以外其他訊息塞到這個佇列裡

    recvc      chan pb.Message

    //叢集配置資訊佇列,當叢集節點改變時,需要將修改資訊塞到這個佇列裡

    confc      chan pb.ConfChange

    //外部通過這個佇列獲取修改後叢集配置資訊

    confstatec chan pb.ConfState

    //已經準備好apply的資訊佇列

    readyc     chan Ready

    //每次apply好了以後往這個佇列裡塞個空物件。通知可以繼續準備Ready訊息。

    advancec   chan struct{}

    //tick資訊佇列,用於呼叫心跳

    tickc      chan struct{}

    done       chan struct{}

    stop       chan struct{}

    status     chan chan Status

    logger Logger

}

 

4、小結

本文簡要介紹了raft協議和etcd的框架,介紹了etcd內部的和訊息流的處理。後續將分心跳和選舉、資料同步、資料持久化等不同專題詳細講述etcd的內部機制。

相關文章