架構設計|基於 raft-listener 實現實時同步的主備叢集

NebulaGraph發表於2024-04-17

背景以及需求

  1. 線上業務對資料庫可用性可靠性要求較高,要求需要有雙 AZ 的主備容災機制。
  2. 主備叢集要求資料和 schema 資訊實時同步,資料同步平均時延要求在 1s 之內,p99 要求在 2s 之內。
  3. 主備叢集資料要求一致
  4. 要求能夠在主叢集故障時高效自動主備倒換或者手動主備倒換,主備倒換期間丟失的資料可找回。

為什麼使用 Listener

Listener:這是一種特殊的 Raft 角色,並不參與投票,也不能用於多副本的資料一致性。

原本的 NebulaGraph 中的 Listener 是一個 Raft 監聽器,它的作用是將資料非同步寫入外部的 Elasticsearch 叢集,並在查詢時去查詢 ES 以實現全文索引的功能。

這裡我們需要的是 Listener 的監聽能力,用於快速同步資料到其他叢集,並且是非同步的執行,不影響主叢集的正常讀寫。

這裡我們需要定義兩個新的 Listener 型別:

  1. Meta Listener:用於同步表結構以及其他後設資料資訊
  2. Storage Listener:用於同步 storaged 服務的資料

這樣 storaged 服務和 metad 服務的 part leader 節點接受到寫請求時,除了同步一份資料給 follower 節點,也會同步一份給各自的 listener 節點。

備叢集如何接受資料?

現在我們面臨幾個問題:

  1. 兩個新增 Listener 在接收到 leader 同步的日誌後,應該如何再同步給備叢集?
  2. 我們需要匹配和解析不同的資料操作,例如新增點、刪除點、刪除邊、刪除帶索引的資料等等操作;
  3. 我們需要將解析到的不同操作的資料重新組裝成一個請求傳送給備叢集的 storaged 服務和 metad 服務;

透過走讀 nebula-storaged 的核心程式碼我們可以看到,無論是 metad 還是 storaged 的各種建立刪除表結構以及各種型別資料的插入,最後都會序列化成一個 wal 的 log 傳送給 follower 以及 listener 節點,最後儲存在 RocksDB 中。

因此,我們的 listener 節點需要具備從 log 日誌中解析並識別操作型別的能力,和封裝成原請求的能力,因為我們需要將操作同步給備叢集的 metad 以及 storaged 服務。

這裡涉及到一個問題,主叢集的 listener 需要如何感知備叢集?備叢集 metad 服務的資訊以及 storaged 服務的資訊?從架構設計上來看,兩個叢集之間應該有一個介面通道互相連線,但又不干涉,如果由 listener 節點直接傳送請求給備叢集的 nebula 程序,兩個叢集的邊界就不是很明顯了。所以這裡我們再引入一個備叢集的服務 listener 服務,它用於接收來自主叢集的 listener 服務的請求,並將請求轉發給自己叢集的 metad 以及 storaged 服務。

這樣做的好處。兩邊叢集的服務模組是對稱的,方便我們後面快速地做主備切換。

Listener 節點的管理和可靠性

為了保證雙 AZ 環境的可靠性,很顯然 Listener 節點也是需要多節點多活的,在 nebula 核心原始碼中是有對於 listener 的管理邏輯,但是比較簡單,我們還需要設計一個 ListenerManager 實現以下幾點能力:

  1. listener 節點註冊以及刪除命令
  2. listener 節點動態負載均衡(儘量每個 space 各個 part 分佈的 listener 要均勻)
  3. listener 故障切換

節點註冊管理以及負載均衡都比較簡單好設計,比較重要的一點是故障切換應該怎麼做?

listener 故障切換的設計

listener 節點故障切換的需求可以拆分為以下幾個部分:

  1. listener 同步 wal 日誌資料時週期性記錄同步的進度(commitId && appendLogId);
  2. ListenerManager 感知到 listener 故障後,觸發動態負載均衡機制,將故障 listener 的 part 分配給其他在執行的 listener;
  3. 分配到新 part 的 listener 們獲取原先故障 listener 記錄的同步進度,並以該進度為起始開始同步資料;

至於 listener 同步 wal 日誌資料時週期性記錄同步的進度應該記錄到哪裡?可以是儲存到 metad 服務中,也可以儲存到 storaged 服務對應的 part 中。

nebula 主備切換設計

在聊主備切換之前,我們還需要考慮一件事,那就是雙 AZ 環境中,應該只能有主叢集是可讀可寫的,而其他備叢集應該是隻讀不能寫。這樣是為了保證兩邊資料的最終一致性,備叢集的寫入只能是由主叢集的 listener 請求來寫入的,而不能被 graphd 服務的請求寫入。

所以我們需要對叢集狀態增加一種“只讀模式”,在這種只讀模式下,表明當前叢集狀態是處於備叢集的狀態,拒絕來自 graphd 服務的寫操作。同樣的,備叢集的 listener 節點處在只讀狀態時,也只能接收來自主叢集的請求並轉發給備叢集的程序,拒絕來自備叢集的 wal 日誌同步。

主備倒換髮生時,需要有以下幾個動作:

  1. 主叢集的每個 listener 記錄自己所負責的 part 的同步進度(commitId && appendLogId);
  2. 備叢集的 nebula 服務轉換為可寫;
  3. 備叢集的 listener 節點轉換為可寫,並且開始接收來自自己叢集的 metad 和 storaged 程序的 wal 日誌;
  4. 主叢集的 listener 以及各個服務轉換為只讀狀態,開始接收來自新的主叢集的資料同步請求;

這幾個動作細分下來,最主要的內容就是狀態轉換以及上下文資訊儲存和同步,原主叢集需要儲存自己主備切換前的上文資訊(比如同步進度),新的主叢集需要載入自己的資料同步起始進度(從當前最新的 commitLog 開始)

主備切換過程中的資料丟失問題

很明顯,在上面的設計中,當主備切換髮生時,會有一段時間的“雙主”的階段,在這個階段內,原主叢集的剩餘日誌已經不能再同步給備叢集了,這就是會被丟失的資料。如何恢復這些被丟失的資料,可能的方案有很多,因為原主叢集的同步進度是有記錄的,有哪些資料還沒同步完也是可以查詢到的,所以可以手動或者自動去單獨地同步那一段缺失資料。

當然這種方案也會引入新的問題,這段丟失地資料同步給主叢集后,主叢集會再次同步一遍回現在的備叢集,一段 wal 資料的兩次重複操作,不知道為引起什麼其他的問題。

所以關於主備切換資料丟失的問題,我們還沒有很好的處理方案,感興趣的夥伴歡迎在評論區討論。


感謝你的閱讀 (///▽///)

對圖資料庫 NebulaGraph 感興趣?歡迎前往 GitHub ✨ 檢視原始碼:https://github.com/vesoft-inc/nebula

想和其他圖技術愛好者一起交流心得?和 NebulaGraph 星雲小姐姐 交個朋友再進個交流群;

相關文章