mongodb 副本集之入門篇

做者: 凹凸曼-軍軍mongodb

前言:mongodb 由於高性能、高可用性、支持分片等特性,做爲非關係型數據庫被你們普遍使用。其高可用性主要是體如今 mongodb 的副本集上面(能夠簡單理解爲一主多從的集羣),本篇文章主要從副本集介紹、本地搭建副本集、副本集讀寫數據這三個方面來帶你們認識下 mongodb 副本集。shell

1、 mongodb 副本集介紹

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

主節點只能有一個,全部的寫操做請求都在主節點上面處理。副本節點能夠有多個,經過同步主節點的操做日誌(oplog)來備份主節點數據。後端

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

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

搭建一個副本集集羣最少須要三個節點:一個主節點,兩個備份節點,若是三個節點分佈合理,基本能夠保證線上數據99.9%安全。三個節點的架構以下圖所示:架構

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

當主節點掛掉後,那麼兩個副本節點會進行選舉,從中選舉出一個新的主節點,流程以下:測試

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

  • 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個副本節點參與選舉。

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

安裝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

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

寫關注(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),不定時推送文章:

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

相關文章
相關標籤/搜索