實(shí)戰(zhàn):從副本同步到故障切換)
TDengine 的異地多活方案是我近半年反復(fù)推演和壓測(cè)最多的一類架構(gòu)。起因并不復(fù)雜我們運(yùn)營(yíng)的設(shè)備監(jiān)控平臺(tái)把幾十萬(wàn)測(cè)點(diǎn)的數(shù)據(jù)全部集中到一個(gè)集群某個(gè)機(jī)房的光纜一斷整個(gè)寫入鏈路就會(huì)立刻報(bào)警業(yè)務(wù)側(cè)只能臨時(shí)降級(jí)而“降級(jí)”這個(gè)詞在運(yùn)維值班溝通里往往意味著半夜加班。一開(kāi)始我以為“異地多活”就是把 TDengine 的副本數(shù)調(diào)大、把 dnode 分散到兩個(gè)機(jī)房就完事了。真正推演之后才發(fā)現(xiàn)這條路在跨地域場(chǎng)景下幾乎走不通。這篇文章不打算只講概念而是把我們實(shí)際選型、拓?fù)湓O(shè)計(jì)、故障切換和長(zhǎng)期治理的經(jīng)驗(yàn)整理出來(lái)給想用 TDengine 構(gòu)建高可靠的時(shí)序數(shù)據(jù)中心的團(tuán)隊(duì)一個(gè)可落地的參考。1. 為什么一家做設(shè)備監(jiān)控的團(tuán)隊(duì)會(huì)盯上“異地多活”1.1 先回答多活和備份的本質(zhì)差異很多團(tuán)隊(duì)把“多活”和“備份”混在一起這是第一個(gè)要糾正的認(rèn)知偏差。備份是保留一份數(shù)據(jù)副本平時(shí)不對(duì)外服務(wù)故障發(fā)生后再想辦法恢復(fù)多活則是多個(gè)數(shù)據(jù)中心同時(shí)承擔(dān)讀寫任何一個(gè)點(diǎn)故障時(shí)訪問(wèn)路徑可以在分鐘級(jí)甚至秒級(jí)切換到另一個(gè)點(diǎn)業(yè)務(wù)基本無(wú)感。我們的設(shè)備監(jiān)控平臺(tái)是典型的時(shí)序場(chǎng)景設(shè)備側(cè)采集器不斷上報(bào)電壓、電流、溫度、運(yùn)行狀態(tài)等遙測(cè)數(shù)據(jù)每秒峰值寫入超過(guò)十萬(wàn)條測(cè)點(diǎn)。這些數(shù)據(jù)有三個(gè)明顯特點(diǎn)寫入不能隨便暫停、時(shí)間軸連續(xù)不能斷、業(yè)務(wù)對(duì)缺失窗口的容忍上限通常只有幾分鐘。一旦單機(jī)房停電或者出口光纜中斷采集器還在繼續(xù)產(chǎn)生數(shù)據(jù)本地緩存寫滿之后就開(kāi)始丟點(diǎn)而后臺(tái)的分析報(bào)表、實(shí)時(shí)大屏、報(bào)警規(guī)則全都依賴這條時(shí)序列。備份工具恢復(fù)得再快也沒(méi)法解決“寫入入口已經(jīng)不可用”的問(wèn)題因?yàn)閺墓收习l(fā)生到備份拉起中間的歷史窗口已經(jīng)無(wú)法補(bǔ)救。所以我們?cè)O(shè)計(jì)的目標(biāo)不是“把數(shù)據(jù)復(fù)制一份”而是讓讀寫兩端在任何單點(diǎn)故障下都能繼續(xù)工作。RTO按分鐘算RPO按秒到分鐘算而不是把希望寄托在幾小時(shí)后的手工恢復(fù)上。1.2 什么時(shí)候該上異地多活什么時(shí)候不該硬上不是所有場(chǎng)景都適合異地多活。時(shí)序數(shù)據(jù)的寫入天然是分布式追加模式每一條記錄幾乎都是獨(dú)立事件沒(méi)有跨行事務(wù)、沒(méi)有復(fù)雜的并發(fā)修改這決定了它比關(guān)系型數(shù)據(jù)庫(kù)更容易做多活。但如果你的業(yè)務(wù)強(qiáng)依賴“全局唯一的設(shè)備ID”“原子遞增的序列號(hào)”或者“寫入后立即在所有節(jié)點(diǎn)讀到同一條數(shù)據(jù)”那就要謹(jǐn)慎了這類強(qiáng)一致需求不是時(shí)序數(shù)據(jù)庫(kù)的設(shè)計(jì)重點(diǎn)。我的建議是給數(shù)據(jù)分級(jí)。設(shè)備上報(bào)的遙測(cè)指標(biāo)可以接受幾秒甚至幾十秒的滯后適合走異步同步但控制指令、配置下發(fā)的狀態(tài)記錄直接影響設(shè)備動(dòng)作必須保證不丟不重。分級(jí)之后核心少量表走強(qiáng)一致或者應(yīng)用層特殊處理海量遙測(cè)表走異步同步整體成本會(huì)低很多。千萬(wàn)別把“所有時(shí)序數(shù)據(jù)一視同仁”當(dāng)成省事結(jié)果就是該保的沒(méi)保住該省的沒(méi)省下來(lái)。2. TDengine 自身的冗余機(jī)制replica、選主和多數(shù)派到底怎么工作2.1 從 dnode 到 vnode再到復(fù)制組想設(shè)計(jì)異地多活先得理解 TDengine 集群的冗余單位。一個(gè)集群由若干 dnode 組成dnode 就是跑 taosd 的物理節(jié)點(diǎn)或虛機(jī)集群里還有 mnode 負(fù)責(zé)管理元數(shù)據(jù)、用戶權(quán)限和庫(kù)表結(jié)構(gòu)mnode 本身也是一個(gè)多副本的 Raft 組。業(yè)務(wù)數(shù)據(jù)寫入時(shí)會(huì)按時(shí)間窗口拆分成多個(gè) vnode每個(gè) vnode 可以理解為一段數(shù)據(jù)集的獨(dú)立復(fù)制單元它的多個(gè)副本分散在不同的 dnode 上副本數(shù)量由建庫(kù)時(shí)的 replica 參數(shù)決定。replica1 表示單副本只解決存儲(chǔ)不解決高可用replica3 是最常見(jiàn)的集群高可用配置。注意這里的高可用指的是“單臺(tái)或兩臺(tái)機(jī)器故障時(shí)數(shù)據(jù)不丟、服務(wù)不中斷”它默認(rèn)的網(wǎng)絡(luò)環(huán)境是一個(gè)低延遲、穩(wěn)定的數(shù)據(jù)中心內(nèi)部網(wǎng)絡(luò)。一旦把這三個(gè)副本拆到不同城市、不同機(jī)房問(wèn)題就開(kāi)始變得復(fù)雜了因?yàn)槊總€(gè) vnode 內(nèi)的數(shù)據(jù)同步都要跨物理距離。2.2 一次寫入的完整握手為什么跨地域后就慢了TDengine 的寫入路徑大概是這樣的客戶端把數(shù)據(jù)發(fā)給某個(gè) vnode 的 leaderleader 先寫自己的 WAL然后把日志同步給同組的 follower得到多數(shù)派確認(rèn)后才給客戶端返回成功。這個(gè)過(guò)程不是“發(fā)出去就完事”而是要等網(wǎng)絡(luò)來(lái)回確認(rèn)。在同一機(jī)房的局域網(wǎng)里這個(gè)往返可能不到 1 毫秒你的寫入幾乎感受不到副本同步的開(kāi)銷。可一旦跨城市專線 RTT 也許就有 10 到 30 毫秒如果走公網(wǎng)或者鏈路不穩(wěn)定可能就是 50 毫秒往上。每次寫入都額外付出這么一段等待時(shí)間寫入吞吐量就會(huì)斷崖式下降。可以想象一個(gè)場(chǎng)景三個(gè)秘書(shū)在不同城市共同維護(hù)一份會(huì)議紀(jì)要寫一條記錄之前必須獲得至少兩個(gè)人的確認(rèn)而每一次確認(rèn)都要打一個(gè)跨城電話。大量記錄并行處理時(shí)通訊線路就是最大的瓶頸。這就是為什么“同一集群、replica3、節(jié)點(diǎn)橫跨兩地”在很多異地場(chǎng)景下跑不出理想性能。2.3 多數(shù)派陷阱三副本兩地分布不等于高可用這里有一個(gè)特別容易踩的坑。假設(shè)你在 A 機(jī)房放兩個(gè) dnodeB 機(jī)房放一個(gè) dnode整個(gè)集群三副本??雌饋?lái)“兩個(gè)機(jī)房都有副本應(yīng)該很安全”但 Raft 的多數(shù)派機(jī)制決定了如果 A 機(jī)房整體故障兩個(gè) A 機(jī)房的副本同時(shí)失聯(lián)剩下的那個(gè)副本無(wú)法湊齊多數(shù)派整個(gè)庫(kù)會(huì)拒絕寫入。同樣的問(wèn)題也出現(xiàn)在讀路徑上leader 選舉需要多數(shù)派客戶端訪問(wèn)也需要大多數(shù)節(jié)點(diǎn)正常。所以在跨機(jī)房部署單一集群時(shí)我更推薦“三個(gè)機(jī)房各一個(gè)副本”這種三角形分布任何一個(gè)機(jī)房故障另外兩個(gè)機(jī)房的副本仍然構(gòu)成多數(shù)派服務(wù)可以繼續(xù)。但這樣一來(lái)網(wǎng)絡(luò)鏈路的數(shù)量、鏈路成本、運(yùn)維復(fù)雜度都會(huì)上升本質(zhì)上是在用基礎(chǔ)設(shè)施復(fù)雜度換高可用。3. 三條跨地域數(shù)據(jù)流同步副本、異步同步、雙寫之間的取舍3.1 先給結(jié)論沒(méi)有銀彈只有分層異地多活沒(méi)有一種配置適合所有數(shù)據(jù)。我們最后采用的思路是按數(shù)據(jù)重要性和訪問(wèn)模式把數(shù)據(jù)流拆成三類分別走不同路線。這樣既控制了成本也把真正核心的數(shù)據(jù)放到了最高的可靠性保障上。先看一張對(duì)比表再展開(kāi)講每條路線的適用邊界。維度路線A同一集群跨地域多副本路線B兩套集群異步同步路線C應(yīng)用層雙寫一致性強(qiáng)一致Raft 多數(shù)派確認(rèn)最終一致滯后秒級(jí)到分鐘級(jí)取決于實(shí)現(xiàn)通常最終一致寫入性能受跨地域 RTT 影響明顯寫入各自集群互不阻塞兩個(gè)集群各寫一份應(yīng)用開(kāi)銷大單點(diǎn)故障容忍需節(jié)點(diǎn)分布滿足多數(shù)派任一集群故障仍可服務(wù)任一集群故障仍可服務(wù)回切復(fù)雜度低自動(dòng)選主中需設(shè)計(jì)反向同步高需業(yè)務(wù)層切換和補(bǔ)償運(yùn)維復(fù)雜度低但網(wǎng)絡(luò)要求苛刻中需要維護(hù)同步鏈路高需要冪等機(jī)制適用距離同城/專線低延遲異地、跨地域小數(shù)據(jù)量關(guān)鍵表3.2 同步副本和獨(dú)立集群的根本沖突在“一致性和可用性”路線A的優(yōu)勢(shì)是簡(jiǎn)單數(shù)據(jù)天然就是一份不存在兩套集群之間的差異。只要網(wǎng)絡(luò)足夠好比如同城雙中心、專線延遲穩(wěn)定在 5 毫秒以內(nèi)完全可以把 TDengine 三副本拆到兩個(gè)或三個(gè)機(jī)房利用 Raft 自動(dòng)實(shí)現(xiàn)故障切換。它的短板也恰恰來(lái)自強(qiáng)一致網(wǎng)絡(luò)抖動(dòng)會(huì)直接影響寫入成功率跨城市的長(zhǎng)鏈路帶來(lái)的不確定延遲會(huì)放大每一次寫放大。路線B是我們主推的異地方案。每個(gè)區(qū)域部署一套獨(dú)立集群設(shè)備只寫入本區(qū)域集群然后把增量數(shù)據(jù)異步同步到中心或另一個(gè)集群。這樣做的好處是區(qū)域內(nèi)的寫入完全不受遠(yuǎn)端網(wǎng)絡(luò)影響故障時(shí)本地采集鏈路照樣工作壞處是兩套集群之間有數(shù)據(jù)滯后故障切換時(shí)需要容忍一小段臟數(shù)據(jù)或數(shù)據(jù)空洞回切時(shí)還要處理“舊集群里新寫入的數(shù)據(jù)”和“新集群里已積累的數(shù)據(jù)”如何合并。異步同步的具體實(shí)現(xiàn)可以用官方提供的數(shù)據(jù)同步工具也可以自己寫增量拉取任務(wù)。數(shù)據(jù)方向一般是從生產(chǎn)集群同步到中心查詢集群或者核心生產(chǎn)集群同步到災(zāi)備集群。同步粒度通常按數(shù)據(jù)庫(kù)或者按表來(lái)控制沒(méi)必要把所有歷史冷數(shù)據(jù)都實(shí)時(shí)同步過(guò)去。3.3 雙寫需要注意冪等和亂序路線C聽(tīng)起來(lái)最直接業(yè)務(wù)代碼把同一條數(shù)據(jù)同時(shí)寫入兩套集群。但實(shí)際做起來(lái)最折磨人。首先應(yīng)用側(cè)寫入成本翻倍其次兩條鏈路到達(dá)目的集群的順序可能不一致如果場(chǎng)景里存在“同一時(shí)間戳、同一設(shè)備”的多條記錄后寫入的不一定是最新?tīng)顟B(tài)就需要在目標(biāo)端設(shè)計(jì)冪等或者版本策略。我們的經(jīng)驗(yàn)是雙寫只用于少數(shù)關(guān)鍵表比如設(shè)備啟停記錄、告警確認(rèn)狀態(tài)這類數(shù)據(jù)量小但價(jià)值高的場(chǎng)景海量遙測(cè)數(shù)據(jù)走路線B的異步同步既不壓垮應(yīng)用也不會(huì)因?yàn)橥铰枞诵逆溌贰?. 從“雙集群”到“可切換”一份拓?fù)湓O(shè)計(jì)與配置示例4.1 一套多寫匯聚拓?fù)湮覀兟涞貢r(shí)的真實(shí)環(huán)境是三個(gè)物理位置兩個(gè)生產(chǎn)園區(qū) A、B一個(gè)中心機(jī)房 C。A 和 B 分別部署一套獨(dú)立的 TDengine 集群負(fù)責(zé)承載本園區(qū)設(shè)備的所有寫入中心機(jī)房 C 部署一套匯聚集群通過(guò)異步同步把 A、B 兩套集群的核心庫(kù)表匯聚到一起供數(shù)據(jù)分析和統(tǒng)一看板查詢。這套拓?fù)涞摹岸嗷睢斌w現(xiàn)在兩個(gè)層面寫入層面A 和 B 各自獨(dú)立存活一個(gè)人掛了另一個(gè)不受影響查詢層面中心集群掛了各園區(qū)的本地集群仍然可以繼續(xù)支撐本園區(qū)的實(shí)時(shí)查詢。故障切換時(shí)我們只需要切換接入域名或配置漂移不需要遷移任何歷史數(shù)據(jù)。4.2 庫(kù)表設(shè)計(jì)與同步實(shí)現(xiàn)每個(gè)集群的庫(kù)表結(jié)構(gòu)必須保持完全一致否則同步任務(wù)會(huì)持續(xù)報(bào)錯(cuò)。我們?cè)诎l(fā)布流程里統(tǒng)一管理建庫(kù)建表腳本保證幾個(gè)集群同時(shí)更新 schema。建庫(kù)示例大概是這樣的-- 每個(gè)園區(qū)生產(chǎn)集群 CREATE DATABASE edge_iot DURATION 1d KEEP 365d REPLICA 3 BUFFER 256 WAL_LEVEL 2; -- 中心匯聚集群副本數(shù)可以根據(jù)查詢量調(diào)整 CREATE DATABASE center_iot DURATION 1d KEEP 365d REPLICA 3 BUFFER 256 WAL_LEVEL 2;注意這里 REPLICA3 是單集群內(nèi)的副本數(shù)不是跨集群的同步關(guān)系。跨集群的數(shù)據(jù)復(fù)制走的是同步任務(wù)而不是讓兩套集群組成一個(gè)大集群。同步任務(wù)的邏輯看起來(lái)非常簡(jiǎn)單但細(xì)節(jié)都在容錯(cuò)上。我們用偽代碼描述核心思路# 從源集群增量拉取數(shù)據(jù)寫入目標(biāo)集群 last_ts query_target_last_ts(source_table_name) while True: rows fetch_from_source( startlast_ts, tablesource_table_name, batch1000 ) if not rows: sleep(5) continue write_to_target( target_sql, rows, upsert_strategylast_row_win ) last_ts max(row[ts] for row in rows)這里最關(guān)鍵的參數(shù)是last_ts。它不是一個(gè)簡(jiǎn)單的時(shí)間戳而是要精確記錄“源端已經(jīng)拉到哪一條”并且要定時(shí)持久化否則同步任務(wù)一旦重啟就可能從頭開(kāi)始或跳過(guò)一段數(shù)據(jù)造成空洞或重復(fù)。每個(gè)目標(biāo)表建議加上一個(gè)標(biāo)識(shí)來(lái)源機(jī)房的 tag比如site這樣中心集群在查詢時(shí)可以按來(lái)源域過(guò)濾。因?yàn)闀r(shí)序數(shù)據(jù)的主鍵本質(zhì)是時(shí)間戳加設(shè)備標(biāo)識(shí)不同園區(qū)如果存在相同的設(shè)備編號(hào)不加來(lái)源 tag 就會(huì)互相覆蓋這是一個(gè)我們?cè)谠缙诓冗^(guò)的坑。4.3 流量切換入口盡量別改業(yè)務(wù)代碼流量切換最怕的是讓每一臺(tái)采集器去改配置。我們的做法是所有采集器的寫入地址統(tǒng)一指向一個(gè)內(nèi)部域名域名通過(guò)負(fù)載均衡解析到 A 或者 B 的集群中心查詢統(tǒng)一指向中心域名中心域名后面掛多個(gè)只讀地址。故障發(fā)生時(shí)運(yùn)維只需要在負(fù)載均衡或 DNS 層把域名切到可用集群。應(yīng)用代碼不需要感知后端是誰(shuí)連接池也會(huì)在短暫的失敗重連后自動(dòng)恢復(fù)。為了讓這個(gè)切換更平滑客戶端配置里可以一次配置多個(gè)節(jié)點(diǎn)地址TDengine 原生驅(qū)動(dòng)會(huì)在第一個(gè)地址不可用時(shí)嘗試后面的節(jié)點(diǎn)。5. 真正的考驗(yàn)是故障切換一次跨地域切換的完整復(fù)盤5.1 一次突然的故障演練我們每季度會(huì)做一次切換演練。最近一次模擬的是 A 園區(qū)到中心機(jī)房的專線完全中斷A 園區(qū)集群自身還在正常運(yùn)行但中心查詢集群的同步任務(wù)開(kāi)始積壓。演練時(shí)間線大概是這樣同步時(shí)延監(jiān)控觸發(fā)告警值班人員在 5 分鐘內(nèi)確認(rèn)是專線中斷。A 園區(qū)采集鏈路仍然正常本地集群繼續(xù)寫入業(yè)務(wù)無(wú)感。中心集群由于缺少 A 園區(qū)數(shù)據(jù)大屏和報(bào)表出現(xiàn)局部缺口自動(dòng)切換規(guī)則觸發(fā)后中心域名的查詢流量被切到 A 園區(qū)的本地查詢?nèi)肟?。同步任?wù)進(jìn)入“斷點(diǎn)續(xù)傳”模式每 5 秒探測(cè)一次鏈路鏈路恢復(fù)后自動(dòng)追平。追平后我們把中心查詢域名切回中心集群并抽查最近 5 分鐘數(shù)據(jù)的延遲。這個(gè)過(guò)程聽(tīng)起來(lái)順利但真正執(zhí)行時(shí)出現(xiàn)過(guò)好幾個(gè)問(wèn)題。5.2 切換后真正卡殼的三個(gè)技術(shù)問(wèn)題第一個(gè)問(wèn)題是連接池緩存。我們的應(yīng)用服務(wù)啟動(dòng)時(shí)會(huì)從配置中心拉取 TDengine 地址列表但連接池一旦建立不會(huì)感知域名切換后的新地址。那次演練中雖然域名解析已經(jīng)切到 A 園區(qū)但部分長(zhǎng)期存活的應(yīng)用進(jìn)程仍然持著舊連接等舊連接超時(shí)后才緩慢恢復(fù)。解決辦法不復(fù)雜在應(yīng)用側(cè)設(shè)置連接池的失敗重連周期并且不要用商用的 VIP 而用支持多地址的客戶端配置讓驅(qū)動(dòng)自動(dòng)嘗試備選節(jié)點(diǎn)。第二個(gè)問(wèn)題是設(shè)備離線后補(bǔ)傳。一些老設(shè)備在專線中斷期間把數(shù)據(jù)緩存在本地恢復(fù)后一次性補(bǔ)傳時(shí)間戳可能往前回填。如果目標(biāo)端以同一時(shí)間戳的新數(shù)據(jù)覆蓋舊數(shù)據(jù)就可能把原本正確的記錄給覆蓋掉。后來(lái)我們?cè)趯懭霑r(shí)做了策略區(qū)分實(shí)時(shí)寫入的遙測(cè)數(shù)據(jù)不管順序后到覆蓋先到但狀態(tài)類、事件類數(shù)據(jù)單獨(dú)走雙寫用業(yè)務(wù)側(cè)維護(hù)的序列號(hào)做最終一致。第三個(gè)問(wèn)題是報(bào)警規(guī)則的 label 漂移。中心集群的報(bào)警規(guī)則是基于中心數(shù)據(jù)庫(kù)里的表結(jié)構(gòu)配置的切換查詢?cè)吹?A 園區(qū)后報(bào)警上下文里的 site 字段含義變了導(dǎo)致一些告警誤報(bào)。這個(gè)問(wèn)題的根源是元數(shù)據(jù)管理沒(méi)跟上拓?fù)湓O(shè)計(jì)現(xiàn)在我們會(huì)在每個(gè) tag 里明確標(biāo)注來(lái)源機(jī)和路由路徑切換時(shí)規(guī)則自動(dòng)帶上 site 過(guò)濾條件。5.3 回切比切換更折磨人故障恢復(fù)后的回切往往比故障切換更容易出錯(cuò)。鏈路剛恢復(fù)時(shí)不能立刻把流量整體切回因?yàn)橹行募旱臄?shù)據(jù)還差一個(gè)時(shí)間窗此時(shí)切回來(lái)會(huì)讓大屏看到一段“過(guò)去”。穩(wěn)妥的做法是讓同步任務(wù)先追平確認(rèn)中心集群的 last_ts 已經(jīng)接近 A 園區(qū)集群再開(kāi)啟只讀校驗(yàn)最后再把寫流量或者查詢流量逐步切回。回切時(shí)還要處理一個(gè)容易忽視的問(wèn)題A 園區(qū)集群在中斷期間積累的本地?cái)?shù)據(jù)和中心集群里已同步的舊數(shù)據(jù)可能存在交叉窗口。我們通常以源端為準(zhǔn)把交叉窗口的數(shù)據(jù)做一次全量覆蓋回放保證中心集群的最終狀態(tài)和 A 園區(qū)一致。如果這個(gè)窗口恰好有幾百萬(wàn)條記錄同步回放的時(shí)間要充分評(píng)估否則原以為半小時(shí)能完成的切換最后拖了幾個(gè)小時(shí)。6. 長(zhǎng)期治理把多活從“能看”升級(jí)到“能扛”6.1 六個(gè)比進(jìn)程監(jiān)控更重要的指標(biāo)異地多活做得怎么樣不是看集群進(jìn)程活著沒(méi)有而是看數(shù)據(jù)流是否可持續(xù)。我們?nèi)粘6⒘鶄€(gè)指標(biāo)每個(gè)指標(biāo)都對(duì)應(yīng)一個(gè)具體故障模式。指標(biāo)含義建議監(jiān)控方式同步時(shí)延目標(biāo)集群最新時(shí)間戳與源集群的差值超過(guò) 30 秒告警斷點(diǎn)位置差值同步任務(wù)持久化的 last_ts 與實(shí)際源端差距持續(xù)增長(zhǎng)說(shuō)明鏈路卡死寫入失敗率各集群的寫入拒絕率和超時(shí)率超過(guò)千分之一關(guān)注重新選舉次數(shù)復(fù)制組 leader 切換次數(shù)突增往往意味著網(wǎng)絡(luò)抖動(dòng)vnode 副本狀態(tài)副本數(shù)是否長(zhǎng)期小于預(yù)期小于預(yù)期意味著冗余失效最后寫入時(shí)間目標(biāo)表級(jí)別最近一次成功寫入避免“表級(jí)靜默斷流”其中最后寫入時(shí)間尤其值得關(guān)注。有次我們只看集群整體寫入量完全正常但其實(shí)某張核心表的同步任務(wù)早就停了寫入量被其他大表掩蓋。加了表級(jí)最后寫入時(shí)間監(jiān)控之后這類問(wèn)題當(dāng)天就能暴露。6.2 數(shù)據(jù)對(duì)賬與斷點(diǎn)續(xù)傳異步同步做得再精細(xì)也難免出現(xiàn)偶發(fā)丟數(shù)據(jù)。我們每天凌晨會(huì)跑一次對(duì)賬任務(wù)隨機(jī)抽取若干表比較源端和目標(biāo)端的 last_ts 以及近 24 小時(shí)行數(shù)。偏差超過(guò)閾值就把對(duì)應(yīng)表加入回補(bǔ)隊(duì)列用查詢接口拉取缺失區(qū)間重新寫入。斷點(diǎn)續(xù)傳機(jī)制不能只依賴“同步任務(wù)重啟后從頭掃”。我們每個(gè)同步任務(wù)的斷點(diǎn)狀態(tài)都放在一個(gè)獨(dú)立的狀態(tài)表里而且用 TDengine 自己來(lái)存這樣同步任務(wù)既處理業(yè)務(wù)數(shù)據(jù)也把自身狀態(tài)當(dāng)成時(shí)序數(shù)據(jù)管理。任務(wù)重啟后先從狀態(tài)表里讀最近提交的 last_ts再往上覆蓋一段 10 分鐘的冗余窗口寧可重復(fù)寫一點(diǎn)也不能留空洞。6.3 讓切換變成一件乏味的事異地多活最怕的不是技術(shù)復(fù)雜而是平時(shí)沒(méi)人敢碰切換按鈕。我們第一年只做桌面推演第二年做季度演練第三年把切換流程寫成了半自動(dòng)腳本值班人員只需要在確認(rèn)故障范圍后點(diǎn)一個(gè)“確認(rèn)切換”。這一步特別重要。因?yàn)檎嬲l(fā)生故障時(shí)現(xiàn)場(chǎng)往往一片混亂與其讓焦慮的人去翻手冊(cè)不如把決策路徑簡(jiǎn)到最少。我們的腳本會(huì)先做健康檢查判斷各集群存活狀態(tài)和同步時(shí)延再根據(jù)預(yù)設(shè)策略選擇切換目標(biāo)最后執(zhí)行流量切換并驗(yàn)證新集群的寫入和查詢。給準(zhǔn)備做異地多活的團(tuán)隊(duì)一個(gè)建議不要一上來(lái)就追求“雙活”這個(gè)概念先定義清楚故障發(fā)生后的第一分鐘要做什么、第十分鐘要做什么、第一天要做什么。把這幾分鐘內(nèi)的動(dòng)作做成腳本每季度演練一次剩下的工作都是為了讓我那句話變得可信切換已經(jīng)不是新鮮事而是整個(gè)時(shí)序數(shù)據(jù)中心最普通的例行操作。