何以高可用?
我們以前用Mysql的時候水醋,經(jīng)常是一臺服務(wù)器走天下,如果只是用于學(xué)習(xí)蒜哀,是沒有問題的斩箫,但是在生產(chǎn)環(huán)境中,這樣的風(fēng)險是很大的撵儿,如果服務(wù)器因為網(wǎng)絡(luò)原因或者崩潰了乘客,就會導(dǎo)致數(shù)據(jù)庫一段時間了不可用,這樣的體驗很不好淀歇。
那么應(yīng)該怎么辦呢易核?既然一臺機器不行,我就多上幾臺機器總可以了吧浪默,比如我上個兩臺牡直,讓他們互為主備,相互同步數(shù)據(jù)纳决。想到這里我就只想說一個字井氢,穩(wěn)。
其實redis,mongodb,kafka等分布式應(yīng)用基本上都是這樣的思想
MongoDB也差不多是這樣的思想岳链。它通過復(fù)制集來解決這個問題,MongoDB復(fù)制集由一組Mongod進程組成,包含一個Primary節(jié)點和多個Secondary節(jié)點劲件,Mongodb Driver(客戶端)的所有數(shù)據(jù)都寫入Primary掸哑,Secondary從Primary同步寫入的數(shù)據(jù),以保持復(fù)制集內(nèi)所有成員存儲相同的數(shù)據(jù)集零远,提供數(shù)據(jù)的高可用苗分。
要想成為primary節(jié)點,你必須保證大多數(shù)節(jié)點都同意才行,大多數(shù)的節(jié)點就是副本中一半以上的成員牵辣。
成員總數(shù) | 大多數(shù) | 容忍失敗數(shù) |
---|---|---|
1 | 1 | 0 |
2 | 2 | 0 |
3 | 2 | 1 |
4 | 3 | 1 |
5 | 3 | 2 |
6 | 4 | 2 |
7 | 4 | 3 |
為什么要要求大多數(shù)呢摔癣?其實是為了避免出現(xiàn)兩個primary節(jié)點。比如一個五個節(jié)點的復(fù)制集纬向,其中3個成員不可用择浊,剩下的2個仍然正常工作。這兩個工作的節(jié)點由于不能滿足復(fù)制集大多數(shù)的要求(這個例子中要求要有3個節(jié)點才是大多數(shù))逾条,所以他們無法選擇主節(jié)點琢岩,即使其中有一個節(jié)點是primary節(jié)點,當(dāng)它注意到它無法獲取大多數(shù)節(jié)點的支持時师脂,它就會退位担孔,成為備份節(jié)點江锨。
如果讓這兩個節(jié)點可以選出primary節(jié)點,問題是另外3個節(jié)點可能不是真正掛了糕篇,而只是網(wǎng)絡(luò)不可達而已啄育。另外3個節(jié)點就一定可以選擇出primary節(jié)點,這樣就存在了兩個primary節(jié)點了拌消。
所以要求大多數(shù)就可以避免產(chǎn)生兩個primary節(jié)點的問題挑豌。
如果MongoDB副本集可以擁有多個primary節(jié)點,那么就會面臨寫入沖突的問題,在支持多線程寫入的系統(tǒng)中解決沖突的方式有手動解決和讓操作系統(tǒng)任選一個這兩種方式拼坎,但是這兩種方式都不易實現(xiàn)浮毯,無法保證寫入的數(shù)據(jù)不被其他節(jié)點修改,因此mongodb只支持單一的primary節(jié)點泰鸡,這樣使得開發(fā)更容易债蓝。
當(dāng)一個備份節(jié)點無法與主節(jié)點連通時,它會聯(lián)系并請求其他副本集成員將自己選舉為主節(jié)點盛龄,其他成員會做幾項理性的檢查:自身是否能夠與主節(jié)點連通饰迹?希望被選舉為主節(jié)點的備份節(jié)點的數(shù)據(jù)是否最新?有沒有其他更高優(yōu)先級的成員可以被選舉為主節(jié)點余舶?
如果競選節(jié)點成員能夠得到大多數(shù)投票啊鸭,就會成為主節(jié)點。但是一旦大多數(shù)成員中只有一個否決了本次選舉匿值,選舉就會取消赠制。
在日志中可以看到得票數(shù)為比較大的負數(shù)的情況,因為一張否決票相當(dāng)于10000張贊成票挟憔。如果有2張贊成票钟些,2張否決票,那么選舉結(jié)果就是-19998绊谭,依此類推政恍。
配置選項
我們一般在部署的時候,副本集節(jié)點個數(shù)至少是3個(因為它允許1個失敗),這也就意味著數(shù)據(jù)要被復(fù)制三份达传。
仲裁者(arbiter)
很多人的應(yīng)用程序使用量比較小篙耗,不想保存三份數(shù)據(jù),只想要保存兩份就行了宪赶,保存第三份純粹是浪費宗弯。對于這種部署MongoDB也是支持的。它有一種特殊的成員叫做仲裁者(arbiter)搂妻,它唯一的作用就是參與選舉罕伯,它既不保存數(shù)據(jù)也不為客戶端提供服務(wù),只是為了幫助只有兩個成員的副本集滿足大多數(shù)這個條件而已叽讳。
仲裁者其實也是有缺點的追他。如果真有一個節(jié)點掛了(數(shù)據(jù)無法恢復(fù)),另一個成員稱為主節(jié)點坟募。為了數(shù)據(jù)安全,就需要一個新的備份節(jié)點邑狸,并且將主節(jié)點的數(shù)據(jù)備份到備份節(jié)點懈糯。復(fù)制數(shù)據(jù)會對服務(wù)器造成很大的壓力,會拖慢應(yīng)用程序单雾。相反如果有三個數(shù)據(jù)成員即使其中一個掛了赚哗,仍有一個主節(jié)點和一個備份節(jié)點,不影響正常運作硅堆。這個時候還可以用剩下的那個備份節(jié)點來初始化一個新的備份節(jié)點服務(wù)器屿储,而不依賴于主節(jié)點。所以如果可能盡可能在副本集中使用奇數(shù)個數(shù)據(jù)成員渐逃,而不要使用仲裁者够掠。
優(yōu)先級(priority)
如果想讓一個節(jié)點有更大的機會成為primary的話這需要設(shè)置優(yōu)先級,比如我添加一個優(yōu)先級為2的成員(默認為1)
rs.add({"_id":4, "host": "10.17.28.190:27017", "priority" : 2});
假設(shè)其他都是默認優(yōu)先級茄菊,只要10.17.28.190擁有最新數(shù)據(jù)疯潭,那么當(dāng)前primary節(jié)點就會自動退位,10.17.28.190會被選舉為新的主節(jié)點面殖。如果它的數(shù)據(jù)不夠新竖哩,那么當(dāng)前主節(jié)點就會保持不變。
如果設(shè)置priority為0脊僚,表示不會被選為primary節(jié)點相叁。
投票權(quán)(vote)
由于復(fù)制集成員最多50個,而參與Primary成員投票的最多7個,所以其他成員的vote必須設(shè)置為0(priority也必須為0)。
盡管無投票權(quán)的成員不會在選舉中投票辽幌,但這些成員擁有副本集數(shù)據(jù)的副本增淹,并且可以接受來自客戶端應(yīng)用程序的讀取操作。
隱藏成員(hidden)
客戶端不會像隱藏成員發(fā)送請求舶衬,隱藏成員也不會作為復(fù)制源(盡管當(dāng)其他復(fù)制源不可用時隱藏成員)。因此很多人將不夠強大的服務(wù)器或者備份服務(wù)器隱藏起來赎离。通過設(shè)置hidden:true可以設(shè)置隱藏逛犹,只有優(yōu)先級為0的才能被隱藏。
可使用Hidden節(jié)點做一些數(shù)據(jù)備份梁剔、離線計算的任務(wù)虽画,不會影響復(fù)制集的服務(wù)
延遲備份節(jié)點(slaveDelay)
數(shù)據(jù)可能會因為人為錯誤而遭到毀滅性的破壞,為了防止這類問題荣病,可以使用slaveDelay設(shè)置一個延遲的備份節(jié)點码撰。
延遲備份節(jié)點的數(shù)據(jù)回比主節(jié)點延遲指定的時間(單位是秒),slaveDelay要求優(yōu)先級是0,如果應(yīng)用會將讀請求路由到備份節(jié)點个盆,應(yīng)該將延遲備份節(jié)點隱藏掉脖岛,以免讀請求被路由到延遲備份節(jié)點朵栖。
因Delayed節(jié)點的數(shù)據(jù)比Primary落后一段時間,當(dāng)錯誤或者無效的數(shù)據(jù)寫入Primary時柴梆,可通過Delayed節(jié)點的數(shù)據(jù)來恢復(fù)到之前的時間點陨溅。
修改副本集配置
比如我有個副本集叫做rs0,我想修改增加或者刪除成員,修改成員的配置(vote,hidden,priority等)可以通過reconfig命令
https://docs.mongodb.com/manual/reference/method/rs.reconfig/
cfg = rs.conf();
cfg.members[1].priority = 2;
rs.reconfig(cfg);
同步
Primary與Secondary之間通過oplog來同步數(shù)據(jù)绍在,Primary上的寫操作完成后门扇,會向特殊的local.oplog.rs特殊集合寫入一條oplog,Secondary不斷的從Primary取新的oplog并應(yīng)用偿渡。
因oplog的數(shù)據(jù)會不斷增加臼寄,local.oplog.rs被設(shè)置成為一個capped集合,當(dāng)容量達到配置上限時溜宽,會將最舊的數(shù)據(jù)刪除掉吉拳。由于復(fù)制操作的過程是先復(fù)制數(shù)據(jù)在寫入oplog,oplog必須具有冪等性坑质,即重復(fù)應(yīng)用也會得到相同的結(jié)果合武。
我向test庫的coll集合插入了一條數(shù)據(jù)之后(db.coll.insert({count:1})
),調(diào)用db.isMaster()命令可以看到當(dāng)前節(jié)點的最后一次寫入時間戳
> 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")
}
}
命令會返回很多數(shù)據(jù)涡扼,這里我只列出了小部分稼跳,可以看到我們當(dāng)前所在節(jié)點是master節(jié)點(primary),如果當(dāng)前節(jié)點不是primary,也會通過primary屬性告訴你當(dāng)前primary節(jié)點是哪個,同時最后一次寫入的時間戳是1572509087。
此時我們登錄另一臺secondary節(jié)點,切換到local數(shù)據(jù)庫,執(zhí)行命令db.oplog.rs.find()
命令,會返回很多條數(shù)據(jù)吃沪,這里我們查看最后一條即可
{
"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的值是一致的,證明我們的數(shù)據(jù)是最新的汤善,如果你這個時候訪問其他節(jié)點查看oplog.rs的數(shù)據(jù),會發(fā)現(xiàn)數(shù)據(jù)是一模一樣的票彪。
在來解釋下字段含義
- ts : 操作時間红淡,當(dāng)前timestamp + 計數(shù)器,計數(shù)器每秒都被重置
- h:操作的全局唯一標(biāo)識
- v:oplog版本信息
- op:操作類型
- i:插入操作
- u:更新操作
- d:刪除操作
- c:執(zhí)行命令(如createDatabase降铸,dropDatabase)
- n:空操作在旱,特殊用途
- ns:操作針對的集合
- o:操作內(nèi)容,可以看到我這里插入的字段是count推掸,值是1
- o2:操作查詢條件桶蝎,僅update操作包含該字段
初始化同步
副本集中的成員啟動之后,就會檢查自身狀態(tài)谅畅,確定是否可以從某個成員那里進行同步登渣。如果不行的話,它會嘗試從副本的另一個成員那里進行完整的數(shù)據(jù)復(fù)制毡泻。這個過程就是初始化同步(initial syncing)胜茧。
init sync過程包含如下步驟
- 準(zhǔn)備工作刪除所有已存在的數(shù)據(jù)庫,以一個全新的狀態(tài)開始同步
- 將同步源的所有記錄全部復(fù)制到本地(除了local)
- oplog同步第一步,克隆過程中的所有操作都會被記錄到oplog中仇味,如果有文檔在克隆過程中被移動了呻顽,就有可能會被遺漏雹顺,導(dǎo)致沒有被克隆,對于這樣的文檔芬位,可能需要重新克隆
- oplog同步過程第二步无拗,將第一個oplog同步中的操作記錄下來
- 創(chuàng)建索引
- 如果當(dāng)前節(jié)點的數(shù)據(jù)仍然遠遠落后于同步源,那么oplog同步過程的最后一步就是將創(chuàng)建索引期間的所有操作全部同步過來昧碉,防止該成員成為備份節(jié)點英染。
- 完成初始化同步之后,切換到普通同步狀態(tài)被饿,這時當(dāng)前成員就可以稱為備份節(jié)點了四康。
總結(jié)起來就是從其他節(jié)點同步全量數(shù)據(jù),然后不過從Primary的local.oplog.rs集合里查詢最新的oplog并應(yīng)用到自身狭握。
查詢固定集合使用的tailable cursor(https://docs.mongodb.com/manual/core/tailable-cursors/)
Primary選舉
Primary選舉除了在復(fù)制集初始化時發(fā)生闪金,還有如下場景
- 復(fù)制集reconfig
- Secondary節(jié)點檢測到Primary宕機時,會觸發(fā)新Primary的選舉
- 當(dāng)有Primary節(jié)點主動stepDown(主動降級為Secondary)時论颅,也會觸發(fā)新的Primary選舉
Primary的選舉受節(jié)點間心跳哎垦、優(yōu)先級、最新的oplog時間等多種因素影響恃疯。
節(jié)點間心跳
復(fù)制集成員間默認每2s會發(fā)送一次心跳信息漏设,如果10s未收到某個節(jié)點的心跳,則認為該節(jié)點已宕機今妄;如果宕機的節(jié)點為Primary郑口,Secondary(前提是可被選為Primary)會發(fā)起新的Primary選舉。
心跳是為了知道其他成員狀態(tài),哪個是主節(jié)點盾鳞,哪個可以作為同步源犬性,哪個掛掉了等等信息
成員狀態(tài):
- STARTUP : 剛啟動時處于這個狀態(tài),加載副本集成功后就進入STARTUP2狀態(tài)
- STARTUP2 : 整個初始化同步都處于這個狀態(tài)腾仅,這個狀態(tài)下乒裆,MongDB會創(chuàng)建幾個線程,用于處理復(fù)制和選舉推励,然后就會切換到RECOVERING狀態(tài)
- RECOVERING : 表示運行正常鹤耍,當(dāng)暫時不能處理讀取請求。如果有成員處于這個狀態(tài)吹艇,可能會造成輕微系統(tǒng)過載
- ARBITER : 仲裁者處于這個狀態(tài)
- DOWN : 一個正常運行的成員不可達惰蜜,就處于DOWN狀態(tài)昂拂。這個狀態(tài)有可能是網(wǎng)絡(luò)問題
- UNKNOWN : 成員無法到達其他任何成員受神,其他成員就知道無法它處于什么狀態(tài),就會處于UNKNOWN格侯。表明這個未知狀態(tài)的成員掛掉了鼻听〔浦或者兩個成員間存在網(wǎng)絡(luò)訪問問題。
- REMOVED : 被移除副本集時處于的狀態(tài),添加回來后撑碴,就會回到正常狀態(tài)
- ROLLBACK : 處于數(shù)據(jù)回滾時就處于ROLLBACK狀態(tài)撑教。回滾結(jié)束后醉拓,會換為RECOVERING狀態(tài)伟姐,然后成為備份節(jié)點。
- FATAL : 發(fā)生不可挽回錯誤亿卤,也不再嘗試恢復(fù)愤兵,就處于這個狀態(tài)。這個時候通常應(yīng)該重啟服務(wù)器
節(jié)點優(yōu)先級
- 每個節(jié)點都傾向于投票給優(yōu)先級最高的節(jié)點
- 優(yōu)先級為0的節(jié)點不會主動發(fā)起Primary選舉
- 當(dāng)Primary發(fā)現(xiàn)有優(yōu)先級更高Secondary排吴,并且該Secondary的數(shù)據(jù)落后在10s內(nèi)秆乳,則Primary會主動降級,讓優(yōu)先級更高的Secondary有成為Primary的機會钻哩。
OpTime
最新optime(最近一條oplog的時間戳)的節(jié)點才能被選為主,請看上面對oplog.rs的分析屹堰。
網(wǎng)絡(luò)分區(qū)
只有大多數(shù)投票節(jié)點間保持網(wǎng)絡(luò)連通,才有機會被選Primary街氢;如果Primary與大多數(shù)的節(jié)點斷開連接扯键,Primary會主動降級為Secondary。當(dāng)發(fā)生網(wǎng)絡(luò)分區(qū)時阳仔,可能在短時間內(nèi)出現(xiàn)多個Primary忧陪,故Driver在寫入時,最好設(shè)置大多數(shù)成功的策略近范,這樣即使出現(xiàn)多個Primary嘶摊,也只有一個Primary能成功寫入大多數(shù)。
復(fù)制集的讀寫設(shè)置
Read Preference
默認情況下评矩,復(fù)制集的所有讀請求都發(fā)到Primary叶堆,Driver可通過設(shè)置Read Preference來將讀請求路由到其他的節(jié)點。
- primary: 默認規(guī)則斥杜,所有讀請求發(fā)到Primary
- primaryPreferred: Primary優(yōu)先虱颗,如果Primary不可達,請求Secondary
- secondary: 所有的讀請求都發(fā)到secondary
- secondaryPreferred:Secondary優(yōu)先蔗喂,當(dāng)所有Secondary不可達時忘渔,請求Primary
- nearest:讀請求發(fā)送到最近的可達節(jié)點上(通過ping探測得出最近的節(jié)點)
Write Concern
默認情況下,Primary完成寫操作即返回缰儿,Driver可通過設(shè)置Write Concern來設(shè)置寫成功的規(guī)則畦粮。
如下的write concern規(guī)則設(shè)置寫必須在大多數(shù)節(jié)點上成功,超時時間為5s。
db.products.insert(
{ item: "envelopes", qty : 100, type: "Clasp" },
{ writeConcern: { w: majority, wtimeout: 5000 } }
)
上面的設(shè)置方式是針對單個請求的宣赔,也可以修改副本集默認的write concern预麸,這樣就不用每個請求單獨設(shè)置。
cfg = rs.conf()
cfg.settings = {}
cfg.settings.getLastErrorDefaults = { w: "majority", wtimeout: 5000 }
rs.reconfig(cfg)
回滾(rollback)
Primary執(zhí)行了一個寫請求之后掛了儒将,但是備份節(jié)點還沒有來得及復(fù)制這次操作吏祸。新選舉出來的主節(jié)點結(jié)就會漏掉這次寫操作。當(dāng)舊Primary恢復(fù)之后钩蚊,就要回滾部分操作贡翘。
比如一個復(fù)制集存在兩個數(shù)據(jù)中心,DC1中存在A(primary),B兩個節(jié)點,DC2中存在C,D,E這三個節(jié)點。
如果DC1出現(xiàn)了故障砰逻。其中DC1這個數(shù)據(jù)中心的最后的操作是126,但是126沒有被復(fù)制到另外的數(shù)據(jù)中心床估。所以DC2中服務(wù)器最新的操作是125
DC2的數(shù)據(jù)中心仍然滿足副本集大多數(shù)的要求(5臺,DC2有3臺),因此其中一個會被選舉成為新的主節(jié)點诱渤,這個節(jié)點會繼續(xù)處理后續(xù)的寫入操作丐巫。
當(dāng)網(wǎng)絡(luò)恢復(fù)之后,DC1中心的服務(wù)器就會從其他服務(wù)器同步126之后的操作勺美,但是無法找到递胧。這種時候DC1中的A,B就會進入回滾過程。
回滾回將失敗之前未復(fù)制的操作撤銷赡茸。擁有126操作的服務(wù)器會在DC2的服務(wù)器的oplog尋找共同的操作點缎脾。這里會定位125,這是兩個數(shù)據(jù)中心相匹配的最后一個操作占卧。
這時遗菠,服務(wù)器會查看這些沒有被復(fù)制的操作,將受這些操作影響的文檔寫入一個.bson文件华蜒,保存在數(shù)據(jù)目錄下的rollback目錄中辙纬。
如果126是一個更新操作,服務(wù)器回將126更新的文檔寫入collectionName.bson文件叭喜。如果想要恢復(fù)被回滾的操作贺拣,可以使用mongorestore命令。