consul 原始碼解析(一)raft 協議實現
consul
相信大家已經知道了,在日常的開發以及運維中也會常常聽到 consul
這個詞,但是不是所有的人都知道它是什麼?它在運維中扮演了什麼樣的角色呢?
首先,我們來看下 consul
的官網中是怎麼形容自己的:
Service Discovery And Configuration Make Easy
讓服務發現以及配置變得更簡單,這個就是 consul
在 micro service
橫行的今天想要為我們解決的問題。
在微服務的世界中,運維人員變得越來越累了,以往可能 1臺物理機部署 3~4 個應用,就可以將提供完整的服務,而且維護也變得極為容易,寫指令碼就是了。但如今,一旦進入的 micro service
的世界,提供相同的服務可能就需要 20~40 個應用了,如此一來,運維人員的工作量以及壓力大大增加,但是卻得不到肉眼可見的好處。
先來說下 consul
為我們提供的四大元件:
Service Discovery: 當某個應用可用的時候,可以向
consul
客戶端註冊自己,或者讓consul
客戶端通過配置發現自己,這樣,如果有需要這個應用的其他應用就可以通過 consul 快速找到一個可用的應用了。Health Check:
consul
客戶端提供任意數量的健康檢查,包括對應用保持心跳、主機物理資源監控等。健康檢查可以被operator
檢測並操作,防止流量進入不健康的主機。KV Store: 應用按需使用
consul
的 KV儲存 ,可以用於動態配置、功能標記、協調、領袖選舉等,通過客戶端的 HTTP 介面可以靈活方便使用。Multi Datacenter:
consul
提供開箱即用的多資料中心,這意味著使用者不需要擔心需要建立額外的抽象層讓業務擴充套件到多個區域。
我們可以發現,微服務治理 的所有解決方案在 consul
的四大元件下都能得到很好的解決。
為了深入瞭解 consul
,我們先來看下幾個專有名詞:
Agent: 代理是
consul
叢集中每個成員的基本單位,他們以守護程式的形式存在,代理有 客戶端 以及 服務端 兩種角色執行。所有的節點都必須執行一個代理,它們相互之間通過 DNS 或者 HTTP 介面保持健康檢查並同步資料。Client: 客戶端 是代理的其中一種角色,它會將所有的 RPC 請求轉發到 服務端 代理,而 客戶端 本身是 無狀態 的,而且只會佔用非常少的資源並消耗少量的 網路頻寬 ,建議每個應用節點都執行一個 客戶端 。
Server: 服務端 相對而言是一個非常重的代理,它的主要工作包括參與 raft仲裁 、 維護叢集狀態 、 響應RPC查詢 、與其他的資料中心 交換資料、將 查詢轉發給Leader / 其他資料中心 等。可以說,服務端是
consul
叢集中最重要的角色,所以建議將其放置在相對獨立的主機上,並且一個叢集(資料中心)中至少需要 3 個以上的 服務端 才能保證 最終一致性 。Datacenter: 資料中心 很好理解,其實就是一個 低延遲、高頻寬 的私有網路環境,一個穩定的資料中心環境對
raft協議
來說非常重要,否則及其可能出現資料不同步、服務質量下降等情況。Gossip: 一種保證 最終一致性 的分散式協議,常用於 點對點通訊 ,模擬人與人之間的交流從而達到理想中的 最終一致性 。而
consul
通過 UDP 使用該協議提供 成員管理 、失敗檢測 、事件廣播 等功能。Raft: 是一種保證 強一致性 的分散式協議,
consul
使用該協議提供 服務端 們的資料一致性,所以我們會在consul
原始碼解析中會重點講述這個演算法是如何被應用的。
// agent/agent.go
// delegate 定義了代理的公共介面
type delegate interface {
Encrypted() bool // 資料是否進行了加密
GetLANCoordinate() (lib.CoordinateSet, error) // 獲取區域網內為 Coordinate 角色的服務端
Leave() error // 離開叢集
LANMembers() []serf.Member // 區域網內的所有成員
LANMemberAllSegments() ([]serf.Member, error) // 區域網內所有段區的成員
LANSegmentMembers(segment string) ([]serf.Member, error) // 區域網內某個段區的所有成員
LocalMember() serf.Member // 本機成員
JoinLAN(addrs []string) (n int, err error) // 加入一個或多個段區
RemoveFailedNode(node string) error // 嘗試移除某個異常的節點
RPC(method string, args interface{}, reply interface{}) error // 遠端呼叫
SnapshotRPC(args *structs.SnapshotRequest, in io.Reader, out io.Writer, replyFn structs.SnapshotReqlyFn) error // 發起快照存檔的遠端呼叫
Shutdown() error // 關閉代理
Stats() map[string]map[string]string // 用於獲取應用當前狀態
}
// Agent 代理
type Agent struct {
// 代理的執行時配置,支援 hot reload
config *config.RuntimeConfig
// ... 日誌相關
// 記憶體中收集到的應用、主機等狀態資訊
MemSink *metrics.InmemSink
// 代理的公共介面,而配置項則決定代理的角色
delegate delegate
// 本地策略執行的管理者
acls *aclManager
// 保證策略執行者的許可權,可實時更新,覆蓋本地配置檔案
tokens *token.Store
// 儲存本地節點、應用、心跳的狀態,用於反熵
State *local.State
// 負責維持本地與遠端的狀態同步
sync *ae.StateSyncer
// ...各種心跳包(Monitor/HTTP/TCP/TTL/Docker 等)
// 用於接受其他節點傳送過來的事件
eventCh chan serf.UserEvent
// 用環形佇列儲存接受到的所有事件,用 index 指向下一個插入的節點
// 使用讀寫鎖保證資料安全,當一個事件被插入時,會通知 group 中所有的訂閱者
eventBuf []*UserEvent
eventIndex int
eventLock sync.RWMutex
eventNotify NotifyGroup
// 重啟,並返回通知是否重啟成功
reloadCh chan chan error
// 關閉代理前的操作
shutdown bool
shutdownCh chan struct{}
shutdownLock sync.Mutex
// 新增到區域網成功的回撥函式
joinLANNotifier notifier
// 返回重試加入區域網失敗的錯誤資訊
retryJoinCh chan error
// 併發安全的儲存當前所有節點的唯一名稱,用於 RPC傳輸
endpoints map[string]string
endpointsLock sync.RWMutex
// ...為代理提供 DNS/HTTP 的API
// 追蹤當前代理正在執行的所有監控
watchPlans []*watch.Plan
}
// 決定當前啟動的是 server 還是 client 的關鍵程式碼在於
func (a *Agent) Start() error {
// ...
if c.ServerMode {
server, err := consul.NewServerLogger(consulCfg, a.logger, a.tokens)
// error handler
a.delegate = server // 主要差別在這裡
} else {
client, err := consul.NewClientLogger(consulCfg, a.logger, a.tokens)
// error handler
a.delegate = client // 主要差別在這裡
}
a.sync.ClusterSize = func() int { return len(a.delegate.LANMembers()) }
// ...
}
因為相對來說, 客戶端 是比較輕量的代理,所以我們先來先看 客戶端 的結構與實現:
// agent/consul/client.go
type Client struct {
config *Config
// 連線 服務端 的連線池,用 TCP 協議
connPool *pool.ConnPool
// 用於選擇和維護客戶端用於 RPC 請求的服務端
routers *router.Manager
// 用於限制從客戶端到伺服器的 RPC 總數
rpcLimiter *rate.Limiter
// 負責接送來自同一個資料中心的事件
eventCh chan serf.Event
logger *log.Logger
// 儲存當前資料中心的 serf 叢集資訊
serf *serf.Serf
shutdown bool
shutdownCh chan struct{}
shutdownLock sync.Mutex
}
而 Client 所實現的 delegate
介面幾乎都是直接呼叫的 serf
提供的介面,如:
func (c *Client) JoinLAN(addrs []string) (int, error) {
return c.serf.Join(addrs, true)
}
func (c *Client) Leave() error {
c.logger.Printf("[INFO] consul: client starting leave")
// Leave the LAN pool
if c.serf != nil {
if err := c.serf.Leave(); err != nil {
c.logger.Printf("[ERR] consul: Failed to leave LAN Serf cluster: %v", err)
}
}
return nil
}
func (c *Client) KeyManagerLAN() *serf.KeyManager {
return c.serf.KeyManager()
}
而 Server
的由於涉及到 raft 協議,所以其實現複雜的多 (44 個 fields
),先來看下結構:
// agent/consul/server.go
type Server struct {
// 哨兵介面,負責處理xxx TODO
sentinel sentinel.Evaluator
// 負責策略執行的許可權快取 TODO
aclAuthCache *acl.Cache
// 負責策略執行的非許可權快取 TODO
aclCache *aclCache
// 自動更新當前許可權的有效性
autopilotPolicy AutopilotPolicy
// 負責觸發移除無用的應用的檢查
autopilotRemoveDeadCh chan struct{}
// 負責停止自動更新許可權有效的程式碼
autopilotShutdownCh chan struct{}
// 阻塞直至停止自動更新許可權
autopilotWaitGroup sync.WaitGroup
// 儲存當前叢集的健康狀態
clusterHealth structs.OperatorHealthReply
clusterHealthLock sync.RWMutex
config *Config
// 連線其他服務端的連線池
connPool *pool.ConnPool
// ...
// 保證強一致性的 raft 狀態機
fsm *fsm.FSM
// 在相同資料中心的 consul 節點使用 raft 協議保證操作的強一致性
raft *raft.Raft
raftLayer *RaftLayer
raftStore *raftboltdb.BoltStore // 提供 ACID 的高效能 KV資料庫
raftTransport *raft.NetworkTransport
raftInmem *raft.InmemStore
// 通過 setupRaft() 設定,確保server能收到Leader更換通知
raftNotifyCh <-chan bool
// 判斷 Leader 是否準備好提供一致性讀取
readyForConsistentReads int32
// 使用訊號通知該服務端需要退出叢集,並嘗試將 RPC 轉發到其他的服務端上。
leaveCh chan struct{}
// 用於路由公域網的服務端或者由使用者定義的段區域
router *router.Router
// 用於接受進來的請求連線
Listener net.Listener
rpcServer *rpc.Server
rpcTLS *tls.Config
serfLAN *serf.Serf
// 通過段名指引到不同的 serf 叢集中
segmentLAN map[string]*serf.Serf
// 在同一個資料中心中,由服務端組成的 serf 叢集
serfWAN *serf.Serf
// 在當前的資料中心進行服務端追蹤,提供 id 與 address 相互轉換
serverLookup *ServerLookup
// 通知廣播,讓公域網的 serf 例項知道區域網的服務端發生的變化(退出)
floodLock sync.RWMutex
floodCh []chan struct{}
// ...
// 通知需要儲存快照,並重新發起 Leader 選舉
reassertLeaderCh chan chan error
// tombstone 演算法的 GC 調優引數
tombstoneGC *state.TombstoneGC
//
aclReplicationStatus structs.ACLReplicationStatus
aclReplicationStatusLock sync.RWMutex
}
下面來看看 consul
作為微服務治理的基礎架構圖是怎麼樣的:
毫無疑問,raft 協議是維持整個 consul
生態中最重要的一個協議,下面重點來講下 raft 協議:
作為基於 Paxos 的一種變種演算法,它簡化了狀態,通過加入 時間差 、 領導選舉 等概念使一致性演算法變得更簡單、易懂,先來看看其中的專有名詞:
-
Leader election: 領導選舉 是目前落地的一致性演算法不可避免的一個步驟(CASPaxos 好像不需要,不過還沒有深入研究),為了達到一致性,必須要已一個共同認可的節點做出所有操作。而 raft 協議的節點都會處於三種狀態之一:
Leader: 操作的真正處理者,它負責將改動確認生效,並將其同步到其他節點。
Follower: 負責將來自 Leader 的改動請求寫入本地 Log,返回成功。
Candidate: 如果 Leader 發生了故障,沒有收到心跳的 Followers 們會進入 Candidate 狀態,並開始選主,並保持該狀態直到選主結束。
Log Replication: 當 Leader 被選舉出來以後,所有的寫請求都必須要到 Leader 執行,Leader 會將其作為 Log entry 追加到日誌中,然後給其他 Follower 傳送請求,當絕大部分的 (
(N/2)+1
)的 Follower replicated 成功後,就代表該寫入事件成功了,就會返回結果狀態到客戶端。Log Compaction: 在實際的應用場景中,因為磁碟空間是有限的,日誌不能無限地增長,否則即使系統需要重啟也需要耗費大量的時間。所以, raft 會對節點進行
snapshot
操作,執行成功後,會將snapshot
之前的日誌丟棄掉。
為了更快速理解 raft 演算法,我們可以帶著3個問題觀看 動畫 :
Leader 在什麼時候,如何進行選舉?
Log Replication 作用是什麼,如何實現?
節點數量發生變化(增加/減少)時如何處理?
結合 consul
程式碼來理解下 raft 協議:
可以看到 raft 協議中,節點分別處於 3種狀態 :
// raft/state.go
type RaftState uint32
const (
Follower RaftState = iota
Candidate
Leader
// 這個狀態是 hashicorp/raft 特有的,表示節點下線
Shutdown
)
// 獲取當前 raft 節點狀態
func (r *raftState) getState() RaftState {
stateAddr := (*uint32)(&r.state)
return RaftState(atomic.LoadUint32(stateAddr))
}
先來看看在 consul
裡面是如何建立一個 raft node
的:
// raft/api.go
// NewRaft 新建一個 raft 節點,傳入引數除了 config/transport 以外,
// 還有 fsm/logs/store/snapshot ,這是為了可以通過傳遞引數,使崩掉
// 的節點能夠快速恢復
func NewRaft(config *Config, fsm FSM, logs LogStore, stable StableStore, snaps SnapshotStore, trans Transport) (*Raft, error) {
// TODO ...
r = &Raft{
// ...
}
// 預設為 Follower 角色
r.setState(Follower)
// 該處只應該在測試的時候被呼叫,
// 不是必須的主流程,所以單獨拎出來
if conf.StartAsLeader {
r.setState(Leader)
r.setLeader(r.localAddr)
}
r.goFunc(r.run) // 管理 Server 狀態,執行不同的程式碼
r.goFunc(r.runFSM) // 管理狀態機
r.goFunc(r.runSnapshots) // 管理快照
return r, nil
}
// raft/raft.go
func (r *Raft) run() {
for {
select {
case <-r.shutdownCh: // 會被 close(r.shutdownCh) 觸發
r.setLeader("") // 不聯絡 Leader 節點了
return
default:
}
switch r.getState() {
case Follower:
r.runFollower()
case Candidate:
r.runCandidate()
case Leader:
r.runLeader()
// 問題:為什麼沒有判斷 Shutdown 的狀態呢?
}
}
}
// 只有 Follower 狀態的節點會執行該段程式碼
func (r *Raft) runFollower() {
// ...
for {
select {
case rpc := <-rpcCh:
r.processRPC(rpc) // 根據 rpc 的型別進行 附加日誌/備份快照 等操作
// ...
case v := <-r.verifyCh:
// Reject any operations since we are not the leader
v.respond(ErrNotLeader)
case <-heartbeatTimer: // 重頭戲,還記得動畫裡面的演示嗎?
// 重設一個隨機的定時器
heartbeatTimer = randomTimeout(r.conf.HeartbeatTimeout)
// 獲取上一次從 Leader 節點得到聯絡的時間
lastContact := r.LastContact()
if time.Now().Sub(lastContact) < r.conf.HeartbeatTimeout {
continue // 還能聯絡上 Leader ,繼續下一次操作
}
// 從這裡開始就發現聯絡不上 Leader 了,需要重新開始投票!!!
lastLeader := r.Leader() // 先存一下之前的 Leader
r.setLeader("") // 離開這個 Leader 的叢集
if r.configurations.lastestIndex == 0 { // 沒有其他可通訊節點了,終止選舉!!!
// ...
} else if { // 關於選舉權的配置,可以忽略
//...
} else {
// Logger
r.setState(Candidate)
return
}
case <-r.shutdownCh:
return
}
}
}
除了代理的正常維護自己的功能,它還必須處理日誌,所以這裡使用了 有限狀態自動機 (FSM)的方式避免了影響正常節點功能。
擁有相同日誌序列的應用必須歸結於相同的狀態,意味著行為必須是確定性的。
// raft/fsm.go
// FSM 的介面定義了一些能夠操作日誌的有限狀態機的行為
type FSM interface {
// 提交日誌條目,如果該 FSM 在 Leader 節點上執行
// 將返回一個 logFuture 用於等待直至日誌被認為提交成功
Apply(*Log) interface{}
// 將當前日誌進行快照備份,需要注意的是本函式不能與 Apply
// 同時在多個執行緒中被呼叫,但允許在同一執行緒中併發執行
Snapshot() (FSMSnapshot, error)
// 將用於從快照中恢復 FSM 狀態,一旦該函式被呼叫,FSM 必須
// 暫停其他所有呼叫與狀態,直至恢復完畢
Restore(io.ReadCloser) error
}
func (r *Raft) runFSM() {
var lastIndex, lastTerm uint64
// commit 提交日誌
commit := func(req *commitTuple) {
var resp interface{}
if req.log.Type == LogCommand {
resp = r.fsm.Apply(req.log) // 提交日誌條目
}
// 更新索引為提交日誌的值
lastIndex = req.log.Index
lastTerm = req.log.Term
// Leader 節點需要等待日誌 commit
if req.future != nil {
req.future.response = resp
req.future.respond(nil)
}
}
// restore 恢復快照
restore := func(req *restoreFuture) {
meta, source, err := r.snapshots.Open(req.ID) // 讀取快照
// error handle
if err := r.fsm.Restore(source); err != nil {
// error handle
}
source.Close()
// 更新索引為快照中的值
lastIndex = meta.Index
lastTerm = meta.Term
req.respond(nil)
}
// snapshot 生成快照備份
snapshot := func(req *reqSnapshotFuture) {
if lastIndex == 0 {
req.respond(ErrNothingNewToSnapshot)
return
}
snap, err := r.fsm.Snapshot()
// 返回當前的索引給請求
req.index = lastIndex
req.term = lastTerm
req.snapshot = snap
req.respond(err)
}
for {
select {
case ptr := <-r.fsmMutateCh: // 變化通道
switch req := ptr.(type) {
case *commitTuple:
commit(req)
case *restoreFuture:
restore(req)
default:
// panic here...
}
case req := <-r.fsmSnapshotCh: // 快照通道
snapshot(req)
case <-r.shutdownCh:
return
}
}
}
到此,基本上 raft 協議的主幹實現已經講解完全了,此外還有如日誌、快照的 儲存引擎 、請求的 通訊協議 等可以留給有興趣的童鞋自行去讀一下。
相關文章
- 實現 Raft 協議Raft協議
- okhttp 原始碼解析 – http 協議的實現 – 重定向HTTP原始碼協議
- okhttp 原始碼解析 - http 協議的實現 - 重定向HTTP原始碼協議
- raft協議Raft協議
- okhttp 原始碼解析 - 網路協議的實現 - HTTP 之 cookie 管理HTTP原始碼協議Cookie
- HDFS原始碼解析系列一——HDFS通訊協議原始碼協議
- raft協議詳解Raft協議
- raft協議初識Raft協議
- Raft協議精解Raft協議
- [原始碼解析] 機器學習引數伺服器 Paracel (2)--------SSP控制協議實現原始碼機器學習伺服器協議
- Netty 原始碼中對 Redis 協議的實現Netty原始碼Redis協議
- TiKV 原始碼解析系列 – Raft 的優化原始碼Raft優化
- TiKV 原始碼解析系列 - Raft 的優化原始碼Raft優化
- Raft協議學習筆記Raft協議筆記
- Raft 協議學習筆記Raft協議筆記
- 音視訊同步!RTCP 協議解析及程式碼實現TCP協議
- dubbo原始碼解析(三十)遠端呼叫——rest協議原始碼REST協議
- dubbo原始碼解析(三十二)遠端呼叫——thrift協議原始碼協議
- dubbo原始碼解析(三十一)遠端呼叫——rmi協議原始碼協議
- C#實現聯通簡訊Sgip協議程式原始碼C#協議原始碼
- SMB/CIFS協議解析(一)協議
- 玩轉直播系列之RTMP協議和原始碼解析(2)協議原始碼
- 分散式一致性協議Raft全面詳解(建議收藏)分散式協議Raft
- Paxos、Raft不是一致性演算法/協議?Raft演算法協議
- 分散式理論(六) - 一致性協議Raft分散式協議Raft
- tikv/raft-rs:在 Rust 中實現的 Raft 分散式共識演算法原始碼RaftRust分散式演算法原始碼
- Free自由協議鎖倉分紅系統NFT丨Free自由協議開發原始碼解析協議原始碼
- etcd學習(6)-etcd實現raft原始碼解讀Raft原始碼
- ipad協議及原始碼iPad協議原始碼
- Runtime原始碼 protocol(協議)原始碼Protocol協議
- 分散式一致性協議Raft原理與例項分散式協議Raft
- 深入原始碼解析 tapable 實現原理原始碼
- React原始碼解析(一):元件的實現與掛載React原始碼元件
- okhttp 原始碼解析 - 網路協議的實現 - 請求流程: 請求的傳送與響應的接收HTTP原始碼協議
- 透視RPC協議:SOFA-BOLT協議原始碼分析RPC協議原始碼
- etcd套路(二)etcd核心之raft協議Raft協議
- Raft協議:通過TermId大的通過Raft協議
- DHCP協議和dhcpcd原始碼分析協議原始碼