tikv/pd-server的部分代碼閱讀1
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(¤t.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個時間戳, 已經到達硬體瓶頸了.
推薦閱讀: