戰(zhàn):單元化、灰度切流與數(shù)據(jù)收斂)
做過兩輪企業(yè)級(jí)云服務(wù)平臺(tái)的異地多活改造之后我越來越確信一件事真正難的不是在 Kubernetes 上多起幾個(gè)集群而是想清楚哪個(gè)業(yè)務(wù)單元可以切、切過去之后數(shù)據(jù)怎么收斂、切回來的時(shí)候會(huì)不會(huì)把線上打穿。異地多活這四個(gè)字寫進(jìn)方案里只有半頁 PPT落到工程上卻是一整套關(guān)于容量、數(shù)據(jù)、流量和運(yùn)維節(jié)奏的賬本。這篇文章就把這套賬本攤開來講從形態(tài)選型、分層架構(gòu)、單元化改造到一次真實(shí)的灰度切流、校驗(yàn)?zāi)_本、回滾預(yù)案再到那些只有踩過才知道的坑。不論你是剛開始接觸多集群的運(yùn)維同學(xué)還是正在被“兩地三中心”方案評(píng)審折磨的架構(gòu)負(fù)責(zé)人應(yīng)該都能從里面拿到能直接抄的東西。我不會(huì)堆概念會(huì)把每個(gè)關(guān)鍵參數(shù)怎么算、每個(gè)配置為什么這么寫盡量講透。1. 異地多活的目標(biāo)形態(tài)與選型推演1.1 冷備、雙活、多活三種形態(tài)的賬要對(duì)得上很多團(tuán)隊(duì)一開始就用“兩地三中心”這個(gè)說法但落到實(shí)現(xiàn)上有三種完全不同的東西成本和效果差了一個(gè)數(shù)量級(jí)。我在方案評(píng)審會(huì)上最常問的一句話是你要的到底是“掛了能起來”還是“掛了不掉線”這兩件事的投入差著好幾倍。我把常見形態(tài)按 RTO恢復(fù)時(shí)間目標(biāo)、RPO恢復(fù)點(diǎn)目標(biāo)、成本和復(fù)雜度做了個(gè)對(duì)照這張表基本可以直接拿去當(dāng)評(píng)審材料的第一頁形態(tài)典型 RTO典型 RPO額外資源成本工程復(fù)雜度適用業(yè)務(wù)冷備備份重建小時(shí)級(jí)分鐘到小時(shí)級(jí)約 10%~20%低內(nèi)部工具、離線任務(wù)、報(bào)表熱備主備切換分鐘級(jí)秒級(jí)約 50%~70%中中等重要業(yè)務(wù)、非核心交易同城雙活秒級(jí)接近 0約 100%中高核心業(yè)務(wù)的主流做法異地雙活/多活秒級(jí)到分鐘級(jí)秒級(jí)150%~200%高面向全國的 C 端核心鏈路看清楚這張表之后會(huì)發(fā)現(xiàn)一個(gè)殘酷的事實(shí)異地多活的資源成本不是“多買一臺(tái)機(jī)器”而是“每個(gè)站點(diǎn)都要能承接全量”。也就是說如果你在異地建了兩個(gè)站點(diǎn)各自平時(shí)跑 50% 流量那么每個(gè)站點(diǎn)都得按 100% 的峰值容量去設(shè)計(jì)否則一個(gè)站點(diǎn)掛了另一個(gè)站點(diǎn)立刻雪崩。這筆錢必須在立項(xiàng)時(shí)就說清楚不然做到一半被砍預(yù)算架構(gòu)會(huì)被迫降級(jí)成四不像。我的建議是分層實(shí)施核心交易類業(yè)務(wù)做同城雙活 異地只讀/降級(jí)容災(zāi)非核心業(yè)務(wù)做異步異地多活內(nèi)部系統(tǒng)老老實(shí)實(shí)冷備就行。不要指望一套架構(gòu)吃下所有業(yè)務(wù)那是自找麻煩。1.2 為什么把 Kubernetes 作為多活的底座選 Kubernetes 做底座原因不是它時(shí)髦而是它提供了一套“聲明式期望狀態(tài)”的機(jī)制。多活場(chǎng)景里最怕的是什么是同一個(gè)應(yīng)用在兩個(gè)站點(diǎn)跑出了不同的版本、不同的副本數(shù)、不同的配置。K8s 的聲明式模型讓“站點(diǎn) A 和站點(diǎn) B 的應(yīng)用形態(tài)一致”這件事變成了可校驗(yàn)的而不是靠人肉比對(duì)。但必須說清楚 K8s 在這個(gè)場(chǎng)景里能干什么、不能干什么這一點(diǎn)很多方案書寫得含糊它能干的副本數(shù)保障、滾動(dòng)升級(jí)、Pod 調(diào)度約束把負(fù)載打散到不同故障域、健康探針、資源配額、服務(wù)發(fā)現(xiàn)的基礎(chǔ)能力。它不能干的跨集群的服務(wù)發(fā)現(xiàn)原生不支持需要額外組件或自行封裝、跨集群一致性調(diào)度不解決、數(shù)據(jù)層完全不管、跨站點(diǎn)的全局流量調(diào)度也管不了。所以我的架構(gòu)原則是K8s 只管單集群內(nèi)的高可用跨站點(diǎn)的編排能力必須單獨(dú)做一層。這一層可以是多集群管理平臺(tái)也可以是最土的“一套 GitOps 倉庫管所有集群”關(guān)鍵是別指望一個(gè) K8s 原語解決跨站點(diǎn)問題。另外一個(gè)容易被忽略的點(diǎn)多集群之后控制面本身的運(yùn)維成本是非線性增長的。三個(gè)集群、五個(gè)集群還能靠腳本到了十幾個(gè)集群很多企業(yè)按業(yè)務(wù)域拆集群就必須有統(tǒng)一納管否則每次升級(jí)都是噩夢(mèng)。我在項(xiàng)目里踩過這個(gè)坑后面會(huì)講。1.3 從 RTO 和 RPO 倒推數(shù)據(jù)層分級(jí)數(shù)據(jù)層是異地多活里最貴的部分也是最容易出問題的地方。我的做法是先按業(yè)務(wù)域做分級(jí)再倒推技術(shù)方案而不是先選技術(shù)再套業(yè)務(wù)。大致分三檔強(qiáng)一致核心域賬戶余額、庫存扣減、訂單狀態(tài)機(jī)。這類數(shù)據(jù)跨站點(diǎn)同步只能接受秒級(jí)延遲且寫入必須收斂到單一主副本跨站點(diǎn)做雙寫幾乎必然出問題。最終一致業(yè)務(wù)域消息、通知、日志、積分流水。可以用異步同步加業(yè)務(wù)側(cè)補(bǔ)償容忍分鐘級(jí)延遲??芍亟ㄓ蚓彺妗⑺阉魉饕?、推薦特征。這類數(shù)據(jù)根本不參與同步故障時(shí)直接從源數(shù)據(jù)重建重建時(shí)間要算進(jìn) RTO。分級(jí)之后你會(huì)發(fā)現(xiàn)一個(gè)很有用的結(jié)論真正需要“異地多活”的數(shù)據(jù)其實(shí)很少大部分?jǐn)?shù)據(jù)是最終一致的剩下的是可重建的。把范圍收窄方案立刻變得可實(shí)現(xiàn)。2. 分層拆解流量、應(yīng)用、數(shù)據(jù)三條主線2.1 流量入口層DNS 只是兜底別當(dāng)主力流量層最常見的錯(cuò)誤設(shè)計(jì)是把所有切換壓力壓在 DNS 上。DNS 有 TTL 緩存很多運(yùn)營商和地方解析服務(wù)器不遵守 TTL你改了解析可能幾十分鐘甚至幾小時(shí)還有流量打到舊地址上。拿著這個(gè)做秒級(jí)切換是不現(xiàn)實(shí)的。我的分層設(shè)計(jì)是這樣的第一層全局流量調(diào)度基于 Anycast 或?qū)>€接入的入口 IP按健康狀態(tài)和就近原則分流。這一層的切換是秒級(jí)的。第二層應(yīng)用網(wǎng)關(guān)路由請(qǐng)求到了網(wǎng)關(guān)之后根據(jù)用戶身份做單元路由決定這一筆請(qǐng)求應(yīng)該落在哪個(gè)單元。這一層是真正決定數(shù)據(jù)一致性的關(guān)鍵。第三層DNS 兜底只在整體站點(diǎn)級(jí)故障時(shí)使用TTL 設(shè)得比較短比如 60 秒但要有心理準(zhǔn)備它不會(huì)立刻生效。第二層的單元路由是核心。具體做法是從用戶標(biāo)識(shí)用戶 ID、設(shè)備 ID、租戶 ID里取一個(gè)穩(wěn)定的哈希值按分片數(shù)取?;蛘哂靡恢滦怨B涞焦潭ǖ膯卧?。這一步的規(guī)則必須保證同一個(gè)用戶永遠(yuǎn)路由到同一個(gè)單元否則數(shù)據(jù)會(huì)出現(xiàn)跨單元寫沖突。這個(gè)映射表要落庫、要能動(dòng)態(tài)調(diào)整、要有版本號(hào)并且每個(gè)單元本地都要緩存一份副本避免全局路由表成為新的單點(diǎn)。注意分片數(shù)一旦定了后續(xù)擴(kuò)容會(huì)很痛苦。我的經(jīng)驗(yàn)是初始就按未來三年的量規(guī)劃寧可分片多一點(diǎn)比如 64 或 128 片讓一個(gè)單元承載多個(gè)分片也不要一開始就按 8 片、16 片劃分。2.2 應(yīng)用層多集群編排與跨集群服務(wù)發(fā)現(xiàn)應(yīng)用層要解決的問題是“同一個(gè)服務(wù)在多個(gè)集群里怎么保持形態(tài)一致”。我實(shí)際采用的方案是 GitOps 驅(qū)動(dòng)加統(tǒng)一控制面而不是引入重量級(jí)的聯(lián)邦調(diào)度。具體形態(tài)每個(gè)集群有一個(gè)獨(dú)立的 K8s 控制面保持故障隔離單集群的控制面故障不影響其他站點(diǎn)。所有部署描述文件放在一個(gè)配置倉庫里按“公共基線 站點(diǎn)覆蓋”的方式組織目錄站點(diǎn)的差異只允許出現(xiàn)在覆蓋層禁止在環(huán)境里手工改配置。一個(gè)輕量的同步代理部署在每個(gè)集群里持續(xù)拉取期望狀態(tài)并對(duì)齊拒絕漂移。目錄結(jié)構(gòu)大概是這樣這是我在多個(gè)項(xiàng)目里都沿用的組織方式deploy/ ├── base/ # 全站點(diǎn)共享的基線 │ ├── app-order/ │ └── app-gateway/ └── overlays/ ├── site-a/ │ ├── kustomization.yaml │ └── patch-replicas.yaml ├── site-b/ │ ├── kustomization.yaml │ └── patch-replicas.yaml └── site-c/ └── kustomization.yaml跨集群服務(wù)發(fā)現(xiàn)這塊務(wù)實(shí)一點(diǎn)的做法是“不做通用服務(wù)發(fā)現(xiàn)”。微服務(wù)之間的調(diào)用盡量收斂在單元內(nèi)閉環(huán)只有少數(shù)幾個(gè)明確需要跨站點(diǎn)的服務(wù)比如全局配置中心、統(tǒng)一鑒權(quán)才通過顯式的對(duì)外域名訪問走網(wǎng)關(guān)而不是走服務(wù)網(wǎng)格的跨集群能力。原因很簡單跨集群服務(wù)發(fā)現(xiàn)一旦放開通用的東西向流量網(wǎng)絡(luò)時(shí)延、故障域、限流全都失控出了問題連調(diào)用鏈都梳理不清。心得能用“域名 網(wǎng)關(guān)”解決的跨站點(diǎn)調(diào)用就不要用服務(wù)網(wǎng)格。我見過一個(gè)團(tuán)隊(duì)把東西向流量全部做成跨集群可達(dá)結(jié)果一次跨域網(wǎng)絡(luò)抖動(dòng)導(dǎo)致所有服務(wù)互相超時(shí)重試雪崩得比單站點(diǎn)還徹底。2.3 數(shù)據(jù)層單元內(nèi)寫閉環(huán)與沖突收斂數(shù)據(jù)同步的形態(tài)取決于業(yè)務(wù)特性我總結(jié)出三條鐵律第一條寫入閉環(huán)在單元內(nèi)。每個(gè)單元的數(shù)據(jù)分片只允許本單元寫跨單元只能讀或者異步同步。這條守住了就不會(huì)有寫沖突。代價(jià)是同一份數(shù)據(jù)不能在兩個(gè)單元同時(shí)被寫也就是“多活”其實(shí)是“按分片多活”不是“同一份數(shù)據(jù)到處都能寫”。第二條單向同步優(yōu)于雙向同步。只要業(yè)務(wù)場(chǎng)景允許就做單向復(fù)制主單元寫從單元讀。雙向同步的復(fù)雜度高出一個(gè)臺(tái)階需要處理循環(huán)復(fù)制、時(shí)鐘偏差、更新丟失等問題維護(hù)成本長期來看非常高。第三條沖突處理必須有確定的收斂規(guī)則。如果確實(shí)需要雙向?qū)懕热缒承?qiáng)實(shí)時(shí)場(chǎng)景必須明確沖突解決策略按版本號(hào)大的勝出、按時(shí)間戳后到的勝出還是按業(yè)務(wù)規(guī)則合并。這里有個(gè)巨坑Last-Write-Wins 依賴時(shí)鐘而跨站點(diǎn)時(shí)鐘偏差是常態(tài)。我在生產(chǎn)環(huán)境見過兩個(gè)站點(diǎn)之間幾十毫秒的時(shí)鐘漂移導(dǎo)致數(shù)據(jù)被舊值覆蓋。如果你的同步鏈路依賴時(shí)間戳比較務(wù)必先把時(shí)鐘同步做扎實(shí)并且在比較時(shí)引入邏輯版本號(hào)作為第一優(yōu)先級(jí)。3. 集群搭建與單元化改造的實(shí)操要點(diǎn)3.1 集群規(guī)格與節(jié)點(diǎn)池規(guī)劃參數(shù)怎么算集群規(guī)劃是很多人憑感覺定的地方我給一套自己一直在用的算法。假設(shè)目標(biāo)單個(gè)站點(diǎn)承載 1200 個(gè) Pod業(yè)務(wù)峰值時(shí)每 Pod 的 CPU 請(qǐng)求 2 核、內(nèi)存請(qǐng)求 4Gi系統(tǒng)組件和 DaemonSet 預(yù)留 15% 資源。單節(jié)點(diǎn)選 32 核 128Gi 的規(guī)格那么可調(diào)度 CPU 32 × (1 ? 0.15) ≈ 27 核可調(diào)度內(nèi)存 128 × (1 ? 0.15) ≈ 108Gi按 CPU 算能放 ?27 / 2? 13 個(gè) Pod按內(nèi)存算能放 ?108 / 4? 27 個(gè) Pod取瓶頸 13 個(gè)但這只是理論值還要打散到不同可用區(qū)和故障域?qū)嶋H按 10 個(gè)/節(jié)點(diǎn)規(guī)劃那么 1200 個(gè) Pod 需要 120 個(gè)節(jié)點(diǎn)。再考慮故障冗余如果單站點(diǎn)要能承受一個(gè)可用區(qū)整體故障而集群跨三個(gè)可用區(qū)那么每個(gè)可用區(qū)需要 40 個(gè)節(jié)點(diǎn)承擔(dān)剩余兩個(gè)可用區(qū)要能接住這 40 個(gè)節(jié)點(diǎn)上的負(fù)載即單區(qū)需承載 60 個(gè)節(jié)點(diǎn)的量總量需要 180 個(gè)節(jié)點(diǎn)。這個(gè)數(shù)字明顯比拍腦袋的 120 高 50%但它才是真實(shí)的可用容量。很多方案書里寫的“資源利用率 60%”背后其實(shí)是沒算故障接管后的水位。節(jié)點(diǎn)池我會(huì)按用途拆成三類節(jié)點(diǎn)池用途規(guī)格傾向特殊配置通用池?zé)o狀態(tài)業(yè)務(wù) Pod32C128G1:4 內(nèi)存比開啟自動(dòng)擴(kuò)縮容計(jì)算池?cái)?shù)據(jù)處理、批任務(wù)64C128G高主頻打污點(diǎn)只接特定任務(wù)系統(tǒng)池入口網(wǎng)關(guān)、監(jiān)控、日志16C64G禁止業(yè)務(wù) Pod 調(diào)度系統(tǒng)池單獨(dú)隔離非常重要。我遇到過一次線上事故監(jiān)控組件和業(yè)務(wù) Pod 搶內(nèi)存導(dǎo)致節(jié)點(diǎn) OOM監(jiān)控先掛然后沒人知道業(yè)務(wù)也在掛。系統(tǒng)組件和業(yè)務(wù)必須物理隔離。3.2 單元標(biāo)簽體系與調(diào)度約束落地單元化改造的第一步不是改代碼是給所有資源打標(biāo)簽。我用的標(biāo)簽體系只有四個(gè)核心鍵labels: topology.example.com/site: site-a # 站點(diǎn) topology.example.com/zone: zone-a1 # 可用區(qū) topology.example.com/cell: cell-01 # 單元 topology.example.com/tier: core # 業(yè)務(wù)分級(jí)調(diào)度約束分兩層。第一層是站點(diǎn)親和保證 Pod 只落在自己站點(diǎn)的節(jié)點(diǎn)上第二層是故障域打散保證同一服務(wù)的副本不在同一個(gè)可用區(qū)。apiVersion: apps/v1 kind: Deployment metadata: name: app-order spec: replicas: 24 template: spec: affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: topology.example.com/site operator: In values: [site-a] - key: topology.example.com/cell operator: In values: [cell-01, cell-02] topologySpreadConstraints: - maxSkew: 1 topologyKey: topology.example.com/zone whenUnsatisfiable: DoNotSchedule labelSelector: matchLabels: app: app-order這里有兩個(gè)參數(shù)值得展開說。maxSkew: 1表示任意兩個(gè)可用區(qū)之間的副本數(shù)差不超過 1這是最嚴(yán)格的打散代價(jià)是節(jié)點(diǎn)數(shù)必須夠副本數(shù)也得被可用區(qū)數(shù)整除或其倍數(shù)附近。如果副本數(shù)是 24可用區(qū)是 3 個(gè)那每個(gè)區(qū)就是 8 個(gè)正好。whenUnsatisfiable: DoNotSchedule是硬約束寧可 Pod 掛起也不違反打散這在多活場(chǎng)景里是對(duì)的——寧可少幾個(gè)副本也不要所有副本擠在一個(gè)區(qū)里。同時(shí)務(wù)必配上 PodDisruptionBudget否則節(jié)點(diǎn)維護(hù)時(shí)的驅(qū)逐會(huì)把一個(gè)服務(wù)的副本一次性帶走apiVersion: policy/v1 kind: PodDisruptionBudget metadata: name: app-order-pdb spec: minAvailable: 80% selector: matchLabels: app: app-orderminAvailable: 80%的意思是主動(dòng)驅(qū)逐時(shí)至少保留 80% 的副本可用。這個(gè)值不要設(shè)成 100%否則節(jié)點(diǎn)永遠(yuǎn)排不空運(yùn)維會(huì)瘋。3.3 配置與鏡像的跨域分發(fā)多站點(diǎn)之后鏡像拉取會(huì)成為一個(gè)隱性成本。一個(gè) 800MB 的鏡像三個(gè)站點(diǎn)各拉一次跨域帶寬和拉取時(shí)間都很可觀遇到大規(guī)模擴(kuò)容時(shí)更明顯。我的做法是每個(gè)站點(diǎn)部署本地鏡像倉庫做上游代理緩存首次拉取走跨域后續(xù)命中本地緩存。配置分發(fā)則要堅(jiān)決杜絕“每個(gè)站點(diǎn)手工改 ConfigMap”。我踩過的坑是某次緊急修復(fù)只在主站點(diǎn)改了配置另外兩個(gè)站點(diǎn)忘了同步結(jié)果一個(gè)功能在部分用戶那里表現(xiàn)不一致排查了整整一天。后來我們上了配置版本號(hào)機(jī)制每個(gè) ConfigMap 都帶一個(gè)config-version注解同步代理比對(duì)版本號(hào)不一致就告警。對(duì)于需要按單元取不同值的配置比如單元路由表、分片規(guī)則用一份全局配置加運(yùn)行時(shí)加載的方式而不是每個(gè)站點(diǎn)一份副本。全局配置的變更流程要像代碼一樣走評(píng)審和灰度這一點(diǎn)很多人不當(dāng)回事直到一次配置事故。4. 一次灰度切流的完整實(shí)操鏈路4.1 切流前的準(zhǔn)入檢查清單切流不是點(diǎn)一下按鈕是一個(gè)有前置條件的流程。我固定下來一份檢查清單每次切流前逐項(xiàng)確認(rèn)這個(gè)習(xí)慣救過我好幾次檢查項(xiàng)判斷標(biāo)準(zhǔn)不通過的處置目標(biāo)站點(diǎn)容量水位切流后 CPU 峰值 65%內(nèi)存 70%擴(kuò)容后再切數(shù)據(jù)同步延遲持續(xù) 10 分鐘 P99 500ms排查同步鏈路目標(biāo)站點(diǎn)副本健康全部 Ready無反復(fù)重啟修復(fù)后再切依賴服務(wù)可達(dá)性跨站點(diǎn)依賴調(diào)用成功率 99.9%補(bǔ)鏈路或降級(jí)方案回滾預(yù)案就緒回滾腳本已演練耗時(shí)已實(shí)測(cè)補(bǔ)演練觀測(cè)看板關(guān)鍵指標(biāo)已接入告警已生效補(bǔ)監(jiān)控這份清單里我最看重的是“回滾預(yù)案已實(shí)測(cè)”。很多團(tuán)隊(duì)的回滾預(yù)案寫在文檔里從來沒跑過真出問題時(shí)才發(fā)現(xiàn)回滾腳本依賴的那個(gè)配置項(xiàng)早就改名了。4.2 灰度切流的步驟與觀察指標(biāo)切流按用戶分片灰度而不是按流量比例隨機(jī)。隨機(jī)切流會(huì)導(dǎo)致同一個(gè)用戶的請(qǐng)求在兩個(gè)站點(diǎn)之間跳會(huì)話和數(shù)據(jù)都可能出問題。按分片切一個(gè)分片內(nèi)的用戶是完整的、穩(wěn)定的。我用的節(jié)奏是 1% → 5% → 20% → 50% → 100%每一檔至少觀察 30 分鐘而且必須跨過一次業(yè)務(wù)高峰。這個(gè)“跨高峰”的要求很多人不理解覺得多余。但低谷期切流是自欺欺人真正的容量問題只在高峰暴露。每一檔要盯的指標(biāo)不多但必須盯死錯(cuò)誤率5xx 占比與切流前基線對(duì)比漲幅超過 0.1 個(gè)百分點(diǎn)就暫停。P99 時(shí)延注意區(qū)分“接口時(shí)延”和“端到端時(shí)延”跨站點(diǎn)調(diào)用會(huì)讓 P99 明顯抬升要看抬升是否在預(yù)算內(nèi)。數(shù)據(jù)同步延遲這是最容易出事的指標(biāo)切流后寫入量翻倍同步鏈路容易被打滿。目標(biāo)站點(diǎn)資源水位尤其是連接數(shù)和線程池這兩項(xiàng)擴(kuò)容不敏感。業(yè)務(wù)核心指標(biāo)下單成功率、支付成功率。技術(shù)指標(biāo)全綠但業(yè)務(wù)指標(biāo)掉的場(chǎng)景我遇到過原因是某個(gè)下游服務(wù)按站點(diǎn)做了限流切流后配額不夠。4.3 數(shù)據(jù)校驗(yàn)?zāi)_本與切換后的比對(duì)切流后必須做數(shù)據(jù)比對(duì)這是判斷“真的切成功了”的唯一依據(jù)。我常用三招按成本從低到高第一招總量核對(duì)。比對(duì)源站點(diǎn)和目標(biāo)站點(diǎn)的關(guān)鍵表行數(shù)、關(guān)鍵金額匯總值。便宜但有效能發(fā)現(xiàn)大范圍丟失。第二招抽樣明細(xì)比對(duì)。按用戶 ID 抽樣把兩個(gè)站點(diǎn)的記錄拉出來做字段級(jí) diff。下面這個(gè)腳本是我們實(shí)際在用的簡化版#!/usr/bin/env bash set -euo pipefail SAMPLE_SIZE500 BIZ_DATE$(date -d yesterday %Y-%m-%d) OUT_DIR./diff_out/${BIZ_DATE} mkdir -p ${OUT_DIR} # 從全局抽樣服務(wù)取出待校驗(yàn)的用戶 ID 清單 curl -s http://sample-svc.internal/pick?date${BIZ_DATE}n${SAMPLE_SIZE} \ -o ${OUT_DIR}/ids.txt total0 mismatch0 while read -r uid; do [ -z ${uid} ] continue total$((total 1)) if ! diff (fetch_record site-a ${uid} ${BIZ_DATE}) \ (fetch_record site-b ${uid} ${BIZ_DATE}) /dev/null; then mismatch$((mismatch 1)) echo ${uid} ${OUT_DIR}/mismatch.txt fi done ${OUT_DIR}/ids.txt echo 校驗(yàn)總數(shù): ${total}, 不一致: ${mismatch} # 不一致率超過 0.5% 直接失敗阻斷后續(xù)切流 awk -v m${mismatch} -v t${total} BEGIN { exit (m/t 0.005) ? 1 : 0 }第三招變更捕獲回放比對(duì)。從數(shù)據(jù)變更日志里提取一段時(shí)間窗口的所有寫操作在目標(biāo)站點(diǎn)重放并校驗(yàn)最終狀態(tài)。這個(gè)成本最高只在核心交易鏈路切換時(shí)做。提示校驗(yàn)?zāi)_本里的失敗閾值不要設(shè)成 0??缯军c(diǎn)異步同步天然存在延遲切流瞬間的不一致是正常的關(guān)鍵是看它是否在一個(gè)時(shí)間窗口內(nèi)收斂。我設(shè)的閾值是5 分鐘窗口內(nèi)不一致率降到 0.01% 以下視為收斂正常。5. 常見問題與排查技巧實(shí)錄5.1 跨域網(wǎng)絡(luò)抖動(dòng)與健康檢查誤判這是我遇到最多、也最隱蔽的一類問題??缯军c(diǎn)專線的延遲不是恒定的偶爾會(huì)出現(xiàn)幾十毫秒的抖動(dòng)甚至短時(shí)丟包。如果你的健康檢查探針超時(shí)設(shè)得比較緊比如 1 秒超時(shí)、2 秒間隔一次網(wǎng)絡(luò)抖動(dòng)就可能讓一大批 Pod 同時(shí)變成 NotReady然后被 Service 剔除流量瞬間打到剩余節(jié)點(diǎn)上連鎖反應(yīng)。我的調(diào)參原則是跨站點(diǎn)場(chǎng)景下健康檢查的容忍度要比單站點(diǎn)寬 2 到 3 倍。livenessProbe: httpGet: path: /healthz port: 8080 initialDelaySeconds: 30 periodSeconds: 15 timeoutSeconds: 5 failureThreshold: 5 readinessProbe: httpGet: path: /ready port: 8080 periodSeconds: 10 timeoutSeconds: 3 failureThreshold: 3關(guān)鍵點(diǎn)在于把 liveness 的failureThreshold調(diào)大。liveness 探測(cè)失敗會(huì)重啟容器這是最重的處置絕不能因?yàn)橐淮味秳?dòng)畫就重啟。readiness 可以相對(duì)敏感因?yàn)樗皇钦髁看鷥r(jià)小。還有一個(gè)細(xì)節(jié)探針的請(qǐng)求應(yīng)該走本地回環(huán)或者本機(jī) Sidecar不要讓它繞一圈外部依賴。我見過探針的/ready接口內(nèi)部會(huì)去查數(shù)據(jù)庫數(shù)據(jù)庫一抖所有 Pod 一起 NotReady本來只是慢查詢最后演變成全站不可用。探針只反映“進(jìn)程自身是否可服務(wù)”不要在里面做重依賴檢查。5.2 腦裂、時(shí)鐘與冪等的組合坑腦裂這個(gè)詞在多活場(chǎng)景里出現(xiàn)的頻率很高但真正的原因往往不是網(wǎng)絡(luò)分區(qū)而是故障判定邏輯太激進(jìn)。比如主備模式下備站點(diǎn)連續(xù) 3 次心跳失敗就提升為主而心超時(shí)是 1 秒那么一次 3 秒的網(wǎng)絡(luò)抖動(dòng)就會(huì)導(dǎo)致雙主。我的做法是把自動(dòng)切換的門檻設(shè)得比人工介入慢一些。聽起來反直覺但對(duì)于核心交易系統(tǒng)一次錯(cuò)誤的自動(dòng)切換造成的損失遠(yuǎn)大于多停幾分鐘。具體參數(shù)心跳間隔 2 秒判定失敗次數(shù) 10 次也就是 20 秒才認(rèn)為主站點(diǎn)失聯(lián)觸發(fā)切換前還要做一次跨站點(diǎn)確認(rèn)比如通過第三個(gè)站點(diǎn)的仲裁服務(wù)投票。三個(gè)站點(diǎn)在這里體現(xiàn)了真正的價(jià)值——沒有第三點(diǎn)雙站點(diǎn)架構(gòu)在腦裂時(shí)是沒法自證的。時(shí)鐘問題更細(xì)碎??缯军c(diǎn)的服務(wù)器時(shí)間偏差哪怕只有幾十毫秒也會(huì)影響基于時(shí)間戳的沖突解決前面提過分布式鎖的租約判斷日志排查時(shí)的因果順序還原限流、熔斷的時(shí)間窗口計(jì)算我要求所有節(jié)點(diǎn)接入統(tǒng)一的時(shí)間同步源并且做偏移監(jiān)控偏差超過 50ms 就告警。這不是潔癖是必要的。冪等這塊我的經(jīng)驗(yàn)是不要指望下游保證冪等要自己在入口生成冪等鍵。做法是請(qǐng)求進(jìn)來時(shí)從業(yè)務(wù)流水號(hào)或“用戶 ID 業(yè)務(wù)類型 客戶端時(shí)間戳”生成一個(gè)冪等鍵寫入分布式緩存帶過期時(shí)間??缯军c(diǎn)重試時(shí)這個(gè)鍵要能跨站點(diǎn)識(shí)別所以緩存也必須是跨站點(diǎn)同步的或者干脆落到數(shù)據(jù)庫唯一索引上。后者更笨但更可靠我在核心交易鏈路上就用的唯一索引方案。5.3 問題速查表把上面這些坑整理成一張表出問題時(shí)按圖索驥會(huì)快很多現(xiàn)象高概率原因排查動(dòng)作處置大批 Pod 同時(shí) NotReady健康檢查過嚴(yán)、依賴抖動(dòng)看探針配置與依賴耗時(shí)放寬閾值探針去重依賴切流后錯(cuò)誤率陡升目標(biāo)站點(diǎn)限流配額不足查下游按站點(diǎn)的配額配置調(diào)整配額或分檔切流數(shù)據(jù)不一致持續(xù)擴(kuò)大同步鏈路積壓或雙向?qū)憶_突看同步延遲曲線與沖突日志暫停切流回切分片主備雙寫故障判定過于激進(jìn)查心跳日志與切換記錄引入第三點(diǎn)仲裁時(shí)延 P99 抬升明顯跨站點(diǎn)調(diào)用未收斂看調(diào)用鏈的跨域占比收斂到單元內(nèi)閉環(huán)節(jié)點(diǎn)頻繁驅(qū)逐資源請(qǐng)求與實(shí)際用量偏差大對(duì)比 request 與真實(shí)用量修正 request/limit鏡像拉取超時(shí)跨域拉取大鏡像看拉取耗時(shí)分布本地倉庫代理緩存注意這張表里我最想強(qiáng)調(diào)的是“暫停切流回切分片”這個(gè)動(dòng)作。很多團(tuán)隊(duì)在數(shù)據(jù)不一致時(shí)選擇繼續(xù)觀察想等它自己收斂結(jié)果不收斂反而擴(kuò)大最后只能全量回滾損失更大。發(fā)現(xiàn)不一致率超過閾值就立刻停這是紀(jì)律。6. 容量、成本與演練機(jī)制的經(jīng)驗(yàn)賬6.1 冗余系數(shù)怎么定別拍腦袋多活架構(gòu)里最容易被低估的是容量冗余。前面說過兩個(gè)站點(diǎn)各跑 50%任一站點(diǎn)故障后要承接 100%這意味每個(gè)站點(diǎn)必須有 100% 的承載能力也就是總資源是需求的 2 倍。這是理論值實(shí)際還要加余量。我的算法是基礎(chǔ)冗余系數(shù) 站點(diǎn)數(shù) / (站點(diǎn)數(shù) ? 1)兩站點(diǎn)是 2.0三站點(diǎn)是 1.5加上單站點(diǎn)內(nèi)部可用區(qū)故障的冗余三可用區(qū)情況下再乘 1.5加上業(yè)務(wù)增長預(yù)留按未來 6 個(gè)月增長預(yù)估通常乘 1.2 到 1.3加上突發(fā)流量緩沖乘 1.2兩站點(diǎn)、三可用區(qū)的核心業(yè)務(wù)最終系數(shù)是 2.0 × 1.5 × 1.25 × 1.2 ≈ 4.5。這個(gè)數(shù)字第一次算出來的時(shí)候我自己都愣了一下但它解釋了為什么多活的賬單那么嚇人。好消息是實(shí)際落地時(shí)可以做分級(jí)核心鏈路按這個(gè)系數(shù)非核心鏈路只做 2.0 的基礎(chǔ)冗余內(nèi)部系統(tǒng) 1.2 就夠。平均下來整體系數(shù)能壓到 2.5 到 3 左右還算能接受。壓測(cè)口徑也要統(tǒng)一必須是單站點(diǎn)全量承接的壓測(cè)而不是兩個(gè)站點(diǎn)各壓一半。前者才能驗(yàn)證故障場(chǎng)景。我見過團(tuán)隊(duì)壓測(cè)時(shí)兩個(gè)站點(diǎn)同時(shí)施壓各自跑 50%看著指標(biāo)都很好真出故障時(shí)才發(fā)現(xiàn)單站點(diǎn)根本扛不住 100%。6.2 演練要常態(tài)化別等真故障多活架構(gòu)最大的風(fēng)險(xiǎn)是“看起來能用實(shí)際切不過去”。架構(gòu)圖再漂亮沒有經(jīng)過真實(shí)演練的切換鏈路都是紙上的。我的做法是每季度至少一次全鏈路演練每個(gè)月做一次局部演練單服務(wù)、單中間件。演練清單包含這些場(chǎng)景從輕到重單個(gè) Pod 故障用混沌工具隨機(jī)殺 Pod驗(yàn)證副本自愈。單個(gè)節(jié)點(diǎn)故障關(guān)機(jī)驗(yàn)證 Pod 驅(qū)逐和重建觀察重建耗時(shí)。單可用區(qū)故障阻斷該區(qū)的網(wǎng)絡(luò)驗(yàn)證跨區(qū)打散是否生效。數(shù)據(jù)同步鏈路中斷驗(yàn)證積壓恢復(fù)能力和告警及時(shí)性。單站點(diǎn)整體切流驗(yàn)證容量、切換時(shí)長、數(shù)據(jù)收斂。切回演練這個(gè)最容易被跳過但切回往往比切出更難。第 6 項(xiàng)特別重要。切出去是往有余量的方向走切回來是往剛恢復(fù)、狀態(tài)可能還不穩(wěn)定的方向走風(fēng)險(xiǎn)高得多。我堅(jiān)持“切出和切回是兩次獨(dú)立演練”各自有獨(dú)立的檢查清單。演練必須產(chǎn)出可量化的結(jié)論切換耗時(shí)實(shí)測(cè)值、數(shù)據(jù)收斂耗時(shí)實(shí)測(cè)值、故障期間的錯(cuò)誤率峰值、遺留問題清單。沒有這些數(shù)據(jù)的演練等于團(tuán)建。心得我在一次演練中發(fā)現(xiàn)切回腳本里有個(gè)硬編碼的站點(diǎn)名只適合特定的切換方向。這種問題只有真跑才能發(fā)現(xiàn)代碼評(píng)審時(shí)幾乎看不出來。所以演練不僅要跑還要換方向跑。另外補(bǔ)充一個(gè)成本側(cè)的經(jīng)驗(yàn)跨站點(diǎn)的數(shù)據(jù)同步和專線是整個(gè)架構(gòu)里最貴的固定開銷而且會(huì)隨著數(shù)據(jù)量線性增長。我建議在數(shù)據(jù)分級(jí)時(shí)就明確“哪些數(shù)據(jù)真的需要跨站點(diǎn)同步”把可重建數(shù)據(jù)緩存、索引徹底排除把日志類數(shù)據(jù)改成按站點(diǎn)本地存儲(chǔ)加集中查詢能省下不少帶寬。我們做完這一輪瘦身跨域帶寬需求降了將近 40%同步延遲也跟著改善了。我個(gè)人在實(shí)際操作中的體會(huì)是異地多活這件事七分靠設(shè)計(jì)三分靠紀(jì)律。設(shè)計(jì)決定了天花板紀(jì)律決定了你能不能真的站到那個(gè)天花板上。所謂紀(jì)律就是切流前一定走清單、發(fā)現(xiàn)不一致一定先停、每次變更一定留版本號(hào)、每季度一定真演練。這些東西寫出來平平無奇但在我見過的所有出問題的多活項(xiàng)目里出問題的從來不是架構(gòu)選型而是某一次“這次應(yīng)該沒事先跳過檢查吧”。把這幾條守住剩下的就是慢慢打磨參數(shù)和工具鏈的事情了。