tikv/pd-server的部分代碼閱讀1

4 人贊了文章

代碼版本: v2.0.0 9b824d288126173a61ce7d51a71fc4cb12360201

本篇主要內容介紹

PD leader選主,

PD failover階段的pending RPC處理,

TSO分配邏輯.

後續有時間會介紹PD中其他關鍵邏輯.

PD的職責和角色

放置: region管理和遷移,

路由: 訪問那個region,

TSO: OT CC的時間戳分配,

分散式鎖服務: 內嵌etcd集群,

心跳和集群管理.

PD的特點

還做不到scale-out.

集群規模受限於單台PD機器的資源數量. 要支持更大規模的集群, 需要scale-out; 而且存儲資源分池, 限制故障域, 也能提高可靠性和可用性.

PD leader選舉演算法. 在極端情況下, 會出現雙主.

PD leader和etcd leader位於同一服務實例, 為什麼還要競選PD leader呢? 而且在極端情況下, 兩者的故障不同步, 會出現不可用的問題(後文有例子).

TSO的職責獨立且單一. 集群規模擴大之後的心跳處理, 和用戶日益增長的tps需求, 搶奪PD的資源會更加激烈, TSO可以獨立出來.

為什麼要選定一個PD為leader呢?

全局單調遞增ID分配需要有一個leader. 比如{region, Peer}ID的分配和TSO分配.

rebalance等後台維護工作, 需要有一個協調者擁有上帝視角.


Leader PD競選和租約機制

leader PD 唯一性

只有leader PD對外分配時間戳. leader PD的唯一性約束對於時間戳嚴格單調遞增至關重要. 唯一性約束通過租約機制保證.

PD Campaign leader成功後, 置自己為主(enableLeader(true)), 失去leadership後, 辭掉主(enableLeader(false))並且failstop.

func (s *Server) campaignLeader() error { ... s.enableLeader(true) defer s.enableLeader(false) ...}github.com/pingcap/pd/server/leader.go#205 func (s *Server) campaignLeadergithub.com/pingcap/pd/server/leader.go#108 func (s *Server) leaderLoopgithub.com/pingcap/pd/server/leader.go#60 func (s *Server) startLeaderLoopgithub.com/pingcap/pd/server/server.go#286 func (s *Server) Rungithub.com/pingcap/pd/cmd/pd-server/main.go#100 func main()

gRPC請求: 如Tso, AskSplit等.

由leader PD處理, follower PD一般會返回notLeaderError.

GetMembers請求比較特殊, 用來獲取leader PD的地址和cluster_id. 所以follower PD也會處理.

一般, 客戶端會調用switchLeader, 切換到leader PD上.

PD處理請求時, 調用validateRequest檢查自身是否為PD, cluster_ID是否正確. 防止其他集群侵入干擾.

func (s *Server) validateRequest(header *pdpb.RequestHeader) error { if !s.IsLeader() { return notLeaderError } if header.GetClusterId() != s.clusterID { return grpc.Errorf(codes.FailedPrecondition, "mismatch cluster id, need %d but got %d", s.clusterID, header.GetClusterId()) } return nil

restful請求: leader PD處理, follower做為反向代理伺服器, redirect請求給leader PD.

net/http/client.go#490 func (c *Client) Dogithub.com/pingcap/pd/server/api/redirector.go#94 func (p *customReverseProxies) ServeHTTPgithub.com/pingcap/pd/server/api/redirector.go#70 func (h *redirector) ServeHTTP

PD處理gRPC請求時, 調用函數Server.IsLeader檢查PD是否為leader. pending請求和PD failstop存在競爭, 需要精心處理.

restful請求和gRPC請求最終都調用Server.IsLeader確認leadership.

func (s *Server) IsLeader() bool { return atomic.LoadInt64(&s.isLeader) == 1}github.com/pingcap/pd/server/leader.go#40 func (s *Server) IsLeadergithub.com/pingcap/pd/server/grpc_service.go#532 func (s *Server) validateRequestgithub.com/pingcap/pd/server/grpc_service.go#86 func (s *Server) Tso

基於etcd的leader election演算法

思路類似基於zk的選主演算法.

競爭創建具有time-to-live的臨時key: /pd/${cluster_id}/leader, value為{peer, client} urls. 顯然該key也可用於服務發現和leadership的確認.

etcd的臨時key的創建方法:

step1: 創建租約

step2: 用事務寫入key-value並且將key和租約綁定起來.

step3: 用Lessor.KeepLive為租約持續續租(續租發生在租期過來1/3的時間).

github.com/pingcap/pd/vendor/github.com/coreos/etcd/clientv3/lease.go#450 func (l *lessor) recvKeepAlive... nextKeepAlive := time.Now().Add((time.Duration(karesp.TTL) * time.Second) / 3.0) ka.deadline = time.Now().Add(time.Duration(karesp.TTL) * time.Second)...

競爭創建key需要事務支持. 冪等的原子操作, 無法勝任. 至少:

case1: putIfNotExist: 比如hbase的checkAndMutate,

case2: compareAndSwap: 比如zk的setData介面的version validation,

case3: 單行update事務(read-modify-write)支持: 比如BigTable.

etcd支持事務: Txn().If().Then().Else().Commit()

創建臨時key勝出者為leader

發現臨時key已存在或者競爭失敗者為follower

follower使用etcd的watch機制監聽臨時key的mvccpb.DELETE事件

如果leader因為網路原因, etcd不可用, 或者其他原因(resign, SIGXXX, TSO更新失敗)導致leader PD續租失敗, 則失去leadership. 而租約到期後, 臨時key自動被etcd刪除. 而follower也會監聽到事件mvccpb.DELETE, 退出watch, 發起新的競選. 這個過程, 實現failover.

如果熟悉zk選主演算法, 則不難理解etcd的選主演算法.

問題1: 會出現雙主嗎?

單純從演算法本身去考慮, 會出現雙主

出現條件: 服務實例採用獨立etcd集群選主, leader服務實例和etcd集群之間出現network partitioning. 系列事件的時間順序為:

e1: leader和etcd集群出現network partitioning.

e2: leader開始執行keepAlive續租.

e3: etcd因為租約到期, 刪除key.

e4: follower監聽到mvccbp.DELETE事件.

e5: follower選主成功, 對外服務.

e6: leader的執行keepAlive以失敗結束, 關閉keepAlive channel.

e7: leader解除keepAlive channel上的阻塞, failstop.

出現網路斷開的情況下, keepAlive調用會等待一段時間後才能失敗(e2~e7). 在(e5~e7)之間, 出現了雙主.

通過iptables模擬網路斷開, 很容易重現雙主現象.

問題2: 採用embed etcd, 會出現雙主嗎?

如果pd創建clientv3.Client時, 用集群中所有etcd node的url做為endpoints; 則會出現, pd去連接異地pd內嵌的etcd的現象, 如果異地pd崩潰或者出現網路斷開, 則同樣會出現雙主. (雖然gRPC可以切換到可用的etcd服務上, 依然存在雙主的時間窗口).

源碼中, PD的只和本地內嵌的etcd服務建立連接, 兩者之間出現網路斷開的幾率可以忽略不計.

問題3: 存在一種case, 內嵌etcd正常工作, 但PD集群無可用的leader PD.

選主原則: 只有leader etcd node, 才能當選為leader PD.

如果etcd集群正常工作, 但leader PD無法訪問本地內嵌etcd, 則整個pd集群徹底不可用.

可以在docker環境重現該問題. 比如192.168.110.12為leader PD, 我們在該機器上封死leader PD和本地內嵌etcd node的網路.

iptables -A OUTPUT -p tcp -s 192.168.110.12 -d 192.168.110.12 --sport 2379 -j DROP

模擬leader PD和leader etcd node之間的網路斷開, 發現集群無法正常工作. 現象為:

現象1: /pd/${cluster_id}/leader消失, 再也無法創建成功.

現象2: tikv-server的心跳失敗

[ERROR] failed to connect to http://192.168.110.12:2379, Grpc(RpcFailure(RpcStatus { status: Unknown, details: Some("no leader") }))

問題4: leader PD failstop後, 怎麼處理pending RPC呢?

在所有問題中, 這個pending RPC的正確處理, 尤為重要. 即便沒有雙主的問題, pending RPC的問題也依然存在.

比如, 執行Server.AskSplit時, leader PD在執行Server.validateRequest之後發生的failstop. 則當前PD以follower的身份, 還會繼續執行RaftCluster.handleAskSplit函數. 但會有什麼後果呢?

題外話, 看完RaftCluster.handleAskSplit函數之後, 發現這個問題實際上也做了精心的考慮.

handleAskSplit主要邏輯是, 為新分裂的region和組成raft group的peer分配全局唯一ID. 全局ID的分配採用類似TSO的分配策略(更接近Percolator/Omid, 優化比TSO少, 主要原因可能是ID分配屬於低頻操作).

為什麼能夠保證正確性呢?

首先, 同一個PD leader處理並發請求, 用鎖以互斥.

其次, 兩個PD並發分配ID, 要麼從disjoint的兩個ID區間中分配; 要麼老PD更新/pd/${cluster_id}/alloc_id時, 檢查/pd/${cluster_id}/leader發現自己失去leadership, 從而事務提交失敗, 分配ID以失敗返回.

舉一個複雜的情形:

tikv-server C發送AskSplit請求給當前PD leader A, PD leader A處理完Server.validateRequest之後, 擱置了一段時間. 在這段時間內, leadership failover到PD B上. 但A並未失去leadership(即尚未調用enableLeader(false)), tikv-server D發送AskSplit請求給PD leader B, 此時PD A和PD B同時分配region ID和peer ID. 並且A緩存的ID區間已經耗盡. A和B都嘗試更新/pd/${cluster_id}/alloc_id, A因為/pd/${cluster_id}/leader檢查失敗, 事務提交失敗, 分配不出ID.

github.com/pingcap/pd/server/id.go#55 func (alloc *idAllocator) generategithub.com/pingcap/pd/server/id.go#41 func (alloc *idAllocator) Allocgithub.com/pingcap/pd/server/cluster_worker.go#56 func (c *RaftCluster) handleAskSplitgithub.com/pingcap/pd/server/grpc_service.go#430 func (s *Server) AskSplit


TSO實現

高可用時間戳分配方案

TSO為txn分配TID或時間戳, 用於TO CC協議.

時間戳嚴格單調遞增.

高可用

高可用時間戳分配方案, 類似Percolator和Omid實現. 把時間劃分為disjoint區間, leader使用某一個時間區間之前, 先將時間區間持久化到etcd中, 然後緩存在內存中, 時間區間包含一大片時間戳, 從內存中分配時間戳, 直到時間區間用完了, 然後分配下個disjoint時間區間. 如果出現leader failover, 新當選的leader從etcd中載入時間區間, 並且講時間區間移動下一個. 這樣老leader分配出去的時間戳, 一定小於新leader分配的時間戳.

高性能

緩存的時間區間位於內存中, 區間內時間戳用完之後, 才touch一次etcd; 客戶端將時間戳分配請求合併, 分配一批時間戳, 只需一次RTT; 用gRPC stream介面, 可以pipeline多個請求.

PD的TSO實現特色

特色1: 混合時鐘(本地物理時鐘+邏輯時鐘): 可以將物理時間和時間戳聯繫起來, 方便做一些point-in-time查詢. 物理時鐘部分, 精度為1ms; 邏輯時鐘部分小於1ms, 用[0, 262144)區間的值填充.

特色2: 非同步持久化時間區間: 保存下時間區間(Server.updateTimestamp)的操作, 不會佔用時間戳分配(Server.Tso)的延遲. (但本地時鐘撥慢以後, 會有50ms等待引入).

特色3: 客戶端請求合併和批量分配.

PD的TSO的分配/更新/同步始終保持下面不變式

1. 物理時鐘單調遞增, 並且兩次相鄰的更新值至少間隔1ms.2. 邏輯時鐘在本輪的物理時鐘內, 嚴格單調遞增.3. 物理時鐘一定小於持久化的lastSavedTime.4. leader failover後, 設置的初始物理時鐘, 至少比etcd中的lastSavedTime多1ms.5. 用當前本地時間重置物理時鐘. 要等到本地時鐘至少超過物理時鐘1m時,才重置物理時鐘, 否則等待.6. 時間區間寬度至少比更新周期大一個數量級.

PD TSO的實現

時間戳的首次同步

failover或者PD集群重啟後, leader PD首次分配時間戳, 需要等campaignLeader函數完成從etcd載入時間區間.

保存在etcd中的持久化時間區間

/pd/${cluster_id}/timestamp

內存中保存的已分配時間戳

github.com/pingcap/pd/server/server.go#88 Server.ts atomic.Value

時間戳分配邏輯, 如果Server.ts.physical為zeroTime, 說明還沒有同步完成, 等待200ms重試. 直至完成同步.

github.com/pingcap/pd/server/tso.go#154func (s *Server) getRespTS(count uint32) (pdpb.Timestamp, error) { var resp pdpb.Timestamp for i := 0; i < maxRetryCount; i++ { current, ok := s.ts.Load().(*atomicObject) if !ok || current.physical == zeroTime { log.Errorf("we havent synced timestamp ok, wait and retry, retry count %d", i) time.Sleep(200 * time.Millisecond) continue } resp.Physical = current.physical.UnixNano() / int64(time.Millisecond) resp.Logical = atomic.AddInt64(&current.logical, int64(count)) if resp.Logical >= maxLogical { log.Errorf("logical part outside of max logical interval %v, please check ntp time, retry count %d", resp, i) time.Sleep(updateTimestampStep) continue } return resp, nil } return resp, errors.New("can not get timestamp")}

campaignLeader函數調用syncTimestamp函數從etcd同步時間.

github.com/pingcap/pd/server/tso.go#77 func (s *Server) syncTimestampgithub.com/pingcap/pd/server/leader.go#259 func (s *Server) campaignLeader

周期性地更新物理時鐘和持久化時間區間

github.com/pingcap/pd/server/tso.go#114 func (s *Server) updateTimestampgithub.com/pingcap/pd/server/leader.go#282 func (s *Server) campaignLeader

PD TSO的關鍵點

雖然時間戳分配比較簡單, 但是實現上依然有很多意思的細節:

key1: 本地時間撥慢: 如果當前本地時間未超過物理時鐘1ms, 則updateTimestamp要等待下個周期. 此時如果邏輯時間戳(262144個)也被分配完了, 分配函數也要等待一個更新周期(updateTimestampStep)後, 再次重試.

key2: 容許本地時間意外撥快.

key3: lastSavedTime是對應於/pd/${cluster_id}/timestamp的內存變數. 如果當前時間超過lastSavedTime, 則更新二者為當前時間+TsoSaveInterval(默認3s).

key4: updateTimestamp以周期updateTimestampStep(50ms)調用. 一個TsoSaveInterval內, 有6次機會保存lastSavedTime. 而物理時鐘, 50ms才更新一次. 每個物理時鐘可以分配出262144個時間戳. 所以1s中, 最多可以分配出5242880個時間戳, 已經到達硬體瓶頸了.


推薦閱讀:
相关文章