etcd-raft的線性一致讀方法二:LeaseRead

說明

我們在前面的ReadIndex中說明了etcd-raft如何通過ReadIndex的思路來實現線性一致性讀。雖然在讀的時候只會交互Heartbeat信息,這畢竟還是有代價,所以我們可以考慮做更進一步的優化。

在 Raft 論文裡面,提到了一種通過 clock + heartbeat 的 lease read 優化方法。 leader在發送 heartbeat 的時候,會首先記錄一個時間點 start,當系統大部分節點都回復了 heartbeat response,那麼我們就可以認為 leader 的 lease 有效期可以到 start + election timeout / clock drift bound 這個時間點。

為什麼能夠這麼認為呢?主要是在於 Raft 的選舉機制,因為 follower 會在至少 election timeout 的時間之後,才會重新發生選舉,所以下一個 leader 選出來的時間一定可以保證大於 start + election timeout / clock drift bound。

雖然採用 lease 的做法很高效,但仍然會面臨風險問題,也就是我們有了一個預設的前提,各個伺服器的 CPU clock 的時間是準的,即使有誤差,也會在一個非常小的 bound 範圍裡面,如果各個伺服器之間 clock 走的頻率不一樣,有些太快,有些太慢,這套 lease 機制就可能出問題。

實現

etcd-server對客戶的讀請求處理與前面介紹的ReadIndex方法別無二致,區別在於對ReadeIndex請求的處理,兩種不同的方式走了不同的路徑:

func stepLeader(r *raft, m pb.Message) {n case pb.MsgReadIndex:n if r.quorum() > 1 {n switch r.readOnly.option {n case ReadOnlySafe:n ......n // read lease機制n case ReadOnlyLeaseBased:n var ri uint64n if r.checkQuorum {n ri = r.raftLog.committed n }n if m.From == None || m.From == r.id {n r.readStates = append(r.readStates, n ReadState{n Index: r.raftLog.committed, n RequestCtx: m.Entries[0].Data})n } else {n r.send(pb.Message{To: m.From, n Type: pb.MsgReadIndexResp,n Index: ri,n Entries: m.Entries})n }n }n ...n }n }n}n

如果是使用ReadOnlyLeaseBased選項(即read lease機制),如果ReadIndex請求是來自當前節點的應用,主節點的處理函數就直接將自己的當前commit返回給應用即可,而如果ReadIndex請求是來自集群其他Follower節點,給Follower節點返回MsgReadIndexResp,其中包含Leader的commit index。

對於Follower節點,如果收到應用的ReadIndex請求,無論是ReadIndex還是LeaseRead做法都一樣,直接發送ReadIndex請求給Leader即可。

在LeaseRead方案中,主節點之所以可以直接將自身的commit index信息返回是因為其認為自身當前依然持有作為Leader的尚方寶劍(lease)。為此,Leader節點需要定期檢查自身是否依然是主。

func (r *raft) tickHeartbeat() {n ......n if r.electionElapsed >= r.electionTimeout {n r.electionElapsed = 0 n if r.checkQuorum {n r.Step(pb.Message{From: r.id, n Type: pb.MsgCheckQuorum})n }n if r.state == StateLeader && r.leadTransferee != None {n r.abortLeaderTransfer()n }n }n ......n}n

Leader在發送心跳的時候會進行Quorum檢查,但不是每次心跳都會檢查,只要當前距離上一次選舉的時間間隔不超過系統的electionTimeout時間,我們就認為在該安全周期內不會發生新的選舉(在前面已經說明)。

檢查的原理是發送消息MsgCheckQuorum給自己(注意:該消息是由Leader發送給自己,提醒自己進行Leader身份檢查),該消息處理是:

func stepLeader(r *raft, m pb.Message) {n switch m.Type {n case pb.MsgBeat:n ...n case pb.MsgCheckQuorum:n if !r.checkQuorumActive() {n r.becomeFollower(r.Term, None)n }n returnn case ...:n }n}nn// 檢查多數節點是否存活 nfunc (r *raft) checkQuorumActive() bool {n var act intn for id := range r.prs {n if id == r.id { // self is always active n act++n continuen }nn if r.prs[id].RecentActive {n act++n }n r.prs[id].RecentActive = falsen }n return act >= r.quorum()n}n

主要檢查在於函數checkQuorumActive,該函數會檢查多數從節點是否活躍,根據RecentActive標識位來判斷,同時在檢查後將該標誌位設置為false,避免出現每次檢查都是存活,出現狀態錯誤。

而RecentActive標誌位是Leader節點在收到Follower的消息響應後被設置為true的,這些響應包括心跳響應(MsgHeartbeatResp)與正常的數據同步響應(MsgAppResp)。

func stepLeader(r *raft, m pb.Message) {n ...n switch m.Type {n case pb.MsgAppResp:n pr.RecentActive = truen ......n case pb.MsgHeartbeatResp:n pr.RecentActive = truen }n ...n}n

這些響應保證了Follower任然認可其Leader的身份,而在checkQuorumActive檢查是否多數Follower依然認可Leader的領導者身份,從而保證了Leader當前對lease的持有,並且可以不經任何確認就可以直接返回自身的commit作為ReadIndex的響應。

參考

  • zhuanlan.zhihu.com/p/25

推薦閱讀:

ScyllaDB簡介
HBase 1.0 之後在最近兩年加的一些新功能
【rocksdb源碼分析】使用PinnableSlice減少Get時的內存拷貝

TAG:etcd | 分布式一致性 | 分布式存储 |