做者: 凹凸曼-軍軍mongodb
前言:mongodb 由於高性能、高可用性、支持分片等特性,做爲非關係型數據庫被你們普遍使用。其高可用性主要是體如今 mongodb 的副本集上面(能夠簡單理解爲一主多從的集羣),本篇文章主要從副本集介紹、本地搭建副本集、副本集讀寫數據這三個方面來帶你們認識下 mongodb 副本集。shell
mongodb 副本集(Replica Set)包括主節點(primary)跟副本節點(Secondaries)。數據庫
主節點只能有一個,全部的寫操做請求都在主節點上面處理。副本節點能夠有多個,經過同步主節點的操做日誌(oplog)來備份主節點數據。後端
在主節點掛掉後,有選舉權限的副本節點會自動發起選舉,並從中選舉出新的主節點。安全
副本節點能夠經過配置指定其具體的屬性,好比選舉、隱藏、延遲同步等,最多能夠有50個副本節點,但只能有7個副本節點能參與選舉。雖然副本節點不能處理寫操做,但能夠處理讀請求,這個下文會專門講到。網絡
搭建一個副本集集羣最少須要三個節點:一個主節點,兩個備份節點,若是三個節點分佈合理,基本能夠保證線上數據99.9%安全。三個節點的架構以下圖所示:架構
若是隻有一個主節點,一個副本節點,且沒有資源拿來當第二個副本節點,那就能夠起一個仲裁者節點(arbiter),不存數據,只用來選舉用,以下圖所示:性能
當主節點掛掉後,那麼兩個副本節點會進行選舉,從中選舉出一個新的主節點,流程以下:測試
對於副本集成員屬性,特別須要說明下這幾個:priority、hidden、slaveDelay、tags、votes。ui
對於副本節點,能夠經過該屬性來增大或者減少該節點被選舉成爲主節點的可能性,取值範圍爲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 |
先建立三個目錄來分別存放這三個節點的數據
mkdir -p /data/mongodb/rs0-0 /data/mongodb/rs0-1 /data/mongodb/rs0-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
登陸到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。
殺掉主節點 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
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 } ] }
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
副本集寫關注是指寫入一條數據,主節點處理完成後,須要其餘承載數據的副本節點也確認寫成功後,才能給客戶端返回寫入數據成功。
這個功能主要是解決主節點掛掉後,數據還將來得及同步到副本節點,而致使數據丟失的問題。
能夠配置節點個數,默認配置 {「w」:1},這樣表示主節點寫入數據成功便可給客戶端返回成功,「w」 配置爲2,則表示除了主節點,還須要收到其中一個副本節點返回寫入成功,「w」 還能夠配置爲 "majority",表示須要集羣中大多數承載數據且有選舉權限的節點返回寫入成功。
以下圖所示,P-S-S 結構(一個 primary 節點,兩個 secondary 節點),寫請求裏面帶了w : 「majority" ,那麼主節點寫入完成後,數據同步到第一個副本節點,且第一個副本節點回複數據寫入成功後,纔給客戶端返回成功。
關於寫關注在實際中如何操做,有下面兩種方法:
db.products.insert( { item: "envelopes", qty : 100, type: "Clasp" }, { writeConcern: { w: "majority" , wtimeout: 5000 } } )
cfg = rs.conf() cfg.settings.getLastErrorDefaults = { w: "majority", wtimeout: 5000 } rs.reconfig(cfg)
讀跟寫不同,爲了保持一致性,寫只能經過主節點,但讀能夠選擇主節點,也能夠選擇副本節點,區別是主節點數據最新,副本節點由於同步問題可能會有延遲,但從副本節點讀取數據能夠分散對主節點的壓力。
由於承載數據的節點會有多個,那客戶端如何選擇從那個節點讀呢?主要有3個條件(Tag Sets、 maxStalenessSeconds、Hedged Read),5種模式(primary、primaryPreferred、secondary、secondaryPreferred、nearest)
模式 | 特色 |
---|---|
primary | 全部讀請求都從主節點讀取 |
primaryPreferred | 主節點正常,則全部讀請求都從主節點讀取,若是主節點掛掉,則從符合條件的副本節點讀取 |
secondary | 全部讀請求都從副本節點讀取 |
secondaryPreferred | 全部讀請求都從副本節點讀取,但若是副本節點都掛掉了,那就從主節點讀取 |
nearest | 主要看網絡延遲,選取延遲最小的節點,主節點跟副本節點都可 |
顧名思義,這個能夠給節點加上標籤,而後查找數據時,能夠根據標籤選擇對應的節點,而後在該節點查找數據。能夠經過mongo shell 使用 rs.conf() 查看當前每一個節點下面的 tags, 修改或者添加tags 過程同上面修改 getLastErrorDefaults 配置 ,如: cfg.members[n].tags = { "region": "South", "datacenter": "A" }
顧名思義+1,這個值是指副本節點同步主節點寫入的時間 跟 主節點實際最近寫入時間的對比值,若是主節點掛掉了,那就跟副本集中最新寫入的時間作對比。
這個值建議設置,避免由於部分副本節點網絡緣由致使比較長時間未同步主節點數據,而後讀到比較老的數據。特別注意的是該值須要設置 90s 以上,由於客戶端是定時去校驗副本節點的同步延遲時間,數據不會特別準確,設置比 90s 小,會拋出異常。
該選項是在分片集羣 MongoDB 4.4 版本後才支持,指 mongos 實例路由讀取請求時會同時發給兩個符合條件的副本集節點,而後那個先返回結果就返回這個結果給客戶端。
參數 | 說明 |
---|---|
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
cursor.readPref() 參數分別爲: mode、tag set、hedge options, 具體請求例以下面這樣
db.collection.find({ }).readPref( "secondary", // mode [ { "datacenter": "B" }, { } ], // tag set { enabled: true } // hedge options )
Mongo.setReadPref() 相似,只是預先設置請求條件,這樣就不用每一個請求後面帶上 readPref 條件。
登陸主節點: mongo localhost:27018
插入一條數據: db.nums.insert({name: 「num0」})
在當前節點查詢: db.nums.find()
能夠看到本條數據: { "_id" : ObjectId("5f958687233b11771912ced5"), "name" : "num0" }
登陸副本節點: 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" }
歡迎關注凹凸實驗室博客:aotu.io
或者關注凹凸實驗室公衆號(AOTULabs),不定時推送文章: