一、緒論node
etcd做爲華爲雲PaaS的核心部件,實現了PaaS大多數組件的數據持久化、集羣選舉、狀態同步等功能。如此重要的一個部件,咱們只有深刻地理解其架構設計和內部工做機制,才能更好地學習華爲雲Kubernetes容器技術,笑傲雲原生的「江湖」。本系列將從總體框架再細化到內部流程,對etcd的代碼和設計進行全方位解讀。本文是《深刻淺出etcd》系列的第一篇,重點解析etcd的架構和代碼框架,下文所用到的代碼均基於etcd v3.2.X版本。算法
另,由華爲雲容器服務團隊傾情打造的《雲原生分佈式存儲基石:etcd深刻解析》一書已正式出版,各大平臺均有發售,購書可瞭解更多關於分佈式key—value存儲和etcd的相關內容!數據庫
二、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方法處理消息。(圖中三、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方法持久化存儲日誌條目或者快照(圖中九、10),更新kv緩存。 另外須要將已經同步好的日誌應用到狀態機中,讓狀態機更新狀態和kv存儲,通知等待請求完成的客戶端。所以須要將已經肯定同步好的日誌、快照等信息封裝在一個apply消息中推送到applyc隊列。
2.3.5 EtcdServer的apply處理
EtcdServer會處理這個applyc隊列,會將snapshot和entries都apply到kv存儲中去(圖中8)。最後調用applyWait的Trigger,喚醒客戶端請求的等待線程,返回客戶端的請求。
三、重要的數據結構
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
}
四、小結
本文簡要介紹了raft協議和etcd的框架,介紹了etcd內部的和消息流的處理。後續將分心跳和選舉、數據同步、數據持久化等不一樣專題詳細講述etcd的內部機制。