前邊咱們介紹都是單機MongoDB的使用,在實際開發中不多會用單機MongoDB,由於使用單機會有數據丟失的風險,同時單臺服務器沒法作到高可用性(即當服務器宕機時,沒有替代的服務器頂上來,咱們的業務也就掛了),MongoDB中的副本集能夠完美地解決上邊的兩個問題。html
MongoDB的副本集本質上就是一組mongod進程。複製集的成員有:
1.Primary:主節點,負責全部的寫操做;
2.Secondaries:從節點,同步主節點的數據,保存數據副本;
3.Arbiter:仲裁節點,不保存數據,只有一個投票的做用;
副本集運行過程:主節點是集羣中惟一一個負責寫操做的節點,主節點的寫操做都會記錄在其操做日誌(oplog,是一個 capped collection)中,從節點複製主節點的oplog日誌並執行日誌中的命令,以此保持數據和主節點一致。副本集的全部節點均可以進行讀操做,可是應用程序默認從主節點讀取數據。當主節點一段時間(當前默認爲10s)不和從節點通訊,集羣就會開始投票選取新的主節點。下圖來自官網,描述了一個一主兩從的副本集的結構,應用程序的讀寫操做默認都是經過主節點進行的。mongodb
MongoDB的副本集搭建並不複雜,這裏簡單演示一下搭建過程。搭建mongoDB副本集時,節點的個數最好是奇數,這主要是爲了保證投票順利進行。這裏演示搭建一個一主兩從的副本集,搭建副本集時,每一個節點最好部署在不一樣的設備上,由於我沒有那麼多電腦,因此就採用三臺centos7虛擬機來搭建。shell
爲了方便幾臺設備通訊,咱們在每臺設備上使用 vim /etc/hosts 命令註冊一下主機信息(注意要改爲本身設備的ip),配置以下:數據庫
127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4 ::1 localhost localhost.localdomain localhost6 localhost6.localdomain6 192.168.70.129 mongo01 192.168.70.131 mongo02 192.168.70.133 mongo03
在三臺centos虛擬機上都安裝mongoDB,安裝能夠參考第一篇中的安裝方法,注意副本集的配置相比單機多了一個replication節點,這裏設置副本集的名字爲MongoXset,使用命令 vim /usr/local/mongodb/bin/mongodb.conf 編輯配置文件以下:vim
systemLog: destination: file logAppend: true path: /usr/local/mongodb/logs/mongodb.log storage: dbPath: /usr/local/mongodb/data journal: enabled: true processManagement: fork: true net: port: 27017 bindIp: 0.0.0.0 replication: replSetName: MongoXset
安裝完成後,在Robomongo中鏈接任意一個節點,執行如下命令初始化副本集:centos
//配置 config = { _id:"MongoXset", members:[ {_id:0,host:"192.168.70.129:27017"}, {_id:1,host:"192.168.70.131:27017"}, {_id:2,host:"192.168.70.133:27017"}] }
use admin //初始化 rs.initiate(config)
執行上邊的命令後,咱們的副本集就搭建完成了,執行 rs.status() 查看副本集的狀態,咱們看到192.168.70.133:27017的mongodb是primary(主節點),其餘兩個節點爲secondary(從節點):服務器
{ "set" : "MongoXset", "date" : ISODate("2019-06-30T08:13:34.677Z"), "myState" : 1, "term" : NumberLong(1), "syncingTo" : "", "syncSourceHost" : "", "syncSourceId" : -1, "heartbeatIntervalMillis" : NumberLong(2000), "optimes" : { "lastCommittedOpTime" : { "ts" : Timestamp(1561882407, 1), "t" : NumberLong(1) }, "readConcernMajorityOpTime" : { "ts" : Timestamp(1561882407, 1), "t" : NumberLong(1) }, "appliedOpTime" : { "ts" : Timestamp(1561882407, 1), "t" : NumberLong(1) }, "durableOpTime" : { "ts" : Timestamp(1561882407, 1), "t" : NumberLong(1) } }, "lastStableCheckpointTimestamp" : Timestamp(1561882387, 1), "members" : [ { "_id" : 0, "name" : "192.168.70.129:27017", "health" : 1, "state" : 2, "stateStr" : "SECONDARY", "uptime" : 219, "optime" : { "ts" : Timestamp(1561882407, 1), "t" : NumberLong(1) }, "optimeDurable" : { "ts" : Timestamp(1561882407, 1), "t" : NumberLong(1) }, "optimeDate" : ISODate("2019-06-30T08:13:27Z"), "optimeDurableDate" : ISODate("2019-06-30T08:13:27Z"), "lastHeartbeat" : ISODate("2019-06-30T08:13:33.585Z"), "lastHeartbeatRecv" : ISODate("2019-06-30T08:13:34.465Z"), "pingMs" : NumberLong(0), "lastHeartbeatMessage" : "", "syncingTo" : "192.168.70.133:27017", "syncSourceHost" : "192.168.70.133:27017", "syncSourceId" : 2, "infoMessage" : "", "configVersion" : 1 }, { "_id" : 1, "name" : "192.168.70.131:27017", "health" : 1, "state" : 2, "stateStr" : "SECONDARY", "uptime" : 219, "optime" : { "ts" : Timestamp(1561882407, 1), "t" : NumberLong(1) }, "optimeDurable" : { "ts" : Timestamp(1561882407, 1), "t" : NumberLong(1) }, "optimeDate" : ISODate("2019-06-30T08:13:27Z"), "optimeDurableDate" : ISODate("2019-06-30T08:13:27Z"), "lastHeartbeat" : ISODate("2019-06-30T08:13:33.604Z"), "lastHeartbeatRecv" : ISODate("2019-06-30T08:13:34.458Z"), "pingMs" : NumberLong(0), "lastHeartbeatMessage" : "", "syncingTo" : "192.168.70.133:27017", "syncSourceHost" : "192.168.70.133:27017", "syncSourceId" : 2, "infoMessage" : "", "configVersion" : 1 }, { "_id" : 2, "name" : "192.168.70.133:27017", "health" : 1, "state" : 1, "stateStr" : "PRIMARY", "uptime" : 1281, "optime" : { "ts" : Timestamp(1561882407, 1), "t" : NumberLong(1) }, "optimeDate" : ISODate("2019-06-30T08:13:27Z"), "syncingTo" : "", "syncSourceHost" : "", "syncSourceId" : -1, "infoMessage" : "", "electionTime" : Timestamp(1561882205, 1), "electionDate" : ISODate("2019-06-30T08:10:05Z"), "configVersion" : 1, "self" : true, "lastHeartbeatMessage" : "" } ], "ok" : 1, "operationTime" : Timestamp(1561882407, 1), "$clusterTime" : { "clusterTime" : Timestamp(1561882407, 1), "signature" : { "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="), "keyId" : NumberLong(0) } } }
咱們知道副本集的主節點負責全部寫操做,從節點不能執行寫操做,只會同步主節點的數據。這裏簡單測試一下:鏈接主節點192.168.70.133:27017,執行如下命令插入一條命令:app
鏈接從節點192.168.70.129:27017,上執行下邊的命令,咱們看到從節點是不能插入數據的,可是咱們能夠從從節點查到主節點插入的數據(注意:必須先執行 rs.slaveOk() 後才能進行read操做):dom
測試高可用性:鏈接主節點192.168.70.133:27017,執行命令 use admin db.shutdownServer() 關閉主節點,而後鏈接一個其餘節點執行 rs.status() 查看副本集狀態以下,咱們看到192.168.70.133:27017節點顯示不可用,而192.168.70.129:27017被選舉爲新的主節點:函數
"members" : [ { "_id" : 0, "name" : "192.168.70.129:27017", "health" : 1, "state" : 1, "stateStr" : "PRIMARY", "uptime" : 2919, "optime" : { "ts" : Timestamp(1561885110, 1), "t" : NumberLong(2) }, "optimeDurable" : { "ts" : Timestamp(1561885110, 1), "t" : NumberLong(2) }, "optimeDate" : ISODate("2019-06-30T08:58:30Z"), "optimeDurableDate" : ISODate("2019-06-30T08:58:30Z"), "lastHeartbeat" : ISODate("2019-06-30T08:58:35.900Z"), "lastHeartbeatRecv" : ISODate("2019-06-30T08:58:34.979Z"), "pingMs" : NumberLong(0), "lastHeartbeatMessage" : "", "syncingTo" : "", "syncSourceHost" : "", "syncSourceId" : -1, "infoMessage" : "", "electionTime" : Timestamp(1561884658, 1), "electionDate" : ISODate("2019-06-30T08:50:58Z"), "configVersion" : 1 }, { "_id" : 1, "name" : "192.168.70.131:27017", "health" : 1, "state" : 2, "stateStr" : "SECONDARY", "uptime" : 3892, "optime" : { "ts" : Timestamp(1561885110, 1), "t" : NumberLong(2) }, "optimeDate" : ISODate("2019-06-30T08:58:30Z"), "syncingTo" : "192.168.70.129:27017", "syncSourceHost" : "192.168.70.129:27017", "syncSourceId" : 0, "infoMessage" : "", "configVersion" : 1, "self" : true, "lastHeartbeatMessage" : "" }, { "_id" : 2, "name" : "192.168.70.133:27017", "health" : 0, "state" : 8, "stateStr" : "(not reachable/healthy)", "uptime" : 0, "optime" : { "ts" : Timestamp(0, 0), "t" : NumberLong(-1) }, "optimeDurable" : { "ts" : Timestamp(0, 0), "t" : NumberLong(-1) }, "optimeDate" : ISODate("1970-01-01T00:00:00Z"), "optimeDurableDate" : ISODate("1970-01-01T00:00:00Z"), "lastHeartbeat" : ISODate("2019-06-30T08:58:36.291Z"), "lastHeartbeatRecv" : ISODate("2019-06-30T08:50:59.539Z"), "pingMs" : NumberLong(0), "lastHeartbeatMessage" : "Error connecting to 192.168.70.133:27017 :: caused by :: Connection refused", "syncingTo" : "", "syncSourceHost" : "", "syncSourceId" : -1, "infoMessage" : "", "configVersion" : -1 } ]
在部署的時候,咱們通常更願意讓穩定且性能好的設備在選舉時優先做爲主節點,讓性能差的服務器不能被選舉爲主節點。這就要用到優先級了,各個節點的默認優先級都是1,咱們能夠更改各個節點的優先級,優先級越高,被選舉爲主節點的概率就越大,優先級爲0的節點不能被選舉爲主節點。使用mongo shell執行下邊的命令就能夠更改各個節點的優先級,這裏就再也不具體演示了。
//獲取集羣配置 cfg=rs.config() //設置優先級 cfg.members[0].priority=1 cfg.members[1].priority=100 cfg.members[1].priority=0 //從新加載配置 rs.reconfig(cfg)
這裏彙總了一些管理副本集的相關命令,有興趣的小夥伴能夠本身測試一下:
方法 | 描述 |
rs.status() | 查看副本集狀態 |
rs.initiate(cfg) |
初始化副本集 |
rs.conf() | 獲取副本集的配置 |
rs.reconfig(cfg) | 從新加載配置 |
rs.add(ip:port) | 添加一個節點 |
rs.addArb(ip:port) | 添加一個仲裁節點 |
rs.remove(ip:port) | 刪除一個節點 |
rs.isMaster() | 查看是不是主節點 |
rs.slaveOk() | 讓從節點能夠執行read操做 |
rs.printReplicationInfo() | 查看oplog的大小和時間 |
rs.printSlaveReplicationInfo() | 查看從節點的數據同步狀況 |
前邊咱們已經搭建了一個一主兩從的副本集,狀態爲:192.168.70.129:27017(主節點),192.168.70.131:27017(從節點),192.168.70.133:27017(從節點),如今咱們簡單演示一下怎麼使用C#操做副本集,並實現讀寫分離。
首先添加一些測試數據:鏈接主節點192.168.70.129:27017,執行如下命令插入一些測試數據,接着到兩個從節點分別執行命令 rs.slaveOk() 讓節點能夠進行read操做:
use myDb //清空students中的記錄 db.students.drop() //在students集合中添加測試數據 db.students.insertMany([ {"no":1, "stuName":"jack", "age":23, "classNo":1}, {"no":2, "stuName":"tom", "age":20, "classNo":2}, {"no":3, "stuName":"hanmeimei", "age":22, "classNo":1}, {"no":4, "stuName":"lilei", "age":24, "classNo":2} ])
而後寫一個控制檯程序,使用 Install-Package MongoDB.Driver 添加驅動包,具體代碼以下:
class Program { static void Main(string[] args) { //鏈接數據庫 var client = new MongoClient("mongodb://192.168.70.133:27017, 192.168.70.131:27017, 192.168.70.129:27017"); //獲取database var mydb = client.GetDatabase("myDb"); //設置優先從從節點讀取數據 mydb.WithReadPreference(ReadPreference.Secondary); //mydb.WithReadConcern(ReadConcern.Majority); //mydb.WithWriteConcern(WriteConcern.WMajority);//這裏能夠設置寫入確認級別 //獲取collection var stuCollection = mydb.GetCollection<Student>("students"); //插入一條數據 stuCollection.InsertOne(new Student() { no = 5, stuName = "jim", age = 25 }); //讀取學生列表 List<Student> stuList = stuCollection.AsQueryable().ToList(); stuList.ForEach(s => Console.WriteLine($"學號:{s.no} ,姓名:{s.stuName} ,年齡:{s.age}")); Console.ReadKey(); } } /// <summary> /// 學生類 /// </summary public class Student { public int no { get; set; }//學號 public string stuName { get; set; }//姓名 public int age { get; set; }//年齡 [BsonExtraElements] public BsonDocument others { get; set; } }
注意一點:使用 var client = new MongoClient("mongodb://192.168.70.133:27017, 192.168.70.131:27017, 192.168.70.129:27017"); 獲取client時,驅動程序可以自動判斷哪一個節點是主節點。執行結果以下:
除了副本集,在Mongodb裏面存在另外一種集羣:分片集羣。所謂分片簡單來講就是將一個完整的數據分割後存儲在不一樣的節點上。當MongoDB存儲海量的數據時,一臺機器不足以存儲數據,或者數據過多形成讀寫吞吐量不能知足咱們的需求時,能夠考慮使用分片集羣。
舉一個栗子:例如咱們有1個億的用戶信息,選擇用戶的name列做爲分片鍵(shard key),將用戶信息存儲到兩個Shard Server中,mongoDB會自動根據分片鍵將用戶數據進行分片,假如分片後第一個片(shard1)存儲了名字首字母爲a~m的用戶信息,第二個片(shard2)存儲了名字首字母爲n~z的用戶信息。當咱們要查詢name=jack的用戶時,由於jack的首字母j在a和m之間,因此分片集羣會直接到shard1中查找,這樣查詢效率就會提升不少;若是咱們要插入一個name=tom的用戶,由於t在n~z之間,因此tom的信息會插入shard2中。這個栗子的分片鍵是name,當咱們要查詢生日爲某一天的用戶時(出生日期不是分片鍵),mongoDB仍是會去查詢全部分片服務器的數據。
分片集羣的基本結構以下:
分片集羣主要包含三個組件(都是mongod進程):
1 Shard Server
存儲角色,真實的業務數據都保存在該角色中。在生產環境中每個shard server都應該使用副本集,用於防止數據丟失,實現高可用。
2 Config Server
配置角色,存儲sharing集羣的元數據和配置信息。分片集羣判斷咱們查詢的數據在哪一個shard中,或者要將數據插入到哪個shard中就是由Config Server中的配置決定的。Config Server也要使用副本集充當,否則Config Server宕機,配置信息就無從獲取了。
3 mongos
路由角色,這是應用程序訪問分片集羣的入口,咱們經過鏈接mongos來訪問分片集羣,mongos讓整個集羣看起來就像一個單獨的數據庫。mongos一樣推薦配置成副本集,否則路由角色宕機,應用程序就沒法訪問集羣。
分片集羣各個角色通常都要配置爲副本集,因此須要較多的mongod進程,如sharing 集羣中的三個角色都使用一主兩從的副本集就須要9個mongod進程,這裏就再也不具體演示怎麼去搭建分片集羣,有興趣的小夥伴能夠按照官網文檔搭建,或者參考園友努力哥的文章。
對於中小型應用,使用副本集就能夠知足業務需求,不必使用分片集羣。當數據量很是大時咱們能夠考慮使用分片技術。在開發中使用分片集羣時,只須要把mongos做爲一個簡單的mongoDB實例鏈接便可,至於怎麼去分片存儲和分片查詢會由集羣自動完成。關於mongoDB的副本集和分片技術就簡單介紹到這裏,本節也是mongoDB的最後一篇,更深刻的應用之後在業務須要時繼續研究。若是文中有錯誤的話,但願你們能夠指出,我會及時修改,謝謝!