Consul 一致性讀Consistent 篇

上偏文章我們分析了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的世界。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容