咱們之前用Mysql的時候,常常是一臺服務器走天下,若是隻是用於學習,是沒有問題的,可是在生產環境中,這樣的風險是很大的,若是服務器由於網絡緣由或者崩潰了,就會致使數據庫一段時間了不可用,這樣的體驗很很差。redis
那麼應該怎麼辦呢?既然一臺機器不行,我就多上幾臺機器總能夠了吧,好比我上個兩臺,讓他們互爲主備,相互同步數據。想到這裏我就只想說一個字,穩。sql
其實redis,mongodb,kafka等分佈式應用基本上都是這樣的思想mongodb
MongoDB也差很少是這樣的思想。它經過複製集來解決這個問題,MongoDB複製集由一組Mongod進程組成,包含一個Primary節點和多個Secondary節點,Mongodb Driver(客戶端)的全部數據都寫入Primary,Secondary從Primary同步寫入的數據,以保持複製集內全部成員存儲相同的數據集,提供數據的高可用。數據庫
要想成爲primary節點,你必須保證大多數節點都贊成才行,大多數的節點就是副本中一半以上的成員。安全
成員總數 | 大多數 | 容忍失敗數 |
---|---|---|
1 | 1 | 0 |
2 | 2 | 0 |
3 | 2 | 1 |
4 | 3 | 1 |
5 | 3 | 2 |
6 | 4 | 2 |
7 | 4 | 3 |
爲何要要求大多數呢?實際上是爲了不出現兩個primary節點。好比一個五個節點的複製集,其中3個成員不可用,剩下的2個仍然正常工做。這兩個工做的節點因爲不能知足複製集大多數的要求(這個例子中要求要有3個節點纔是大多數),因此他們沒法選擇主節點,即便其中有一個節點是primary節點,當它注意到它沒法獲取大多數節點的支持時,它就會退位,成爲備份節點。bash
若是讓這兩個節點能夠選出primary節點,問題是另外3個節點可能不是真正掛了,而只是網絡不可達而已。另外3個節點就必定能夠選擇出primary節點,這樣就存在了兩個primary節點了。 因此要求大多數就能夠避免產生兩個primary節點的問題。服務器
若是MongoDB副本集能夠擁有多個primary節點,那麼就會面臨寫入衝突的問題,在支持多線程寫入的系統中解決衝突的方式有手動解決和讓操做系統任選一個這兩種方式,可是這兩種方式都不易實現,沒法保證寫入的數據不被其餘節點修改,所以mongodb只支持單一的primary節點,這樣使得開發更容易。網絡
當一個備份節點沒法與主節點連通時,它會聯繫並請求其餘副本集成員將本身選舉爲主節點,其餘成員會作幾項理性的檢查:自身是否可以與主節點連通?但願被選舉爲主節點的備份節點的數據是否最新?有沒有其餘更高優先級的成員能夠被選舉爲主節點? 若是競選節點成員可以獲得大多數投票,就會成爲主節點。可是一旦大多數成員中只有一個否決了本次選舉,選舉就會取消。多線程
在日誌中能夠看到得票數爲比較大的負數的狀況,由於一張否決票至關於10000張同意票。若是有2張同意票,2張否決票,那麼選舉結果就是-19998,依此類推。app
咱們通常在部署的時候,副本集節點個數至少是3個(由於它容許1個失敗),這也就意味着數據要被複制三份。
不少人的應用程序使用量比較小,不想保存三份數據,只想要保存兩份就好了,保存第三份純粹是浪費。對於這種部署MongoDB也是支持的。它有一種特殊的成員叫作仲裁者(arbiter),它惟一的做用就是參與選舉,它既不保存數據也不爲客戶端提供服務,只是爲了幫助只有兩個成員的副本集知足大多數這個條件而已。
仲裁者其實也是有缺點的。若是真有一個節點掛了(數據沒法恢復),另外一個成員稱爲主節點。爲了數據安全,就須要一個新的備份節點,而且將主節點的數據備份到備份節點。複製數據會對服務器形成很大的壓力,會拖慢應用程序。相反若是有三個數據成員即便其中一個掛了,仍有一個主節點和一個備份節點,不影響正常運做。這個時候還能夠用剩下的那個備份節點來初始化一個新的備份節點服務器,而不依賴於主節點。因此若是可能儘量在副本集中使用奇數個數據成員,而不要使用仲裁者。
若是想讓一個節點有更大的機會成爲primary的話這須要設置優先級,好比我添加一個優先級爲2的成員(默認爲1)
rs.add({"_id":4, "host": "10.17.28.190:27017", "priority" : 2});
複製代碼
假設其餘都是默認優先級,只要10.17.28.190擁有最新數據,那麼當前primary節點就會自動退位,10.17.28.190會被選舉爲新的主節點。若是它的數據不夠新,那麼當前主節點就會保持不變。
若是設置priority爲0,表示不會被選爲primary節點。
因爲複製集成員最多50個,而參與Primary成員投票的最多7個,因此其餘成員的vote必須設置爲0(priority也必須爲0)。 儘管無投票權的成員不會在選舉中投票,但這些成員擁有副本集數據的副本,而且能夠接受來自客戶端應用程序的讀取操做。
客戶端不會像隱藏成員發送請求,隱藏成員也不會做爲複製源(儘管當其餘複製源不可用時隱藏成員)。所以不少人將不夠強大的服務器或者備份服務器隱藏起來。經過設置hidden:true能夠設置隱藏,只有優先級爲0的才能被隱藏。 可以使用Hidden節點作一些數據備份、離線計算的任務,不會影響複製集的服務
數據可能會由於人爲錯誤而遭到毀滅性的破壞,爲了防止這類問題,可使用slaveDelay設置一個延遲的備份節點。
延遲備份節點的數據回比主節點延遲指定的時間(單位是秒),slaveDelay要求優先級是0,若是應用會將讀請求路由到備份節點,應該將延遲備份節點隱藏掉,以避免讀請求被路由到延遲備份節點。
因Delayed節點的數據比Primary落後一段時間,當錯誤或者無效的數據寫入Primary時,可經過Delayed節點的數據來恢復到以前的時間點。
好比我有個副本集叫作rs0,我想修改增長或者刪除成員,修改爲員的配置(vote,hidden,priority等)能夠經過reconfig命令
cfg = rs.conf();
cfg.members[1].priority = 2;
rs.reconfig(cfg);
複製代碼
Primary與Secondary之間經過oplog來同步數據,Primary上的寫操做完成後,會向特殊的local.oplog.rs特殊集合寫入一條oplog,Secondary不斷的從Primary取新的oplog並應用。
因oplog的數據會不斷增長,local.oplog.rs被設置成爲一個capped集合,當容量達到配置上限時,會將最舊的數據刪除掉。因爲複製操做的過程是先複製數據在寫入oplog,oplog必須具備冪等性,即重複應用也會獲得相同的結果。
我向test庫的coll集合插入了一條數據以後(db.coll.insert({count:1})
),調用db.isMaster()命令能夠看到當前節點的最後一次寫入時間戳
> db.isMaster()
{
"ismaster" : true,
"secondary" : false,
"lastWrite" : {
"opTime" : {
"ts" : Timestamp(1572509087, 2),
"t" : NumberLong(1)
},
"lastWriteDate" : ISODate("2019-10-31T08:04:47Z"),
"majorityOpTime" : {
"ts" : Timestamp(1572509087, 2),
"t" : NumberLong(1)
},
"majorityWriteDate" : ISODate("2019-10-31T08:04:47Z")
}
}
複製代碼
命令會返回不少數據,這裏我只列出了小部分,能夠看到咱們當前所在節點是master節點(primary),若是當前節點不是primary,也會經過primary屬性告訴你當前primary節點是哪一個,同時最後一次寫入的時間戳是1572509087。
此時咱們登陸另外一臺secondary節點,切換到local數據庫,執行命令db.oplog.rs.find()
命令,會返回不少條數據,這裏咱們查看最後一條便可
{
"ts" : Timestamp(1572509087, 2),
"t" : NumberLong(1),
"h" : NumberLong("6139682004250579847"),
"v" : 2,
"op" : "i",
"ns" : "test.coll",
"ui" : UUID("1be7f8d0-fde2-4d68-89ea-808f14b326da"),
"wall" : ISODate("2019-10-31T08:04:47.925Z"),
"o" : {
"_id" : ObjectId("5dba959fcf287dfd8727a1bf"),
"count" : 1
}
}
複製代碼
能夠看到oplog的ts和isMater()命令返回的lastTime.opTime.ts的值是一致的,證實咱們的數據是最新的,若是你這個時候訪問其餘節點查看oplog.rs的數據,會發現數據是如出一轍的。 在來解釋下字段含義
副本集中的成員啓動以後,就會檢查自身狀態,肯定是否能夠從某個成員那裏進行同步。若是不行的話,它會嘗試從副本的另外一個成員那裏進行完整的數據複製。這個過程就是初始化同步(initial syncing)。
init sync過程包含以下步驟
總結起來就是從其餘節點同步全量數據,而後不過從Primary的local.oplog.rs集合裏查詢最新的oplog並應用到自身。
查詢固定集合使用的tailable cursor(docs.mongodb.com/manual/core…)
Primary選舉除了在複製集初始化時發生,還有以下場景
Primary的選舉受節點間心跳、優先級、最新的oplog時間等多種因素影響。
複製集成員間默認每2s會發送一次心跳信息,若是10s未收到某個節點的心跳,則認爲該節點已宕機;若是宕機的節點爲Primary,Secondary(前提是可被選爲Primary)會發起新的Primary選舉。
心跳是爲了知道其餘成員狀態,哪一個是主節點,哪一個能夠做爲同步源,哪一個掛掉了等等信息
成員狀態:
最新optime(最近一條oplog的時間戳)的節點才能被選爲主,請看上面對oplog.rs的分析。
只有大多數投票節點間保持網絡連通,纔有機會被選Primary;若是Primary與大多數的節點斷開鏈接,Primary會主動降級爲Secondary。當發生網絡分區時,可能在短期內出現多個Primary,故Driver在寫入時,最好設置大多數成功的策略,這樣即便出現多個Primary,也只有一個Primary能成功寫入大多數。
默認狀況下,複製集的全部讀請求都發到Primary,Driver可經過設置Read Preference來將讀請求路由到其餘的節點。
默認狀況下,Primary完成寫操做即返回,Driver可經過設置Write Concern來設置寫成功的規則。
以下的write concern規則設置寫必須在大多數節點上成功,超時時間爲5s。
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)
複製代碼
Primary執行了一個寫請求以後掛了,可是備份節點尚未來得及複製此次操做。新選舉出來的主節點結就會漏掉此次寫操做。當舊Primary恢復以後,就要回滾部分操做。
好比一個複製集存在兩個數據中心,DC1中存在A(primary),B兩個節點,DC2中存在C,D,E這三個節點。 若是DC1出現了故障。其中DC1這個數據中心的最後的操做是126,可是126沒有被複制到另外的數據中心。因此DC2中服務器最新的操做是125
DC2的數據中心仍然知足副本集大多數的要求(5臺,DC2有3臺),所以其中一個會被選舉成爲新的主節點,這個節點會繼續處理後續的寫入操做。 當網絡恢復以後,DC1中心的服務器就會從其餘服務器同步126以後的操做,可是沒法找到。這種時候DC1中的A,B就會進入回滾過程。
回滾回將失敗以前未複製的操做撤銷。擁有126操做的服務器會在DC2的服務器的oplog尋找共同的操做點。這裏會定位125,這是兩個數據中心相匹配的最後一個操做。
這時,服務器會查看這些沒有被複制的操做,將受這些操做影響的文檔寫入一個.bson文件,保存在數據目錄下的rollback目錄中。
若是126是一個更新操做,服務器回將126更新的文檔寫入collectionName.bson文件。若是想要恢復被回滾的操做,可使用mongorestore命令。