1、緒論
etcd作為華為云PaaS的核心部件,實現(xiàn)了PaaS大多數(shù)組件的數(shù)據(jù)持久化、集群選舉、狀態(tài)同步等功能。如此重要的一個部件,我們只有深入地理解其架構(gòu)設(shè)計和內(nèi)部工作機制,才能更好地學(xué)習(xí)華為云Kubernetes容器技術(shù),笑傲云原生的“江湖”。本系列將從整體框架再細化到內(nèi)部流程,對etcd的代碼和設(shè)計進行全方位解讀。本文是《深入淺出etcd》系列的第一篇,重點解析etcd的架構(gòu)和代碼框架,下文所用到的代碼均基于etcd v3.2.X版本。
成都創(chuàng)新互聯(lián)是專業(yè)的維西網(wǎng)站建設(shè)公司,維西接單;提供成都網(wǎng)站設(shè)計、做網(wǎng)站,網(wǎng)頁設(shè)計,網(wǎng)站設(shè)計,建網(wǎng)站,PHP網(wǎng)站建設(shè)等專業(yè)做網(wǎng)站服務(wù);采用PHP框架,可快速的進行維西網(wǎng)站開發(fā)網(wǎng)頁制作和功能擴展;專業(yè)做搜索引擎喜愛的網(wǎng)站,專業(yè)的做網(wǎng)站團隊,希望更多企業(yè)前來合作!
另,由華為云容器服務(wù)團隊傾情打造的《云原生分布式存儲基石:etcd深入解析》一書已正式出版,各大平臺均有發(fā)售,購書可了解更多關(guān)于分布式key—value存儲和etcd的相關(guān)內(nèi)容!
2、etcd簡介
etcd是一個分布式的key-value存儲系統(tǒng),底層通過Raft協(xié)議進行l(wèi)eader選舉和數(shù)據(jù)備份,對外提供高可用的數(shù)據(jù)存儲,能有效應(yīng)對網(wǎng)絡(luò)問題和機器故障帶來的數(shù)據(jù)丟失問題。同時它還可以提供服務(wù)發(fā)現(xiàn)、分布式鎖、分布式數(shù)據(jù)隊列、分布式通知和協(xié)調(diào)、集群選舉等功能。為什么etcd如此重要?因為etcd是Kubernetes的后端唯一存儲實現(xiàn),毫不夸張地說,etcd就是Kubernetes的“心臟”。
2.1 Raft協(xié)議
要理解etcd分布式協(xié)同的工作原理,必須提到Raft算法。Raft算法是斯坦福的Diego Ongaro、John Ousterhout兩人以易懂(Understandability)為目標(biāo)設(shè)計的一致性共識算法。在此之前,提到共識算法(Consensus Algorithm)必然會提到Paxos,但是Paxos的實現(xiàn)和理解起來都非常復(fù)雜,以至于Raft算法提出者的博士論文中,作者提到,他們用了將近一年時間研究這個算法的各種解釋,但還是沒有完全理解這個算法。Paxos的算法原理和真正實現(xiàn)也有很大的距離,實現(xiàn)Paxos的系統(tǒng),如Chubby,對Paxos進行了很多的改進有優(yōu)化,但是細節(jié)卻是不為人所知的。 Raft協(xié)議采用分治的思想,把分布式協(xié)同的問題分為3個問題:
選舉: 一個新的集群啟動時,或者老的leader故障時,會選舉出一個新的leader;
日志同步: leader必須接受客戶端的日志條目并且將他們同步到集群的所有機器;
安全: 保證任何節(jié)點只要在它的狀態(tài)機中生效了一條日志條目,就不會在相同的key上生效另一條日志條目。
一個Raft集群一般包含數(shù)個節(jié)點,典型的是5個,這樣可以承受其中2個節(jié)點故障。每個節(jié)點實際上就是維護一個狀態(tài)機,節(jié)點在任何時候都處于以下三種狀態(tài)中的一個。
leader:負責(zé)日志的同步管理,處理來自客戶端的請求,與Follower保持這heartBeat的聯(lián)系;
follower:剛啟動時所有節(jié)點為Follower狀態(tài),響應(yīng)Leader的日志同步請求,響應(yīng)Candidate的請求,把請求到Follower的事務(wù)轉(zhuǎn)發(fā)給Leader;
candidate:負責(zé)選舉投票,Raft剛啟動時由一個節(jié)點從Follower轉(zhuǎn)為Candidate發(fā)起選舉,選舉出Leader后從Candidate轉(zhuǎn)為Leader狀態(tài)。
節(jié)點啟動以后,首先都是follower狀態(tài),在follower狀態(tài)下,會有一個選舉超時時間的計時器(這個時間是在配置的超時時間基礎(chǔ)上加一個隨機的時間得來的)。如果在這個時間內(nèi)沒有收到leader發(fā)送的心跳包,則節(jié)點狀態(tài)會變成candidate狀態(tài),也就是變成了候選人,候選人會循環(huán)廣播選舉請求,如果超過半數(shù)的節(jié)點同意選舉請求,則節(jié)點轉(zhuǎn)化為leader狀態(tài)。如果在選舉過程中,發(fā)現(xiàn)已經(jīng)有了leader或者有更高的任期值的選舉信息,則自動變成follower狀態(tài)。處于leader狀態(tài)的節(jié)點如果發(fā)現(xiàn)有更高任期值的leader存在,則也是自動變成follower狀態(tài)。
Raft把時間劃分為任期(Term)(如下圖所示),任期是一個遞增的整數(shù),一個任期是從開始選舉leader到leader失效的這段時間。有點類似于一屆總統(tǒng)任期,只是它的時間是不一定的,也就是說只要leader工作狀態(tài)良好,它可能成為一個獨裁者,一直不下臺。
2.2 etcd的代碼整體架構(gòu)
從大體上可以將其劃分為以下4個模塊:
http:負責(zé)對外提供http訪問接口和http client
raft 狀態(tài)機:根據(jù)接受的raft消息進行狀態(tài)轉(zhuǎn)移,調(diào)用各狀態(tài)下的動作。
wal 日志存儲:持久化存儲日志條目。
相對于v2,v3的主要改動點為:
使用grpc進行peer之間和與客戶端之間通信;
去除單元測試代碼,etcd v2的代碼行數(shù)約40k,v3的代碼行數(shù)約70k。
2.3 典型內(nèi)部處理流程
我們將上面架構(gòu)圖的各個部分進行編號,以便下文的處理流程介紹中,對應(yīng)找到每個流程處理的組件位置。
2.3.1 消息入口
一個etcd節(jié)點運行以后,有3個通道接收外界消息,以kv數(shù)據(jù)的增刪改查請求處理為例,介紹這3個通道的工作機制。 1. client的http調(diào)用:會通過注冊到http模塊的keysHandler的ServeHTTP方法處理。解析好的消息調(diào)用EtcdServer的Do()方法處理。(圖中2) 2. client的grpc調(diào)用:啟動時會向grpc server注冊quotaKVServer對象,quotaKVServer是以組合的方式增強了kvServer這個數(shù)據(jù)結(jié)構(gòu)。grpc消息解析完以后會調(diào)用kvServer的Range、Put、DeleteRange、Txn、Compact等方法。kvServer中包含有一個RaftKV的接口,由EtcdServer這個結(jié)構(gòu)實現(xiàn)。所以最后就是調(diào)用到EtcdServer的Range、Put、DeleteRange、Txn、Compact等方法。(圖中1) 3. 節(jié)點之間的grpc消息:每個EtcdServer中包含有Transport結(jié)構(gòu),Transport中會有一個peers的map,每個peer封裝了節(jié)點到其他某個節(jié)點的通信方式。包括streamReader、streamWriter等,用于消息的發(fā)送和接收。streamReader中有recvc和propc隊列,streamReader處理完接收到的消息會將消息推到這連個隊列中。由peer去處理,peer調(diào)用raftNode的Process方法處理消息。(圖中3、4)
2.3.2 EtcdServer消息處理
對于客戶端消息,調(diào)用到EtcdServer處理時,一般都是先注冊一個等待隊列,調(diào)用node的Propose方法,然后用等待隊列阻塞等待消息處理完成。Propose方法會往propc隊列中推送一條MsgProp消息。 對于節(jié)點間的消息,raftNode的Process是直接調(diào)用node的step方法,將消息推送到node的recvc或者propc隊列中。 可以看到,外界所有消息這時候都到了node結(jié)構(gòu)中的recvc隊列或者propc隊列中。(圖中5)
2.3.3 node處理消息
node啟動時會啟動一個協(xié)程,處理node的各個隊列中的消息,當(dāng)然也包括recvc和propc隊列。從propc和recvc隊列中拿到消息,會調(diào)用raft對象的Step方法,raft對象封裝了raft的協(xié)議數(shù)據(jù)和操作,其對外的Step方法是真正raft協(xié)議狀態(tài)機的步進方法。當(dāng)接收到消息以后,根據(jù)協(xié)議類型、Term字段做相應(yīng)的狀態(tài)改變處理,或者對選舉請求做相應(yīng)處理。對于一般的kv增刪改查數(shù)據(jù)請求消息,會調(diào)用內(nèi)部的step方法。內(nèi)部的step方法是一個可動態(tài)改變的方法,將隨狀態(tài)機的狀態(tài)變化而變化。當(dāng)狀態(tài)機處于leader狀態(tài)時,該方法就是stepLeader;當(dāng)狀態(tài)機處于follower狀態(tài)時,該方法就是stepFollower;當(dāng)狀態(tài)機處于Candidate狀態(tài)時,該方法就是stepCandidate。leader狀態(tài)會直接處理MsgProp消息。將消息中的日志條目存入本地緩存。follower則會直接將MsgProp消息轉(zhuǎn)發(fā)給leader,轉(zhuǎn)發(fā)的過程是將先將消息推送到raft的msgs數(shù)組中。 node處理完消息以后,要么生成了緩存中的日志條目,要么生成了將要發(fā)送出去的消息。緩存中的日志條目需要進一步處理(比如同步和持久化),而消息需要進一步處理發(fā)送出去。處理過程還是在node的這個協(xié)程中,在循環(huán)開始會調(diào)用newReady,將需要進一步處理的日志和需要發(fā)送出去的消息,以及狀態(tài)改變信息,都封裝在一個Ready消息中。Ready消息會推行到readyc隊列中。(圖中5)
2.3.4 raftNode的處理
raftNode的start()方法另外啟動了一個協(xié)程,處理readyc隊列(圖中6)。取出需要發(fā)送的message,調(diào)用transport的Send方法并將其發(fā)送出去(圖中4)。調(diào)用storage的Save方法持久化存儲日志條目或者快照(圖中9、10),更新kv緩存。 另外需要將已經(jīng)同步好的日志應(yīng)用到狀態(tài)機中,讓狀態(tài)機更新狀態(tài)和kv存儲,通知等待請求完成的客戶端。因此需要將已經(jīng)確定同步好的日志、快照等信息封裝在一個apply消息中推送到applyc隊列。
2.3.5 EtcdServer的apply處理
EtcdServer會處理這個applyc隊列,會將snapshot和entries都apply到kv存儲中去(圖中8)。最后調(diào)用applyWait的Trigger,喚醒客戶端請求的等待線程,返回客戶端的請求。
3、重要的數(shù)據(jù)結(jié)構(gòu)
3.1 EtcdServer
type EtcdServer struct {
// 當(dāng)前正在發(fā)送的snapshot數(shù)量
inflightSnapshots int64
//已經(jīng)apply的日志index
appliedIndex uint64
//已經(jīng)提交的日志index,也就是leader確認多數(shù)成員已經(jīng)同步了的日志index
committedIndex uint64
//已經(jīng)持久化到kvstore的index
consistIndex consistentIndex
//配置項
Cfg *ServerConfig
//啟動成功并注冊了自己到cluster,關(guān)閉這個通道。
readych chan struct{}
//重要的數(shù)據(jù)結(jié)果,存儲了raft的狀態(tài)機信息。
r raftNode
//滿多少條日志需要進行snapshot
snapCount uint64
//為了同步調(diào)用情況下讓調(diào)用者阻塞等待調(diào)用結(jié)果的。
w wait.Wait
//下面3個結(jié)果都是為了實現(xiàn)linearizable 讀使用的
readMu sync.RWMutex
readwaitc chan struct{}
readNotifier *notifier
//停止通道
stop chan struct{}
//停止時關(guān)閉這個通道
stopping chan struct{}
//etcd的start函數(shù)中的循環(huán)退出,會關(guān)閉這個通道
done chan struct{}
//錯誤通道,用以傳入不可恢復(fù)的錯誤,關(guān)閉raft狀態(tài)機。
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狀態(tài)機
applyV2 ApplierV2
//v3的applier,用于將commited index apply到raft狀態(tài)機
applyV3 applierV3
//剝?nèi)チ髓b權(quán)和配額功能的applyV3
applyV3Base applierV3
//apply的等待隊列,等待某個index的日志apply完成
applyWait wait.WaitTime
//v3用的kv存儲
kv mvcc.ConsistentWatchableKV
//v3用,作用是實現(xiàn)過期時間
lessor lease.Lessor
//守護后端存儲的鎖,改變后端存儲和獲取后端存儲是使用
bemu sync.Mutex
//后端存儲
be backend.Backend
//存儲鑒權(quán)數(shù)據(jù)
authStore auth.AuthStore
//存儲告警數(shù)據(jù)
alarmStore *alarm.AlarmStore
//當(dāng)前節(jié)點狀態(tài)
stats *stats.ServerStats
//leader狀態(tài)
lstats *stats.LeaderStats
//v2用,實現(xiàn)ttl數(shù)據(jù)過期的
SyncTicker *time.Ticker
//壓縮數(shù)據(jù)的周期任務(wù)
compactor *compactor.Periodic
//用于發(fā)送遠程請求
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節(jié)點,維護Raft狀態(tài)機的步進和狀態(tài)遷移。
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.
//狀態(tài)機當(dāng)前狀態(tài),index代表當(dāng)前已經(jīng)apply到狀態(tài)機的日志index,term是最新日志條目的term,lead是當(dāng)前的leader id
index uint64
term uint64
lead uint64
//包含了node、storage等重要數(shù)據(jù)結(jié)構(gòu)
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隊列,調(diào)用raftNode的Propose即把Propose消息塞到這個隊列里
propc chan pb.Message
//Message隊列,除Propose消息以外其他消息塞到這個隊列里
recvc chan pb.Message
//集群配置信息隊列,當(dāng)集群節(jié)點改變時,需要將修改信息塞到這個隊列里
confc chan pb.ConfChange
//外部通過這個隊列獲取修改后集群配置信息
confstatec chan pb.ConfState
//已經(jīng)準(zhǔn)備好apply的信息隊列
readyc chan Ready
//每次apply好了以后往這個隊列里塞個空對象。通知可以繼續(xù)準(zhǔn)備Ready消息。
advancec chan struct{}
//tick信息隊列,用于調(diào)用心跳
tickc chan struct{}
done chan struct{}
stop chan struct{}
status chan chan Status
logger Logger
}
4、小結(jié)
本文簡要介紹了raft協(xié)議和etcd的框架,介紹了etcd內(nèi)部的和消息流的處理。后續(xù)將分心跳和選舉、數(shù)據(jù)同步、數(shù)據(jù)持久化等不同專題詳細講述etcd的內(nèi)部機制。