讀古今文學網 > MongoDB實戰 > 8.2 副本集 >

8.2 副本集

副本集是對主從複製的一種完善,也是推薦的MongoDB複製策略。我們會從配置一個示例副本集開始,然後描述複製是如何工作的,這些知識對於診斷線上問題是極為重要的。最後會討論一些高級配置細節、故障轉移與恢復,還有最佳部署實踐。

8.2.1 配置

最小的推薦副本集配置由三個節點組成。其中兩個節點是一等的、持久化mongod實例,兩者都能作為副本集的主節點,都有完整的數據副本。集合裡的第三個節點是仲裁節點,不複製數據,只是中立觀察者。正如其名所示,仲裁節點是進行仲裁的:在要求故障轉移時,仲裁節點會幫助選出新的主節點。圖8-1描繪了要配置的副本集。

先為副本集裡的每個成員創建數據目錄:

mkdir /data/node1
mkdir /data/node2
mkdir /data/arbiter
  

接下來,分別為每個成員啟動獨立的mongod。因為要在同一台機器上運行這些進程,最好在獨立的終端窗口裡啟動各個mongod

mongod --replSet myapp --dbpath /data/node1 --port 40000
mongod --replSet myapp --dbpath /data/node2 --port 40001
mongod --replSet myapp --dbpath /data/arbiter --port 40002
  

圖8-1 由一個主節點、一個從節點和一個仲裁節點組成的基本副本集

如果查看mongod的日誌輸出,注意到的第一件事是錯誤消息(提示找不到配置)。這完全正常:

[startReplSets] replSet can't get local.system.replset
config from self or any seed (EMPTYCONFIG)
[startReplSets] replSet info you may need to run replSetInitiate
  

繼續下一步,需要配置副本集。先連接到一個剛啟動的非仲裁節點的mongod上。這裡的例子都是在本地運行mongod進程的,因此將通過本地主機名來進行連接,本例中是arete

連接後運行rs.initiate命令:

> rs.initiate
{
    "info2" : "no configuration explicitly specified -- making one",
    "me" : "arete:40000",
    "info" : "Config now saved locally. Should come online in about a minute
     .",
    "ok" : 1
}
  

一分鐘左右,你就能擁有一個單成員的副本集了。現在再通過rs.add添加其他兩個成員:

> rs.add("localhost:40001")
{ "ok" : 1 }
> rs.add("arete.local:40002", {arbiterOnly: true})
{ "ok" : 1 }
  

注意,在添加第二個節點時指定了arbiterOnly參數,以此創建一個仲裁節點。不久之後(1分鐘內),所有的成員就都在線了。要獲得副本集狀態的摘要信息,可以運行db.isMaster命令:

> db.isMaster
{
  "setName" : "myapp",
  "ismaster" : false,
  "secondary" : true,
  "hosts" : [
    "arete:40001",
    "arete:40000"
  ],
  "arbiters" : [
    "arete:40002"
  ],
  "primary" : "arete:40000",
  "maxBsonObjectSize" : 16777216,
  "ok" : 1
}
  

rs.status方法能提供更詳細的系統信息,可以看到每個節點的狀態信息。下面是完整的狀態信息:

> rs.status
{
    "set" : "myall",
    "date" : ISODate("2011-09-27T22:09:04Z"),
    "myState" : 1,
    "members" : [
        {
            "_id" : 0,
            "name" : "arete:40000",
            "health" : 1,
            "state" : 1,
            "stateStr" : "PRIMARY",
            "optime" : {
                "t" : 1317161329000,
                "i":1
            },
            "optimeDate" : ISODate("2011-09-27T22:08:49Z"),
            "self" : true
        },
        {
            "_id" : 1,
            "name" : "arete:40001",
            "health" : 1,
            "state" : 2,
            "stateStr" : "SECONDARY",
            "uptime" : 59,
            "optime" : {
                "t" : 1317161329000,
                "i":1
            },
            "optimeDate" : ISODate("2011-09-27T22:08:49Z"),
            "lastHeartbeat" : ISODate("2011-09-27T22:09:03Z"),
            "pingMs" : 0
        },
        {
            "_id" : 2,
            "name" : "arete:40002",
            "health" : 1,
            "state" : 7,
            "stateStr" : "ARBITER",
            "uptime" : 5,
            "optime" : {
                 "t":0,
                 "i":0
            },
            "optimeDate" : ISODate("1970-01-01T00:00:00Z"),
            "lastHeartbeat" : ISODate("2011-09-27T22:09:03Z"),
            "pingMs" : 0
        }
    ],
    "ok" : 1
}
  

除非你的MongoDB數據庫裡包含很多數據,否則副本集應該能在30 s內上線。在此期間,每個節點的stateStr字段應該會從RECOVERING變為PRIMARYSECONDARYARBITER

就算副本集的狀態「宣稱」複製已經在運行了,你可能還是希望能看到一些證據。因此,接下來在Shell裡連接到主節點,插入一個文檔:

$ mongo arete:40000
> use bookstore
switched to db bookstore
> db.books.insert({title: "Oliver Twist"})
> show dbs
admin (empty)
bookstore 0.203125GB
local 0.203125GB
  

初始的複製幾乎是立即發生的。在另一個終端窗口中開啟一個新的Shell實例,這次要指向從節點。查詢剛才插入的文檔,應該有如下輸出:

$ mongo arete:40001
> show dbs
admin (empty)
bookstore 0.203125GB
local 0.203125GB
> use bookstore switched to db bookstore
> db.books.find
{ "_id" : ObjectId("4d42ebf28e3c0c32c06bdf20"), "title" : "Oliver Twist" }
  

如果複製確實如顯示的那樣已經在運作了,那就說明已經成功配置了副本集。

能實實在在地看到複製讓人覺得很滿意,但也許自動故障轉移會更有趣一些。現在就來做點測試。要模擬網絡分區需要點技巧,所以我們會選擇一個簡單的方法,殺掉一個節點。你可以殺掉從節點,這只會停止複製,剩餘的節點仍舊保持其當前狀態。如果希望看到系統狀態發生改變,就需要殺掉主節點。標準的CTRL-C或kill -2就能辦到這點。你還可以連上主節點,在Shell裡運行db.shutdownServer

一旦殺掉了主節點,從節點會發現檢測不到主節點的「心跳」了,隨後會把自己「選舉」為主節點。這樣的「選舉」是可行的,因為原始節點中的大多數節點(仲裁者節點和原始的從節點)仍能ping到對方。以下是從節點日誌的片段:

[ReplSetHealthPollTask] replSet info arete:40000 is down (or slow to respond)
Mon Jan 31 22:56:22 [rs Manager] replSet info electSelf 1
Mon Jan 31 22:56:22 [rs Manager] replSet PRIMARY
  

如果連接到新的主節點上檢查副本集狀態,你會發現無法訪問到老的主節點:

> rs.status
{
      "_id" : 0,
      "name" : "arete:40000",
      "health" : 1,
      "state" : 6,
      "stateStr" : "(not reachable/healthy)",
      "uptime" : 0,
      "optime" : {
        "t" : 1296510078000,
        "i":1
      },
      "optimeDate" : ISODate("2011-01-31T21:43:18Z"),
      "lastHeartbeat" : ISODate("2011-02-01T03:29:30Z"),
      "errmsg": "socket exception"
}
  

故障轉移後,副本集就只有兩個節點了。因為仲裁節點沒有數據,只要應用程序只和主節點通信,它就能繼續運作。1即使如此,複製停止了,現在不能再做故障轉移了。老的主節點必須恢復。假設它是正常關閉的,可以讓它再度上線,它會自動以從節點的身份重新加入副本集。你可以試一下,現在就重啟老的主節點。

1. 應用程序有時會查詢從節點來做讀擴展。如果是這樣,此類故障就會導致讀故障。因此在設計應用程序時要時刻把故障轉移放在心頭。本章末尾會有更多相關內容。

以上就是副本集的完整概述,毫無懸念,你會覺得其中一些細節有點棘手。在接下來的兩節裡,你會看到副本集實際是如何運作的,瞭解它的部署、高級配置以及如何處理生產環境中可能出現的複雜場景。

8.2.2 複製的工作原理

副本集依賴於兩個基礎機制:oplog和「心跳」(heartbeat)。oplog讓數據的複製成為可能,而「心跳」則監控健康情況並觸發故障轉移,後續將看到這些機制是如何輪流運作的。你應該已經逐漸開始理解並能預測副本集的行為了,尤其是在故障的情況下。

1. 關於oplog

oplog是MongoDB複製的關鍵。oplog是一個固定集合,位於每個複製節點的local數據庫裡,記錄了所有對數據的變更。每次客戶端向主節點寫入數據,就會自動向主節點的oplog裡添加一個條目,其中包含了足夠的信息來再現數據。一旦寫操作被複製到某個從節點上,從節點的oplog也會保存一條關於寫入的記錄。每個oplog條目都由一個BSON時間戳進行標識,所有從節點都使用這個時間戳來追蹤它們最後應用的條目。2

2. BSON時間戳是一個唯一標識符,由從紀元算起的秒數和一個遞增的計數器值構成。

為了更好地瞭解其原理,讓我們仔細看看真實的oplog以及其中記錄的操作。先在Shell裡連接到上一節啟動的主節點,切換到local數據庫:

> use local
switched to db local
  

local數據庫裡保存了所有的副本集元數據和oplog。當然,這個數據庫本身不能被複製。正如其名,local數據庫裡的數據對本地節點而言是唯一的,因此不該複製。

如果查看local數據庫,你會看到一個名為oplog.rs的集合,每個副本集都會把oplog保存在這個集合裡。你還會看到一些系統集合,以下就是完整的輸出:

> show collections
me
oplog.rs
replset.minvalid
slaves
system.indexes
system.replset
  

replset.minvalid包含了指定副本集成員的初始同步信息,system.replset保存了副本集配置文檔。meslaves用來實現寫關注(本章最後會介紹)。system.indexes是標準索引說明容器。

我們先把精力集中在oplog上,查詢與上一節裡你所添加圖書文檔相關的oplog條目。為此,輸入如下查詢,結果文檔裡會有四個字段,我們將依次討論這些字段:

> db.oplog.rs.findOne({op: "i"})
{ "ts" : { "t" : 1296864947000, "i":1}, "op" : "i", "ns" :
"bookstores.books", "o" : { "_id" : ObjectId("4d4c96b1ec5855af3675d7a1"),
"title" : "Oliver Twist" }
}
  

第一個字段是ts,保存了該條目的BSON時間戳。這裡特別要注意,Shell是用子文檔來顯示時間戳的,包含兩個字段,t是從紀元開始的秒數,i是計數器。也許你會覺得可以像下面這樣來查詢這個條目:

db.oplog.rs.findOne({ts: {t: 1296864947000, i: 1}})
  

實際上,這條查詢返回null。要在查詢中使用時間戳,需要顯式構造一個時間戳對象。所有的驅動都有自己的BSON時間戳構造器,JavaScript也是如此。可以這樣做:

db.oplog.rs.findOne({ts: new Timestamp(1296864947000, 1)})
  

回到那條oplog條目上,第二個字段op表示操作碼(opcode),它告訴從節點該條目表示了什麼操作,本例中的i表示插入。op後的ns標明了有關的命名空間(數據庫和集合),o對插入操作而言包含了所插入文檔的副本。

在查看oplog條目時,你可能會注意到,對於那些影響多個文檔的操作,oplog會將各個部分都分析到位。對於多項更新和大批量刪除來說,會為每個影響到的文檔創建單獨的oplog條目。例如,假設你向集合裡添加了幾本狄更斯的書:

> use bookstore
db.books.insert({title: "A Tale of Two Cities"})
db.books.insert({title: "Great Expectations"})
  

現在集合裡有四本書,讓我們通過一次多項更新來設置作者的名稱:

db.books.update({}, {$set: {author: "Dickens"}}, false, true)
  

在oplog裡會出現什麼呢?

> use local
> db.oplog.$main.find({op: "u"})
{ "ts" : { "t" : 1296944149000, "i":1}, "op" : "u",
"ns" : "bookstore.books",
"o2" : { "_id" : ObjectId("4d4dcb89ec5855af365d4283") },
"o" : { "$set " : { "author" : "Dickens"}}}

{ "ts" : { "t" : 1296944149000, "i":2}, "op" : "u",
"ns" : "bookstore.books",
"o2" : { "_id" : ObjectId("4d4dcb8eec5855af365d4284") },
"o" : { "$set " : { "author" : "Dickens"}}}

{ "ts" : { "t" : 1296944149000, "i":3}, "op" : "u",
"ns" : "bookstore.books",
"o2" : { "_id" : ObjectId("4d4dcbb6ec5855af365d4285") },
"o" : { "$set " : { "author" : "Dickens"}}}
  

如你所見,每個被更新的文檔都有自己的oplog條目。這種正規化是更通用策略中的一部分,它會保證從節點總是能和主節點擁有一樣的數據。要確保這一點,每次應用的操作都必須是冪等的——一個指定的oplog條目被應用多少次都無所謂;結果總是一樣的。其他多文檔操作的行為是一樣的,比如刪除。你可以試試不同的操作,看看它們在oplog裡最終是什麼樣的。

要取得oplog當前狀態的基本信息,可以運行Shell的db.getReplicationInfo方法:

> db.getReplicationInfo
{
    "logSizeMB" : 50074.10546875,
    "usedMB" : 302.123,
    "timeDiff" : 294,
    "timeDiffHours" : 0.08,
    "tFirst" : "Thu Jun 16 2011 21:21:55 GMT-0400 (EDT)",
    "tLast" : "Thu Jun 16 2011 21:26:49 GMT-0400 (EDT)",
    "now" : "Thu Jun 16 2011 21:27:28 GMT-0400 (EDT)"
}
  

這裡有oplog中第一條和最後一條的時間戳,你可以使用$natural排序修飾符手工找到這些oplog條目。例如,下面這條查詢能獲取最後一個條目:db.oplog.rs.find.sort({$natural: -1}).limit(1)

關於複製,還有一件重要的事情,即從節點是如何確定它們在oplog裡的位置的。答案在於從節點自己也有一份oplog。這是對主從複製的一項重大改進,因此值得花些時間深究其中的原理。

假設向副本集的主節點發起寫操作,接下來會發生什麼?寫操作先被記錄下來,添加到主節點的oplog裡。與此同時,所有從節點從主節點複製oplog。因此,當某個從節點準備更新自己時,它做了三件事:首先,查看自己oplog裡最後一條的時間戳;其次,查詢主節點oplog裡所有大於此時間戳的條目;最後,把那些條目添加到自己的oplog裡並應用到自己的庫裡。3也就是說,萬一發生故障,任何被提升為主節點的從節點都會有一個oplog,其他從節點能以它為複製源進行複製。這項特性對副本集的恢復而言是必需的。

3. 開啟Journaling日誌時,文檔會在一個原子事務裡被同時寫入核心數據文件和oplog。

從節點使用長輪詢(long polling)立即應用來自主節點oplog的新條目。因此從節點的數據通常都是最新的。由於網絡分區或從節點本身進行維護造成數據陳舊時,可以使用從節點oplog裡最新的時間戳來監測複製延遲。

2. 停止複製

如果從節點在主節點的oplog裡找不到它所同步的點,那麼會永久停止複製。發生這種情況時,你會在從節點的日誌裡看到如下異常:

repl: replication data too stale, halting
Fri Jan 28 14:19:27 [replsecondary] caught SyncException
  

回憶一下,oplog是一個固定集合,也就是說集合裡的條目最終都會過期。一旦某個從節點沒能在主節點的oplog裡找到它已經同步的點,就無法再保證這個從節點是主節點的完美副本了。因為修復停止複製的唯一途徑是重新完整同步一次主節點的數據,所以要竭盡全力避免這個狀態。為此,要監測從節點的延時情況,針對你的寫入量要有足夠大的oplog。在第10章裡能瞭解到更多與監控有關的內容。接下來我們將討論如何選擇合適的oplog大小。

3. 調整複製OPLOG大小

因為oplog是一個固定集合,所以一旦創建就無法重新設置大小(至少自MongoDB v2.0起是這樣的),4為此要慎重選擇初始oplog大小。

4. 增加固定集合大小的選項已列入計劃特性之列,詳見https://jira.mongodb.org/browse/SERVER-1864。

默認的oplog大小會隨著環境發生變化。在32位系統上,oplog默認是50 MB,而在64位系統上,oplog會增大到1 GB或空餘磁盤空間的5%。5對於多數部署環境,空餘磁盤空間的5%綽綽有餘。對於這種尺寸的oplog,要意識到一旦重寫20次,磁盤就可能滿了。

5. 如果運行的是OS X,這時oplog將是192 MB。這個值較小,原因是會假設OS X的機器是開發機。

因此默認大小並非適用於所有應用程序。如果知道應用程序寫入量會很大,在部署之前應該做些測試。配置好複製,然後以生產環境的寫入量向主節點發起寫操作,像這樣對服務器施壓起碼一小時。完成之後,連接到任意副本集成員上,獲取當前複製信息:

db.getReplicationInfo
  

一旦瞭解了每小時會生成多少oplog,就能決定分配多少oplog空間了。你應該為從節點下線至少八小時做好準備。發生網絡故障或類似事件時,要避免任意節點重新同步完整數據,增加oplog大小能為你爭取更多時間。

如果要改變默認oplog大小,必須在每個成員節點首次啟動時使用mongod--oplogSize選項,其值的單位是兆。可以像這樣啟動一個1 GB oplog的mongod實例:

mongod --replSet myapp --oplogSize 1024
  

4.「心跳」檢測與故障轉移

副本集的「心跳」檢測有助於選舉和故障轉移。默認情況下,每個副本集成員每兩秒鐘ping一次其他所有成員。這樣一來,系統可以弄清自己的健康狀況。在運行rs.status時,你可以看到每個節點上次「心跳」檢測的時間戳和健康狀況(1表示健康,0表示沒有應答)。

只要每個節點都保持健康且有應答,副本集就能快樂地工作下去。但如果哪個節點失去了響應,副本集就會採取措施。每個副本集都希望確認無論何時都恰好存在一個主節點。但這僅在大多數節點可見時才有可能。例如,回顧上一節裡構建的副本集,如果殺掉從節點,大部分節點依然存在,副本集不會改變狀態,只是簡單地等待從節點重新上線。如果殺掉主節點,大部分節點依然存在,但沒有主節點了。因此從節點被自動提升為主節點。如果碰巧有多個從節點,那麼會推選狀態最新的從節點提升為主節點。

但還有其他可能的場景。假設從節點和仲裁節點都被殺掉了,只剩下主節點,但沒有多數節點——原來的三個節點裡只有一個仍處於健康狀態。在這種情況下,在主節點的日誌裡會有如下消息:

Tue Feb 1 11:26:38 [rs Manager] replSet can't see a majority of the set,
    relinquishing primary
Tue Feb 1 11:26:38 [rs Manager] replSet relinquishing primary state
Tue Feb 1 11:26:38 [rs Manager] replSet SECONDARY
  

沒有了多數節點,主節點會把自己降級為從節點。這讓人有點費解,但仔細想想,如果該節點仍然作為主節點的話會發生什情況?如果出於某些網絡原因心跳檢測失敗了,那麼其他節點仍然是在線的。如果仲裁節點和從節點依然健在,並且能看到對方,那麼根據多數節點原則,剩下的從節點會變成主節點。要是原來的主節點並未降級,那麼你頓時就陷入了不堪一擊的局面:副本集中有兩個主節點。如果應用程序繼續運行,就可能對兩個不同的主節點做讀寫操作,肯定會有不一致,並伴隨著奇怪的現象。因此,當主節點看不到多數節點時,必須降級為從節點。

5. 提交與回滾

關於副本集,還有最後一點需要理解,那就是提交的概念。本質上,你可以一直向主節點做寫操作,但那些寫操作在被複製到大多數節點前,都不會被認為是已提交的。這裡所說的已提交是什麼意思呢?最好舉個例子來做說明。仍以上一節構建的副本集為例,你向主節點發起一系列寫操作,出於某些原因(連接問題、從節點為備份而下線、從節點有延遲等)沒被複製到從節點。現在假設從節點突然被提升為主節點了,你向新的主節點寫數據,而最終老的主節點再次上線,嘗試從新的主節點做複製。這裡的問題在於老的主節點裡有一系列寫操作並未出現在新主節點的oplog裡。這就會觸發回滾。

在回滾時,所有未複製到大多數節點的寫操作都會被撤銷。也就是說會將它們從從節點的oplog和它們所在的集合裡刪掉。要是某個從節點裡登記了一條刪除,那麼該節點會從其他副本裡找到被刪除的文檔並進行恢復。刪除集合以及更新文檔的情況也是一樣的。

相關節點數據路徑的rollback子目錄中保存了被回滾的寫操作。針對每個有回滾寫操作的集合,會創建一個單獨的BSON文件,文件名裡包含了回滾的時間。在需要恢復被回滾的文檔時,可以用bsondump工具來查看這些BSON文件,並可以通過mongorestore手工進行恢復。

萬一你真的不得不恢復被回滾的數據,你就會意識到應該避免這種情況,幸運的是,從某種程度上來說,這是可以辦到的。要是應用程序能容忍額外的寫延時,那麼就能用上稍後會介紹的寫關注,以此確保每次(也可能是每隔幾次)寫操作都能被複製到大多數節點上。使用寫關注,或者更通用一點,監控複製的延遲,能幫助你減輕甚至避免回滾帶來的全部問題。

本節中你瞭解了很多複製的內部細節,可能比預想的還要多,但這些知識遲早會派上用處的。在生產環境裡診斷問題時,理解複製是如何工作的會非常有用。

8.2.3 管理

雖然MongoDB提供了自動化功能,但副本集其實還有些潛在的複雜配置選項,接下來,我將詳細介紹這些選項。為了讓配置簡單一些,我也會就哪些選項是能被安全忽略的給出建議。

1. 配置細節

這裡我會介紹一些與副本集相關的mongod啟動選項,並且描述副本集配置文檔的結構。

  • 複製選項

先前,你學習了如何使用Shell的rs.initiaters.add方法初始化副本集。這些方法很方便,但它們隱藏了某些副本集配置選項。這裡你將看到如何使用配置文檔初始化並修改一個副本集的配置。

配置文檔裡說明了副本集的配置。要創建配置文檔,先為_id添加一個值,要和傳給--replSet參數的值保持一致:

> config = {_id: "myapp", members: }
{ "_id" : "myapp", "members" : [ ] }
  

members也是配置文檔的一部分,可以像下面這樣進行定義:

config.members.push({_id: 0, host: 'arete:40000'})
config.members.push({_id: 1, host: 'arete:40001'})
config.members.push({_id: 2, host: 'arete:40002', arbiterOnly: true})
  

你的配置文檔看起來應該是這樣的:

> config
{
  "_id" : "myapp",
  "members" : [
    {
      "_id" : 0,
      "host" : "arete:40000"
    },
    {
      "_id" : 1,
      "host" : "arete:40001"
    },
    {
      "_id" : 2,
      "host" : "arete:40002",
      "arbiterOnly" : true
    }
  ]
}
  

隨後可以把該文檔作為rs.initiate的第一個參數,用這個方法來初始化副本集。

嚴格說來,該文檔由以下部分組成:包含副本集名稱的_id字段、members數組(指定了3~12個成員),以及一個可選的子文檔(用來指定某些全局設置)。示例副本集裡使用了最少的配置參數,外加可選的arbiterOnly設置。

文檔中要求有一個_id字段,與副本集的名稱相匹配。初始化命令會驗證每個成員節點在啟動時是否都在--replSet選項裡用了這個名稱。每個副本集成員都要有一個_id字段,包含從0開始遞增的整數,還要有一個host字段,提供主機名和可選的端口。

這裡通過rs.initiate方法初始化了副本集,它是對replSetInitiate命令的簡單封裝。因此,可以像這樣啟動副本集:

db.runCommand({replSetInitiate: config});
  

config就是一個簡單的變量,持有配置文檔。一旦初始化完畢,每個集合成員都會在local數據庫的system.replset集合裡保存一份配置文檔的副本。如果查詢該集合,你會看到該文檔現在有一個版本號了。每次修改副本集的配置,都必須遞增這一版本號。

要修改副本集的配置,有一個單獨的方法replSetReconfig,它接受一個新的配置文檔。新文檔可以添加或刪除集合成員,還可以修改成員說明和全局配置選項。修改配置文檔、增加版本號,以及把它傳給replSetReconfig方法,這整個過程很麻煩,所以在Shell裡有一些輔助方法來簡化這個過程。可以在Shell裡輸入rs.help,查看這些輔助方法的列表。注意,你已經用過rs.add了。

請牢記一點,無論何時,要是重新配置副本集導致重新選舉新的主節點,那麼所有客戶端的連接都會被關閉。這是為了確保客戶端不會向從節點發送fire-and-forget風格的寫操作。

如果你對通過驅動配置副本集感興趣的話,可以瞭解一下rs.add是如何實現的。在Shell提示符裡輸入rs.add(不帶括號的方法),看看這個方法的工作原理。

  • 配置文檔選項

到目前為止,我們都局限在最簡單的副本集配置文檔裡。但這些文檔還支持很多選項,無論是針對副本集成員還是整個副本集。我們將從成員選項開始進行介紹。注意,你已經見過_idhostarbiterOnly了,下面還會一起詳細介紹其他選項。

  • _id(必填) 唯一的遞增整數,表示成員ID。這些_id值從0開始,每添加一個成員就加1。

  • host(必填) 保存了成員主機名的字符串,帶有可選的端口號。如果提供了端口號,需要用冒號與主機名分隔(例如arete:30000)。如果沒有指定端口號,則使用默認端口27017。

  • arbiterOnly 一個布爾值,truefalse,標明該成員是否是仲裁節點。仲裁節點只保存配置數據。它們是輕量級成員,參與主節點選舉但本身不參與複製。

  • priority 一個0~1000的整數,幫助確定該節點被選舉為主節點的可能性。在副本集初始化和故障轉移時,集合會嘗試將優先級最高的節點推選為主節點,只要它的數據是最新的。也有一些場景裡,你希望某個節點永遠都不會成為主節點(比方說,一個位於從數據中心的災難恢復節點)。在這些情況中,可以把優先級設置為0。遇到isMaster命令,帶有優先級0的節點會被標記為被動節點,永遠都不會被選舉為主節點。

  • votes 所有副本集成員默認都有一票。votes設置讓你能給某個單獨的成員更多投票。如果要使用該選項,請格外小心。首先,在各個成員的投票數不一致時,很難推測副本集的故障轉移行為。其次,絕大多數生產部署環境裡,每個成員只有一票的配置工作得都十分理想。因此,要是確定要修改某個指定成員的投票數,一定要經過深思熟慮,並仔細模擬各種故障場景。

  • hidden 一個布爾值,如果為true,在isMaster命令生成的響應裡則不會出現該節點。因為MongoDB驅動依賴於isMaster來獲取副本集的拓撲情況,所以隱藏一個成員能避免驅動自動訪問它。該設置能同buildIndexes協同使用,使用時必須有slaveDelay

  • buildIndexes 一個布爾值,默認為true,確定該成員是否會構建索引。僅當該成員永遠不會成為主節點時(那些優先級為0的節點),才能將它設置為false。該選項是為那些只會用作備份的節點設計的。如果備份索引很重要,那麼就不要使用它。

  • slaveDelay 指定從節點要比主節點延遲的秒數。該選項只能用於永遠不會成為主節點的節點。所以如果要把slaveDelay設置為大於0的值,務必保證將優先級設置為0。 可以通過延遲從節點來抵禦某些用戶錯誤。例如,如果有一個延遲30分鐘的從節點,管理員不小心刪除了數據庫,那麼在問題擴散之前,你有30分鐘做出反應。

  • tags 包含一個任意鍵值對集合的文檔,通常用來標識成員在某個數據中心或機架的位置。標籤被用來指定寫關注的粒度和讀設置(8.4.9節裡會做詳細的討論)。

以上就是針對單個副本集成員的所有選項。還有兩個全局副本集配置參數,位於settings鍵中。在副本集配置文檔裡,它們是這樣的:

{
  settings: {
    getLastErrorDefaults: {w: 1},
    getLastErrorModes: {
      multiDC: { dc: 2 }
    }
  }
}
  
  • getLastErrorDefaults 當客戶端不帶參數調用getLastError時,默認的參數是由這個文檔指定的。要謹慎對待該選項,因為它也可能設置了驅動中getLastError的全局默認值,你可以想像這樣一種情況:應用程序開發者調用了getLastError,但他沒有意識到管理員在服務器上指定了一個默認值。

關於getLastError更詳細的信息,可以查看3.2.3節與寫關注相關的部分。簡單起見,要指定所有寫操作都要在500 ms內被複製到至少兩個成員上,可以像這樣進行配置:

settings: { getLastErrorDefaults: {w: 2, wtimeout: 500} }。
  
  • getLastErrorModes 為getLastError命令定義了額外模式的文檔。這個特性依賴於副本集標籤,詳見8.4.4節。

2. 副本集狀態

通過replSetGetStatus命令能夠看到副本集及其成員的狀態。要在Shell裡調用該命令,可以運行rs.status輔助方法。結果文檔標識了現存成員及其各自的狀態、正常運行時間和oplog時間。瞭解副本集成員的狀態是非常重要的;在表8-1里可以看到完整的狀態值列表。

表8-1 副本集狀態

狀  態狀態字符串說  明 0STARTUP表示節點正在通過ping與其他節點溝通,分享配置數據 1PRIMARY這是主節點。副本集總是有且僅有一個主節點 2SECONDARY這是只讀的從節點。該節點在故障轉移時可能會成為主節點,當且僅當其優先級大於0並且沒有被標記為隱藏時 3RECOVERING該節點不能用於讀寫操作。通常會在故障轉移或添加新節點後看到這個狀態。在恢復時,數據文件通常正在同步中;可以查看正在恢復的節點的日誌進行驗證 4FATAL網絡連接仍然建立著,但節點對ping沒響應了。節點被標記為FATAL,通常說明托管該節點的機器發生了致命錯誤 5STARTUP2初始數據文件正在同步中 6UNKNOWN還在等待建立網絡連接 7ARBITER該節點是仲裁節點 8DOWN該節點早些時候還能訪問並正常運行,但現在對「心跳」檢測沒應答了 9ROLLBACK正在進行回滾

當所有節點的狀態都是1、2或7,並且至少有一個節點是主節點時,可以認為副本集是穩定且在線的。可以在外部腳本裡使用replSetGetStatus命令來監控全局狀態、複製延時以及正常運行時間,建議在生產環境部署中這樣做。6

6. 除了運行狀態命令,還可以通過Web控制台看到有用的信息。第10章討論了Web控制台,並給出了一些結合副本集的使用示例。

3. 故障轉移與恢復

你在示例副本集裡已經看過幾個故障轉移的例子了。這裡,我總結一下故障轉移的規則,提供幾個處理恢復的建議。

當配置中的所有成員都能和其他成員通信時,副本集就能上線了。每個節點默認都有一票投票,那些投票最終會幫助得出投票結果,選出主節點。這意味著只要兩個節點(和投票)就能啟動副本集了。但初始的投票數還能決定發生故障轉移時,什麼才能構成多數節點。

讓我們假設你配置了一個由三個完整副本(沒有仲裁節點)組成的副本集,這也達到了自動故障轉移的推薦最小配置。如果主節點發生故障了,剩下的從節點仍能看到對方,那麼就能選出新的主節點。如何做出選擇呢?擁有最新oplog(或更高優先級)的從節點會被選為主節點。

  • 故障模式與恢復

恢復是在故障後將副本集還原到原始狀態的過程。有兩大類故障需要處理。第一類包含所謂的無損故障(clean failure),仍然可以認為該節點的數據文件是完好無損的。網絡分區(network partition)就是一個例子,若某個節點失去了與其他節點的連接,你只需要等待重新建立連接就行了,被分割開的節點也會重新變為副本集中的成員。還有一個類似的情況,某個節點的mongod進程出於某些原因被終止了,但它可以恢復正常在線狀態。7同樣的,一旦進程重啟,它就能重新加入集合了。

7. 舉例來說,如果MongoDB是正常關閉的,那你肯定知道數據文件是好的。或者,如果使用了Journaling日誌,不管是如何結束的,MongoDB實例都能恢復。

第二類故障包含所有明確故障(categorical failure),某個節點的數據文件不存在或者必須假設已經損壞。非正常關閉mongod進程,又沒有開啟Journaling日誌,以及硬盤崩潰都屬於此類故障。恢復明確故障節點的唯一途徑就是重新同步或利用最近的備份完全替換數據文件,讓我們輪流看下這兩種策略。

要完全重新同步,在故障節點上的某個空數據目錄裡啟動一個mongod。只要主機名和端口號沒有改變,新的mongod會重新加入副本集,隨後重新同步全部現有數據。如果主機名或者端口號有變化,那麼在mongod重新上線後,你還需要重新配置副本集。舉個例子,假設節點arete:40001的數據無法恢復,你在foobar:40000啟動了一個新節點。你可以重新配置副本集,只需抓取配置文檔,修改第二個節點的host屬性,隨後將其傳給rs.reconfig方法:

> use local
> config = db.system.replset.findOne
{
  "_id" : "myapp", "version" : 1,
  "members" : [
    {
      "_id" : 0,
      "host" : "arete:30000"
    },
    {
      "_id" : 1,
      "host" : "arete:30001"
    },
    {
      "_id" : 2,
      "host" : "arete:30002"
    }
  ]
}
> config.members[1].host = "foobar:40000"
arete:40000
> rs.reconfig(config)
  

現在副本集可以識別新節點了,而新節點應該能從現有節點同步數據了。

除了通過完全重新同步進行恢復,還可以通過最近的備份進行恢復。通常都會使用某個從節點來進行備份8,方法是製作數據文件的快照並離線存儲。僅當備份中的oplog不比當前副本集成員的oplog舊時,才能通過備份進行恢復。也就是說,備份的oplog裡的最新操作必須仍存在於線上oplog裡。可以用db.getReplicationInfo提供的信息立即確定情況是否如此。在進行恢復時,不要忘記考慮還原備份所需的時間。要是備份裡最新的oplog條目在從備份複製到新機器的過程有可能變舊,那麼最好還是進行完全重新同步吧。

8. 第10章會詳細討論備份。

但通過備份進行恢復速度更快,部分原因是不用從零開始重新構建索引。要從備份進行恢復,將備份的數據文件複製到mongod的數據路徑裡。應該會自動開始重新同步的,你可以檢查日誌或者運行rs.status進行驗證。

4. 部署策略

你已經知道了副本集最多可以包含12個節點,看過了一組令人眼花繚亂的配置選項表格,以及故障轉移與恢復所要考慮的內容。配置副本集的方式有很多,但在本節中,我只會討論那些適用於大多數情況的配置方式。

提供自動故障轉移的最小副本集配置就是先前所構建的那個,包含兩個副本和一個仲裁節點。在生產環境中,仲裁節點可以運行在應用服務器上,而副本則運行於自己的機器上。對於多數生產環境中的應用而言,這種配置既經濟又高效。

但是對於那些對正常運行時間有嚴格要求的應用程序而言,副本集中需要包含三個完整的副本。那個額外的副本能帶來什麼好處呢?請想像這樣一個場景:一個節點徹底損壞了。在恢復損壞的節點時,你還有兩個正常的節點可用。只要第三個節點在線並正在恢復(這可能需要幾個小時),副本集仍能自動故障轉移到擁有最新數據的節點上。

一些應用程序要求有兩個數據中心來做冗余,三個成員的副本集在這種情況下仍然適用。技巧在於讓其中一個數據中心僅用於災難恢復。圖8-2就是一個例子。其中,主數據中心運行了副本集的主節點和一個從節點,備用數據中心裡的從節點作為被動節點(優先級為0)。

圖8-2 成員分佈在兩個數據中心的三節點副本集

在這個配置中,副本集的主節點始終是數據中心A裡兩個節點的其中之一。你可以在損失任意一個節點或者任意一個數據中心的情況下,保持應用程序在線。故障轉移通常都是自動的,除非數據中心A的節點都發生了故障。同時損失兩個節點的情況很少見,通常表現為數據中心A完全故障或者網絡分區。要迅速恢復,可以關閉數據中心B裡的節點,不帶 --replSet參數進行重啟。除此之外,還可以在數據中心B裡啟動兩個新節點,隨後強制進行副本集重新配置。照道理不該在大多數節點無法訪問時重新配置副本集,但在緊急情況下可以使用force選項這麼做。例如,假設定義了一個新的配置文檔config,可以像下面這樣強制進行重新配置:

> rs.reconfig(config, {force: true})
  

和所有生產系統一樣,測試是關鍵,請確保在類似於生產環境的預發佈環境中對所有典型故障轉移和恢復場景進行測試。瞭解副本集在這些故障情況下會有何表現,這會讓你在發生緊急情況時更從容不迫、處亂不驚。