概念:
在瞭解了這篇文章之後,可以進行該篇文章的說明和測試。MongoDB 副本集(Replica Set)是有自動故障恢復功能的主從叢集,有一個Primary節點和一個或多個Secondary節點組成。類似於MySQL的MMM架構。更多關於副本集的介紹請見官網。也可以在google、baidu上查閱。
副本集中資料同步過程:Primary節點寫入資料,Secondary通過讀取Primary的oplog得到複製資訊,開始複製資料並且將複製資訊寫入到自己的oplog。如果某個操作失敗,則備份節點停止從當前資料來源複製資料。如果某個備份節點由於某些原因掛掉了,當重新啟動後,就會自動從oplog的最後一個操作開始同步,同步完成後,將資訊寫入自己的oplog,由於複製操作是先複製資料,複製完成後再寫入oplog,有可能相同的操作會同步兩份,不過MongoDB在設計之初就考慮到這個問題,將oplog的同一個操作執行多次,與執行一次的效果是一樣的。簡單的說就是:
當Primary節點完成資料操作後,Secondary會做出一系列的動作保證資料的同步:
1:檢查自己local庫的oplog.rs集合找出最近的時間戳。
2:檢查Primary節點local庫oplog.rs集合,找出大於此時間戳的記錄。
3:將找到的記錄插入到自己的oplog.rs集合中,並執行這些操作。
副本集的同步和主從同步一樣,都是非同步同步的過程,不同的是副本集有個自動故障轉移的功能。其原理是:slave端從primary端獲取日誌,然後在自己身上完全順序的執行日誌所記錄的各種操作(該日誌是不記錄查詢操作的),這個日誌就是local資料 庫中的oplog.rs表,預設在64位機器上這個表是比較大的,佔磁碟大小的5%,oplog.rs的大小可以在啟動引數中設 定:--oplogSize 1000,單位是M。
注意:在副本集的環境中,要是所有的Secondary都當機了,只剩下Primary。最後Primary會變成Secondary,不能提供服務。
一:環境搭建
1:準備伺服器
192.168.200.25 192.168.200.245 192.168.200.252
2:安裝
http://www.cnblogs.com/zhoujinyi/archive/2013/06/02/3113868.html
3:修改配置,只需要開啟:replSet 引數即可。格式為:
192.168.200.252: --replSet = mmm/192.168.200.245:27017 # mmm是副本集的名稱,192.168.200.25:27017 為例項的位子。 192.168.200.245: --replSet = mmm/192.168.200.252:27017 192.168.200.25: --replSet = mmm/192.168.200.252:27017,192.168.200.245:27017
4:啟動
啟動後會提示:
replSet info you may need to run replSetInitiate -- rs.initiate() in the shell -- if that is not already done
說明需要進行初始化操作,初始化操作只能執行一次。
5:初始化副本集
登入任意一臺機器的MongoDB執行:因為是全新的副本集所以可以任意進入一臺執行;要是有一臺有資料,則需要在有資料上執行;要多臺有資料則不能初始化。
zhoujy@zhoujy:~$ mongo --host=192.168.200.252 MongoDB shell version: 2.4.6 connecting to: 192.168.200.252:27017/test > rs.initiate({"_id":"mmm","members":[ ... {"_id":1, ... "host":"192.168.200.252:27017", ... "priority":1 ... }, ... {"_id":2, ... "host":"192.168.200.245:27017", ... "priority":1 ... } ... ]}) { "info" : "Config now saved locally. Should come online in about a minute.", "ok" : 1 } ######
"_id": 副本集的名稱
"members": 副本集的伺服器列表
"_id": 伺服器的唯一ID
"host": 伺服器主機
"priority": 是優先順序,預設為1,優先順序0為被動節點,不能成為活躍節點。優先順序不位0則按照有大到小選出活躍節點。
"arbiterOnly": 仲裁節點,只參與投票,不接收資料,也不能成為活躍節點。
> rs.status() { "set" : "mmm", "date" : ISODate("2014-02-18T04:03:53Z"), "myState" : 1, "members" : [ { "_id" : 1, "name" : "192.168.200.252:27017", "health" : 1, "state" : 1, "stateStr" : "PRIMARY", "uptime" : 76, "optime" : Timestamp(1392696191, 1), "optimeDate" : ISODate("2014-02-18T04:03:11Z"), "self" : true }, { "_id" : 2, "name" : "192.168.200.245:27017", "health" : 1, "state" : 2, "stateStr" : "SECONDARY", "uptime" : 35, "optime" : Timestamp(1392696191, 1), "optimeDate" : ISODate("2014-02-18T04:03:11Z"), "lastHeartbeat" : ISODate("2014-02-18T04:03:52Z"), "lastHeartbeatRecv" : ISODate("2014-02-18T04:03:53Z"), "pingMs" : 0, "syncingTo" : "192.168.200.252:27017" } ], "ok" : 1 }
6:日誌
檢視252上的日誌:
Tue Feb 18 12:03:29.334 [rsMgr] replSet PRIMARY ………… ………… Tue Feb 18 12:03:40.341 [rsHealthPoll] replSet member 192.168.200.245:27017 is now in state SECONDARY
至此,整個副本集已經搭建成功了。
上面的的副本集只有2臺伺服器,還有一臺怎麼新增?除了在初始化的時候新增,還有什麼方法可以後期增刪節點?
二:維護操作
1:增刪節點。
把25服務加入到副本集中:
rs.add("192.168.200.25:27017")
mmm:PRIMARY> rs.add("192.168.200.25:27017") { "ok" : 1 } mmm:PRIMARY> rs.status() { "set" : "mmm", "date" : ISODate("2014-02-18T04:53:00Z"), "myState" : 1, "members" : [ { "_id" : 1, "name" : "192.168.200.252:27017", "health" : 1, "state" : 1, "stateStr" : "PRIMARY", "uptime" : 3023, "optime" : Timestamp(1392699177, 1), "optimeDate" : ISODate("2014-02-18T04:52:57Z"), "self" : true }, { "_id" : 2, "name" : "192.168.200.245:27017", "health" : 1, "state" : 2, "stateStr" : "SECONDARY", "uptime" : 2982, "optime" : Timestamp(1392699177, 1), "optimeDate" : ISODate("2014-02-18T04:52:57Z"), "lastHeartbeat" : ISODate("2014-02-18T04:52:59Z"), "lastHeartbeatRecv" : ISODate("2014-02-18T04:53:00Z"), "pingMs" : 0, "syncingTo" : "192.168.200.252:27017" }, { "_id" : 3, "name" : "192.168.200.25:27017", "health" : 1, "state" : 6, "stateStr" : "UNKNOWN", #等一會就變成了 SECONDARY "uptime" : 3, "optime" : Timestamp(0, 0), "optimeDate" : ISODate("1970-01-01T00:00:00Z"), "lastHeartbeat" : ISODate("2014-02-18T04:52:59Z"), "lastHeartbeatRecv" : ISODate("1970-01-01T00:00:00Z"), "pingMs" : 0, "lastHeartbeatMessage" : "still initializing" } ], "ok" : 1 }
把25服務從副本集中刪除:
rs.remove("192.168.200.25:27017")
mmm:PRIMARY> rs.remove("192.168.200.25:27017") Tue Feb 18 13:01:09.298 DBClientCursor::init call() failed Tue Feb 18 13:01:09.299 Error: error doing query: failed at src/mongo/shell/query.js:78 Tue Feb 18 13:01:09.300 trying reconnect to 192.168.200.252:27017 Tue Feb 18 13:01:09.301 reconnect 192.168.200.252:27017 ok mmm:PRIMARY> rs.status() { "set" : "mmm", "date" : ISODate("2014-02-18T05:01:19Z"), "myState" : 1, "members" : [ { "_id" : 1, "name" : "192.168.200.252:27017", "health" : 1, "state" : 1, "stateStr" : "PRIMARY", "uptime" : 3522, "optime" : Timestamp(1392699669, 1), "optimeDate" : ISODate("2014-02-18T05:01:09Z"), "self" : true }, { "_id" : 2, "name" : "192.168.200.245:27017", "health" : 1, "state" : 2, "stateStr" : "SECONDARY", "uptime" : 10, "optime" : Timestamp(1392699669, 1), "optimeDate" : ISODate("2014-02-18T05:01:09Z"), "lastHeartbeat" : ISODate("2014-02-18T05:01:19Z"), "lastHeartbeatRecv" : ISODate("2014-02-18T05:01:18Z"), "pingMs" : 0, "lastHeartbeatMessage" : "syncing to: 192.168.200.252:27017", "syncingTo" : "192.168.200.252:27017" } ], "ok" : 1 }
192.168.200.25 的節點已經被移除。
2:檢視複製的情況
db.printSlaveReplicationInfo()
mmm:PRIMARY> db.printSlaveReplicationInfo() source: 192.168.200.245:27017 syncedTo: Tue Feb 18 2014 13:02:35 GMT+0800 (CST) = 145 secs ago (0.04hrs) source: 192.168.200.25:27017 syncedTo: Tue Feb 18 2014 13:02:35 GMT+0800 (CST) = 145 secs ago (0.04hrs)
source:從庫的ip和埠。
syncedTo:目前的同步情況,以及最後一次同步的時間。
從上面可以看出,在資料庫內容不變的情況下他是不同步的,資料庫變動就會馬上同步。
3:檢視副本集的狀態
rs.status()
mmm:PRIMARY> rs.status() { "set" : "mmm", "date" : ISODate("2014-02-18T05:12:28Z"), "myState" : 1, "members" : [ { "_id" : 1, "name" : "192.168.200.252:27017", "health" : 1, "state" : 1, "stateStr" : "PRIMARY", "uptime" : 4191, "optime" : Timestamp(1392699755, 1), "optimeDate" : ISODate("2014-02-18T05:02:35Z"), "self" : true }, { "_id" : 2, "name" : "192.168.200.245:27017", "health" : 1, "state" : 2, "stateStr" : "SECONDARY", "uptime" : 679, "optime" : Timestamp(1392699755, 1), "optimeDate" : ISODate("2014-02-18T05:02:35Z"), "lastHeartbeat" : ISODate("2014-02-18T05:12:27Z"), "lastHeartbeatRecv" : ISODate("2014-02-18T05:12:27Z"), "pingMs" : 0, "syncingTo" : "192.168.200.252:27017" }, { "_id" : 3, "name" : "192.168.200.25:27017", "health" : 1, "state" : 2, "stateStr" : "SECONDARY", "uptime" : 593, "optime" : Timestamp(1392699755, 1), "optimeDate" : ISODate("2014-02-18T05:02:35Z"), "lastHeartbeat" : ISODate("2014-02-18T05:12:28Z"), "lastHeartbeatRecv" : ISODate("2014-02-18T05:12:28Z"), "pingMs" : 0, "syncingTo" : "192.168.200.252:27017" } ], "ok" : 1 }
4:副本集的配置
rs.conf()/rs.config()
mmm:PRIMARY> rs.conf() { "_id" : "mmm", "version" : 4, "members" : [ { "_id" : 1, "host" : "192.168.200.252:27017" }, { "_id" : 2, "host" : "192.168.200.245:27017" }, { "_id" : 3, "host" : "192.168.200.25:27017" } ] }
5:操作Secondary
預設情況下,Secondary是不提供服務的,即不能讀和寫。會提示:
error: { "$err" : "not master and slaveOk=false", "code" : 13435 }
在特殊情況下需要讀的話則需要:
rs.slaveOk() ,只對當前連線有效。
mmm:SECONDARY> db.test.find() error: { "$err" : "not master and slaveOk=false", "code" : 13435 } mmm:SECONDARY> rs.slaveOk() mmm:SECONDARY> db.test.find() { "_id" : ObjectId("5302edfa8c9151a5013b978e"), "a" : 1 }
6:更新ing
三:測試
1:測試副本集資料複製功能
在Primary(192.168.200.252:27017)上插入資料:
mmm:PRIMARY> for(var i=0;i<10000;i++){db.test.insert({"name":"test"+i,"age":123})} mmm:PRIMARY> db.test.count() 10001
在Secondary上檢視是否已經同步:
mmm:SECONDARY> rs.slaveOk() mmm:SECONDARY> db.test.count() 10001
資料已經同步。
2:測試副本集故障轉移功能
關閉Primary節點,檢視其他2個節點的情況:
mmm:PRIMARY> rs.status() { "set" : "mmm", "date" : ISODate("2014-02-18T05:38:54Z"), "myState" : 1, "members" : [ { "_id" : 1, "name" : "192.168.200.252:27017", "health" : 1, "state" : 1, "stateStr" : "PRIMARY", "uptime" : 5777, "optime" : Timestamp(1392701576, 2678), "optimeDate" : ISODate("2014-02-18T05:32:56Z"), "self" : true }, { "_id" : 2, "name" : "192.168.200.245:27017", "health" : 1, "state" : 2, "stateStr" : "SECONDARY", "uptime" : 2265, "optime" : Timestamp(1392701576, 2678), "optimeDate" : ISODate("2014-02-18T05:32:56Z"), "lastHeartbeat" : ISODate("2014-02-18T05:38:54Z"), "lastHeartbeatRecv" : ISODate("2014-02-18T05:38:53Z"), "pingMs" : 0, "syncingTo" : "192.168.200.252:27017" }, { "_id" : 3, "name" : "192.168.200.25:27017", "health" : 1, "state" : 2, "stateStr" : "SECONDARY", "uptime" : 2179, "optime" : Timestamp(1392701576, 2678), "optimeDate" : ISODate("2014-02-18T05:32:56Z"), "lastHeartbeat" : ISODate("2014-02-18T05:38:54Z"), "lastHeartbeatRecv" : ISODate("2014-02-18T05:38:53Z"), "pingMs" : 0, "syncingTo" : "192.168.200.252:27017" } ], "ok" : 1 } #關閉 mmm:PRIMARY> use admin switched to db admin mmm:PRIMARY> db.shutdownServer() #進入任意一臺: mmm:SECONDARY> rs.status() { "set" : "mmm", "date" : ISODate("2014-02-18T05:47:41Z"), "myState" : 2, "syncingTo" : "192.168.200.25:27017", "members" : [ { "_id" : 1, "name" : "192.168.200.252:27017", "health" : 0, "state" : 8, "stateStr" : "(not reachable/healthy)", "uptime" : 0, "optime" : Timestamp(1392701576, 2678), "optimeDate" : ISODate("2014-02-18T05:32:56Z"), "lastHeartbeat" : ISODate("2014-02-18T05:47:40Z"), "lastHeartbeatRecv" : ISODate("2014-02-18T05:45:57Z"), "pingMs" : 0 }, { "_id" : 2, "name" : "192.168.200.245:27017", "health" : 1, "state" : 2, "stateStr" : "SECONDARY", "uptime" : 5888, "optime" : Timestamp(1392701576, 2678), "optimeDate" : ISODate("2014-02-18T05:32:56Z"), "errmsg" : "syncing to: 192.168.200.25:27017", "self" : true }, { "_id" : 3, "name" : "192.168.200.25:27017", "health" : 1, "state" : 1, "stateStr" : "PRIMARY", "uptime" : 2292, "optime" : Timestamp(1392701576, 2678), "optimeDate" : ISODate("2014-02-18T05:32:56Z"), "lastHeartbeat" : ISODate("2014-02-18T05:47:40Z"), "lastHeartbeatRecv" : ISODate("2014-02-18T05:47:39Z"), "pingMs" : 0, "syncingTo" : "192.168.200.252:27017" } ], "ok" : 1 }
看到192.168.200.25:27017 已經從 SECONDARY 變成了 PRIMARY。具體的資訊可以通過日誌檔案得知。繼續操作:
在新主上插入:
mmm:PRIMARY> for(var i=0;i<10000;i++){db.test.insert({"name":"test"+i,"age":123})} mmm:PRIMARY> db.test.count() 20001
重啟啟動之前關閉的192.168.200.252:27017
mmm:SECONDARY> rs.status() { "set" : "mmm", "date" : ISODate("2014-02-18T05:45:14Z"), "myState" : 2, "syncingTo" : "192.168.200.245:27017", "members" : [ { "_id" : 1, "name" : "192.168.200.252:27017", "health" : 1, "state" : 2, "stateStr" : "SECONDARY", "uptime" : 12, "optime" : Timestamp(1392702168, 8187), "optimeDate" : ISODate("2014-02-18T05:42:48Z"), "errmsg" : "syncing to: 192.168.200.245:27017", "self" : true }, { "_id" : 2, "name" : "192.168.200.245:27017", "health" : 1, "state" : 2, "stateStr" : "SECONDARY", "uptime" : 11, "optime" : Timestamp(1392702168, 8187), "optimeDate" : ISODate("2014-02-18T05:42:48Z"), "lastHeartbeat" : ISODate("2014-02-18T05:45:13Z"), "lastHeartbeatRecv" : ISODate("2014-02-18T05:45:12Z"), "pingMs" : 0, "syncingTo" : "192.168.200.25:27017" }, { "_id" : 3, "name" : "192.168.200.25:27017", "health" : 1, "state" : 1, "stateStr" : "PRIMARY", "uptime" : 9, "optime" : Timestamp(1392702168, 8187), "optimeDate" : ISODate("2014-02-18T05:42:48Z"), "lastHeartbeat" : ISODate("2014-02-18T05:45:13Z"), "lastHeartbeatRecv" : ISODate("2014-02-18T05:45:13Z"), "pingMs" : 0 } ], "ok" : 1 }
啟動之前的主,發現其變成了SECONDARY,在新主插入的資料,是否已經同步:
mmm:SECONDARY> db.test.count() Tue Feb 18 13:47:03.634 count failed: { "note" : "from execCommand", "ok" : 0, "errmsg" : "not master" } at src/mongo/shell/query.js:180 mmm:SECONDARY> rs.slaveOk() mmm:SECONDARY> db.test.count() 20001
已經同步。
注意:
所有的Secondary都當機、或則副本集中只剩下一個節點,則該節點只能為Secondary節點,也就意味著整個叢集智慧進行讀操作而不能進行寫操作,當其他的恢復時,之前的primary節點仍然是primary節點。
當某個節點當機後重新啟動該節點會有一段的時間(時間長短視叢集的資料量和當機時間而定)導致整個叢集中所有節點都成為secondary而無法進行寫操作(如果應用程式沒有設定相應的ReadReference也可能不能進行讀取操作)。
官方推薦的最小的副本集也應該具備一個primary節點和兩個secondary節點。兩個節點的副本集不具備真正的故障轉移能力。
四:應用
1:手動切換Primary節點到自己給定的節點
上面已經提到過了優先集priority,因為預設的都是1,所以只需要把給定的伺服器的priority加到最大即可。讓245 成為主節點,操作如下:
mmm:PRIMARY> rs.conf() #檢視配置 { "_id" : "mmm", "version" : 6, #每改變一次叢集的配置,副本集的version都會加1。 "members" : [ { "_id" : 1, "host" : "192.168.200.252:27017" }, { "_id" : 2, "host" : "192.168.200.245:27017" }, { "_id" : 3, "host" : "192.168.200.25:27017" } ] } mmm:PRIMARY> rs.status() #檢視狀態 { "set" : "mmm", "date" : ISODate("2014-02-18T07:25:51Z"), "myState" : 1, "members" : [ { "_id" : 1, "name" : "192.168.200.252:27017", "health" : 1, "state" : 2, "stateStr" : "SECONDARY", "uptime" : 47, "optime" : Timestamp(1392708304, 1), "optimeDate" : ISODate("2014-02-18T07:25:04Z"), "lastHeartbeat" : ISODate("2014-02-18T07:25:50Z"), "lastHeartbeatRecv" : ISODate("2014-02-18T07:25:50Z"), "pingMs" : 0, "lastHeartbeatMessage" : "syncing to: 192.168.200.25:27017", "syncingTo" : "192.168.200.25:27017" }, { "_id" : 2, "name" : "192.168.200.245:27017", "health" : 1, "state" : 2, "stateStr" : "SECONDARY", "uptime" : 47, "optime" : Timestamp(1392708304, 1), "optimeDate" : ISODate("2014-02-18T07:25:04Z"), "lastHeartbeat" : ISODate("2014-02-18T07:25:50Z"), "lastHeartbeatRecv" : ISODate("2014-02-18T07:25:51Z"), "pingMs" : 0, "lastHeartbeatMessage" : "syncing to: 192.168.200.25:27017", "syncingTo" : "192.168.200.25:27017" }, { "_id" : 3, "name" : "192.168.200.25:27017", "health" : 1, "state" : 1, "stateStr" : "PRIMARY", "uptime" : 13019, "optime" : Timestamp(1392708304, 1), "optimeDate" : ISODate("2014-02-18T07:25:04Z"), "self" : true } ], "ok" : 1 }
mmm:PRIMARY> cfg=rs.conf() # { "_id" : "mmm", "version" : 4, "members" : [ { "_id" : 1, "host" : "192.168.200.252:27017" }, { "_id" : 2, "host" : "192.168.200.245:27017" }, { "_id" : 3, "host" : "192.168.200.25:27017" } ] } mmm:PRIMARY> cfg.members[1].priority=2 #修改priority 2 mmm:PRIMARY> rs.reconfig(cfg) #重新載入配置檔案,強制了副本集進行一次選舉,優先順序高的成為Primary。在這之間整個叢集的所有節點都是secondary mmm:SECONDARY> rs.status() { "set" : "mmm", "date" : ISODate("2014-02-18T07:27:38Z"), "myState" : 2, "syncingTo" : "192.168.200.245:27017", "members" : [ { "_id" : 1, "name" : "192.168.200.252:27017", "health" : 1, "state" : 2, "stateStr" : "SECONDARY", "uptime" : 71, "optime" : Timestamp(1392708387, 1), "optimeDate" : ISODate("2014-02-18T07:26:27Z"), "lastHeartbeat" : ISODate("2014-02-18T07:27:37Z"), "lastHeartbeatRecv" : ISODate("2014-02-18T07:27:38Z"), "pingMs" : 0, "lastHeartbeatMessage" : "syncing to: 192.168.200.245:27017", "syncingTo" : "192.168.200.245:27017" }, { "_id" : 2, "name" : "192.168.200.245:27017", "health" : 1, "state" : 1, "stateStr" : "PRIMARY", "uptime" : 71, "optime" : Timestamp(1392708387, 1), "optimeDate" : ISODate("2014-02-18T07:26:27Z"), "lastHeartbeat" : ISODate("2014-02-18T07:27:37Z"), "lastHeartbeatRecv" : ISODate("2014-02-18T07:27:38Z"), "pingMs" : 0, "syncingTo" : "192.168.200.25:27017" }, { "_id" : 3, "name" : "192.168.200.25:27017", "health" : 1, "state" : 2, "stateStr" : "SECONDARY", "uptime" : 13126, "optime" : Timestamp(1392708387, 1), "optimeDate" : ISODate("2014-02-18T07:26:27Z"), "errmsg" : "syncing to: 192.168.200.245:27017", "self" : true } ], "ok" : 1 }
這樣,給定的245伺服器就成為了主節點。
2:新增仲裁節點
把25節點刪除,重啟。再新增讓其為仲裁節點:
rs.addArb("192.168.200.25:27017")
mmm:PRIMARY> rs.status() { "set" : "mmm", "date" : ISODate("2014-02-18T08:14:36Z"), "myState" : 1, "members" : [ { "_id" : 1, "name" : "192.168.200.252:27017", "health" : 1, "state" : 2, "stateStr" : "SECONDARY", "uptime" : 795, "optime" : Timestamp(1392711068, 100), "optimeDate" : ISODate("2014-02-18T08:11:08Z"), "lastHeartbeat" : ISODate("2014-02-18T08:14:35Z"), "lastHeartbeatRecv" : ISODate("2014-02-18T08:14:35Z"), "pingMs" : 0, "syncingTo" : "192.168.200.245:27017" }, { "_id" : 2, "name" : "192.168.200.245:27017", "health" : 1, "state" : 1, "stateStr" : "PRIMARY", "uptime" : 14703, "optime" : Timestamp(1392711068, 100), "optimeDate" : ISODate("2014-02-18T08:11:08Z"), "self" : true }, { "_id" : 3, "name" : "192.168.200.25:27017", "health" : 1, "state" : 7, "stateStr" : "ARBITER", "uptime" : 26, "lastHeartbeat" : ISODate("2014-02-18T08:14:34Z"), "lastHeartbeatRecv" : ISODate("2014-02-18T08:14:34Z"), "pingMs" : 0, "syncingTo" : "192.168.200.252:27017" } ], "ok" : 1 } mmm:PRIMARY> rs.conf() { "_id" : "mmm", "version" : 9, "members" : [ { "_id" : 1, "host" : "192.168.200.252:27017" }, { "_id" : 2, "host" : "192.168.200.245:27017", "priority" : 2 }, { "_id" : 3, "host" : "192.168.200.25:27017", "arbiterOnly" : true } ] }
上面說明已經讓25伺服器成為仲裁節點。副本集要求參與選舉投票(vote)的節點數為奇數,當我們實際環境中因為機器等原因限制只有兩個(或偶數)的節點,這時為了實現 Automatic Failover引入另一類節點:仲裁者(arbiter),仲裁者只參與投票不擁有實際的資料,並且不提供任何服務,因此它對物理資源要求不嚴格。
通過實際測試發現,當整個副本集叢集中達到50%的節點(包括仲裁節點)不可用的時候,剩下的節點只能成為secondary節點,整個叢集只能讀不能 寫。比如叢集中有1個primary節點,2個secondary節點,加1個arbit節點時:當兩個secondary節點掛掉了,那麼剩下的原來的 primary節點也只能降級為secondary節點;當叢集中有1個primary節點,1個secondary節點和1個arbit節點,這時即使 primary節點掛了,剩下的secondary節點也會自動成為primary節點。因為仲裁節點不復制資料,因此利用仲裁節點可以實現最少的機器開 銷達到兩個節點熱備的效果。
3:新增備份節點
hidden(成員用於支援專用功能):這樣設定後此機器在讀寫中都不可見,並且不會被選舉為Primary,但是可以投票,一般用於備份資料。
把25節點刪除,重啟。再新增讓其為hidden節點:
mmm:PRIMARY> rs.add({"_id":3,"host":"192.168.200.25:27017","priority":0,"hidden":true}) { "down" : [ "192.168.200.25:27017" ], "ok" : 1 } mmm:PRIMARY> rs.conf() { "_id" : "mmm", "version" : 17, "members" : [ { "_id" : 1, "host" : "192.168.200.252:27017" }, { "_id" : 2, "host" : "192.168.200.245:27017" }, { "_id" : 3, "host" : "192.168.200.25:27017", "priority" : 0, "hidden" : true } ] }
測試其能否參與投票:關閉當前的Primary,檢視是否自動轉移Primary
關閉Primary(252): mmm:PRIMARY> use admin switched to db admin mmm:PRIMARY> db.shutdownServer() 連另一個連結察看: mmm:PRIMARY> rs.status() { "set" : "mmm", "date" : ISODate("2014-02-19T09:11:45Z"), "myState" : 1, "members" : [ { "_id" : 1, "name" : "192.168.200.252:27017", "health" : 1, "state" : 1, "stateStr" :"(not reachable/healthy)", "uptime" : 4817, "optime" : Timestamp(1392801006, 1), "optimeDate" : ISODate("2014-02-19T09:10:06Z"), "self" : true }, { "_id" : 2, "name" : "192.168.200.245:27017", "health" : 1, "state" : 2, "stateStr" : "PRIMARY", "uptime" : 401, "optime" : Timestamp(1392801006, 1), "optimeDate" : ISODate("2014-02-19T09:10:06Z"), "lastHeartbeat" : ISODate("2014-02-19T09:11:44Z"), "lastHeartbeatRecv" : ISODate("2014-02-19T09:11:43Z"), "pingMs" : 0, "syncingTo" : "192.168.200.252:27017" }, { "_id" : 3, "name" : "192.168.200.25:27017", "health" : 1, "state" : 2, "stateStr" : "SECONDARY", "uptime" : 99, "optime" : Timestamp(1392801006, 1), "optimeDate" : ISODate("2014-02-19T09:10:06Z"), "lastHeartbeat" : ISODate("2014-02-19T09:11:44Z"), "lastHeartbeatRecv" : ISODate("2014-02-19T09:11:43Z"), "pingMs" : 0, "syncingTo" : "192.168.200.252:27017" } ], "ok" : 1 } 上面說明Primary已經轉移,說明hidden具有投票的權利,繼續檢視是否有資料複製的功能。 ##### mmm:PRIMARY> db.test.count() 20210 mmm:PRIMARY> for(var i=0;i<90;i++){db.test.insert({"name":"test"+i,"age":123})} mmm:PRIMARY> db.test.count() 20300 Secondady: mmm:SECONDARY> db.test.count() Wed Feb 19 17:18:19.469 count failed: { "note" : "from execCommand", "ok" : 0, "errmsg" : "not master" } at src/mongo/shell/query.js:180 mmm:SECONDARY> rs.slaveOk() mmm:SECONDARY> db.test.count() 20300 上面說明hidden具有資料複製的功能
後面大家可以在上面進行備份了,後一篇會介紹如何備份、還原以及一些日常維護需要的操作。
4:新增延遲節點
Delayed(成員用於支援專用功能):可以指定一個時間延遲從primary節點同步資料。主要用於處理誤刪除資料馬上同步到從節點導致的不一致問題。
把25節點刪除,重啟。再新增讓其為Delayed節點:
mmm:PRIMARY> rs.add({"_id":3,"host":"192.168.200.25:27017","priority":0,"hidden":true,"slaveDelay":60}) #語法 { "down" : [ "192.168.200.25:27017" ], "ok" : 1 } mmm:PRIMARY> rs.conf() { "_id" : "mmm", "version" : 19, "members" : [ { "_id" : 1, "host" : "192.168.200.252:27017" }, { "_id" : 2, "host" : "192.168.200.245:27017" }, { "_id" : 3, "host" : "192.168.200.25:27017", "priority" : 0, "slaveDelay" : 60, "hidden" : true } ] }
測試:操作Primary,看資料是否60s後同步到delayed節點。
mmm:PRIMARY> db.test.count() 20300 mmm:PRIMARY> for(var i=0;i<200;i++){db.test.insert({"name":"test"+i,"age":123})} mmm:PRIMARY> db.test.count() 20500 Delayed: mmm:SECONDARY> db.test.count() 20300 #60秒之後 mmm:SECONDARY> db.test.count() 20500
上面說明delayed能夠成功的把同步操作延遲60秒執行。除了上面的成員之外,還有:
Secondary-Only:不能成為primary節點,只能作為secondary副本節點,防止一些效能不高的節點成為主節點。
Non-Voting:沒有選舉權的secondary節點,純粹的備份資料節點。
具體成員資訊如下:
|
成為primary |
對客戶端可見 |
參與投票 |
延遲同步 |
複製資料 |
Default |
√ |
√ |
√ |
∕ |
√ |
Secondary-Only |
∕ |
√ |
√ |
∕ |
√ |
Hidden |
∕ |
∕ |
√ |
∕ |
√ |
Delayed |
∕ |
√ |
√ |
√ |
√ |
Arbiters |
∕ |
∕ |
√ |
∕ |
∕ |
Non-Voting |
√ |
√ |
∕ |
∕ |
√ |
5:讀寫分離
MongoDB副本集對讀寫分離的支援是通過Read Preferences特性進行支援的,這個特性非常複雜和靈活。
應用程式驅動通過read reference來設定如何對副本集進行讀取操作,預設的,客戶端驅動所有的讀操作都是直接訪問primary節點的,從而保證了資料的嚴格一致性。
支援五種的read preference模式:官網說明
primary
主節點,預設模式,讀操作只在主節點,如果主節點不可用,報錯或者丟擲異常。
primaryPreferred
首選主節點,大多情況下讀操作在主節點,如果主節點不可用,如故障轉移,讀操作在從節點。
secondary
從節點,讀操作只在從節點, 如果從節點不可用,報錯或者丟擲異常。
secondaryPreferred
首選從節點,大多情況下讀操作在從節點,特殊情況(如單主節點架構)讀操作在主節點。
nearest
最鄰近節點,讀操作在最鄰近的成員,可能是主節點或者從節點,關於最鄰近的成員請參考
注意:2.2版本之前的MongoDB對Read Preference支援的還不完全,如果客戶端驅動採用primaryPreferred實際上讀取操作都會被路由到secondary節點。
因為讀寫分離是通過修改程式的driver的,故這裡就不做說明,具體的可以參考這篇文章或則可以在google上查閱。
驗證:(Python)
通過python來驗證MongoDB ReplSet的特性。
1:主節點斷開,看是否影響寫入
指令碼:
#coding:utf-8 import time from pymongo import ReplicaSetConnection conn = ReplicaSetConnection("192.168.200.201:27017,192.168.200.202:27017,192.168.200.204:27017", replicaSet="drug",read_preference=2, safe=True) #列印Primary伺服器
#print conn.primary
#列印所有伺服器
#print conn.seeds
#列印Secondary伺服器
#print conn.secondaries
#print conn.read_preference
#print conn.server_info()
for i in xrange(1000): try: conn.test.tt.insert({"name":"test" + str(i)}) time.sleep(1) print conn.primary print conn.secondaries except: pass
指令碼執行列印出的內容:
zhoujy@zhoujy:~$ python test.py (u'192.168.200.201', 27017) set([('192.168.200.202', 27017), (u'192.168.200.204', 27017)]) (u'192.168.200.201', 27017) set([('192.168.200.202', 27017), (u'192.168.200.204', 27017)]) (u'192.168.200.201', 27017) set([('192.168.200.202', 27017), (u'192.168.200.204', 27017)]) (u'192.168.200.201', 27017) set([('192.168.200.202', 27017), (u'192.168.200.204', 27017)]) (u'192.168.200.201', 27017) set([('192.168.200.202', 27017), (u'192.168.200.204', 27017)]) ('192.168.200.202', 27017) ##Primary當機,選舉產生新Primary set([(u'192.168.200.204', 27017)]) ('192.168.200.202', 27017) set([(u'192.168.200.204', 27017)]) ('192.168.200.202', 27017) set([(u'192.168.200.204', 27017)]) ('192.168.200.202', 27017) set([(u'192.168.200.204', 27017)]) ('192.168.200.202', 27017) set([(u'192.168.200.204', 27017)]) ('192.168.200.202', 27017) set([(u'192.168.200.204', 27017), (u'192.168.200.201', 27017)]) ##開啟之前當機的Primary,變成了Secondary ('192.168.200.202', 27017) set([(u'192.168.200.204', 27017), (u'192.168.200.201', 27017)]) ('192.168.200.202', 27017) set([(u'192.168.200.204', 27017), (u'192.168.200.201', 27017)])
體操作如下:
在執行指令碼的時候,模擬Primary當機,再把其開啟。看到其從201(Primary)上遷移到202上,201變成了Secondary。檢視插入的資料發現其中間有一段資料丟失了。
{ "name" : "GOODODOO15" } { "name" : "GOODODOO592" } { "name" : "GOODODOO593" }
其實這部分資料是由於在選舉過程期間丟失的,要是不允許資料丟失,則把在選舉期間的資料放到佇列中,等到找到新的Primary,再寫入。
上面的指令碼可能會出現操作時退出,這要看xrange()裡的數量了,所以用一個迴圈修改(更直觀):
#coding:utf-8 import time from pymongo import ReplicaSetConnection conn = ReplicaSetConnection("192.168.200.201:27017,192.168.200.202:27017,192.168.200.204:27017", replicaSet="drug",read_preference=2, safe=True) #列印Primary伺服器 #print conn.primary #列印所有伺服器 #print conn.seeds #列印Secondary伺服器 #print conn.secondaries #print conn.read_preference #print conn.server_info() while True: try: for i in xrange(100): conn.test.tt.insert({"name":"test" + str(i)}) print "test" + str(i) time.sleep(2) print conn.primary print conn.secondaries print '\n' except: pass
上面的實驗證明了:在Primary當機的時候,程式指令碼仍可以寫入,不需要人為的去幹預。只是期間需要10s左右(選舉時間)的時間會出現不可用,進一步說明,寫操作時在Primary上進行的。
2:主節點斷開,看是否影響讀取
指令碼:
#coding:utf-8 import time from pymongo import ReplicaSetConnection conn = ReplicaSetConnection("192.168.200.201:27017,192.168.200.202:27017,192.168.200.204:27017", replicaSet="drug",read_preference=2, safe=True) #列印Primary伺服器 #print conn.primary #列印所有伺服器 #print conn.seeds #列印Secondary伺服器 #print conn.secondaries #print conn.read_preference #print conn.server_info() for i in xrange(1000): time.sleep(1) obj=conn.test.tt.find({},{"_id":0,"name":1}).skip(i).limit(1) for item in obj: print item.values() print conn.primary print conn.secondaries
指令碼執行列印出的內容:
zhoujy@zhoujy:~$ python tt.py [u'GOODODOO0'] (u'192.168.200.201', 27017) set([('192.168.200.202', 27017), (u'192.168.200.204', 27017)]) [u'GOODODOO1'] (u'192.168.200.201', 27017) set([('192.168.200.202', 27017), (u'192.168.200.204', 27017)]) [u'GOODODOO2'] (u'192.168.200.201', 27017) set([('192.168.200.202', 27017), (u'192.168.200.204', 27017)]) ………… ………… [u'GOODODOO604'] (u'192.168.200.201', 27017) set([('192.168.200.202', 27017), (u'192.168.200.204', 27017)]) [u'GOODODOO605'] ##主當機(201),再開啟,沒有影響,繼續讀取下一條 ('192.168.200.202', 27017) set([(u'192.168.200.204', 27017), (u'192.168.200.201', 27017)]) [u'GOODODOO606'] ('192.168.200.202', 27017) set([(u'192.168.200.204', 27017), (u'192.168.200.201', 27017)]) [u'GOODODOO607'] ('192.168.200.202', 27017) set([(u'192.168.200.204', 27017), (u'192.168.200.201', 27017)]) ………… ………… [u'test8'] ('192.168.200.202', 27017) set([(u'192.168.200.204', 27017), (u'192.168.200.201', 27017)]) [u'test9'] ('192.168.200.202', 27017) set([(u'192.168.200.204', 27017), (u'192.168.200.201', 27017)]) [u'test10'] ##主再次當機,不開啟,沒有影響,繼續讀取下一條 (u'192.168.200.204', 27017) set([(u'192.168.200.201', 27017)]) [u'test11'] (u'192.168.200.204', 27017) set([(u'192.168.200.201', 27017)]) [u'test12'] (u'192.168.200.204', 27017) set([(u'192.168.200.201', 27017)])
具體操作如下:
在執行指令碼的時候,模擬Primary當機,再把其開啟。看到201(Primary)上遷移到202上,201變成了Secondary,讀取資料沒有間斷。再讓Primary當機,不開啟,讀取也不受影響。
上面的實驗證明了:在Primary當機的時候,程式指令碼仍可以讀取,不需要人為的去幹預。一進步說明,讀取是在Secondary上面。
總結:
剛接觸MongoDB,能想到的就這些,後期發現一些新的知識點會不定時更新該文章。
更多資訊見:
http://www.cnblogs.com/magialmoon/p/3251330.html
http://www.cnblogs.com/magialmoon/p/3261849.html
http://www.cnblogs.com/magialmoon/p/3268963.html
http://www.lanceyan.com/tech/mongodb/mongodb_cluster_1.html
http://www.lanceyan.com/tech/mongodb/mongodb_repset1.html
http://m.blog.csdn.net/blog/lance_yan/19332981
http://www.cnblogs.com/geekma/archive/2013/05/09/3068988.html
讀寫分離:
http://blog.chinaunix.net/uid-15795819-id-3075952.html
http://blog.csdn.net/kyfxbl/article/details/12219483