和麵試官這樣吹MongoDB 複製集!
何以高可用?
我們以前用Mysql的時候,經常是一臺伺服器走天下,如果只是用於學習,是沒有問題的,但是在生產環境中,這樣的風險是很大的,如果伺服器因為網路原因或者崩潰了,就會導致資料庫一段時間了不可用,這樣的體驗很不好。
那麼應該怎麼辦呢?既然一臺機器不行,我就多上幾臺機器總可以了吧,比如我上個兩臺,讓他們互為主備,相互同步資料。想到這裡我就只想說一個字,穩。
其實redis,mongodb,kafka等分散式應用基本上都是這樣的思想
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節點,當它注意到它無法獲取大多數節點的支援時,它就會退位,成為備份節點。
如果讓這兩個節點可以選出primary節點,問題是另外3個節點可能不是真正掛了,而只是網路不可達而已。另外3個節點就一定可以選擇出primary節點,這樣就存在了兩個primary節點了。 所以要求大多數就可以避免產生兩個primary節點的問題。
如果MongoDB副本集可以擁有多個primary節點,那麼就會面臨寫入衝突的問題,在支援多執行緒寫入的系統中解決衝突的方式有手動解決和讓作業系統任選一個這兩種方式,但是這兩種方式都不易實現,無法保證寫入的資料不被其他節點修改,因此mongodb只支援單一的primary節點,這樣使得開發更容易。
當一個備份節點無法與主節點連通時,它會聯絡並請求其他副本整合員將自己選舉為主節點,其他成員會做幾項理性的檢查:自身是否能夠與主節點連通?希望被選舉為主節點的備份節點的資料是否最新?有沒有其他更高優先順序的成員可以被選舉為主節點? 如果競選節點成員能夠得到大多數投票,就會成為主節點。但是一旦大多數成員中只有一個否決了本次選舉,選舉就會取消。
在日誌中可以看到得票數為比較大的負數的情況,因為一張否決票相當於10000張贊成票。如果有2張贊成票,2張否決票,那麼選舉結果就是-19998,依此類推。
配置選項
我們一般在部署的時候,副本集節點個數至少是3個(因為它允許1個失敗),這也就意味著資料要被複制三份。
仲裁者(arbiter)
很多人的應用程式使用量比較小,不想儲存三份資料,只想要儲存兩份就行了,儲存第三份純粹是浪費。對於這種部署MongoDB也是支援的。它有一種特殊的成員叫做仲裁者(arbiter),它唯一的作用就是參與選舉,它既不儲存資料也不為客戶端提供服務,只是為了幫助只有兩個成員的副本集滿足大多數這個條件而已。
仲裁者其實也是有缺點的。如果真有一個節點掛了(資料無法恢復),另一個成員稱為主節點。為了資料安全,就需要一個新的備份節點,並且將主節點的資料備份到備份節點。複製資料會對伺服器造成很大的壓力,會拖慢應用程式。相反如果有三個資料成員即使其中一個掛了,仍有一個主節點和一個備份節點,不影響正常運作。這個時候還可以用剩下的那個備份節點來初始化一個新的備份節點伺服器,而不依賴於主節點。所以如果可能儘可能在副本集中使用奇數個資料成員,而不要使用仲裁者。
優先順序(priority)
如果想讓一個節點有更大的機會成為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節點。
投票權(vote)
由於複製整合員最多50個,而參與Primary成員投票的最多7個,所以其他成員的vote必須設定為0(priority也必須為0)。 儘管無投票權的成員不會在選舉中投票,但這些成員擁有副本集資料的副本,並且可以接受來自客戶端應用程式的讀取操作。
隱藏成員(hidden)
客戶端不會像隱藏成員傳送請求,隱藏成員也不會作為複製源(儘管當其他複製源不可用時隱藏成員)。因此很多人將不夠強大的伺服器或者備份伺服器隱藏起來。通過設定hidden:true可以設定隱藏,只有優先順序為0的才能被隱藏。 可使用Hidden節點做一些資料備份、離線計算的任務,不會影響複製集的服務
延遲備份節點(slaveDelay)
資料可能會因為人為錯誤而遭到毀滅性的破壞,為了防止這類問題,可以使用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,"majorityWriteDate" : ISODate("2019-10-31T08:04:47Z")
}
}
複製程式碼
命令會返回很多資料,這裡我只列出了小部分,可以看到我們當前所在節點是master節點(primary),如果當前節點不是primary,也會通過primary屬性告訴你當前primary節點是哪個,同時最後一次寫入的時間戳是1572509087。
此時我們登入另一臺secondary節點,切換到local資料庫,執行命令db.oplog.rs.find()
命令,會返回很多條資料,這裡我們檢視最後一條即可
{
"ts" : Timestamp(1572509087,"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的資料,會發現資料是一模一樣的。 在來解釋下欄位含義
- ts : 操作時間,當前timestamp + 計數器,計數器每秒都被重置
- h:操作的全域性唯一標識
- v:oplog版本資訊
- op:操作型別
- i:插入操作
- u:更新操作
- d:刪除操作
- c:執行命令(如createDatabase,dropDatabase)
- n:空操作,特殊用途
- ns:操作針對的集合
- o:操作內容,可以看到我這裡插入的欄位是count,值是1
- o2:操作查詢條件,僅update操作包含該欄位
初始化同步
副本集中的成員啟動之後,就會檢查自身狀態,確定是否可以從某個成員那裡進行同步。如果不行的話,它會嘗試從副本的另一個成員那裡進行完整的資料複製。這個過程就是初始化同步(initial syncing)。
init sync過程包含如下步驟
- 準備工作刪除所有已存在的資料庫,以一個全新的狀態開始同步
- 將同步源的所有記錄全部複製到本地(除了local)
- oplog同步第一步,克隆過程中的所有操作都會被記錄到oplog中,如果有檔案在克隆過程中被移動了,就有可能會被遺漏,導致沒有被克隆,對於這樣的檔案,可能需要重新克隆
- oplog同步過程第二步,將第一個oplog同步中的操作記錄下來
- 建立索引
- 如果當前節點的資料仍然遠遠落後於同步源,那麼oplog同步過程的最後一步就是將建立索引期間的所有操作全部同步過來,防止該成員成為備份節點。
- 完成初始化同步之後,切換到普通同步狀態,這時當前成員就可以稱為備份節點了。
總結起來就是從其他節點同步全量資料,然後不過從Primary的local.oplog.rs集合裡查詢最新的oplog並應用到自身。
查詢固定集合使用的tailable cursor(docs.mongodb.com/manual/core…)
Primary選舉
Primary選舉除了在複製集初始化時發生,還有如下場景
- 複製集reconfig
- Secondary節點檢測到Primary宕機時,會觸發新Primary的選舉
- 當有Primary節點主動stepDown(主動降級為Secondary)時,也會觸發新的Primary選舉
Primary的選舉受節點間心跳、優先順序、最新的oplog時間等多種因素影響。
節點間心跳
複製整合員間預設每2s會傳送一次心跳資訊,如果10s未收到某個節點的心跳,則認為該節點已宕機;如果宕機的節點為Primary,Secondary(前提是可被選為Primary)會發起新的Primary選舉。
心跳是為了知道其他成員狀態,哪個是主節點,哪個可以作為同步源,哪個掛掉了等等資訊
成員狀態:
- STARTUP : 剛啟動時處於這個狀態,載入副本整合功後就進入STARTUP2狀態
- STARTUP2 : 整個初始化同步都處於這個狀態,這個狀態下,MongDB會建立幾個執行緒,用於處理複製和選舉,然後就會切換到RECOVERING狀態
- RECOVERING : 表示執行正常,當暫時不能處理讀取請求。如果有成員處於這個狀態,可能會造成輕微系統過載
- ARBITER : 仲裁者處於這個狀態
- DOWN : 一個正常執行的成員不可達,就處於DOWN狀態。這個狀態有可能是網路問題
- UNKNOWN : 成員無法到達其他任何成員,其他成員就知道無法它處於什麼狀態,就會處於UNKNOWN。表明這個未知狀態的成員掛掉了。或者兩個成員間存在網路訪問問題。
- REMOVED : 被移除副本集時處於的狀態,添加回來後,就會回到正常狀態
- ROLLBACK : 處於資料回滾時就處於ROLLBACK狀態。回滾結束後,會換為RECOVERING狀態,然後成為備份節點。
- FATAL : 發生不可挽回錯誤,也不再嘗試恢復,就處於這個狀態。這個時候通常應該重啟伺服器
節點優先順序
- 每個節點都傾向於投票給優先順序最高的節點
- 優先順序為0的節點不會主動發起Primary選舉
- 當Primary發現有優先順序更高Secondary,並且該Secondary的資料落後在10s內,則Primary會主動降級,讓優先順序更高的Secondary有成為Primary的機會。
OpTime
最新optime(最近一條oplog的時間戳)的節點才能被選為主,請看上面對oplog.rs的分析。
網路分割槽
只有大多數投票節點間保持網路連通,才有機會被選Primary;如果Primary與大多數的節點斷開連線,Primary會主動降級為Secondary。當發生網路分割槽時,可能在短時間內出現多個Primary,故Driver在寫入時,最好設定大多數成功的策略,這樣即使出現多個Primary,也只有一個Primary能成功寫入大多數。
複製集的讀寫設定
Read Preference
預設情況下,複製集的所有讀請求都發到Primary,Driver可通過設定Read Preference來將讀請求路由到其他的節點。
- primary: 預設規則,所有讀請求發到Primary
- primaryPreferred: Primary優先,如果Primary不可達,請求Secondary
- secondary: 所有的讀請求都發到secondary
- secondaryPreferred:Secondary優先,當所有Secondary不可達時,請求Primary
- nearest:讀請求傳送到最近的可達節點上(通過ping探測得出最近的節點)
Write Concern
預設情況下,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)
複製程式碼
回滾(rollback)
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命令。