1. 程式人生 > 其它 >Mongo副本集的管理

Mongo副本集的管理

  本章介紹副本集管理的相關知識,包括:

    • 維護獨立的成員;
    • 在多種不同情況下配置副本集;
    • 獲取oplog相關資訊,以及調整oplog大小;
    • 特殊的副本集配置
    • 從主從模式切換到副本集模式。

1.以單機模式啟動成員

  許多維護工作不能在備份節點上進行(因為要執行寫操作),也不能在主節點上進行。後面幾節會經常提到以單機模式(Standalonemode)啟動伺服器。這是指要重啟成員伺服器,讓它成為一個單機執行的伺服器,而不再是一個副本整合員(這只是臨時的)。

  在以單機模式啟動伺服器之前,先看一下伺服器的命令列引數:

> db.serverCmdLineOpts()
{
        
"argv" : [ "mongod", "-f", "/var/lib/mongod.conf"], "parsed" : { "repISet": "mySet", "port": "27017", "dbpath"; "/var/lib/db" }, "ok" : 1 }

  如果要對這臺伺服器進行維護,可以重啟伺服器,重啟時不使用replSet選項。這樣它就會成為一個單機的mongod,可以對其進行讀和寫。我們不希望副本集中的其他伺服器聯絡到這臺伺服器,所以可以讓它監聽不同的埠(這樣副本集的其他成員就找不到它了)。最後,保持dbpath的值不變,因為重啟後要對這臺伺服器的資料做一些操作。好了,我們最終可以用下面這樣的引數啟動伺服器:

$ mongod --port 30000 --dbpath /var/lib/db

  現在這臺伺服器已經在單機模式中運行了,監聽著30000埠的連線請求。副本集中的其他成員仍然會試圖連線到它的27017埠,所以會連線失敗,其他成員就會以為這臺伺服器掛掉了。

  當在這臺伺服器上執行完維護工作之後,可以以最原始的引數重新啟動它。啟動之後, 它會自動與副本集中的其他成員進行同步,將維護期間落下的操作全部複製過來。

2.副本集配置

  副本集配置總是以一個文件的形式儲存在local.system.replSet集合中。副本集中所 有成員的這個文件都是相同的。絕對不要使用update更新這個文件,應該使用rs輔助函式或者replSetReconfig命令修改副本集配置。

2.1 建立副本集

  建立副本集的步驟很簡單,首先啟動所有成員伺服器,然後使用rs.initiate命令將配置檔案傳遞給其中一個成員:

>  var config = {
…  "id" : setName,
…  "members" : [
...    {"_id" :  0,    "host" : host1},
...    {"_id" :  1,    "host" : host2},
...    {"_id" :  2,    "host" : host3}
...  ]}
>  rs.initiate(config)

  應該總是傳遞一個配置物件給rs.initiate,否則MongoDB會自動生成一個針對單成員副本集的配置,其中的主機名可能不是你希望的。

  只需要對副本集中的一個成員呼叫rs.initiate就可以了。收到initiate命令的成員會自動將配置檔案傳遞給副本集中的其他成員。

2.2 修改副本整合員

  向副本集中新增新成員時,這個新成員的資料目錄要麼是空的(在這種情況下,新成員會執行初始化同步),要麼新成員擁有一份其他成員的資料副本。

  連線到主節點並且新增新成員:

>  rs.add("spock:27017")

  也可以以文件的形式為新成員指定更復雜的配置:

>  rs.add({"_id" : 5, "host" : "spock:27017", "priority" : 0, "hidden" : true})

  可以根據"host"欄位將成員從副本集中移除:

>  rs.remove("spock:27017")

  可以通過rs.reconfig修改副本整合員的配置。修改副本整合員配置時,有幾個限制需要注意:

    • 不能修改成員的"_id"欄位;
    • 不能將接收rs.reconfig命令的成員(通常是主節點)的優先順序設為0;
    • 不能將仲裁者成員變為非仲裁者成員,反之亦然;
    • 不能將"buildlndexes":false的成員修改為"buildlndexes":true。

  需要注意的是,可以修改成員的"host"欄位。這意味著,如果為副本整合員指定了不正確的主機名(比如使用了公網IP而不是內網IP),之後可以重新修改成員的主機名。下面是一個修改主機名的例子:

>  var config = rs.config()
>  config.members[0].host = "spock:27017" 
spock:27017
>  rs.reconfig(config)

  修改其他選項的方式也是一樣的:使用rs.config得到當前配置檔案,修改配置檔案,將修改後的配置檔案傳遞給rs.reconfig就可以了。

2.3 建立比較大的副本集

  副本集最多隻能擁有12個成員,其中只有7個成員擁有投票權。這是為了減少心跳請求的網路流量(每個成員都要向其他所有成員傳送心跳請求)和選舉花費的時間。實際上,副本集還有更多的限制,如果需要11個以上的備份節點,參考本章第5節。

  如果要建立7個以上成員的副本集,只有7個成員可以擁有投票權,需要將其他成員的投票數量設定為0:

>  rs.add({"_id" : 7, "host" : "server-7:27017", "votes" : 0})

  這樣可以阻止這些成員在選舉中投主動票,雖然它們仍然可以投否決票。

  應該儘量避免修改成員的投票數量。投票可能會對選舉和一致性產生怪異的、不直觀的影響。應該只在建立包含7個以上成員的副本集或者是希望阻止自動故障轉移(本章第五節)時,使用"votes"選項。很多開發者會誤以為讓成員擁有更多投票權會使這個成員更容易被選為主節點(實際上根本不會)。如果希望某個成員可以優先被選舉為主節點,應該使用優先順序。

2.4 強制重新配置

  如果副本集無法再達到“大多數”要求的話,那麼它就無法選舉出新的主節點,這時你可能會希望重新配置副本集。這看起來有點奇怪,因為通常都是將配置檔案傳送給主節點。在這種情況下,可以在備份節點上呼叫rs.reconfig強制重新配置(forcereconfigure)副本集。在shell中連線到一個備份節點,使用"force"選項執行rs.reconfig命令:

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

  強制重新配置與普通的重新配置要遵守同樣的規則:必須使用正確的reconfig選項將有效的、格式完好的配置檔案傳送給成員。"force"選項不允許無效的配置,而且只允許將配置傳送給備份節點。

  強制重新配置會跳過大量的數值直接將副本集的"version"設為一個比較大的值。 可能會見到跳過數千的情況,這很正常:這是為了防止"version"欄位衝突(以防不同的網路域中都在進行重新配置)。備份節點收到新的配置檔案之後,就會修改自身的配置,並且將新的配置傳送給副本集中的其他成員。副本集的其他成員收到新的配置檔案之後,會判斷配置檔案的傳送者是否是它們當前配置中的一個成員,如果是,才會用新的配置檔案對自己進行重新配置。所以,如果新的配置會修改某些成員的主機名,應該將新的配置傳送給主機名不發生變化的成員。如果新的配置檔案修改了所有成員的主機名,應該關閉副本集的每一個成員,以單機模式啟動,手動修改local.system.replset文件,然後重新啟動。

3.修改成員狀態

  為進行維護或響應載入,有多種方式可以手動修改成員的狀態。注意,無法強制將某個成員變成主節點,除非對副本集做適當的配置。

3.1 把主節點變為備份節點

  可以使用stepDown函式將主節點降級為備份節點:

> rs.stepDown()

  這個命令可以讓主節點退化為備份節點,並維持60秒。如果這段時間內沒有新的主節點被選舉出來,這個節點就可以要求重新進行選舉。如果希望主節點退化為備份節點並持續更長(或者更短)的時間,可以自己指定時間(以秒為單位):

> rs.stepDown(600) // 10 分鐘

3.2 阻止選舉

  如果需要對主節點做一些維護,但是不希望這段時間內將其他成員選舉為主節點,那麼可以在每個備份節點上執行freeze命令,以強制它們始終處於備份節點狀態:

>  rs.freeze(10000)

  這個命令也會接受一個以秒錶示的時間,表示在多長時間內保持備份節點狀態。

  維護完成之後,如果想“釋放”其他成員,可以再次執行freeze命令,將時間指定為0即可:

>  rs.freeze(0)

  這樣,其他成員就可以在必要時申請被選舉為主節點。

  也可以在主節點上執行rs.freeze(0),這樣可以將退位的主節點重新變為主節點。

3.3 使用維護模式

  當在副本整合員上執行某個非常耗時的操作時,這個成員就人進入維護模式(maintenancemode):強制成員進入RECOVERING狀態。有時,成員會自動進入維護模式,比如在成員上做壓縮時。壓縮開始之後,成員會進入RECOVERING狀態,這樣就不會有讀請求傳送給這個成員。客戶端會停止從這個成員讀取資料(如果之前有從這個成員讀資料的話),這個成員也不能再作為複製源。也可以通過執行replSetMaintenanceMode命令強制一個成員進入維護模式。如果一個成員遠遠落後於主節點,你不希望它繼續處理讀請求時,可以強制讓這個成員進入維護模式。例如,下面這個指令碼會自動檢測成員是否落後於主節點30秒以上,如果是,就強制將這個成員轉入維護模式:

function maybeMaintenanceMode() {
        var local = db.getSisterDB("local");
        //如果成員不是備份節點(它可能是主節點 
       //或者已經處於維護狀態),就直接返回 
       if (!local.isMaster().secondary) { 
            return;
        }
       //査找這個成員最後一次操作的時間
       var last = local.oplog.rs.find().sort({"$natural" : -1}).next(); 
       var lastTime = last['ts']['t'];
      //如果落後主節點30秒以上
      if (lastTime < (new Date()).getTime()-30) {
         db.adminCommand({"replSetMaintenanceMode" : true});
       }
       };

  將成員從維護模式中恢復,可以使用如下命令:

> db.adminCommand({"replSetMaintenanceMode" : false});

4.監控複製

  監控副本集的狀態非常重要:不僅要監控是否所有成員都可用,也要監控每個成員處於什麼狀態,以及每個成員的資料新舊程度。有多個命令可以用來査看副本集相關資訊。

  與複製相關的故障通常都是很短暫的:一個伺服器剛才還連線不到另一個伺服器, 但是現在又可以連上了。要檢視這樣的問題,最簡單的方式就是査看日誌。確保自己知道日誌的儲存位置(而且真的被儲存下來),確保能夠訪問到它們。

4.1 獲取狀態

  replSetGetStatus是一個非常有用的命令,可以返回副本集中每個成員的當前資訊(這裡的“當前”是從每個成員自身的角度來說的)。這個命令還有一個對應的輔助函式rs.status:

> rs.status()
       "set" : "spock",
       "date" : ISODate("2012-10-17T18:17:52Z"), 
       "myState" : 2,
       "syncingTo" : "server-1:27017",
       "members":[
                 {
                      "_id" : 0,
                      "name" : "server-1:27017", 
                      "health" : 1, 
                      "state" : 1,
                      "stateStr" : "PRIMARY",
                      "uptime" : 74824,
                      "optime" :{ "t" : 1350496621000, "i" : 1 }, 
                      "optimeDate" : ISODate("2012-10-17T17:57:01Z"), 
                      "lastHeartbeat" : ISODate("2012-10-17T17:57:09Z"), 
                      "pingMs" : 3, 
                  },
                  {
                      "_id" : 1,
                      "name" : "server-2:27017",
                      "health" : 1,
                      "state" : 2,
                      "stateStr" : "SECONDARY",
                      "uptime" : 161989,
                      "optime" :{ "t" : 1350377549000, "i" : 500 }, 
                      "optimeDate" : ISODate("2012-10-17T17:57:00Z"), 
                      "self" : true
                  },
                  {
                      "id" : 2,
                      "name" : "server-3:27017", 
                      "health" : 1,
                      "state" : 3,
                      "stateStr" : "RECOVERING", 
                      "uptime" : 24300,
                      "optime" :{ "t" : 1350411407000, "i" : 739 },
                      "optimeDate" : ISODate("2012-10-16T18:16:47Z"),
                      "lastHeartbeat" : IS0Date( ,,2012-10-17T17:57:01Z"),
                      "pingMs" : 12,
                      "errmsg" : "still syncing, not yet to minValid optime 507e9a30:851"
                    }
           ],
           "ok" : 1
}

  下面分別介紹幾個最有用的欄位。

  • self

  這個欄位只會出現在執行rs.status()函式的成員資訊中,在本例中是server-2。

  • stateStr

  用於描述伺服器狀態的字串。

  • uptime

  從成員可達一直到現在所經歷的時間,單位是秒。對於"self"成員,這個值是從成員啟動一直到現在的時間。因此,server-2已經啟動161 989秒了(大約45小時)。server-1在過去的21小時中一直處於可用狀態,server-3在過去7小時中一直處於可用狀態。

  • optimeDate

  每個成員的oplog中最後-個操作發生的時間(也就是操作被同步過來的時間)。 注意,這裡的狀態是每個成員通過心跳報告上來的狀態,所以optime跟實際時間可能會有幾秒鐘的偏差。

  • lastHeartbeat

  當前伺服器最後一次收到其他成員心跳的時間。如果網路故障或者當前伺服器比較繁忙,這個時間可能會是2秒鐘之前。

  • pingMs

  心跳從當前伺服器到達某個成員所花費的平均時間,可以根據這個欄位選擇從哪個成員進行同步。

  • errmsg

  成員在心跳請求中返回的狀態資訊。這個欄位的內容通常只是一些狀態資訊,而不是錯誤資訊。例如,server-3的"errmsg”欄位表示它正處於初始化同步過程中。這裡的十六進位制數字507e9a30:851是某個操作對應的時間戳,server-3至少要同步完這個操作才能完成同步過程。

  有幾個欄位的資訊是重複的:"state"與"stateStr"都表示成員的狀態,只是 "state"的值是狀態的內部表示法。"health"僅僅表示給定的伺服器是否可達(可達是1,不可達是0),而從"state"和"stateStr"欄位也可以得到這樣的資訊(如果伺服器不可達,它們的值會是UNKNOWN或者DOWN)。類似地,"optime"和"optimeDate"的值也是相同的,只是表示方式不同:一個是用從新紀元開始的毫秒數表示的,另一個用一種更適合閱讀的方式表示。

  注意:這份報告是以執行rs.status()命令的成員的角度得出的:由於網路故障,這份報告可能不準確或者有些過時。

4.2 複製圖譜

  如果在備份節點上執行rs.status(),輸出資訊中會有一個名為"syncingTo"的頂級欄位,用於表示當前成員正在從哪個成員處進行復制。如果在每個成員上執行replSetGetStatus命令,就可以弄清楚複製圖譜(replicationgraph)。假設server1表示連線到server1的資料庫連線,server2表示連線到server2的資料庫連線,以此類推,然後分別在這些連線上執行下面的命令:

>  server1.adminCommand({replSetGetStatus: 1})['syncingTo'] 
server0:27017
>  server2.adminCommand({replSetGetStatus: 1})['syncingTo'] 
server1:27017
>  server3.adminCommand({replSetGetStatus: 1})['syncingTo'] 
server1:27017
>  server4.adminCommand({replSetGetStatus: 1})['syncingTo'] 
server2:27017

  所以,serverO是server1的同步源,server1是server2 和server3 的同步源,server2 是server4的同步源。

  MongoDB根據ping時間選擇同步源。一個成員向另一個成員傳送心跳請求,就可以知道心跳請求所耗費的時間。MongoDB維護著不同成員間請求的平均花費時間。選擇同步源時,會選擇一個離自己比較近而且資料比自己新的成員(所以,不會出現迴圈複製的問題,每個成員要麼從主節點複製,要麼從資料比它新的成員處複製)。

  因此,如果在備份資料中心中新增一個新成員,它很可能會從與自己同在一個數據中心內的其他成員處複製,而不是從位於另一個數據中心的主節點處複製(這樣可以減少網路流量)。

  但是,自動複製鏈(automaticreplicationchaining)也有一些缺點:複製鏈越長,將寫操作複製到所有伺服器所花費的時間就越長。假設所有伺服器都位於同一個資料中心內,然後,由於網路速度異常,新新增一個成員之後,MongoDB的複製鏈如圖12-2所示。

  通常不太可能發生這樣的情況,但是並非不可能。但這種情況通常是不可取的:複製鏈中的每個備份節點都要比它前面的備份節點稍微落後一點。只要出現這種狀況,可以用replSetSyncFrom(或者是它對應的輔助函式rs.syncFrom(})命令修改成員的複製源進行修復。

  連線到需要修改複製源的備份節點,執行這個命令,為其指定一個複製源:

> secondary.adminCommand({"replSetSyncFrom" : "server0:27017"})

  可能要花費幾秒鐘的時間才能切換到新的複製源。如果在這個成員上再次執行rs.status(),會發現"syncingTo"欄位的值已經變成了 "server0:27017"。

  現在,server4會一直從serverO進行復制,直到serverO不可用或者遠遠落後於其他 成員為止。

4.3 複製迴圈

  如果複製鏈中出現了環,那麼就稱為發生了複製迴圈。例如,A從B處同步資料,B從C處同步資料,C從A處同步資料,這就是一個複製迴圈。因為複製迴圈中的成員都不可能成為主節點,所以這些成員無法複製新的寫操作,就會越來越落後。另一方面,如果每個成員都是自動選取複製源,那麼複製迴圈是不可能發生的。

  但是,使用replSetSyncFrom強制為成員設定複製源時,就可能會出現複製循壞。在手動修改成員的複製源時,應該仔細査看rs.status()的輸出資訊,避免造成複製迴圈。當用repl_SetSyncFrom為成員指定一個並不比它領先的成員作為複製源時,系統會給出警告,但是仍然允許這麼做。

4.4 禁用複製鏈

  當一個備份節點從另一個備份節點(而不是主節點)複製資料時,就會形成複製鏈。 前面說過,成員會自動選擇其他成員作為複製源。可以禁用複製鏈,強制要求每個成員都從主節點進行復制,只需要將"allowChaining"設定為false即可(如果不指定這個選項,預設是true):

>  var config = rs.config()
>  //如果設定子物件不存在,就自動建立一個空的
>  config.settings = config.settings | | {}
>  config.settings.allowChaining = false
>  rs.reconfig(config)

  將allowChaining設定為false之後,所有成員都會從主節點複製資料。如果主節點變得不可用,那麼各個成員就會從其他備份節點處複製資料。

4.5 計算延遲

  跟蹤複製情況的一個重要指標是備份節點與主節點之間的延遲程度。延遲(lag)是指備份節點相對於主節點的落後程度,是主節點最後一次操作的時間戳與備份節點最後一次操作的時間戳的差。

  可以使用rs.status()査看成員的複製狀態,也可以通過在主節點上執行db.printReplicationInfo()(這個命令的輸出資訊中包括oplog相關資訊),或者在備份節點上執行db.printSlaveReplicationInfo()快速得到一份摘要資訊。注意,這兩個都是db的函式,而不是rs的。

  db.printRepUcationlnfo的輸出中包括主節點的oplog資訊:

> db.printReplicationInfo(); 
       configured oplog size:            10.48576MB 
       log length start to end:           34secs (0.01hrs)
       oplog first event time:            Tue Mar 30 2010 16:42:57 GMT-0400 (EDT) 
       oplog last event time:             Tue Mar 30 2010 16:43:31 GMT-0400 (EDT)
       now:                               Tue Mar 30 2010 16:43:37 GMT-0400 (EDT)

  上面的輸出資訊中包含了oplog的大小,以及oplog中包含的操作的時間範圍。在本例中,oplog的大小大約是10MB,而且只包含大約最近30秒的操作。

  在實際的部署中,oplog會大得多。我們希望oplog的長度至少要能夠容納一次完整同步的所有操作。這樣,備份節點就不會在完成初始化同步之前與oplog脫節。

  在備份節點上執行db.printSlaveReplicationInfo(),可以得到當前成員的複製源,以及當前成員相對複製源的落後程度等資訊:

> db.printSlaveReplicationInfo(); 
        source:    server-0:27017
        syncedTo: Tue Mar 30 2012 16:44:01 GMT-0400 (EDT)
        =12secs ago (0hrs)

  這樣就可以知道當前成員正在從哪個成員處複製資料。在這個例子中,備份節點比主節點落後12秒。

  注意:副本整合員的延遲是相對於主節點來說的,而不是表示需要多長時間才能更新到最新。在一個寫操作非常少的系統中,有可能會造成延遲過大的幻覺。假設一小時執行一次寫操作。剛剛執行完這次寫操作之後,複製之前,備份節點會落後於主節點一小時。但是,只需要幾毫秒時,備份節點就可以追上主節點。當監控低吞吐量的系統時,這個值可能會造成迷惑。

4.6 調整oplog大小

  可以將主節點的oplog長度看作維護工作的時間窗。如果主節點的oplog長度是一小時,那麼你就只有一小時的時間可以用於修復各種錯誤,不然的話備份節點可能會落後於主節點太多,導致不得不重新進行完全同步。所以,你通常可能希望oplog能夠儲存幾天或者一個星期的資料,從而給自己預留足夠的時間,用於處理各種突發狀況。

  可惜,在oplog被填滿之前很難知道它的長度,也沒有辦法在伺服器執行期間調整oplog大小。但是,可以依次將每臺伺服器下線,調整它的oplog,然後重新把它新增到副本集中。記住,每一個可能成為主節點的伺服器都應該擁有足夠大的oplog,以預留足夠的時間窗用於進行維護。如果要增加oplog大小,可以按照如下步驟。

  (1)如果當前伺服器是主節點,讓它退位,以便讓其他成員的資料能夠儘快更新到與它一致。

  (2) 關閉當前伺服器。

  (3) 將當前伺服器以單機模式啟動。

  (4) 臨時將oplog中的最後一條insert操作儲存到其他集合中:

>  use local
>  // op: "i"用於査找最後一條insert操作
>  var cursor = db.oplog.rs.find({"op" : "i"})
>  var lastlnsert = cursor.sort({"$natural" : -1}).limit(1).next()
>  db.tempLastOp.save(lastlnsert)
> 
>  //確保儲存成功,這非常重要!
>  db.tempLastOp.findOne()

  也可以使用最後一項update或者delete操作,但是$操作符不能插入到集合中。

  (5) 刪除當前的oplog:

>  db.oplog.rs.drop()

  (6) 建立一個新的oplog:

>  db.createCollection("oplog.rs", {"capped" : true, "size" : 10000})

  (7) 將最後一條操作記錄寫回oplog:

>  var temp = db.tempLastOp.findOne()
>  db.oplog.rs.insert(temp)
> 
>  //要確保插入成功
>  db.oplog.rs.findOne()

  確保最後一條操作記錄成功插入oplog。如果沒有插入成功,把當前伺服器新增到副本集之後,它會刪除所有資料,然後重新進行一次完整同步。

  (8) 最後,將當前伺服器作為副本整合員重新啟動。注意,由於這時它的oplog只有一條記錄,所以在一段時間內無法知道oplog的真實長度。另外,這個伺服器現在也並不適合作為其他成員的複製源。

  通常不應該減小oplog的大小:即使oplog可能會有幾個月那麼長,但是通常總是有足夠的硬碟空間來儲存oplog,oplog並不會佔用任何珍貴的資源(比如CPU或RAM)。

4.7 從延遲備份節點中恢復

  假設有人不小心刪除了一個數據庫,幸好你有一個延遲備份節點。現在,需要放棄其他成員的資料,明確將延遲備份節點指定為資料來源。有幾種方法可以使用。

  下面介紹最簡單的方法。

    (1) 關閉所有其他成員。

    (2) 刪除其他成員資料目錄中的所有資料。確保每個成員(除了延遲備份節點)的資料目錄都是空的。

    (3)重啟所有成員,然後它們會自動從延遲備份節點中複製資料。

  這種方式非常簡單,但是,在其他成員完成初始化同步之前,副本集中將只有一個成員可用(延遲備份節點)而且這個成員很可能會過載。

  根據資料量的不同,第二種方式可能更好,也可能更差。

    (1) 關閉所有成員,包括延遲備份節點。

    (2) 刪除其他成員(除了延遲備份節點)的資料目錄。

    (3)將延遲備份節點的資料檔案複製到其他伺服器。

    (4)重啟所有成員。

  注意,這樣會導致所有伺服器都與延遲備份節點擁有同樣大小的oplog,這可能不是你想要的。

4.8 建立索引

  如果向主節點發送建立索引的命令,主節點會正常建立索引,然後備份節點在複製 “建立索引”操作時也會建立索引。這是最簡單的建立索引的方式,但是建立索引是一個需要消耗大量資源的操作,可能會導致成員不可用。如果所有備份節點都在同一時間開始建立索引,那麼幾乎所有成員都會不可用,一直到索引建立完成。

  因此,可能你會希望每次只在一個成員上建立索引,以降低對應用程式的影響。如果要這麼做,有下面幾個步驟。

    (1) 關閉一個備份節點伺服器。

    (2) 將這個伺服器以單機模式啟動。

    (3)在單機模式下建立索引。

    (4)索引建立完成之後,將伺服器作為副本整合員重新啟動。

    (5)對副本集中的每個備份節點重複第(1)步~第(4)步。

  現在副本集的每個成員(除了主節點)都已經成功建立了索引。現在你有兩個選擇, 應該根據自己的實際情況選擇一個對生產系統影響最小的方式。

    (1)在主節點上建立索引。如果系統會有一段負載比較小的“空閒期”,那會是非常好的建立索引的時機。也可以修改讀取首選項,在主節點建立索引期間,將讀操作傳送到備份節點上。

  主節點建立索引之後,備份節點仍然會複製這個操作,但是由於備份節點中已經有了同樣的索引,實際上不會再次建立索引。

    (2)讓主節點退化為備份節點,對這個伺服器執行上面的4步。這時就會發生故障轉移,在主節點退化為備份節點建立索引期間,會有新的節點被選舉為主節點,保證系統正常運轉。索引建立完成之後,可以重新將伺服器新增到副本集。

  注意,可以使用這種技術為某個備份節點建立與其他成員不同的索引。這種方式在做離線資料處理時會非常有用,但是,如果某個備份節點的索引與其他成員不同,那麼它永遠不能成為主節點:應該將它的優先順序設為0。

  如果要建立唯一索引,需要先確保主節點中沒有被插入重複的資料,或者應該首先為主節點建立唯一索引。否則,可能會有重複資料插入主節點,這會導致備份節點複製時出錯,如果遇到這樣的錯誤,備份節點會將自己關閉。你不得不以單機模式啟動這臺伺服器,刪除唯一索引,然後重新將其加入副本集。

4.9 在預算有限的情況下進行復制

  如果預算有限,不能使用多臺高效能伺服器,可以考慮將備份節點只用於災難恢復, 這樣的備份節點不需要太大的RAM和太好的CPU,也不需要太髙的磁碟IO。這樣,始終將高效能伺服器作為主節點,比較便宜的伺服器只用於備份,不處理任何客戶端請求(將客戶端配置為將全部讀請求傳送到主節點)。對於這樣的備份節點,應該設定這些選項。

  • "priority":0

  優先順序為0的備份節點永遠不會成為主節點。

  • "hidden":true

  將備份節點設為隱藏,客戶端就無法將讀請求傳送給它了。

  • "buildlndexes":false

  這個選項是可選的,如果在備份節點上建立索引的話,會極大地降低備份節點的效能。如果不在備份節點上建立索引,那麼從備份節點中恢復資料之後,需要重新建立索引。

  • "votes":0

  在只有兩臺伺服器的情況下,如果將備份節點的投票數設為0,那麼當備份節點掛掉之後,主節點仍然會一直是主節點,不會因為達不到“大多數”的要求而退位。如果還有第三臺伺服器(即使它是你的應用伺服器),那麼應該在第三臺伺服器上執行一個仲裁者成員,而不是將第三臺伺服器的投票數量設為0。

  在沒有足夠的預算購買多臺高效能伺服器的情況下,可以用這樣的備份節點來保證系統和資料安全。

4.10 主節點如何跟蹤延遲

  作為其他成員的同步源的成員會維護一個名為local.slaves的集合,這個集合中儲存 著所有正從當前成員進行資料同步的成員,以及每個成員的資料新舊程度。如果使用w引數執行査詢,MongoDB會根據這些資訊確定是否有足夠多、足夠新的備份節點可以用來處理査詢。

  local.slaves集合實際上是記憶體中資料結構的“回聲”,所以其中的資料可能會有幾秒鐘的延遲:

>    db.slaves.find()
{ "_id" : ObjectId("4c1287178e00e93dl858567c"), "host" : "10.4.1.100",
  "ns" : "local.oplog.rs", "syncedTo" :{ "t" : 1276282710000, "i" : 1 } }
{ "_id" : Objectld("4c128730e6e5c3096f40e0de"), "host" : "10.4.1.101",
  "ns" : "local.oplog.rs", "syncedTo" :{ "t" : 1276282710000, "i" : 1 } }

  每個伺服器的"_id"欄位非常重要:它是所有正在從當前成員進行資料同步的伺服器的識別符號。連線到一個成員,然後査詢local.me集合就可以知道一個成員的識別符號:

>  db.me.find0ne()
{ "_id" : Objectld("50e6edb517c789e46695212f"), "host" : "server-1" }

  非常偶然的情況下,由於網路故障,可能會發現有多臺伺服器擁有相同的識別符號。在這種情況下,只能知道其中一臺伺服器相對於主節點的新舊程度。所以,這可能會導致應用程式故障(如果應用程式需要等待特定數量的伺服器完成寫操作)和分片問題(資料遷移被複制到“大多數”備份節點之前,無法繼續做資料遷移)。如果多臺伺服器擁有相同的"_id",可以依次登入到每臺伺服器,刪除local.me集合,然後重新啟動mongod。啟動時,mongod會使用新的"_id"重新生成local.me集合。

  如果伺服器的地址發生了改變(假定沒有變,但是主機名變了),可能會在本地資料庫的日誌中看到鍵重複異常(duplicatekeyexception)。遇到這種情況時,刪除local.slaves集合即可(這比之前的例子簡單,因為只需要清除舊資料即可,不需要處理資料衝突)。

  mongod不會清理local.slaves集合,所以,它可能會列出某個幾個月之前就不再把該成員作為同步源的伺服器(或者是已經不在副本集內的成員)。由於MongoDB只是把這個集合用於報告複本集狀態,所以這個集合中的過時資料並不會有什麼影響。如果你覺得這個集合中的舊資料會造成困惑或者是過於混亂,可以將整個集合刪除。幾秒鐘之後,如果有新的伺服器將當前成員作為複製源的話,這個集合就會重新生成。

  如果備份節點之間形成了複製鏈,你可能會注意到某個特定的伺服器在主節點的local.slaves集合中有多個文件。這是因為,毎個備份節點都會將複製請求轉發給它的複製源,這樣主節點就能夠知道每個備份節點的同步源。這稱為“影同步”(ghostsyncs),因為這些請求並不會要求進行資料同步,只是把每個備份節點的同步源報告給主節點。

  提示:local資料庫只用於維護複製相關資訊,它並不會被複制。因此,如果希望某些資料只存在於特定的機器上,可以將這些資料儲存在local資料庫的集合中。

5.主從模式

  MongoDB最初支援一種比較傳統的主從模式(master-slave),在這種模式下,MongoDB不會做自動故障轉移,而且需要明確宣告主節點和從節點。有兩種情形應該使用主從模式而不是副本集:需要多於11個備份節點,或者是需要複製單個數據庫。除非迫不得已,否則都應該使用副本集。副本集更易維護,而且功能齊全。主從模式以後會被廢棄,當副本集能夠支援無限資料的成員時,主從模式很可能會被立即廢棄。

  但是,有時可能確實需要11臺以上的備份節點(從節點),或者是需要複製單個數據庫。這些情況下,應該使用主從模式。

  如果要將伺服器設為主節點,可以使用--master選項啟動伺服器。對於從節點,有兩個可用的選項:--slave和--sourcemaster。--source用於指定同步源的主機名和埠號。注意,不要使用--replSet選項,因為現在是要設定主從模式,而不是副本集。

  假如有兩臺伺服器,server-0和server-1,可以這麼做:

$ # server-0 
$ mongod --master 
$
$ # server-1
$ mongod --slave --source server-0:27017

  這樣,主從模式就設定成功了,不需要其他的設定。在主節點執行的寫操作,會被複制到從節點上。

  主從模式也可以用於複製單個數據庫。可以使用--only選項選擇需要進行復制的資料庫。

$ mongod --slave --source server-1:27017 --only super-important-db

  驅動程式不會自動將讀請求傳送給從節點。如果要從從節點讀取資料,需要顯式地建立一個連線到從節點的資料庫連線。

5.1 從主從模式切換到副本集模式

  從主從模式切換到副本集模式,需要停機一段時間,步驟如下。

    (1) 停止系統的所有寫操作。這非常重要,因為在主從模式下,從節點並不會維護一份oplog,所以它無法將升級期間落下的操作同步過來。

    (2) 關閉所有的mongod伺服器。

    (3) 使用--replSet選項重啟主節點,不再使用--master。

    (4) 初始化這個只有一個成員的副本集,這個成員會成為副本集中的主節點。

    (5) 使用--replSet和--fastsync選項啟動從節點。通常,如果向副本集中新增一個沒有oplog的成員,這個成員會立即進入完全的初始化同步過程。fastsync選項用於告訴新成員不會擔心oplog的問題,直接從主節點最新的操作開始同步即可。

    (6) 使用rs.add()將之前的從節點加入副本集。

    (7) 對每個從節點,重複第(5)步和第(6)步。

    (8) 當所有從節點都變為備份節點之後,就可以開啟系統的寫功能了。

    (9) 從配置檔案、命令列別名和記憶體中刪除fastsync選項。這是一個非常危險的選項,它會使成員啟動時跳過一些需要同步的操作。只有在從主從模式切換到副本集時才可以使用這個命令。現在已經切換完成了,不再需要這個選項了。

  現在,主從模式已經被切換為副本集了。

5.2 讓副本集模仿主從模式的行為

  通常你會希望主節點長時間可用,因此,萬一主節點不可用,應該允許自動故障轉移。但是,對於某些副本集,你可能會要求手動選擇新的主節點,不允許進行自動故障轉移。這樣的話,副本集的行為就跟主從模式一樣了(對於這種情況,建議使用主從模式,而不是使用副本集)。為了實現這個目的,需要重新配置副本集,將所有成員(除主節點之外)的priority和votes設為0。這樣一來,如果主節點掛了,不會有任何成員尋求被選舉為主節點。另外,如果所有備份節點都掛了,主節點也仍然會一直保持主節點狀態,不會退位(因為它是整個系統中唯一一個擁有投票權的成員)。

  下面的配置檔案會建立一個具有5個成員的副本集,其中server-0會始終作為主節點,其他4個成員會始終作為備份節點:

>  rs.config()
{
       "_id" :  "spock",
       "members" : [
                 { "_id" : 0, "host" : "server-0:27017"},
                 { "_id" : 1, "host" : "server-1:27017", "priority" : 0, "votes" : 0},
                 { "_id" : 2, "host" : "server-2:27017", "priority" : 0, "votes" : 0},
                 { "_id" : 3, "host" : "server-3:27017", "priority" : 0, "votes" : 0},
                 { "_id" : 4, "host" : "server-4:27017", "priority" : 0, "votes" : 0},
         ]
}

  如果主節點掛了,管理員必須手動選出新的主節點。

  如果要手動將某個備份節點提升為主節點,首先要連線到這個備份節點,然後執行強制重新配置,將它的priority和votes修改為1,同時將先前的主節點的priority和votes修改為 0。

  例如,如果server-0掛了,可以連線到希望提升為新的主節點的備份節點(比如server-1),然後以下面的方式修改配置:

>  var config = rs.config()
>  config.members[1].priority = 1
>  config.members[1].votes = 1
>  config.members[0].priority = 0
>  config.members[0].votes = 0
>  rs.reconfig(config, {"force" : true})

  現在,如果執行rs.config(),就可以看到新的副本集配置資訊了:

>  rs.config()
{
       "_id" :  "spock",
       "version" : 3
       "members" : [
                 { "_id" : 0, "host" : "server-0:27017", "priority" : 0, "votes" : 0},
                 { "_id" : 1, "host" : "server-1:27017"},
                 { "_id" : 2, "host" : "server-2:27017", "priority" : 0, "votes" : 0},
                 { "_id" : 3, "host" : "server-3:27017", "priority" : 0, "votes" : 0},
                 { "_id" : 4, "host" : "server-4:27017", "priority" : 0, "votes" : 0}
         ]
}

  如果新的主節點又掛了,可以重複上面的步驟,手工將某個備份節點提升為新的主節點。

作者:小家電維修

相見有時,後會無期。