mongodb 副本集之入門篇


作者: 凹凸曼-軍軍

前言:mongodb 因為高性能、高可用性、支持分片等特性,作為非關系型數據庫被大家廣泛使用。其高可用性主要是體現在 mongodb 的副本集上面(可以簡單理解為一主多從的集群),本篇文章主要從副本集介紹、本地搭建副本集、副本集讀寫數據這三個方面來帶大家認識下 mongodb 副本集。

一、 mongodb 副本集介紹

mongodb 副本集(Replica Set)包括主節點(primary)跟副本節點(Secondaries)。

主節點只能有一個,所有的寫操作請求都在主節點上面處理。副本節點可以有多個,通過同步主節點的操作日志(oplog)來備份主節點數據。

在主節點掛掉后,有選舉權限的副本節點會自動發起選舉,並從中選舉出新的主節點。

副本節點可以通過配置指定其具體的屬性,比如選舉、隱藏、延遲同步等,最多可以有50個副本節點,但只能有7個副本節點能參與選舉。雖然副本節點不能處理寫操作,但可以處理讀請求,這個下文會專門講到。

搭建一個副本集集群最少需要三個節點:一個主節點,兩個備份節點,如果三個節點分布合理,基本可以保證線上數據99.9%安全。三個節點的架構如下圖所示:

如果只有一個主節點,一個副本節點,且沒有資源拿來當第二個副本節點,那就可以起一個仲裁者節點(arbiter),不存數據,只用來選舉用,如下圖所示:

當主節點掛掉后,那么兩個副本節點會進行選舉,從中選舉出一個新的主節點,流程如下:

對於副本集成員屬性,特別需要說明下這幾個:priority、hidden、slaveDelay、tags、votes。

  • priority

對於副本節點,可以通過該屬性來增大或者減小該節點被選舉成為主節點的可能性,取值范圍為0-1000(如果是arbiters,則取值只有0或者1),數據越大,成為主節點的可能性越大,如果被配置為0,那么他就不能被選舉成為主節點,而且也不能主動發起選舉。

這種特性一般會被用在有多個數據中心的情況下,比如一個主數據中心,一個備份數據中心,主數據中心速度會更快,如果主節點掛掉,我們肯定希望新主節點也在主數據中心產生,那么我們就可以設置在備份數據中心的副本節點優先級為0,如下圖所示:

  • hidden

    隱藏節點會從主節點同步數據,但對客戶端不可見,在mongo shell 執行 db.isMaster() 方法也不會展示該節點,隱藏節點必須Priority為0,即不可以被選舉成為主節點。但是如果有配置選舉權限的話,可以參與選舉。

    因為隱藏節點對客戶端不可見,所以跟客戶端不會互相影響,可以用來備份數據或者跑一些后端定時任務之類的操作,具體如下圖,4個備份節點都從主節點同步數據,其中1個為隱藏節點:

  • slaveDelay

    延遲同步即延遲從主節點同步數據,比如延遲時間配置的1小時,現在時間是 09:52,那么延遲節點中只同步到主節點 08:52 之前的數據。另外需要注意延遲節點必須是隱藏節點,且Priority為0。

    那這個延遲節點有什么用呢?有過數據庫誤操作慘痛經歷的開發者肯定知道答案,那就是為了防止數據庫誤操作,比如更新服務前,一般會先執行數據庫更新腳本,如果腳本有問題,且操作前未做備份,那數據可能就找不回了。但如果說配置了延遲節點,那誤操作完,還有該節點可以兜底,只能說該功能真是貼心。具體延遲節點如下圖所展示:

  • tags

    支持對副本集成員打標簽,在查詢數據時會用到,比如找到對應標簽的副本節點,然后從該節點讀取數據,這點也非常有用,可以根據標簽對節點分類,查詢數據時不同服務的客戶端指定其對應的標簽的節點,對某個標簽的節點數量進行增加或減少,也不怕會影響到使用其他標簽的服務。Tags 的具體使用,文章下面章節也會講到。

  • votes

    表示節點是否有權限參與選舉,最大可以配置7個副本節點參與選舉。

二、副本集的搭建以及測試

安裝mongodb 教程:https://docs.mongodb.com/manual/installation/

我們來搭建一套 P-S-S 結構的副本集(1個 Primary 節點,2個 Secondary 節點),大致過程為:先啟動三個不同端口的 mongod 進程,然后在 mongo shell 中執行命令初始化副本集。

啟動單個mongod 實例的命令為:

mongod --replSet rs0 --port 27017 --bind_ip localhost,<hostname(s)|ip address(es)> --dbpath /data/mongodb/rs0-0 --oplogSize 128

參數說明:

參數 說明 示例
replSet 副本集名稱 rs0
port mongod 實例端口 27017
bind_ip 訪問該實例的地址列表,只是本機訪問可以設置為localhost 或者 127.0.0.1,生產環境建議使用內部域名 Localhost
dbpath 數據存放位置 /data/mongodb/rs0-0
oplogSize 操作日志大小 128

搭建步驟如下:

  1. 先創建三個目錄來分別存放這三個節點的數據

    mkdir -p /data/mongodb/rs0-0 /data/mongodb/rs0-1 /data/mongodb/rs0-2

  2. 分別啟動三個mongod 進程,端口分別為:27018,27019,27020

第一個:
mongod --replSet rs0 --port 27018 --bind_ip localhost --dbpath /data/mongodb/rs0-0 --oplogSize 128

第二個:
mongod --replSet rs0 --port 27019 --bind_ip localhost --dbpath /data/mongodb/rs0-1 --oplogSize 128

第三個:
mongod --replSet rs0 --port 27020 --bind_ip localhost --dbpath /data/mongodb/rs0-2 --oplogSize 128

  1. 使用 mongo 進入第一個 mongod 示例,使用 rs.initiate() 進行初始化

登錄到27018: mongo localhost:27018

執行:

rsconf = {
    _id: "rs0",
    members: [
      {
        _id: 0,
        host: "localhost:27018"
      },
      {
        _id: 1,
        host: "localhost:27019"
      },
      {
        _id: 2,
        host: "localhost:27020"
      }
    ]
}

rs.initiate( rsconf )

以上就已經完成了一個副本集的搭建,在 mongo shell 中執行 rs.conf() 可以看到每個節點中 host、arbiterOnly、hidden、priority、 votes、slaveDelay等屬性,是不是超級簡單。。

執行 rs.conf() ,結果展示如下:

rs.conf()
{
    "_id" : "rs0",
    "version" : 1,
    "protocolVersion" : NumberLong(1),
    "writeConcernMajorityJournalDefault" : true,
    "members" : [
      {
        "_id" : 0,
        "host" : "localhost:27018",
        "arbiterOnly" : false,
        "buildIndexes" : true,
        "hidden" : false,
        "priority" : 1,
        "tags" : {
  
        },
        "slaveDelay" : NumberLong(0),
        "votes" : 1
      },
      {
        "_id" : 1,
        "host" : "localhost:27019",
        "arbiterOnly" : false,
        "buildIndexes" : true,
        "hidden" : false,
        "priority" : 1,
        "tags" : {
  
        },
        "slaveDelay" : NumberLong(0),
        "votes" : 1
      },
      {
        "_id" : 2,
        "host" : "localhost:27020",
        "arbiterOnly" : false,
        "buildIndexes" : true,
        "hidden" : false,
        "priority" : 1,
        "tags" : {
  
        },
        "slaveDelay" : NumberLong(0),
        "votes" : 1
      }
    ],
    "settings" : {
      "chainingAllowed" : true,
      "heartbeatIntervalMillis" : 2000,
      "heartbeatTimeoutSecs" : 10,
      "electionTimeoutMillis" : 10000,
      "catchUpTimeoutMillis" : -1,
      "catchUpTakeoverDelayMillis" : 30000,
      "getLastErrorModes" : {
  
      },
      "getLastErrorDefaults" : {
        "w" : 1,
        "wtimeout" : 0
      },
      "replicaSetId" : ObjectId("5f957f12974186fc616688fb")
    }
}

特別注意下:在 mongo shell 中,有 rs 跟 db。

  • rs 是指副本集,有rs.initiate(),rs.conf(), rs.reconfig(), rs.add() 等操作副本集的方法
  • db 是指數據庫,其下是對數據庫的一些操作,比如下面會用到 db.isMaster(), db.collection.find(), db.collection.insert() 等。

我們再來測試下 Automatic Failover

  1. 可以直接停掉主節點localhost:27018 來測試下主節點掛掉后,副本節點重新選舉出新的主節點,即自動故障轉移(Automatic Failover)

殺掉主節點 27018后,可以看到 27019 的輸出日志里面選舉部分,27019 發起選舉,並成功參選成為主節點:

2020-10-26T21:43:58.156+0800 I  REPL     [replexec-304] Scheduling remote command request for vote request: RemoteCommand 100694 -- target:localhost:27018 db:admin cmd:{ replSetRequestVotes: 1, setName: "rs0", dryRun: false, term: 17, candidateIndex: 1, configVersion: 1, lastCommittedOp: { ts: Timestamp(1603719830, 1), t: 16 } }
2020-10-26T21:43:58.156+0800 I  REPL     [replexec-304] Scheduling remote command request for vote request: RemoteCommand 100695 -- target:localhost:27020 db:admin cmd:{ replSetRequestVotes: 1, setName: "rs0", dryRun: false, term: 17, candidateIndex: 1, configVersion: 1, lastCommittedOp: { ts: Timestamp(1603719830, 1), t: 16 } }
2020-10-26T21:43:58.159+0800 I  ELECTION [replexec-301] VoteRequester(term 17) received an invalid response from localhost:27018: ShutdownInProgress: In the process of shutting down; response message: { operationTime: Timestamp(1603719830, 1), ok: 0.0, errmsg: "In the process of shutting down", code: 91, codeName: "ShutdownInProgress", $clusterTime: { clusterTime: Timestamp(1603719830, 1), signature: { hash: BinData(0, 0000000000000000000000000000000000000000), keyId: 0 } } }
2020-10-26T21:43:58.164+0800 I  ELECTION [replexec-305] VoteRequester(term 17) received a yes vote from localhost:27020; response message: { term: 17, voteGranted: true, reason: "", ok: 1.0, $clusterTime: { clusterTime: Timestamp(1603719830, 1), signature: { hash: BinData(0, 0000000000000000000000000000000000000000), keyId: 0 } }, operationTime: Timestamp(1603719830, 1) }
2020-10-26T21:43:58.164+0800 I  ELECTION [replexec-304] election succeeded, assuming primary role in term 17
  1. 然后執行 rs.status() 查看當前副本集情況,可以看到27019變為主節點,27018 顯示已掛掉 health = 0
rs.status()
{
    "set" : "rs0",
    "date" : ISODate("2020-10-26T13:44:22.071Z"),
    "myState" : 1,
    "heartbeatIntervalMillis" : NumberLong(2000),
    "majorityVoteCount" : 2,
    "writeMajorityCount" : 2,
    "members" : [
      {
        "_id" : 0,
        "name" : "localhost:27018",
        "ip" : "127.0.0.1",
        "health" : 0,
        "state" : 8,
        "stateStr" : "(not reachable/healthy)",
        "uptime" : 0,
        "optime" : {
          "ts" : Timestamp(0, 0),
          "t" : NumberLong(-1)
        },
        "optimeDurable" : {
          "ts" : Timestamp(0, 0),
          "t" : NumberLong(-1)
        },
        "optimeDate" : ISODate("1970-01-01T00:00:00Z"),
        "optimeDurableDate" : ISODate("1970-01-01T00:00:00Z"),
        "lastHeartbeat" : ISODate("2020-10-26T13:44:20.202Z"),
        "lastHeartbeatRecv" : ISODate("2020-10-26T13:43:57.861Z"),
        "pingMs" : NumberLong(0),
        "lastHeartbeatMessage" : "Error connecting to localhost:27018 (127.0.0.1:27018) :: caused by :: Connection refused",
        "syncingTo" : "",
        "syncSourceHost" : "",
        "syncSourceId" : -1,
        "infoMessage" : "",
        "configVersion" : -1
      },
      {
        "_id" : 1,
        "name" : "localhost:27019",
        "ip" : "127.0.0.1",
        "health" : 1,
        "state" : 1,
        "stateStr" : "PRIMARY",
        "uptime" : 85318,
        "optime" : {
          "ts" : Timestamp(1603719858, 1),
          "t" : NumberLong(17)
        },
        "optimeDate" : ISODate("2020-10-26T13:44:18Z"),
        "syncingTo" : "",
        "syncSourceHost" : "",
        "syncSourceId" : -1,
        "infoMessage" : "",
        "electionTime" : Timestamp(1603719838, 1),
        "electionDate" : ISODate("2020-10-26T13:43:58Z"),
        "configVersion" : 1,
        "self" : true,
        "lastHeartbeatMessage" : ""
      },
      {
        "_id" : 2,
        "name" : "localhost:27020",
        "ip" : "127.0.0.1",
        "health" : 1,
        "state" : 2,
        "stateStr" : "SECONDARY",
        "uptime" : 52468,
        "optime" : {
          "ts" : Timestamp(1603719858, 1),
          "t" : NumberLong(17)
        },
        "optimeDurable" : {
          "ts" : Timestamp(1603719858, 1),
          "t" : NumberLong(17)
        },
        "optimeDate" : ISODate("2020-10-26T13:44:18Z"),
        "optimeDurableDate" : ISODate("2020-10-26T13:44:18Z"),
        "lastHeartbeat" : ISODate("2020-10-26T13:44:20.200Z"),
        "lastHeartbeatRecv" : ISODate("2020-10-26T13:44:21.517Z"),
        "pingMs" : NumberLong(0),
        "lastHeartbeatMessage" : "",
        "syncingTo" : "localhost:27019",
        "syncSourceHost" : "localhost:27019",
        "syncSourceId" : 1,
        "infoMessage" : "",
        "configVersion" : 1
      }
    ]
}
  1. 再次啟動27018:
    mongod --replSet rs0 --port 27018 --bind_ip localhost --dbpath /data/mongodb/rs0-0 --oplogSize 128

可以在節點 27019 日志中看到已檢測到 27018,並且已變為副本節點,通過rs.status 查看結果也是如此。

2020-10-26T21:52:06.871+0800 I  REPL     [replexec-305] Member localhost:27018 is now in state SECONDARY

三、副本集寫跟讀的一些特性

寫關注(Write concern)

副本集寫關注是指寫入一條數據,主節點處理完成后,需要其他承載數據的副本節點也確認寫成功后,才能給客戶端返回寫入數據成功。

這個功能主要是解決主節點掛掉后,數據還未來得及同步到副本節點,而導致數據丟失的問題。

可以配置節點個數,默認配置 {“w”:1},這樣表示主節點寫入數據成功即可給客戶端返回成功,“w” 配置為2,則表示除了主節點,還需要收到其中一個副本節點返回寫入成功,“w” 還可以配置為 "majority",表示需要集群中大多數承載數據且有選舉權限的節點返回寫入成功。

如下圖所示,P-S-S 結構(一個 primary 節點,兩個 secondary 節點),寫請求里面帶了w : “majority" ,那么主節點寫入完成后,數據同步到第一個副本節點,且第一個副本節點回復數據寫入成功后,才給客戶端返回成功。

關於寫關注在實際中如何操作,有下面兩種方法:

  1. 在寫請求中指定 writeConcern 相關參數,如下:
db.products.insert(
    { item: "envelopes", qty : 100, type: "Clasp" },
    { writeConcern: { w: "majority" , wtimeout: 5000 } }
)
  1. 修改副本集 getLastErrorDefaults 配置,如下:
cfg = rs.conf()
cfg.settings.getLastErrorDefaults = { w: "majority", wtimeout: 5000 }
rs.reconfig(cfg)

讀偏好 (Read preference)

讀跟寫不一樣,為了保持一致性,寫只能通過主節點,但讀可以選擇主節點,也可以選擇副本節點,區別是主節點數據最新,副本節點因為同步問題可能會有延遲,但從副本節點讀取數據可以分散對主節點的壓力。

因為承載數據的節點會有多個,那客戶端如何選擇從那個節點讀呢?主要有3個條件(Tag Sets、 maxStalenessSeconds、Hedged Read),5種模式(primary、primaryPreferred、secondary、secondaryPreferred、nearest)

首先說一下 5種模式,其特點如下表所示:

模式 特點
primary 所有讀請求都從主節點讀取
primaryPreferred 主節點正常,則所有讀請求都從主節點讀取,如果主節點掛掉,則從符合條件的副本節點讀取
secondary 所有讀請求都從副本節點讀取
secondaryPreferred 所有讀請求都從副本節點讀取,但如果副本節點都掛掉了,那就從主節點讀取
nearest 主要看網絡延遲,選取延遲最小的節點,主節點跟副本節點均可

再說下3個條件,條件是在符合模式的基礎上,再根據條件刪選具體的節點

  1. Tag Sets(標簽)

顧名思義,這個可以給節點加上標簽,然后查找數據時,可以根據標簽選擇對應的節點,然后在該節點查找數據。可以通過mongo shell 使用 rs.conf() 查看當前每個節點下面的 tags, 修改或者添加tags 過程同上面修改 getLastErrorDefaults 配置 ,如: cfg.members[n].tags = { "region": "South", "datacenter": "A" }

  1. maxStalenessSeconds (可容忍的最大同步延遲)

顧名思義+1,這個值是指副本節點同步主節點寫入的時間 跟 主節點實際最近寫入時間的對比值,如果主節點掛掉了,那就跟副本集中最新寫入的時間做對比。

這個值建議設置,避免因為部分副本節點網絡原因導致比較長時間未同步主節點數據,然后讀到比較老的數據。特別注意的是該值需要設置 90s 以上,因為客戶端是定時去校驗副本節點的同步延遲時間,數據不會特別准確,設置比 90s 小,會拋出異常。

  1. Hedged Read (對沖讀取)

該選項是在分片集群 MongoDB 4.4 版本后才支持,指 mongos 實例路由讀取請求時會同時發給兩個符合條件的副本集節點,然后那個先返回結果就返回這個結果給客戶端。

那問題來了,如此好用的模式以及條件在查詢請求中如何使用呢?

  1. 在代碼中連接數據庫,使用 connection string uri 時,可以加上下面的這三個參數
參數 說明
readPreference 模式,枚舉值有:primary(默認值)、 primaryPreferred、secondary、secondaryPreferred、nearest
maxStalenessSeconds 最大同步延時秒數,取值0 - 90 會報錯, -1 表示沒有最大值
readPreferenceTags 標簽,如果標簽是 { "dc": "ny", "rack": "r1" }, 則在uri 為 readPreferenceTags=dc:ny,rack:r1

例如下面:

mongodb://db0.example.com,db1.example.com,db2.example.com/?replicaSet=myRepl&readPreference=secondary&maxStalenessSeconds=120&readPreferenceTags=dc:ny,rack:r1

  1. 在mogo shell 中,可以使用 cursor.readPref() 或者 Mongo.setReadPref()

cursor.readPref() 參數分別為: mode、tag set、hedge options, 具體請求例如下面這樣

db.collection.find({ }).readPref(
    "secondary",                      // mode
    [ { "datacenter": "B" },  { } ],  // tag set
    { enabled: true }                 // hedge options
)

Mongo.setReadPref() 類似,只是預先設置請求條件,這樣就不用每個請求后面帶上 readPref 條件。

可以在搭建好的集群中簡單測試下該功能

  1. 登錄主節點: mongo localhost:27018

  2. 插入一條數據: db.nums.insert({name: “num0”})

    在當前節點查詢: db.nums.find()

    可以看到本條數據: { "_id" : ObjectId("5f958687233b11771912ced5"), "name" : "num0" }

  3. 登錄副本節點: mongo localhost:27019

查詢:db.nums.find()

因為查詢模式默認為 primary,所以在副本節點查詢會報錯,如下:

Error: error: {
  "operationTime" : Timestamp(1603788383, 1),
  "ok" : 0,
  "errmsg" : "not master and slaveOk=false",
  "code" : 13435,
  "codeName" : "NotMasterNoSlaveOk",
  "$clusterTime" : {
    "clusterTime" : Timestamp(1603788383, 1),
    "signature" : {
      "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
      "keyId" : NumberLong(0)
    }
  }
}

查詢時指定模式為 “secondary”: db.nums.find().readPref(“secondary")

就可以查詢到插入的數據: { "_id" : ObjectId("5f958687233b11771912ced5"), "name" : "num0" }

結語

以上內容都是閱讀 MongoDB 官方文檔后,然后挑簡單且重要的一些點做的總結,如果大家對 MongoDB 感興趣,建議直接啃一啃官方文檔

歡迎關注凹凸實驗室博客:aotu.io

或者關注凹凸實驗室公眾號(AOTULabs),不定時推送文章:

歡迎關注凹凸實驗室公眾號


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM