上偏文章我們分析了consul 一致性default 和stale 兩種模式的一致性讀的實現原理,讓我們回憶下,
Stale模式
鏈接任何一個server節點都可以讀,容忍過期的數據,
Default 模式
這個是我們大多數人用的模式,需要從leader返回數據,如果agent鏈接到server是follower節點,則需要轉發給leader來處理。
Consistent 模式
而且我們也說了Consistent 一致性讀比default模式還要嚴格,除了需要leader返回數據外,還需要確認當前leader是否唯一,怎么確認,就需要和follower 通信來確認,就是問下幾個從節點,嘿,我還是leader嗎,如果超過半數的人都響應是,則恭喜你,還是leader,就可以返回數據了。否則返回失敗。
下面我們分析下原理是怎么實現的,初步摸排了下,這個過程還挺復雜的,所以需要單獨再開一篇文章來說明。
檢查入口
consul server 出來所有的請求基本上都支持阻塞查詢,即blockingQuery方法,consul 在計算好超時時間后,會做是否為Consistent模式的讀。
// Validate
// If the read must be consistent we verify that we are still the leader.
// queryOpts 這里檢查是否一致性讀
if queryOpts.GetRequireConsistent() {
//如果是則通過consistentRead來實現
if err := s.consistentRead(); err != nil {
return err
}
}
//不是本地之間返回
我們來分析consistentRead的邏輯, 關鍵就時發起一個leader驗證的請求,然后等待結果,代碼如下:
// consistentRead is used to ensure we do not perform a stale
// read. This is done by verifying leadership before the read.
func (s *Server) consistentRead() error {
defer metrics.MeasureSince([]string{"rpc", "consistentRead"}, time.Now())
future := s.raft.VerifyLeader()
//查詢請求會阻塞在這里,即future.Error(),需要等過半驗證成功才返回。
if err := future.Error(); err != nil {
return err //fail fast if leader verification fails
}
// poll consistent read readiness, wait for up to RPCHoldTimeout milliseconds
if s.isReadyForConsistentReads() {
return nil
}
jitter := lib.RandomStagger(s.config.RPCHoldTimeout / jitterFraction)
deadline := time.Now().Add(s.config.RPCHoldTimeout)
for time.Now().Before(deadline) {
select {
case <-time.After(jitter):
// Drop through and check before we loop again.
case <-s.shutdownCh:
return fmt.Errorf("shutdown waiting for leader")
}
if s.isReadyForConsistentReads() {
return nil
}
}
return structs.ErrNotReadyForConsistentReads
}
我們的查詢就阻塞在future.Error() 這里,沒有驗證完,這里是不會響應的。
那關鍵就在s.raft.VerifyLeader()這里,我們繼續往下挖,下面是實現:
// VerifyLeader is used to ensure the current node is still
// the leader. This can be done to prevent stale reads when a
// new leader has potentially been elected.
func (r *Raft) VerifyLeader() Future {
metrics.IncrCounter([]string{"raft", "verify_leader"}, 1)
verifyFuture := &verifyFuture{}
//init 會初始化errCh,頁就是前面error會阻塞在這個channel上。
verifyFuture.init()
select {
case <-r.shutdownCh:
return errorFuture{ErrRaftShutdown}
case r.verifyCh <- verifyFuture:
//寫一個future到verifyCh,consul leader主協程會watch這個verifyCh,會觸發驗證的邏輯
return verifyFuture
}
}
老外寫的代碼還是很友好的,注釋寫的很清楚,看代碼的同時還能看寫的注釋,頁大概明白這個方法的意圖,這個VerifyLeader 的核心邏輯就是創建一個verifyFuture,這個future很關鍵,關鍵指標都在這里,定義如下:
/ verifyFuture is used to verify the current node is still
// the leader. This is to prevent a stale read.
type verifyFuture struct {
deferError
notifyCh chan *verifyFuture
quorumSize int //過半大小,默認是0
votes int //驗證leader時follower響應ok,時votes會+1,如果超過quorumSize,則認為還是leader。
voteLock sync.Mutex
}
votes 是每個follower返回驗證成功時,會對votes+1,然后判斷是否大于過半quorumSize,默認是0,如果是過半,那恭喜你,目前你還是leader,可以執行當前這次讀請求。
下面我們要看consul是怎么給follower發送驗證請求的,通過上面的代碼,可以看出,是向leader的verifyCh 寫了一個future,所以肯定有一個go routing 會阻塞在這個verifyCh channel上,這就是異步通知, 下面就是leader的主協程登場了。
Leader 主協程
leader 完成初始化后,最后會啟動一個循環函數,先看下定義,同樣注釋也說明的很清楚。
// leaderLoop is the hot loop for a leader. It is invoked
// after all the various leader setup is done.
func (r *Raft) leaderLoop() {
...
這個leaderLoop會監聽很多channel,比如rpc請求,commit等,其中一個就有verifyCh的channel,代碼如下:
case v := <-r.verifyCh:
if v.quorumSize == 0 {
// Just dispatched, start the verification
r.verifyLeader(v)
} else if v.votes < v.quorumSize {
// Early return, means there must be a new leader
r.logger.Warn("new leader elected, stepping down")
r.setState(Follower)
delete(r.leaderState.notify, v)
for _, repl := range r.leaderState.replState {
repl.cleanNotify(v)
}
v.respond(ErrNotLeader)
} else {
// Quorum of members agree, we are still leader
delete(r.leaderState.notify, v)
for _, repl := range r.leaderState.replState {
repl.cleanNotify(v)
}
v.respond(nil)
}
我們前面查詢的goroutine 通過發了一個verifyFuture 給verifyCh,leader 的main goroutine 就監聽在這里,我們前面也說了,quorumSize 默認是0,所以第一次是會觸發verifyLeader的邏輯,什么時候觸發另外兩個的邏輯呢,要等驗證超過一半的請求返還了,就會再給verifyCh發一個消息,這時候,正常情況下就是找v.respond的邏輯,最終通知最前面的query go routine 阻塞就會被喚醒。
verifyLeader的核心邏輯就是初始化了quorumSize,另外就是對follower循環,consul 有個 replicate go routine 會和follower發心跳信息,每個follower一個,除了定時發心跳外,還支持實時觸發心態,也就是監聽notifyCh 這個channel,這個leader的go routine會發一個空的struct給這個channel來觸發,會給每個follower都發一個類型為rpcAppendEntries的消息,核心代碼如下:
// Trigger immediate heartbeats
for _, repl := range r.leaderState.replState {
repl.notifyLock.Lock()
repl.notify[v] = struct{}{}
repl.notifyLock.Unlock()
//通知主動發一個心調到follower server
asyncNotifyCh(repl.notifyCh)
}
consul leader 對每個follower 維持一個heartbeat ,核心代碼如下:
for {
// Wait for the next heartbeat interval or forced notify
select {
case <-s.notifyCh://通知即刻執行
case <-randomTimeout(r.conf.HeartbeatTimeout / 10)://定時執行
case <-stopCh:
return
}
start := time.Now()
if err := r.trans.AppendEntries(s.peer.ID, s.peer.Address, &req, &resp); err != nil {
r.logger.Error("failed to heartbeat to", "peer", s.peer.Address, "error", err)
failures++
select {
case <-time.After(backoff(failureWait, failures, maxFailureScale)):
case <-stopCh:
}
} else {
//更新時間
s.setLastContact()
failures = 0
metrics.MeasureSince([]string{"raft", "replication", "heartbeat", string(s.peer.ID)}, start)
s.notifyAll(resp.Success)
}
}
繞了這么大半圈,真正驗證的關鍵代碼總算出來了,也就是 r.trans.AppendEntries執行的,這里就不用分析了,就是發一個rpc請求給follower。如果成功。通過s.notifyAll(resp.Success) 來通知前面的future,核心代碼如下:
// vote is used to respond to a verifyFuture.
// This may block when responding on the notifyCh.
func (v *verifyFuture) vote(leader bool) {
v.voteLock.Lock()
defer v.voteLock.Unlock()
// Guard against having notified already
if v.notifyCh == nil {
return
}
if leader {
v.votes++
//防止一個follower響應就通知了leader,比如5臺的時候,一臺響應了+自己也就是2
if v.votes >= v.quorumSize {
v.notifyCh <- v
v.notifyCh = nil
}
} else {
v.notifyCh <- v
v.notifyCh = nil
}
}
如果follower響應成功,也就是認為你還是leader,則對votes加1,v.notifyCh 這里其實就是我們前面leader 的verifych, 通過幻想我們前面的查詢請求阻塞在future.error哪里,整個經過這么一個復雜的流程才能完成一次正常的讀請求,如果請求follower超時,則會等待一定的時間。繼續請求。
總結
consul 官方文檔說Consistent 模式讀,為了實現這個強一致性讀,consul 在背后做了這么多的事情,詳細你看了這篇文章,以及前面的一篇文章,對consul的三種模式的讀,應該有了一個全面的了解,在用的時候也能根據你的業務場景做出正確的選擇,對consul 感興趣的同學可以點關注,后面再繼續分享consul的文章,帶你看明白consul的世界。