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