快速掌握mongoDB(六)——讀寫分離的副本集實現和Sharing介紹

撈月亮的猴子發表於2019-07-23

 1 mongoDB副本集

1 副本集簡介

  前邊我們介紹都是單機MongoDB的使用,在實際開發中很少會用單機MongoDB,因為使用單機會有資料丟失的風險,同時單臺伺服器無法做到高可用性(即當伺服器當機時,沒有替代的伺服器頂上來,我們的業務也就掛了),MongoDB中的副本集可以完美地解決上邊的兩個問題。

  MongoDB的副本集本質上就是一組mongod程式。複製集的成員有:
    1.Primary:主節點,負責所有的寫操作;
    2.Secondaries:從節點,同步主節點的資料,儲存資料副本;
    3.Arbiter:仲裁節點,不儲存資料,只有一個投票的作用;
  副本集執行過程:主節點是叢集中唯一一個負責寫操作的節點,主節點的寫操作都會記錄在其操作日誌(oplog,是一個 capped collection)中,從節點複製主節點的oplog日誌並執行日誌中的命令,以此保持資料和主節點一致。副本集的所有節點都可以進行讀操作,但是應用程式預設從主節點讀取資料。當主節點一段時間(當前預設為10s)不和從節點通訊,叢集就會開始投票選取新的主節點。下圖來自官網,描述了一個一主兩從的副本集的結構,應用程式的讀寫操作預設都是通過主節點進行的。

 

2 副本集搭建

  MongoDB的副本集搭建並不複雜,這裡簡單演示一下搭建過程。搭建mongoDB副本集時,節點的個數最好是奇數,這主要是為了保證投票順利進行。這裡演示搭建一個一主兩從的副本集,搭建副本集時,每個節點最好部署在不同的裝置上,因為我沒有那麼多電腦,所以就採用三臺centos7虛擬機器來搭建。

第一步 安裝mongoDB  

  為了方便幾臺裝置通訊,我們在每臺裝置上使用 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 編輯配置檔案如下:

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中連線任意一個節點,執行以下命令初始化副本集:

//配置
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)
        }
    }
}

 3 測試副本集

  我們知道副本集的主節點負責所有寫操作,從節點不能執行寫操作,只會同步主節點的資料。這裡簡單測試一下:連線主節點192.168.70.133:27017,執行以下命令插入一條命令:

  連線從節點192.168.70.129:27017,上執行下邊的命令,我們看到從節點是不能插入資料的,但是我們可以從從節點查到主節點插入的資料(注意:必須先執行 rs.slaveOk() 後才能進行read操作):

  測試高可用性:連線主節點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
        }
    ]

 4 設定節點的優先順序

  在部署的時候,我們一般更願意讓穩定且效能好的裝置在選舉時優先作為主節點,讓效能差的伺服器不能被選舉為主節點。這就要用到優先順序了,各個節點的預設優先順序都是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)

3 副本集管理的常用函式

  這裡彙總了一些管理副本集的相關命令,有興趣的小夥伴可以自己測試一下:

方法 描述
 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() 檢視從節點的資料同步情況

4 C#驅動之讀寫分離實現

  前邊我們已經搭建了一個一主兩從的副本集,狀態為: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時,驅動程式能夠自動判斷哪個節點是主節點。執行結果如下:

 2 Sharing分片簡介

  除了副本集,在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的最後一篇,更深入的應用以後在業務需要時繼續研究。如果文中有錯誤的話,希望大家可以指出,我會及時修改,謝謝!

 

相關文章