全部產品
Search
文件中心

ApsaraDB for MongoDB:MongoDB 複製集原理深度分析

更新時間:Jun 08, 2024

MongoDB複製集由一組MongoDB執行個體(進程)組成,包含一個Primary節點和多個Secondary節點,MongoDB Driver(用戶端)的所有資料都寫入Primary,Secondary從Primary同步寫入的資料,以保持複製集內所有成員儲存相同的資料集,提供資料的高可用。

下圖(圖片源於MongoDB官方文檔)是一個典型的MongoDB複製集,包含一個Primary節點和兩個Secondary節點。

Primary選舉(一)

複製集通過replSetInitiate命令(或mongoshell的rs.initiate())進行初始化,初始化後各個成員間開始發送心跳訊息,並發起Primary選舉操作,獲得大多數成員投票支援的節點,會成為Primary,其餘節點成為Secondary。

初始化複製集

    config = {
        _id : "my_replica_set",
        members : [
             {_id : 0, host : "rs1.example.net:27017"},
             {_id : 1, host : "rs2.example.net:27017"},
             {_id : 2, host : "rs3.example.net:27017"},
       ]
    }
    rs.initiate(config)

“大多數”的定義

假設複製集內投票成員(後續介紹)數量為N,則大多數為N/2 + 1,當複製集記憶體活成員數量不足大多數時,整個複製集將無法選舉出Primary,複製集將無法提供寫服務,處於唯讀狀態。

投票成員數

大多數

容忍失效數

1

1

0

2

2

0

3

2

1

4

3

1

5

3

2

6

4

2

7

4

3

通常建議將複製整合員數量設定為奇數,從上表可以看出3個節點和4個節點的複製集都只能容忍1個節點失效,從服務可用性的角度看,其效果是一樣的,但無疑4個節點能提供更可靠的資料存放區。

特殊的Secondary節點

正常情況下,複製集的Secondary會參與Primary選舉(自身也可能會被選為Primary),並從Primary同步最新寫入的資料,以保證與Primary儲存相同的資料。

Secondary可以提供讀服務,增加Secondary節點可以提供複製集的讀服務能力,同時提升複製集的可用性。另外,MongoDB支援對複製集的Secondary節點進行靈活的配置,以適應多種情境的需求。

  • Arbiter

    Arbiter節點只參與投票,不能被選為Primary,並且不從Primary同步資料。

    比如你部署了一個2個節點的複製集,1個Primary,1個Secondary,任意節點宕機,複製集將不能提供服務了(無法選出Primary),這時可以給複製集添加一個Arbiter節點,即使有節點宕機,仍能選出Primary。

    Arbiter本身不儲存資料,是非常輕量級的服務,當複製整合員為偶數時,最好加入一個Arbiter節點,以提升複製集可用性。

  • Priority0

    Priority0節點的選舉優先順序為0,不會被選舉為Primary。

    比如你跨機房A、B部署了一個複製集,並且想指定Primary必須在A機房,這時可以將B機房的複製整合員Priority設定為0,這樣Primary就一定會是A機房的成員。

    說明

    如果這樣部署,最好將大多數節點部署在A機房,否則網路磁碟分割時可能無法選出Primary。

  • Vote0

    MongoDB 3.0裡,複製整合員最多50個,參與Primary選舉投票的成員最多7個,其他成員(Vote0)的vote屬性必須設定為0,即不參與投票。

  • Hidden

    Hidden節點不能被選為主(Priority為0),並且對Driver不可見。

    因Hidden節點不會接受Driver的請求,可使用Hidden節點做一些資料備份、離線計算的任務,不會影響複製集的服務。

  • Delayed

    Delayed節點必須是Hidden節點,並且其資料落後於Primary一段時間(可配置,比如1個小時)。

    因Delayed節點的資料比Primary落後一段時間,當錯誤或者無效的資料寫入Primary時,可通過Delayed節點的資料來恢複到之前的時間點。

Primary選舉 (二)

Primary選舉除了在複製集初始化時發生,還有如下情境:

  • 複製集被reconfig

    Secondary節點檢測到Primary宕機時,會觸發新Primary的選舉,當有Primary節點主動StepDown(主動降級為Secondary)時,也會觸發新的Primary選舉。Primary的選舉受節點間心跳、優先順序、最新的oplog時間等多種因素影響。

    • 節點優先順序

      每個節點都傾向於投票給優先順序最高的節點。優先順序為0的節點不會主動發起Primary選舉。當Primary發現有優先順序更高Secondary,並且該Secondary的資料落後在10秒內,則Primary會主動降級,讓優先順序更高的Secondary有成為Primary的機會。

    • Optime

      擁有最新optime(最近一條oplog的時間戳記)的節點才能被選為Primary。

  • 網路磁碟分割

    只有在大多數投票節點間保持網路連通,才有機會被選Primary;如果Primary與大多數的節點中斷連線,Primary會主動降級為Secondary。當發生網路磁碟分割時,可能在短時間內出現多個Primary,所以Driver在寫入時,最好設定大多數成功的策略,這樣即使出現多個Primary,也只有一個Primary能成功寫入大多數。

資料同步

Primary與Secondary之間通過oplog來同步資料,Primary上的寫操作完成後,會向特殊的local.oplog.rs集合寫入一條oplog,Secondary不斷的從Primary擷取新的oplog並應用。

因oplog的資料會不斷增加,local.oplog.rs被設定成為一個capped集合,當容量達到配置上限時,會將最舊的資料刪除掉。另外考慮到oplog在Secondary上可能重複應用,oplog必須具有等冪性,即重複應用也會得到相同的結果。

如下oplog的格式,包含ts、h、op、ns、o等欄位。

    {
      "ts" : Timestamp(1446011584, 2),
      "h" : NumberLong("1687359108795812092"), 
      "v" : 2, 
      "op" : "i", 
      "ns" : "test.nosql", 
      "o" : { "_id" : ObjectId("563062c0b085733f34ab4129"), "name" : "mongodb", "score" : "100" } 
    }

欄位說明如下:

  • ts:操作時間,當前timestamp + 計數器,計數器每秒都被重設。

  • h:操作的全域唯一標識。

  • v:oplog版本資訊。

  • op:操作類型,取值說明:

    • i:插入操作。

    • u:更新操作。

    • d:刪除操作。

    • c:執行命令(如createDatabase,dropDatabase)。

    • n:空操作,特殊用途。

  • ns:操作針對的集合。

  • o:操作內容。

  • o2:操作查詢條件,僅update操作包含該欄位。

Secondary初次同步資料時,會先執行init sync,從Primary(或其他資料更新的Secondary)同步全量資料,然後不斷通過執行tailable cursor從Primary的local.oplog.rs集合裡查詢最新的oplog並應用到自身。

init sync過程:

  1. T1時間,從Primary同步所有資料庫的資料(local除外),通過listDatabases+ listCollections + cloneCollection命令組合完成,假設T2時間完成所有操作。

  2. 從Primary應用T1到T2時間段內的所有oplog,可能部分操作已經包含在步驟1,但由於oplog的等冪性,可重複應用。

  3. 根據Primary各集合的index設定,在Secondary上為相應集合建立index。(每個集合_id的index已在步驟1中完成)。

    說明

    oplog集合的大小應根據DB規模及應用寫入需求合理配置,配置得太大,會造成儲存空間的浪費;配置得太小,可能造成Secondary的init sync一直無法成功。比如在步驟1裡由於DB資料太多、並且oplog配置太小,導致oplog不足以儲存T1和T2時間內的所有oplog,這就使得Secondary無法從Primary上同步完整的資料集。

修改複製集配置

當需要修改複製集時,比如增加成員、刪除成員、或者修改成員配置(如priority、vote、hidden、delayed等屬性),可通過replSetReconfig命令(rs.reconfig())對複製集進行重新設定。

比如將複製集的第2個成員的priority屬性設定為2,可執行如下命令:

    cfg = rs.conf();
    cfg.members[1].priority = 2;
    rs.reconfig(cfg);

異常處理(rollback)

當Primary宕機時,如果有資料未同步到Secondary,並且在Primary重新加入時,新的Primary上已經發生了寫操作,則舊Primary需要復原部分操作,以保證資料集與新的Primary一致。

舊Primary將復原的資料寫到單獨的rollback目錄下,資料庫管理員可根據需要使用mongorestore進行恢複。

複製集的讀寫設定

  • Read Preference

    預設情況下,複製集的所有讀請求都發到Primary,Driver可通過設定Read Preference來將讀請求路由到其他的節點。

    • primary:預設規則,所有讀請求發到Primary。

    • primaryPreferred:Primary優先,如果Primary不可達,請求Secondary。

    • secondary:所有的讀請求都發到Secondary。

    • secondaryPreferred:Secondary優先,當所有Secondary不可達時,請求Primary。

    • nearest:讀請求發送到最近的可達節點上(通過ping探測得出最近的節點)。

  • Write Concern

    預設情況下,Primary完成寫操作即返回,Driver可通過配置Write Concern來設定寫成功的規則,詳情請參見Write Concern

    如下的write concern規則設定寫必須在大多數節點上成功,逾時時間為5秒。

        db.products.insert(
          { item: "envelopes", qty : 100, type: "Clasp" },
          { writeConcern: { w: "majority", wtimeout: 5000 } }
        )

    上面的設定方式是針對單個請求的,也可以修改複本集預設的write concern,這樣就不用單獨設定每個請求。

        cfg = rs.conf()
        cfg.settings = {}
        cfg.settings.getLastErrorDefaults = { w: "majority", wtimeout: 5000 }
        rs.reconfig(cfg)