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

背景以及需求

  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 星雲小姐姐 交個朋友再進個交流羣;   

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章