同城雙活:如何實現機房之間的資料同步?

2024.10.15

在業務初期,為了控制投入成本,許多公司通常只使用一個機房提供服務。但隨著業務的發展和流量的成長,對服務回應速度和可用性的要求逐漸提高,這時就需要考慮在不同地區部署服務,以提供更好的使用者體驗。這也是網路公司在流量成長階段的必經之路。

我之前所在的公司連續三年流量不斷成長。有一次,機房的對外網路突然斷開,導致線上服務全面離線,網路供應商也無法聯絡。由於沒有備用機房,我們花了三天時間緊急協調,重新拉線路才恢復服務。這次事故造成的影響非常大,公司損失達千萬元。吸取了這次教訓後,我們將服務遷移到了更大型的機房,並決定在同一城市建造雙機房,以提高服務的可用性。這樣,當一個機房發生故障時,使用者可以透過HttpDNS 介面快速切換到另一個正常的機房。

為了確保在一個機房故障時,另一個機房能夠直接接管流量,我們對兩個機房的設備進行了1:1 的購買。但如果讓其中一個機房長時間處於冷備狀態會造成資源浪費,因此我們希望兩個機房能同時對外提供服務,也就是實現同城雙活。不過,雙活方案的一個關鍵問題是如何實現雙機房之間的資料庫同步。

核心資料中心設計

由於資料庫使用的是主從架構,因此全網只能有一個主庫來進行資料更新。我們只能在一個機房部署主庫,然後由這個機房將資料同步到其他備份機房。雖然兩個機房之間有專線連接,但網路的完全穩定性無法保證。如果網路發生故障,我們需要確保機房之間在網路恢復後能夠快速恢復資料同步。

有人可能會認為直接採用分散式資料庫可以解決這個問題。然而,改變現有的服務體系並全面遷移到分散式資料庫不僅需要相當長的時間,成本也非常高昂,對大多數公司來說並不實際。因此,我們需要考慮如何改造現有系統,實現同城雙活機房的資料庫同步。這也正是我們的目標

核心資料庫中心方案是常見的實作方式,此方案只適合相距不超過50 公里的機房。

圖片圖片

在這個方案中,主庫集中部署在一個核心機房,其餘機房中的資料庫則作為從庫。當有資料修改請求時,核心機房的主庫會先完成修改,然後透過主從同步將更新的資料傳輸到其他備份機房的從庫。

由於用戶通常是從快取中獲取信息,為了降低主從同步的延遲,備份機房會將更新後的資料直接寫入本地快取。同時,用戶端會在本機記錄下資料修改的最後時間戳記(若沒有,則記錄目前時間)。當客戶端向服務端發起請求時,服務端會自動比較快取中該資料的更新時間與客戶端本地的修改時間。如果快取中的更新時間早於客戶端記錄的時間,服務端會觸發同步操作,嘗試在從庫中查找最新數據;若從庫中沒有最新數據,則從主庫中獲取最新數據並更新到該機房的快取中。

透過這種方式,可以有效避免機房之間的資料更新延遲問題,從而確保使用者能更及時地取得最新的資料。

圖片圖片

此外,客戶端還會透過請求調度接口,使用戶在短時間內只訪問同一個機房,避免用戶在多個機房之間來回切換時,因資料在不同機房同時修改而產生更新合併衝突。整體來看,這種方案設計相對簡單,但也存在一些明顯的缺點。

例如,如果核心機房發生故障,其他機房將無法執行資料更新。故障期間,需要人工切換各個代理程式(proxy)的主從庫配置才能恢復服務,故障復原後也需要手動介入以恢復主從同步。此外,由於主從同步有一定的延遲,剛更新的資料在備用機房中會有短暫的不可見時間,這種延遲會導致業務邏輯中需要人工處理這種情況,整體操作較為繁瑣,增加了實現的複雜性。

這裡我給你一個常見的網路延遲參考:

同機房伺服器:0.1 ms同城伺服器(100 公里以內) :1ms(10 倍同機房)北京到上海:38ms(380 倍同機房)北京到廣州:53ms(530 倍同機房)

需要注意的是,上述設計只是RTT 請求,而機房間的同步涉及多次順序疊加的請求操作。如果要大規模更新數據,主從庫的同步延遲將更為顯著。因此,這種雙活機房方案的資料量不能過大,且業務更新資料的頻率也不能太高。另外,如果服務對強一致性有要求,即所有操作都必須在主庫“遠端執行”,這也會加大主從同步的延遲。

除了以上問題,雙機房之間的專線偶爾也會故障。我曾經遇過一次專線斷開持續了兩小時,期間只能暫時透過公網來保持同步,但公網同步不穩定,延遲在10ms~500ms 之間波動,導致主從延遲超過1 分鐘。幸運的是,由於用戶中心服務主要依賴長期快取的數據,業務主要流程沒有受到太大影響,只是用戶修改資訊的速度變得很慢。

雙機房同步也可能偶發主從同步中斷的情況,因此建議設定警告處理機制。一旦發生此情況,應立即向故障警報群發送通知,由DBA 人員進行手動修復。此外,我還遇到在主從不同步期間,用戶註冊時自增ID 出現重複,導致主鍵衝突。為此,我建議將自增ID 替換為基於SnowFlake 演算法產生的ID,以減少主鍵衝突的風險。

總的來說,儘管這種核心資料庫的中心化方案實現了同城雙活,但人力投入成本非常高。 DBA 需要手動維護同步,一旦主從同步中斷,恢復起來相當耗時耗力,且研發人員也需要隨時關注主從不同步的情況。因此,我推薦使用另一種方​​案:資料庫同步工具Otter。

跨機房同步神器:Otter

Otter 是阿里開發的資料庫同步工具,它可以快速實現跨機房、跨城市、跨國家的資料同步。如下圖所示,其核心實作是透過Canal 監控主庫MySQL 的Row binlog,將資料更新並行同步給其他機房的MySQL。

因為我們要實現同城雙機房雙活,所以這裡我們用Otter 來實現同城雙主(注意:雙主不通用,不建議一致要求高的業務使用),這樣雙活機房可以雙向同步:

如上圖所示,每個機房內都有自己的主庫和從庫,快取可以跨機房主從同步,也可以是本地的主從同步,這取決於特定的業務需求。 Otter 使用Canal 將機房內主庫的資料變更同步到Otter Node 中,然後透過Otter 的SETL(Select, Extract, Transform, Load)機制整理後,再將資料同步到對方機房的Node 節點,從而實現雙機房之間的資料同步。

這裡需要提到Otter 處理資料衝突的方式,以解決雙機房同時修改同一條資料的問題。 Otter 中的資料衝突分為兩類:行衝突和欄位衝突。行衝突可以透過比較資料的修改時間來解決,或是在發生衝突時進行回源查詢來覆寫目標函式庫。而對於欄位衝突,可以根據修改時間覆蓋,也可以合併多個修改操作。例如,如果a 機房和b 機房分別對某一欄位進行了-1 的操作,合併後該欄位的最終修改值為-2,以此實現資料的最終一致性。

但要注意的是,這種合併策略並不適用於庫存類別的資料管理,因為可能會導致超賣現象。如果有類似的需求,建議使用長期快取來處理,以避免並發修改導致的資料不一致問題。

總結

機房之間的資料同步一直是行業中的難題,由於其高昂的實現成本,如果無法實現雙活,那麼必然會有一個機房以1:1 的機器數量在空跑。且在發生故障時,也無法保證冷備機房能夠立即對外提供服務。然而,雙活模式的維護成本也不低,機房之間的資料同步經常會因網路延遲或資料衝突而中斷,最終導致兩個機房資料不一致。

還好Otter 在資料同步方面採取了多種措施,能夠在大多數情況下保證資料的完整性,並降低同城雙活的實現難度。即便如此,在業務運作中,我們仍需手動整理業務流程,以盡量避免多個機房同時修改同一條資料。為此,我們可以透過HttpDNS 調度,讓使用者在一段時間內只在一個機房內活躍,減少資料衝突的可能性。對於頻繁修改、資源爭搶較高的服務,通常在機房本地執行完整事務操作,避免跨機房同時修改帶來的同步錯誤。