什么是副本集:

  • 是一组维护相同数据集的mongod进程
  • 提供冗余,自动故障转移和高可用性
  • 提供读取可伸缩性
  • 内部概念或多或少与MySQL的概念相似
    • PRIMARY概念与MySQL复制中的MASTER大致相同
    • SECONDARY概念与MySQL复制中的SLAVE大致相同。
    • 数据复制是异步的,就像在MySQL中一样。我们不确定辅助设备是否与主设备保持同步。但是在MongoDB中,我们可以在写操作上定义“写问题”来定义需要什么类型的确认。例如,我们可以定义一个写入需要确认是否至少有一个辅助节点已经应用了该文档,或者我们甚至可以要求所有辅助节点都需要是最新的。
    • 在读取oplog的节点之间复制数据,这是一个包含修改和插入文档的特殊上限集合。这个概念类似于MySQL binlog,但它们的工作方式不同。oplog的一个特点是它内部的每个操作都是幂等的。这意味着即使应用一次或多次,oplog操作也会产生相同的结果。例如,您不能删除两次。如果您再次应用相同的删除操作,那将是一个无操作。插入和更新也是如此。
    • 由于oplog是数据库中的集合,我们可以轻松地查询它。oplog的内容是在系统的每个集合上插入或更新的文档。由于oplog是一个上限集合,因此它具有固定且可配置的大小。因此,oplog的大小也表示我们过去可以为事件花多长时间。作为一个粗略的例子:如果我们的数据库每天总共1GB插入,更新或删除文档,拥有3GB的oplog意味着我们可以存储或多或少三天的事件。

副本集的工作原理

下图显示了应用程序查询三节点副本集的典型环境。

Percona MongoDB 4 搭建副本集

在正常操作期间,副本集只有一个节点作为PRIMARY而所有其他节点都是SECONDARY。PRIMARY成员是唯一接收写入的成员。它更新其本地集合和文档以及oplog。然后,oplog事件通过复制通道发送到所有SECONDARY节点。每个SECONDARY节点在本地和异步上应用对本地数据和oplog的相同修改。

下图在内部显示了副本集的工作原理。每个节点都连接到所有其他节点,并且有一个心跳机制来ping任何其他节点。心跳具有用于ping节点的可配置时间,默认值为10秒。

Percona MongoDB 4 搭建副本集

如果所有节点都响应心跳确认,则群集继续工作。如果其中一个节点崩溃,例如PRIMARY(最坏的情况),则发生涉及剩余节点的选举

当SECONDARY在配置的超时后没有收到对心跳的响应时,它会要求进行选举。仍然存活的节点投票支持新的PRIMARY。选举阶段通常不需要很长时间,选举算法足够复杂,让他们选择最佳的次要成为新的主要。让我们说这是次要的,与死亡的初级主要是最新的。

除了主要崩溃之外,还有一些节点要求进行选举的情况:将节点添加到副本集时,在“启动副本集”期间或在某些维护活动期间。这种选举不是本文的目的。

副本集在选举成功完成之前无法处理写入操作,但如果将此类查询配置为在辅助节点上运行,则可以继续提供读取查询(稍后我们将对此进行讨论)。选举正确完成后,群集将恢复正常操作。

要正常工作,副本集需要具有奇数个成员。在网络分裂的情况下,只有奇数个成员确保我们在其中一个子集中拥有大多数投票。在具有大多数节点的子集中选择新的PRIMARY。

因此,三个是副本集的最小节点数,以确保高可用性。

仲裁节点

由于每个节点都需要拥有完整的数据副本,因此如果您拥有庞大的数据库,则需要提供至少三台具有大量磁盘,内存和CPU资源的计算机。这可能很昂贵。

幸运的是,您可以将其中一个节点配置为Arbiter,这是一个不复制数据的特殊成员。它是空的,但它可以在选举期间投票。

使用仲裁节点是维持奇数成员的一个很好的解决方案,而不需要花费很多钱来使第三个节点像其他节点一样强大。仲裁节点不能被选为新主节点,因为它没有数据。

Percona MongoDB 4 搭建副本集

环境

ip 主机名 系统
172.18.11.142 nodejs1 Centos 7.6
172.18.11.143 nodejs2 Centos 7.6
172.18.11.144 nodejs3 Centos 7.6

安装

# 下载安装包并安装
mkdir -p /opt/mongodb/
cat <<EOF > /opt/mongodb/mongodb_down.sh
cd /opt/mongodb/
wget https://www.percona.com/downloads/percona-server-mongodb-LATEST/percona-server-mongodb-4.0.9-4/binary/redhat/7/x86_64/percona-server-mongodb-shell-4.0.9-4.el7.x86_64.rpm
wget https://www.percona.com/downloads/percona-server-mongodb-LATEST/percona-server-mongodb-4.0.9-4/binary/redhat/7/x86_64/percona-server-mongodb-mongos-4.0.9-4.el7.x86_64.rpm
wget https://www.percona.com/downloads/percona-server-mongodb-LATEST/percona-server-mongodb-4.0.9-4/binary/redhat/7/x86_64/percona-server-mongodb-tools-4.0.9-4.el7.x86_64.rpm
wget https://www.percona.com/downloads/percona-server-mongodb-LATEST/percona-server-mongodb-4.0.9-4/binary/redhat/7/x86_64/percona-server-mongodb-server-4.0.9-4.el7.x86_64.rpm
wget https://www.percona.com/downloads/percona-server-mongodb-LATEST/percona-server-mongodb-4.0.9-4/binary/redhat/7/x86_64/percona-server-mongodb-4.0.9-4.el7.x86_64.rpm
EOF

bash -x /opt/mongodb/mongodb_down.sh
yum localinstall /opt/mongodb/*.rpm -y
systemctl enable mongod

主机名

cat <<EOF >> /etc/hosts
192.168.0.249 k8s-m1
192.168.0.250 k8s-n1
192.168.0.251 k8s-n2
EOF

配置文件

cat <<EOF > /etc/mongod.conf
storage:
  dbPath: /var/lib/mongo
  journal:
    enabled: true
systemLog:
  destination: file
  logAppend: true
  path: /var/log/mongo/mongod.log
processManagement:
  fork: true
  pidFilePath: /var/run/mongod.pid
net:
  port: 27017
  bindIp: 0.0.0.0
EOF

启动

systemctl start mongod

副本集的名称

副本集的名称这里使用 rs-smy,将副本集名称放置于每一台主机的 /etc/mongod.conf

replication:
     replSetName: "rs-smy"

重启所有服务器

systemctl restart mongod

初始化集群

随便连接到一个节点,发出 rs.initiate() 让副本集直到有哪些成员

rs.initiate( {
_id: "rs-smy",
members: [
{ _id: 0, host: "172.18.11.142:27017" },
{ _id: 1, host: "172.18.11.143:27017" },
{ _id: 2, host: "172.18.11.144:27017" }
] })

发出命令后,MongoDB使用默认配置启动复制过程。选择PRIMARY节点,现在将创建的所有文档将在SECONDARY节点上异步复制。

我们可以通过查看mongo shell提示符来验证复制是否正常。一旦副本集启动并运行,提示应该在PRIMARY节点上如下:

rs-smy:PRIMARY>

在SECONDARY节点上这样:

rs-test:SECONDARY>

一些有用的命令

有几个命令可以调查并在副本集上执行一些管理任务。这里有几个。

要调查副本集配置,您可以在任何节点上发出rs.conf()

rs-smy:SECONDARY> rs.conf()
{
    "_id" : "rs-smy",
    "version" : 1,
    "protocolVersion" : NumberLong(1),
    "writeConcernMajorityJournalDefault" : true,
    "members" : [
        {
            "_id" : 0,
            "host" : "172.18.11.142:27017",
            "arbiterOnly" : false,
            "buildIndexes" : true,
            "hidden" : false,
            "priority" : 1,
            "tags" : {

            },
            "slaveDelay" : NumberLong(0),
            "votes" : 1
        },
        {
            "_id" : 1,
            "host" : "172.18.11.143:27017",
            "arbiterOnly" : false,
            "buildIndexes" : true,
            "hidden" : false,
            "priority" : 1,
            "tags" : {

            },
            "slaveDelay" : NumberLong(0),
            "votes" : 1
        },
        {
            "_id" : 2,
            "host" : "172.18.11.144:27017",
            "arbiterOnly" : false,
            "buildIndexes" : true,
            "hidden" : false,
            "priority" : 1,
            "tags" : {

            },
            "slaveDelay" : NumberLong(0),
            "votes" : 1
        }
    ],
    "settings" : {
        "chainingAllowed" : true,
        "heartbeatIntervalMillis" : 2000,
        "heartbeatTimeoutSecs" : 10,
        "electionTimeoutMillis" : 10000,
        "catchUpTimeoutMillis" : -1,
        "catchUpTakeoverDelayMillis" : 30000,
        "getLastErrorModes" : {

        },
        "getLastErrorDefaults" : {
            "w" : 1,
            "wtimeout" : 0
        },
        "replicaSetId" : ObjectId("5ce54845de4f73c1e97ea042")
    }
}

我们可以看到有关已配置节点的信息,无论是仲裁还是隐藏,优先级以及有关心跳过程的其他详细信息。

要调查副本集状态,您可以在任何节点上发出rs.status()

s-smy:SECONDARY> rs.status()
{
    "set" : "rs-smy",
    "date" : ISODate("2019-05-23T10:12:44.663Z"),
    "myState" : 2,
    "term" : NumberLong(7),
    "syncingTo" : "172.18.11.144:27017",
    "syncSourceHost" : "172.18.11.144:27017",
    "syncSourceId" : 2,
    "heartbeatIntervalMillis" : NumberLong(2000),
    "optimes" : {
        "lastCommittedOpTime" : {
            "ts" : Timestamp(1558606360, 1),
            "t" : NumberLong(7)
        },
        "readConcernMajorityOpTime" : {
            "ts" : Timestamp(1558606360, 1),
            "t" : NumberLong(7)
        },
        "appliedOpTime" : {
            "ts" : Timestamp(1558606360, 1),
            "t" : NumberLong(7)
        },
        "durableOpTime" : {
            "ts" : Timestamp(1558606360, 1),
            "t" : NumberLong(7)
        }
    },
    "lastStableCheckpointTimestamp" : Timestamp(1558606350, 1),
    "members" : [
        {
            "_id" : 0,
            "name" : "172.18.11.142:27017",
            "health" : 1,
            "state" : 2,
            "stateStr" : "SECONDARY",
            "uptime" : 24733,
            "optime" : {
                "ts" : Timestamp(1558606360, 1),
                "t" : NumberLong(7)
            },
            "optimeDate" : ISODate("2019-05-23T10:12:40Z"),
            "syncingTo" : "172.18.11.144:27017",
            "syncSourceHost" : "172.18.11.144:27017",
            "syncSourceId" : 2,
            "infoMessage" : "",
            "configVersion" : 1,
            "self" : true,
            "lastHeartbeatMessage" : ""
        },
        {
            "_id" : 1,
            "name" : "172.18.11.143:27017",
            "health" : 1,
            "state" : 2,
            "stateStr" : "SECONDARY",
            "uptime" : 24523,
            "optime" : {
                "ts" : Timestamp(1558606360, 1),
                "t" : NumberLong(7)
            },
            "optimeDurable" : {
                "ts" : Timestamp(1558606360, 1),
                "t" : NumberLong(7)
            },
            "optimeDate" : ISODate("2019-05-23T10:12:40Z"),
            "optimeDurableDate" : ISODate("2019-05-23T10:12:40Z"),
            "lastHeartbeat" : ISODate("2019-05-23T10:12:44.463Z"),
            "lastHeartbeatRecv" : ISODate("2019-05-23T10:12:44.448Z"),
            "pingMs" : NumberLong(0),
            "lastHeartbeatMessage" : "",
            "syncingTo" : "172.18.11.144:27017",
            "syncSourceHost" : "172.18.11.144:27017",
            "syncSourceId" : 2,
            "infoMessage" : "",
            "configVersion" : 1
        },
        {
            "_id" : 2,
            "name" : "172.18.11.144:27017",
            "health" : 1,
            "state" : 1,
            "stateStr" : "PRIMARY",
            "uptime" : 24519,
            "optime" : {
                "ts" : Timestamp(1558606360, 1),
                "t" : NumberLong(7)
            },
            "optimeDurable" : {
                "ts" : Timestamp(1558606360, 1),
                "t" : NumberLong(7)
            },
            "optimeDate" : ISODate("2019-05-23T10:12:40Z"),
            "optimeDurableDate" : ISODate("2019-05-23T10:12:40Z"),
            "lastHeartbeat" : ISODate("2019-05-23T10:12:44.464Z"),
            "lastHeartbeatRecv" : ISODate("2019-05-23T10:12:44.433Z"),
            "pingMs" : NumberLong(0),
            "lastHeartbeatMessage" : "",
            "syncingTo" : "",
            "syncSourceHost" : "",
            "syncSourceId" : -1,
            "infoMessage" : "",
            "electionTime" : Timestamp(1558594808, 1),
            "electionDate" : ISODate("2019-05-23T07:00:08Z"),
            "configVersion" : 1
        }
    ],
    "ok" : 1,
    "operationTime" : Timestamp(1558606360, 1),
    "$clusterTime" : {
        "clusterTime" : Timestamp(1558606360, 1),
        "signature" : {
            "hash" : BinData(0,"eo6zfAdAzzCTw/rQj+OWbd7Vots="),
            "keyId" : NumberLong("6693835933885661186")
        }
    }
}

可以看到哪个是PRIMARY,哪个是SECONDARY

测试复制

连接到PRIMARY节点并创建例子:

rs-smy:PRIMARY> use test
switched to db test
rs-test:PRIMARY> db.foo.insert( {name:"Bruce", surname:"Dickinson"} )
WriteResult({ "nInserted" : 1 })
rs-smy:PRIMARY> db.foo.find().pretty()
{
    "_id" : ObjectId("5ae05ac27e6680071caf94b7")
    "name" : "Bruce"
    "surname" : "Dickinson"
}

然后连接到SECONDARY节点并查找相同的文档。

请记住,您无法连接到SECONDARY节点以读取数据。默认情况下,只允许在PRIMARY上进行读写操作。因此,如果要读取SECONDARY节点上的数据,首先需要发出rs.slaveOK()命令。如果您不这样做,您将收到错误。

rs-test:SECONDARY> rs.slaveOK()
rs-test:SECONDARY> show collections
local
<strong>foo</strong>
rs-test:SECONDARY> db.foo.find().pretty()
{
     "_id" : ObjectId("5ae05ac27e6680071caf94b7")
     "name" : "Bruce"
     "surname" : "Dickinson"
}

SECONDARY节点已经复制了集合foo和插入文档的创建。