:從「記錄驅(qū)動輪詢」到「vttablet 自述權(quán)威狀態(tài)」)
Vitess TabletManager 模型重構(gòu)從「記錄驅(qū)動輪詢」到「vttablet 自述權(quán)威狀態(tài)」【免費(fèi)下載鏈接】vitessVitess is a database clustering system for horizontal scaling of MySQL.項(xiàng)目地址: https://gitcode.com/gh_mirrors/vi/vitess導(dǎo)讀本文基于 Vitess 倉庫中的設(shè)計文檔 TabletManagerModel.md剖析 Vitess tablet 狀態(tài)管理模型的一次關(guān)鍵演進(jìn)將 tablet 記錄topo 中的 Tablet record從「權(quán)威事實(shí)」降級為「發(fā)現(xiàn)用緩存」讓 vttablet 進(jìn)程成為自身狀態(tài)的唯一權(quán)威發(fā)布者。讀完本文你將理解為何單次 RPC 往返即可完成狀態(tài)變更、為何主庫選舉必須把 topo 當(dāng)作例外權(quán)威、vttablet 啟動時信任哪些 topo 字段以及當(dāng)前倉庫源碼tm_state.go、tm_init.go是如何落地這套模型的。背景舊模型的脆弱之處設(shè)計文檔首先描述了舊模型的運(yùn)行方式tablet 記錄被視為權(quán)威authoritative。具體表現(xiàn)為vttablet 進(jìn)程持續(xù)輪詢poll自己在 topo 中的 tablet 記錄并對記錄的變化做出反應(yīng)代碼中散布著大量「先更新 tablet 記錄再調(diào)用RefreshTablet或RefreshState」的調(diào)用點(diǎn)。這套「改記錄 主動刷新」的兩段式流程存在兩個問題不符合實(shí)際運(yùn)維方式更新 tablet 記錄并期望 tablet 自己刷新并不能帶來任何實(shí)際收益鏈路脆弱每一次狀態(tài)變更都把多余的組件topo 存儲 輪詢觀察者拉進(jìn)動作鏈中鏈路越長失敗概率越高。因此文檔提出的新模型是tablet 進(jìn)程vttablet是自己當(dāng)前狀態(tài)的權(quán)威來源它把狀態(tài)「發(fā)布」到 tablet 記錄中而該記錄僅用于發(fā)現(xiàn)discovery。換句話說topo 中的 tablet 記錄不再是「命令源頭」而是「狀態(tài)展示板」。新模型核心單次 RPC 往返 盡力而為發(fā)布vttablet 存活時直接 RPC不再輪詢在新模型下所有需要改變 tablet 狀態(tài)的流程直接向 vttablet 發(fā)起一次 RPC 請求tablet 立即執(zhí)行該請求執(zhí)行成功后vttablet盡力而為best-effort地把新狀態(tài)寫回 tablet 記錄如果寫回失敗vttablet 會持續(xù)重試直到成功。這一設(shè)計帶來的核心收益是請求只需一次到 tablet 的往返即可成功場景結(jié)果RPC 請求失敗操作視為失敗直接返回錯誤RPC 成功、但 tablet 記錄更新失敗操作仍然成功記錄稍后會被 vttablet 的發(fā)布重試補(bǔ)齊這里的關(guān)鍵哲學(xué)是操作的成功不依賴 topo 寫入的即時成功。因?yàn)?tablet 記錄遲早會收斂到真實(shí)狀態(tài)調(diào)用方無需為了「寫記錄」而多等一次 topo 往返。源碼印證publishStateLocked 與 retryPublish在 tm_state.go 中publishStateLocked正是這套「盡力而為發(fā)布」的實(shí)現(xiàn)func (ts *tmState) publishStateLocked(ctx context.Context) { // ... _, err : ts.tm.TopoServer.UpdateTabletFields(ctx, ts.tm.tabletAlias, func(tablet *topodatapb.Tablet) error { if err : topotools.CheckOwnership(tablet, ts.tablet); err ! nil { // ... return topo.NewError(topo.NoUpdateNeeded, ) } proto.Reset(tablet) proto.Merge(tablet, ts.tablet) return nil }) if err ! nil { // ... log.Error(fmt.Sprintf(Unable to publish state to topo, will keep retrying: %v, err)) ts.isPublishing true // Keep retrying until success. go ts.retryPublish() } }注意幾個細(xì)節(jié)寫入使用UpdateTabletFields并經(jīng)過topotools.CheckOwnership校驗(yàn)確保只有 tablet 自己或擁有者能改寫自己的記錄發(fā)布失敗后進(jìn)入retryPublish()后臺重試循環(huán)tm_state.go每輪間隔由--publish-retry-interval控制默認(rèn)30 秒見 tm_state.go一個特殊分支如果UpdateTabletFields返回NoNode有人把 tablet 記錄刪了vttablet 會認(rèn)為自己的身份已不存在主動向servenv.ExitChan發(fā)送 SIGTERM 優(yōu)雅退出——因?yàn)橛涗洷粍h意味著它已不屬于任何拓?fù)洹_@套「先執(zhí)行、后發(fā)布、失敗續(xù)傳」的結(jié)構(gòu)與設(shè)計文檔描述的模型完全一致。vttablet 宕機(jī)或不可達(dá)時操作直接失敗如果 vttablet 不可達(dá)操作直接失敗。設(shè)計文檔特別指出這種失敗模式并不比「我們無法更新 tablet 記錄」更糟。理由很樸素?zé)o論采用哪種模型vttablet 不可達(dá)時狀態(tài)變更都無法完成而且代碼本來就默認(rèn) tablet 記錄可能與 vttablet 實(shí)際狀態(tài)不同步——這是 Vitess 長期存在并已被各方代碼如 health check、vtgate 的發(fā)現(xiàn)邏輯處理過的前提新模型只是把這個前提顯式化了。RefreshState保留 API但語義收窄RefreshState仍然作為一個 API 存在但它的用途被明確限制為針對 global topo 中狀態(tài)變化的刷新例如 shard 記錄、SrvKeyspace 的變化而不再用于「刷新自身 tablet 記錄」。源碼中的實(shí)現(xiàn)印證了這一點(diǎn)——rpc_actions.go 中RefreshState只是轉(zhuǎn)調(diào)tmState.RefreshFromTopo而后者讀取的是shard 記錄和 SrvKeyspace見 tm_state.gofunc (ts *tmState) RefreshFromTopo(ctx context.Context) error { // ... shardInfo, err : ts.tm.TopoServer.GetShard(ctx, ts.Keyspace(), ts.Shard()) // ... srvKeyspace, err : ts.tm.TopoServer.GetSrvKeyspace(ctx, ts.tm.tabletAlias.Cell, ts.Keyspace()) // ... return ts.RefreshFromTopoInfo(ctx, shardInfo, srvKeyspace) }RefreshFromTopoInfo會從中提取分片級信息是否處于 reshardingSourceShards、TabletControls 中的 denied tables 規(guī)則、SrvKeyspace 各 partition 中本分片是否 serving 等并據(jù)此調(diào)用updateLocked調(diào)整本地查詢服務(wù)狀態(tài)tm_state.go。也就是說刷新不再是為了「同步自己的記錄」而是為了感知全局拓?fù)浞制?、keyspace、serving 狀態(tài)的變化。兩個例外哪里仍然信任 topo設(shè)計文檔明確列出了兩個 topo 依然作為權(quán)威的例外場景這兩處也正是新模型邊界的關(guān)鍵。例外一集群主庫選舉Cluster Leadership對于「誰是這個分片的主庫primary」這類流程topo 是權(quán)威。對于這類請求tablet 會先嘗試更新自己的記錄成功后才算操作成功——順序與常規(guī)流程相反。原因在于新的集群主庫選舉cluster leadership redesign機(jī)制的依賴分片記錄shard record中的PrimaryAlias與PrimaryTermStartTime是全體組件vtgate、vttablet、vtorc判定當(dāng)前主庫的唯一依據(jù)必須先落盤才能避免「DB 層已是雙主、而 topo 層毫不知情」的腦裂窗口。源碼中可以看到這一順序被嚴(yán)格保證——tm_state.go 的ChangeTabletTypeif tabletType topodatapb.TabletType_PRIMARY { primaryTermStartTime protoutil.TimeToProto(time.Now()) // Update the tablet record first. _, err : topotools.ChangeType(ctx, ts.tm.TopoServer, ts.tm.tabletAlias, tabletType, primaryTermStartTime) if err ! nil { // 讀取驗(yàn)證或持續(xù)重試直到確認(rèn)寫入成功 // ... } } err : ts.updateTypeAndPublish(ctx, tabletType, primaryTermStartTime, action)轉(zhuǎn)為 PRIMARY 時先通過topotools.ChangeType寫 topo若寫失敗代碼會反復(fù)讀取 topo 驗(yàn)證寫入是否真的發(fā)生了因?yàn)椴淮_定是「沒寫進(jìn)去」還是「寫了但響應(yīng)丟失」確認(rèn)Type與PrimaryTermStartTime都匹配后才繼續(xù)本地狀態(tài)切換。updateTypeAndPublish中還注釋解釋了順序的必要性只有當(dāng) topo 更新完成之后才調(diào)用SetReadOnly(false)避免出現(xiàn)「DB 層雙主、Vitess 層只認(rèn)一個主」的情況tm_state.go。例外二vttablet 啟動時vttablet 啟動時會把來自 topo 的以下信息當(dāng)作權(quán)威KeyspaceShardTablet TypeDBName設(shè)計文檔要求如果這些信息與 init 參數(shù)不匹配進(jìn)程應(yīng)直接退出exit而不是帶著錯誤的身份進(jìn)入服務(wù)。源碼中這一校驗(yàn)體現(xiàn)在 tm_init.go 的initTablet里——當(dāng) tablet 記錄已存在NodeExists時oldTablet, err : tm.TopoServer.GetTablet(ctx, tablet.Alias) // ... // Sanity check the keyspace and shard if oldTablet.Keyspace ! tablet.Keyspace || oldTablet.Shard ! tablet.Shard { return fmt.Errorf(initTablet failed because existing tablet keyspace and shard %v/%v differ from the provided ones %v/%v, oldTablet.Keyspace, oldTablet.Shard, tablet.Keyspace, tablet.Shard) }keyspace與shard不一致時直接返回錯誤、拒絕啟動。而這些值正是通過一系列init-*啟動參數(shù)注入的注冊于 tm_init.go參數(shù)說明--init-keyspace該 tablet 所屬 keyspace--init-shard該 tablet 所屬 shard--init-tablet-type初始 tablet 類型合法值僅REPLICA、RDONLY、EXPERIMENTAL、SPARE默認(rèn)REPLICA構(gòu)建時校驗(yàn)見 tm_init.go--init-db-name-override覆蓋 vttablet 使用的數(shù)據(jù)庫名缺省為vt_keyspacename--init-tablet-type-lookup實(shí)驗(yàn)性重啟時從已有 topo 記錄中恢復(fù) tablet 類型使 RDONLY/DRAINED 等角色在重啟后保持見 tm_init.go--init-timeoutinit 階段超時默認(rèn) 1 分鐘--init-tags逗號分隔的key:value標(biāo)簽列表寫入 tablet 記錄BuildTabletFromInputtm_init.go會基于這些參數(shù)構(gòu)造初始 tablet 記錄checkMysql還會把探測到的 MySQL 地址/端口合并進(jìn)記錄tm_init.go。設(shè)計文檔還提出一個可選項(xiàng)如果 tablet 類型是 primary可以在確認(rèn)自己是主庫之前強(qiáng)制與 shard 記錄做一次同步force a sync。源碼中的checkPrimaryShiptm_init.go正是這類邏輯——它會讀取 shard 記錄中的PrimaryAlias與PrimaryTermStartTime與已有 tablet 記錄交叉比對shard 記錄指向本 tablet 且舊記錄也同意 → 帶上舊記錄的PrimaryTermStartTime恢復(fù)為 PRIMARYshard 記錄指向本 tablet 但舊記錄不是 primary → 采用 shard 記錄的PrimaryTermStartTime升主舊記錄是 primary 但 shard 不認(rèn) → 僅當(dāng)本 tablet 的PrimaryTermStartTime更新時才接管否則保持 replica。這套啟動期邏輯確?!钢鲙焐矸荨乖跇O端重啟場景下如舊主先重啟、新主還在接管中不會產(chǎn)生雙主或丟主。新模型的收益設(shè)計文檔總結(jié)了四個層面的收益vttablet 成為 tablet 記錄的權(quán)威所有者不再需要持續(xù)輪詢記錄也無需處理記錄中可能出現(xiàn)的意外或非法變更復(fù)雜度大幅下降自由覆蓋本地記錄既然假設(shè)沒有別人會改這條記錄vttablet 就可以放心地用本地副本整體覆蓋 tablet 記錄而無需做字段級合并或同步協(xié)商源碼中proto.Resetproto.Merge的整記錄覆蓋正是這一假設(shè)的體現(xiàn)流程從兩步變一步大量流程從「改記錄 刷新」兩段式變成「單次 RPC」調(diào)用鏈變短、失敗面變窄減輕 topo 負(fù)載tablet 不再輪詢topo 的讀壓力顯著下降。過渡路徑Transition當(dāng)前倉庫的落地現(xiàn)狀設(shè)計文檔給出的過渡分兩步第一步把所有調(diào)用點(diǎn)改成對 tablet 的單次往返 RPC此時 tabletmanager 仍然更新 tablet 記錄并繼續(xù)依賴現(xiàn)有的輪詢器poller第二步當(dāng) tabletmanager 已成為其記錄的唯一所有者sole owner后把行為切換為非輪詢non-polling。從當(dāng)前倉庫源碼結(jié)構(gòu)看這套演進(jìn)仍在推進(jìn)中且已經(jīng)能看到大量第一步的痕跡所有狀態(tài)變更ChangeType、SetServingType引發(fā)的狀態(tài)調(diào)整等都經(jīng)由 rpc_actions.go 的 RPC 入口直接驅(qū)動配合actionSema重量級信號量保證同一時刻只有一個動作在執(zhí)行記錄寫入統(tǒng)一收斂到tmState.publishStateLockedretryPublish的「發(fā)布 重試」模式同時shard_sync.go 中仍然存在shardSyncLoop它維護(hù)對 shard 記錄的 topo watch僅 primary 時監(jiān)聽并通過notifyChan接收 tablet 本地狀態(tài)變化通知保持 tablet 記錄與 shard 記錄如 PrimaryTermStartTime的一致性重試間隔由--shard-sync-retry-delay控制默認(rèn) 30 秒。這說明當(dāng)前實(shí)現(xiàn)正處于「tablet 已自述狀態(tài)、但 shard 級同步與輪詢輔助機(jī)制仍在」的過渡階段——主庫選舉對 topo 的強(qiáng)依賴?yán)庖粵Q定了 shard 記錄的 watch 在短期內(nèi)不可完全移除。小結(jié)TabletManager 模型的這次重構(gòu)本質(zhì)是職責(zé)歸位vttablet 負(fù)責(zé)「我是誰、我處于什么狀態(tài)」topo 中的 tablet 記錄退化為「給 vtgate 等組件做發(fā)現(xiàn)用的緩存視圖」。單次 RPC 往返、盡力而為發(fā)布與后臺重試、啟動時的字段強(qiáng)校驗(yàn)、主庫選舉時 topo 優(yōu)先——這四個支柱構(gòu)成了新模型的完整輪廓。對于想在 Vitess 上做二次開發(fā)或運(yùn)維調(diào)優(yōu)的讀者tm_state.go狀態(tài)發(fā)布與刷新、tm_init.go啟動與初始化和 shard_sync.goshard 記錄同步是理解這套模型最直接的三個入口。【免費(fèi)下載鏈接】vitessVitess is a database clustering system for horizontal scaling of MySQL.項(xiàng)目地址: https://gitcode.com/gh_mirrors/vi/vitess創(chuàng)作聲明:本文部分內(nèi)容由AI輔助生成(AIGC),僅供參考