萬字詳解 TDengine 2-0 數據複製模塊設計

**導讀:**TDengine 分佈式集羣功能已經開源,集羣功能中最重要的一個模塊是數據複製 (replication),現將該模塊的設計分享出來,供大家參考。歡迎大家對着設計文檔和 GitHub 上的源代碼一起看,歡迎各種反饋。

1: 數據複製概述

數據複製 (Replication) 是指同一份數據在多個物理地點保存。它的目的是防止數據丟失,提高系統的高可用性(High Availability),而且通過應用訪問多個副本,提升數據查詢性能。

在高可靠的大數據系統裏,數據複製是必不可少的一大功能。數據複製又分爲實時複製與非實時複製。實時複製是指任何數據的更新 (包括數據的增加、刪除、修改)操作,會被實時的複製到所有副本,這樣任何一臺機器宕機或網絡出現故障,整個系統還能提供最新的數據,保證系統的正常工作。而非實時複製,是指傳統的數據備份操作,按照固定的時間週期,將一份數據全量或增量複製到其他地方。如果主節點宕機,副本是很大可能沒有最新數據,因此在有些場景是無法滿足要求的。

TDengine 面向的是物聯網場景,需要支持數據的實時複製,來最大程度保證系統的可靠性。實時複製有兩種方式,一種是異步複製,一種是同步複製。異步複製 (Asynchronous Replication) 是指數據由 Master 轉發給 Slave 後,Master 並不需要等待 Slave 回覆確認,這種方式效率高,但有極小的概率會丟失數據。同步複製是指 Master 將數據轉發給 Slave 後,需要等待 Slave 的回覆確認,纔會通知應用寫入成功,這種方式效率偏低,但能保證數據絕不丟失。

數據複製是與數據存儲(寫入、讀取)密切相關的,但兩者又是相對獨立,可以完全脫耦的。在 TDengine 系統中,有兩種不同類型的數據,一種是時序數據,由 TSDB 模塊負責;一種是元數據 (Meta Data), 由 MNODE 負責。這兩種性質不同的數據都需要同步功能。數據複製模塊通過不同的實例啓動配置參數,爲這兩種類型數據都提供同步功能。

在閱讀本文之前,請先閱讀《TDengine 2.0 整體架構》,瞭解 TDengine 的集羣設計和基本概念。

TDengine 架構示意圖

特別註明:本文中提到數據更新操作包括數據的增加、刪除與修改。

2: 基本概念和定義

TDengine 裏存在 vnode, mnode, vnode 用來存儲時序數據,mnode 用來存儲元數據。但從同步數據複製的模塊來看,兩者沒有本質的區別,因此本文裏的虛擬節點不僅包括 vnode, 也包括 mnode, vgoup 也指 mnode group, 除非特別註明。

版本 (version):

一個虛擬節點組裏多個虛擬節點互爲備份,來保證數據的有效與可靠,是依靠虛擬節點組的數據版本號來維持的。TDengine2.0 設計裏,對於版本的定義如下:客戶端發起增加、刪除、修改的流程,無論是一條記錄還是多條,只要是在一個請求裏,這個數據更新請求被 TDengine 的一個虛擬節點收到後,經過合法性檢查後,可以被寫入系統時,就會被分配一個版本號。這個版本號在一個虛擬節點裏從 1 開始,是單調連續遞增的。無論這條記錄是採集的時序數據還是 meta data, 一樣處理。當 Master 轉發一個寫入請求到 slave 時,必須帶上版本號。一個虛擬節點將一數據更新請求寫入 WAL 時,需要帶上版本號。

不同虛擬節點組的數據版本號是完全獨立的,互不相干的。版本號本質上是數據更新記錄的 transaction ID,但用來標識數據集的版本。

角色 (role):

Quorum:

指數據寫入成功所需要的確認數。對於異步複製,quorum 設爲 1,具有 master 角色的虛擬節點自己確認即可。對於同步複製,需要至少大於等於 2。原則上,Quorum >=1 並且 Quorum <= replication(副本數)。這個參數在啓動一個同步模塊實例時需要提供。

WAL:

TDengine 的 WAL(Write Ahead Log) 與 cassandra 的 commit log, mySQL 的 bin log, Postgres 的 WAL 沒本質區別。沒有寫入數據庫文件,還保存在內存的數據都會先存在 WAL。當數據已經成功寫入數據庫數據文件,相應的 WAL 會被刪除。但需要特別指明的是,在 TDengine 系統裏,有幾點:

複製實例:

複製模塊只是一可執行的代碼,複製實例是指正在運行的複製模塊的一個實例,一個節點裏,可以存在多個實例。原則上,一個節點有多少虛擬節點,就可以啓動多少實例。對於副本數爲 1 的場景,應用可以決定是否需要啓動同步實例。應用啓動一個同步模塊的實例時,需要提供的就是虛擬節點組的配置信息,包括:

3: 數據複製模塊的基本工作原理

TDengine 採取的是 Master-Slave 模式進行同步,與流行的 RAFT 一致性算法比較一致。總結下來,有幾點:

  1. 一個 vgroup 裏有一到多個虛擬節點,每個虛擬節點都有自己的角色

  2. 客戶端只能向角色是 master 的虛擬節點發起數據更新操作,因爲 master 具有最新版本的數據,如果向非 Master 發起數據更新操作,會直接收到錯誤

  3. 客戶端可以向 master, 也可以向角色是 Slave 的虛擬節點發起查詢操作,但不能對 unsynced 的虛擬節點發起任何操作

  4. 如果 master 不存在,這個 vgroup 是不能對外提供數據更新和查詢服務的

  5. master 收到客戶端的數據更新操作時,會將其轉發給 slave 節點

  6. 一個虛擬節點的版本號比 master 低的時候,會發起數據恢復流程,成功後,纔會成爲 slave

數據實時複製有三個主要流程:選主、數據轉發、數據恢復。後續做詳細討論。

4: 虛擬節點之間的網絡鏈接

虛擬節點之間通過 TCP 進行鏈接,節點之間的狀態交換、數據包的轉發都是通過這個 TCP 鏈接 (peerFd) 進行。爲避免競爭,兩個虛擬節點之間的 TCP 鏈接,總是由 IP 地址 (UINT32) 小的節點作爲 TCP 客戶端發起。一旦 TCP 鏈接被中斷,虛擬節點能通過 TCP socket 自動檢測到,將對方標爲 offline。如果監測到任何錯誤(比如數據恢復流程),虛擬節點將主動重置該鏈接。

一旦作爲客戶端的節點鏈接不成或中斷,它將週期性的每隔一秒鐘去試圖去鏈接一次。因爲 TCP 本身有心跳機制,虛擬節點之間不再另行提供心跳。

如果一個 unsynced 節點要發起數據恢復流程,它與 Master 將建立起專有的 TCP 鏈接 (syncFd)。數據恢復完成後,該鏈接會被關閉。而且爲限制資源的使用,系統只容許一定數量(配置參數 tsMaxSyncNum) 的數據恢復的 socket 存在。如果超過這個數字,系統會將新的數據恢復請求延後處理。

任意一個節點,無論有多少虛擬節點,都會啓動而且只會啓動一個 TCP server, 來接受來自其他虛擬節點的上述兩類 TCP 的鏈接請求。當 TCP socket 建立起來,客戶端側發送的消息體裏會帶有 vgId(全局唯一的 vgroup ID), TCP 服務器側會檢查該 vgId 是否已經在該節點啓動運行。如果已經啓動運行,就接受其請求。如果不存在,就直接將鏈接請求關閉。在 TDengine 代碼裏,mnode group 的 vgId 設置爲 1。

5: 選主流程

當同一組的兩個虛擬節點之間 (vnode A, vnode B) 建立連接後,他們互換 status 消息。status 消息裏包含本地存儲的同一虛擬節點組內所有虛擬節點的 role 和 version。

如果一個虛擬節點 (vnode A) 檢測到與同一虛擬節點組內另外一虛擬節點(vnode B)的鏈接中斷,vnode A 將立即把 vnode B 的 role 設置爲 offline。無論是接收到另外一虛擬節點發來的 status 消息,還是檢測與另外一虛擬節點的鏈接中斷,該虛擬節點都將進入狀態處理流程。狀態處理流程的規則如下:

  1. 如果檢測到在線的節點數沒有超過一半,則將自己的狀態設置爲 unsynced.

  2. 如果在線的虛擬節點數超過一半,會檢查 master 節點是否存在,如果存在,則會決定是否將自己狀態改爲 slave 或啓動數據恢復流程

  3. 如果 master 不存在,則會檢查自己保存的各虛擬節點的狀態信息與從另一節點接收到的是否一致,如果一致,說明節點組裏狀態已經穩定一致,則會觸發選舉流程。如果不一致,說明狀態還沒趨於一致,即使 master 不存在,也不進行選主。由於要求狀態信息一致才進行選舉,每個虛擬節點根據同樣的信息,會選出同一個虛擬節點做 master,無需投票表決。

  4. 自己的狀態是根據規則自己決定並修改的,並不需要其他節點同意,包括成爲 master。一個節點無權修改其他節點的狀態。

  5. 如果一個虛擬節點檢測到自己或其他虛擬節點的 role 發生改變,該節點會廣播它自己保存的各個虛擬節點的狀態信息(role 和 version).

具體的流程圖如下:

選擇 Master 的具體規則如下:

  1. 如果只有一個副本,該副本永遠就是 master

  2. 所有副本都在線時,版本最高的被選爲 master

  3. 在線的虛擬節點數過半,而且有虛擬節點是 slave 的話,該虛擬節點自動成爲 master

  4. 對於 2 和 3,如果多個虛擬節點滿足成爲 master 的要求,那麼虛擬節點組的節點列表裏,最前面的選爲 master

按照上面的規則,如果所有虛擬節點都是 unsynced(比如全部重啓),只有所有虛擬節點上線,才能選出 master,該虛擬節點組才能開始對外提供服務。當一個虛擬節點的 role 發生改變時,sync 模塊回通過回調函數 notifyRole 通知應用。

6: 數據轉發流程

如果 vnode A 是 master, vnode B 是 slave, vnode A 能接受客戶端的寫請求,而 vnode B 不能。當 vnode A 收到寫的請求後,遵循下面的流程:

  1. 應用對寫請求做基本的合法性檢查,通過,則給改請求包打上一個版本號 (version, 單調遞增)。

  2. 應用將打上版本號的寫請求封裝一個 WAL Head, 寫入 WAL(Write Ahead Log)

  3. 應用調用 API syncForwardToPeer,如多 vnode B 是 slave 狀態,sync 模塊將包含 WAL Head 的數據包通過 Forward 消息發送給 vnode B,否則就不轉發。

  4. vnode B 收到 Forward 消息後,調用回調函數 writeToCache, 交給應用處理。

  5. vnode B 應用在寫入成功後,都需要調用 syncAckForward 通知 sync 模塊已經寫入成功。

  6. 如果 quorum 大於 1,vnode B 需要等待應用的回覆確認,收到確認後,vnode B 發送 Forward Response 消息給 node A。

  7. 如果 quorum 大於 1,vnode A 需要等待 vnode B 或其他副本對 Forward 消息的確認。

  8. 如果 quorum 大於 1,vnode A 收到 quorum-1 條確認消息後,調用回調函數 confirmForward,通知應用寫入成功。

  9. 如果 quorum 爲 1,上述 6,7,8 步不會發生。

  10. 如果要等待 slave 的確認,master 會啓動 2 秒的定時器(可配置),如果超時,則認爲失敗。

對於回覆確認,sync 模塊提供的是異步回調函數,因此 APP 在調用 syncForwardToPeer 之後,無需等待,可以處理下一個操作。在 Master 與 Slave 的 TCP 鏈接管道里,可能有多個 Forward 消息,這些消息是嚴格按照應用提供的順序排好的。對於 Forward Response 也是一樣,TCP 管道里存在多個,但都是排序好的。這個順序,SYNC 模塊並沒有做特別的事情,是由 APP 單線程順序寫來保證的 (TDengine 裏每個 vnode 的寫數據,都是單線程)。

7: 數據恢復流程

如果一虛擬節點 (vnode B) 處於 unsynced 狀態,master 存在(vnode A),而且其版本號比 master 的低,它將立即啓動數據恢復流程。在理解恢復流程時,需要澄清幾個關於文件的概念和處理規則。

  1. 每個文件(無論是 archived data 的 file 還是 wal) 都有一個 index, 這需要應用來維護 (vnode 裏,該 index 就是 fileId*3 + 0/1/2, 對應 data, head 與 last 三個文件)。如果 index 爲 0,表示系統裏最老的數據文件。對於 mnode 裏的文件,數量是固定的,對應於 acct, user, db, table 等文件。

  2. 任何一個數據文件 (file) 有名字、大小,還有一個 magic number。只有文件名、大小與 magic number 一致時,兩個文件才判斷是一樣的,無需同步。Magic number 可以是 checksum, 也可以是簡單的文件大小。怎麼計算 magic,換句話說,如何檢測數據文件是否有效,完全由應用決定。

  3. 文件名的處理有點複雜,因爲每臺服務器的路徑可能不一致。比如 node A 的 TDengine 的數據文件存放在 /etc/taos 目錄下,而 node B 的數據存放在 /home/jhtao 目錄下。因此同步模塊需要應用在啓動一個同步實例時提供一個 path,這樣兩臺服務器的絕對路徑可以不一樣,但仍然可以做對比,做同步。

  4. 當 sync 模塊調用回調函數 getFileInfo 獲得數據文件信息時,有如下的規則:

  1. 當 sync 模塊調用回調函數 getWalInfo 獲得 wal 信息時,有如下規則:
  1. 無論是 getFileInfo, 還是 getWalInfo, 只要獲取出錯(不是文件不存在),返回 - 1 即可,系統會報錯,停止同步。

整個數據恢復流程分爲兩大步驟,第一步,先恢復 archived data(file), 然後恢復 wal。具體流程如下:

  1. 通過已經建立的 TCP 鏈接,發送 sync req 給 master 節點。

  2. master 收到 sync req 後,以 client 的身份,向 vnode B 主動建立一新的專用於同步的 TCP 鏈接(syncFd)。

  3. 新的 TCP 鏈接建立成功後,master 將開始 retrieve 流程,對應的,vnode B 將同步啓動 restore 流程。

  4. Retrieve/Restore 流程裏,先處理所有 archived data (vnode 裏的 data, head, last 文件),後處理 WAL data。

  5. 對於 archived data,master 將通過回調函數 getFileInfo 獲取數據文件的基本信息,包括文件名、magic 以及文件大小。

  6. master 將獲得的文件名、magic 以及文件大小發給 vnode B。

  7. vnode B 將回調函數 getFile 獲得 magic 和文件大小,如果兩者一致,就認爲無需同步,如果兩者不一致 ,就認爲需要同步。vnode B 將結果通過消息 FileAck 發回 master。

  8. 如果文件需要同步,master 就調用 sendfile 把整個文件發往 vnode B。

  9. 如果文件不需要同步,master(vnode A) 就重複 5,6,7,8,直到所有文件被處理完。

對於 WAL 同步,流程如下:

  1. master 節點調用回調函數 getWalInfo,獲取 WAL 的文件名。

  2. 如果 getWalInfo 返回值大於 0,表示該文件還不是最後一個 WAL,因此 master 調用 sendfile 一下把該文件發送給 vnode B。

  3. 如果 getWalInfo 返回時爲 0,表示該文件是最後一個 WAL,因爲文件可能還處於寫的狀態中,sync 模塊要根據 WAL Head 的定義逐條讀出記錄,然後發往 vnode B。

  4. vnode A 讀取 TCP 鏈接傳來的數據,按照 WAL Head,逐條讀取,如果版本號比現有的大,調用回調函數 writeToCache,交給應用處理。如果小,直接扔掉。

  5. 上述流程循環,直到所有 WAL 文件都被處理完。處理完後,master 就會將新來的數據包通過 Forward 消息轉發給 slave。

從同步文件啓動起,sync 模塊會通過 inotify 監控所有處理過的 file 以及 wal。一旦發現被處理過的文件有更新變化,同步流程將中止,會重新啓動。因爲有可能落盤操作正在進行(比如歷史數據導入,內存數據落盤),把已經處理過的文件進行了修改,需要重新同步纔行。

對於最後一個 WAL (LastWal) 的處理邏輯有點複雜,因爲這個文件往往是打開寫的狀態,有很多場景需要考慮,比如:

sync 模塊通過 inotify 監控 LastWal 文件的更新和關閉操作。而且在確認已經儘可能讀完 LastWal 的數據後,會將對方同步狀態設置爲 SYNC_CACHE。該狀態下,master 節點會將新的記錄轉發給 vnode B,而此時 vnode B 並沒有完成同步,需要把這些轉發包先存在 recv buffer 裏,等 WAL 處理完後,vnode A 再把 recv buffer 裏的數據包通過回調 writeToCache 交給應用處理。

等 vnode B 把這些 buffered forwards 處理完,同步流程纔算結束,vnode B 正式變爲 slave。

8: Master 分佈均勻性問題

因爲 Master 負責寫、轉發,消耗的資源會更多,因此 Master 在整個集羣裏分佈均勻比較理想。

但在 TDengine 的設計裏,如果多個虛擬節點都符合 master 條件,TDengine 選在列表中最前面的做 Master, 這樣是否導致在集羣裏,Master 數量的分佈不均勻問題呢?這取決於應用的設計。

給一個具體例子,系統裏僅僅有三個節點,IP 地址分別爲 IP1, IP2, IP3. 在各個節點上,TDengine 創建了多個虛擬節點組,每個虛擬節點組都有三個副本。如果三個副本的順序在所有虛擬節點組裏都是 IP1, IP2, IP3, 那毫無疑問,master 將集中在 IP1 這個節點,這是我們不想看到的。

但是,如果在創建虛擬節點組時,增加隨機性,這個問題就不存在了。比如在 vgroup 1, 順序是 IP1, IP2, IP3, 在 vgroup 2 裏,順序是 IP2, IP3, IP1, 在 vgroup 3 裏,順序是 IP3, IP1, IP2。最後 master 的分佈會是均勻的。

因此在創建一個虛擬節點組時,應用需要保證節點的順序是 round robin 或完全隨機。

9: 少數虛擬節點寫入成功的問題

在某種情況下,寫入成功的確認數大於 0,但小於配置的 Quorum, 雖然有虛擬節點數據更新成功,master 仍然會認爲數據更新失敗,並通知客戶端寫入失敗。

這個時候,系統存在數據不一致的問題,因爲有的虛擬節點已經寫入成功,而有的寫入失敗。一個處理方式是,Master 重置 (reset) 與其他虛擬節點的連接,該虛擬節點組將自動進入選舉流程。按照規則,已經成功寫入數據的虛擬節點將成爲新的 master,組內的其他虛擬節點將從 master 那裏恢復數據。

因爲寫入失敗,客戶端會重新寫入數據。但對於 TDengine 而言,是 OK 的。因爲時序數據都是有時間戳的,時間戳相同的數據更新操作,第一次會執行,但第二次會自動扔掉。對於 Meta Data(增加、刪除庫、表等等)的操作,也是 OK 的。一張表、庫已經被創建或刪除,再創建或刪除,不會被執行的。

在 TDengine 的設計裏,虛擬節點與虛擬節點之間,是一個 TCP 鏈接,是一個 pipeline,數據塊一個接一個按順序在這個 pipeline 裏等待處理。一旦某個數據塊的處理失敗,這個鏈接會被重置,後續的數據塊的處理都會失敗。因此不會存在 Pipeline 裏一個數據塊更新失敗,但下一個數據塊成功的可能。

10: Split Brain 的問題

選舉流程中,有個強制要求,那就是一定有超過半數的虛擬節點在線。但是如果 replication 正好是偶數,這個時候,完全可能存在 splt brain 問題。

爲解決這個問題,TDengine 提供 Arbitrator 的解決方法。Arbitrator 是一個節點,它的任務就是接受任何虛擬節點的鏈接請求,並保持它。

在啓動複製模塊實例時,在配置參數中,應用可以提供 Arbitrator 的 IP 地址。

如果是奇數個副本,複製模塊不會與這個 arbitrator 去建立鏈接,但如果是偶數個副本,就會主動去建立鏈接。

Arbitrator 的程序 tarbitrator.c 在複製模塊的同一目錄, 編譯整個系統時,會在 bin 目錄生成。命令行參數 “-?” 查看可以配置的參數,比如綁定的 IP 地址,監聽的端口號。

11: 與 RAFT 相比的異同

數據一致性協議流行的有兩種,Paxos 與 Raft. 本設計的實現與 Raft 有很多類同之處,下面做一些比較。

相同之處

不同之處

如果整個虛擬節點組全部宕機,重啓,但不是所有虛擬節點都上線,這個時候 TDengine 是不會選出 master 的,因爲未上線的節點有可能有最高 version 的數據。而 RAFT 協議,只要超過半數上線,就會選出 Leader。

12: Meta Data 的數據複製

TDengine 裏存在時序數據,也存在 Meta Data。Meta Data 對數據的可靠性要求更高,那麼 TDengine 設計能否滿足要求呢?下面做個仔細分析。

TDengine 裏 Meta Data 包括以下:

上述的 account, user, DB, vgroup, table, stable, mnode, dnode 都有自己的屬性,這些屬性是 TDengine 自己定義的,不會開放給用戶進行修改。這些 Meta Data 的查詢都比較簡單,都可以採用 key-value 模型進行存儲。這些 Meta Data 還具有幾個特點:

  1. 上述的 Meta Data 之間有一定的層級關係,比如必須先創建 DB,才能創建 table, stable。只有先創建 dnode,纔可能創建 vnode, 纔可能創建 vgroup。因此他們創建的順序是絕對不能錯的。

  2. 在客戶端應用的數據更新操作得到 TDengine 服務器側確認後,所執行的數據更新操作絕對不能丟失。否則會造成客戶端應用與服務器的數據不一致。

  3. 上述的 Meta Data 是容許重複操作的。比如插入新記錄後,再插入一次,刪除一次後,再刪除一次,更新一次後,再更新一次,不會對系統產生任何影響,不會改變系統任何狀態。

對於特點 1,本設計裏,數據的寫入是單線程的,按照到達的先後順序,給每個數據更新操作打上版本號,版本號大的記錄一定是晚於版本號小的寫入系統,數據寫入順序是 100% 保證的,絕對不會讓版本號大的記錄先寫入。複製過程中,數據塊的轉發也是嚴格按照順序進行的,因此 TDengine 的數據複製設計是能保證 Meta Data 的創建順序的。

對於特點 2,只要 Quorum 數設置等於 replica,那麼一定能保證回覆確認過的數據更新操作不會在服務器側丟失。即使某節點永不起來,只要超過一半的節點還是 online, 查詢服務不會受到任何影響。這時,如果某個節點離線超過一定時長,系統可以自動補充新的節點,以保證在線的節點數在絕大部分時間是 100% 的。

對於特點 3,完全可能發生,服務器確實持久化存儲了某一數據更新操作,但客戶端應用出了問題,認爲操作不成功,它會重新發起操作。但對於 Meta Data 而言,沒有關係,客戶端可以再次發起同樣的操作,不會有任何影響。

總結來看,只要 quorum 設置大於一,本數據複製的設計是能滿足 Meta Data 的需求的。目前,還沒有發現漏洞。

**作者簡介:**陶建輝,濤思數據創始人。1994 年到美國留學,1997 年起,先後在芝加哥 Motorola、3Com 等公司從事無線互聯網的研發工作。2008 年初回到北京創辦和信,後被聯發科收購。2013 年初創辦快樂媽咪,後被太平洋網絡收購。2017 年 5 月創辦濤思數據,不僅主導了 TDengine 的整體架構設計,還貢獻了 4 萬多行代碼,每天仍然戰鬥在研發第一線。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/4shoabOuBfcCb3z8wiNC4g