Mongodb 集群搭建
一、简介
mongodb 集群有三种方式:Master slave 主从模式、Replica Set 副本集模式、Sharding 分片集模式
Master slave 主从模式:主节点写入,数据同步到 Slave 节点,Slave 节点提供数据查询,最大的问题就是可用性差,MongoDB 3.6
起已不推荐使用主从模式,自 MongoDB 3.2
起,分片群集组件已弃用主从复制。因为 Master-Slave
其中 Master 宕机后不能自动恢复,只能靠人为操作,可靠性也差,操作不当就存在丢数据的风险,这种模式被 Replica Set
所替代 。
Replica Set 副本集模式:一个 Primary 节点用于写入数据,其它的 Secondary 节点用于查询数据,适合读写少的场景,是目前较为主流的架构方式,Primary 节点挂了,会自动从 Secondary 节点选出新的 Primary 节点,提供数据写入操作。
Sharding 分片集模式:将不同的数据分配在不同的机器中,也就是数据的横向扩展,单个机器只存储整个数据中的一部分,这样通过横向增加机器的数量来提高集群的存储和计算能力。
二、副本集模式
一个副本集中Primary
节点上能够完成读写操作,Secondary
节点仅能用于读操作。Primary
节点需要记录所有改变数据库状态的操作,这些记录保存在 oplog
中,这个文件存储在 local
数据库,各个Secondary
节点通过此 oplog
来复制数据并应用于本地,保持本地的数据与主节点的一致。oplog
具有幂等性,即无论执行几次其结果一致,这个比 mysql
的二进制日志更好用。
2.1 副本集的模式角色
- 副本集:一个副本集就是一组MongoDB实例组成的集群,由一个主(Primary)服务器和多个备份(Secondary)服务器构成
- 主节点(primary):主节点接收所有写入操作。主节点将对其数据集所做的所有更改记录到其 oplog(operation log,操作日志)日志。
- 副节点(secondary):复制主节点的 oplog日志 并将操作应用到其数据集,如果主节点不可用,一个合格的副节点将被选举为新的主节点。
- 仲裁节点(arbiter):负载选举,当主节点不可用(主节点客观下线状态),它将从副节点中选一个作为主节点。
2.2 副本集的好处
- 高可用:
- 主节点故障,可以迅速自动选举出新的主节点,保证服务器正常运行
- 提供自动 failover 功能
- 灾难恢复
- 发生故障时,可以从其他节点恢复数据
- 读写分离
- 主节点用于写入数据,从节点读数据
2.3 副本集集群架构原理
副本集的基本架构由3台服务器组成,有2种组成模式:
三成员的复制集,有3个主从节点 (1主2从)
两成员的复制集,有2个主从节点,1个仲裁节点(arbiter)。(1主1从1仲裁)
2.3.1 oplog
的组成结构
{
"ts" : Timestamp(1446011584, 2),
"h" : NumberLong("1687359108795812092"),
"v" : 2,
"op" : "i",
"ns" : "test.nosql",
"o" : { "_id" : ObjectId("563062c0b085733f34ab4129"), "name" : "mongodb", "score" : "10"}
}
ts:操作时间,当前timestamp + 计数器,计数器每秒都被重置
h:操作的全局唯一标识
v:oplog版本信息
op:操作类型
i:插入操作
u:更新操作
d:删除操作
c:执行命令(如createDatabase,dropDatabase)
n:空操作,特殊用途
ns:操作针对的集合
o:操作内容
o2:更新查询条件,仅update操作包含该字段
副本集数据同步分为初始化同步
和keep复制同步
。初始化同步指全量从主节点同步数据,如果Primary
节点数据量比较大同步时间会比较长。而keep
复制指初始化同步过后,节点之间的实时同步一般是增量同步。
初始化同步有以下两种情况会触发:
- Secondary 第一次加入。
- Secondary 落后的数据量超过了 oplog 的大小,这样也会被全量复制
2.3.2 primary选举
在 MongoDB 副本集中,主节点(Primary)的选举是一个自动进行的过程,旨在确保高可用性和数据一致性。当主节点不可用时,副本集会自动选举一个新的主节点。但也存在无法选出主节点的情况,如果当前存活成员数量不足"大多数"时,则无法选出 Primary。
主节点选举的过程
- 心跳检测: 副本集中的每个节点会定期发送心跳请求(默认每2秒一次)来检查其他节点的健康状况。
- 检测主节点故障: 如果某个节点在一定时间内(默认10秒)没有收到主节点的心跳响应,它会认为主节点已宕机。
- 发起选举: 检测到主节点故障的节点会发起一次选举,向其他节点发送选举请求。
- 投票: 其他节点会根据一定的条件投票决定是否支持发起选举的节点成为新的主节点。投票条件包括:
- 优先级:节点的优先级越高,越有可能被选为主节点。
- 操作日志(Oplog):节点的操作日志越新,越有可能被选为主节点。
- 任期:节点的任期(term)越新,越有可能被选为主节点。
- 选举结果: 如果发起选举的节点获得了大多数票数,它将被选为新的主节点,并开始接受写操作。如果没有节点获得多数票,选举失败,一段时间后会重新发起选举。
大多数的定义:
副本集的投票成员数量为N,那么大多数就为 N/2+1,如果当前存活成员数量不足"大多数"时,则无法选出 Primary,整个副本集无法提供写服务,处于只读状态。
投票成员数 | 大多数 | 容忍失效数 |
---|---|---|
1 | 1 | 0 |
2 | 2 | 0 |
3 | 2 | 1 |
4 | 3 | 1 |
5 | 3 | 2 |
6 | 4 | 2 |
7 | 4 | 3 |
2.3.3 副本集成员定义
成员 | 说明 |
---|---|
Secondary | 正常情况下,复制集的Seconary会参与Primary选举(自身也可能会被选为Primary),并从Primary同步最新写入的数据,以保证与Primary存储相同的数据。Secondary可以提供读服务,增加Secondary节点可以提供复制集的读服务能力,同时提升复制集的可用性。另外,Mongodb支持对复制集的Secondary节点进行灵活的配置,以适应多种场景的需求。 |
Arbiter | Arbiter节点只参与投票,不能被选为Primary,并且不从Primary同步数据。比如你部署了一个2个节点的复制集,1个Primary,1个Secondary,任意节点宕机,复制集将不能提供服务了(无法选出Primary),这时可以给复制集添加一个Arbiter节点,即使有节点宕机,仍能选出Primary。Arbiter本身不存储数据,是非常轻量级的服务,当复制集成员为偶数时,最好加入一个Arbiter节点,以提升复制集可用性。 |
Priority0 | Priority0节点的选举优先级为0,不会被选举为Primary比如你跨机房A、B部署了一个复制集,并且想指定Primary必须在A机房,这时可以将B机房的复制集成员Priority设置为0,这样Primary就一定会是A机房的成员。(注意:如果这样部署,最好将『大多数』节点部署在A机房,否则网络分区时可能无法选出Primary)。 |
Vote0 | Mongodb 3.0里,复制集成员最多50个,参与Primary选举投票的成员最多7个,其他成员(Vote0)的vote属性必须设置为0,即不参与投票。 |
Hidden | Hidden节点不能被选为主(Priority为0),并且对Driver不可见。因Hidden节点不会接受Driver的请求,可使用Hidden节点做一些数据备份、离线计算的任务,不会影响复制集的服务。 |
Delayed | Delayed节点必须是Hidden节点,并且其数据落后与Primary一段时间(可配置,比如1个小时)。因Delayed节点的数据比Primary落后一段时间,当错误或者无效的数据写入Primary时,可通过Delayed节点的数据来恢复到之前的时间点。 |
2.3.4 三节点架构模型
三个主从节点
一个主节点,两个从节点,主节点宕机时,两个从节点通过primary 选举出新的主节点,如果旧的主节点恢复之后,作为从节点加入到副本集
当主节点(Primary)宕机之后,两个从节点会进行竞选,当原主节点恢复之后作为从节点加入
两个主从节点+一个仲裁节点
一个主节点,一个从节点,一个仲裁(Arbiter)节点,主节点宕机之后,依然会进行投票选举,但仲裁(Arbiter)节点只能投票,不能成为主节点
主节点宕机之后,投票选出新的主节点,这里由于 arbiter 节点,没有复制数据,只能作为投票人,不能参与竞选,一般在资源有限的情况使用。
2.4 副本集部署
2.4.1 环境准备
准备了三台阿里云的ESC按量计费的服务器:
120.77.xx.xx
121.199.xx.xx
8.154.xx.xx
2.4.2 mongodb.conf 配置文件
# where to write logging data.
systemLog:
destination: file
logAppend: true
path: /usr/local/mongodb/logs/mongod.log
# Where and how to store data.
storage:
dbPath: /data/mongodb
journal:
enabled: true
engine: wiredTiger
wiredTiger:
engineConfig:
cacheSizeGB: 1
# how the process runs
processManagement:
fork: true
pidFilePath: /usr/local/mongodb/mongod.pid
timeZoneInfo: /usr/share/zoneinfo
# network interfaces
net:
port: 27017
bindIp: 0.0.0.0
security:
#authorization: enabled
javascriptEnabled: false
replication:
replSetName: rep1iset1
如果配置了启用授权 security.authorization
的情况下,如果配置 replication 副本集,MongoDB就会要求提供一个密钥文件(KeyFile),以确保副本集成员之间的安全通信。这个密钥文件用于内部身份验证,确保只有合法的成员才能加入副本集。
BadValue: security.keyFile is required when authorization is enabled with replica sets
2.4.3 KeyFile 生成步骤
创建密钥文件
创建一个密钥文件,该文件应包含一个随机生成的字符串,长度至少为6个字符,最多为1024个字符。建议使用更长的字符串以提高安全性。
openssl rand -base64 756 > /usr/local/mongodb/keyfile
设置密钥文件权限
确保密钥文件的权限设置为只有 MongoDB 进程可以读取。
chmod 600 /usr/local/mongodb/keyfile chown mongodb:mongodb /usr/local/mongodb/keyfile
> 创建 KeyFile 之后将它复制到副本集的所有节点
**修改配置文件**
```yaml
security:
authorization: enabled
keyFile: /usr/local/mongodb/keyfile
javascriptEnabled: false
replication:
replSetName: rep1iset1
2.4.4 初始化副本集配置
- 初始化副本集异常情况
# 1.未配置副本集
> rs.status()
{
"ok" : 0,
"errmsg" : "not running with --replSet",
"code" : 76,
"codeName" : "NoReplicationEnabled"
}
> rs.initiate({ _id: "re1",members: [{ _id: 0, host: "xx.xx.xx.xx:27017" }]})
# 2.mongodb.conf 配置文件中没有replication的配置, 需要指定replication.replSetName
{
"ok" : 0,
"errmsg" : "This node was not started with the replSet option",
"code" : 76,
"codeName" : "NoReplicationEnabled"
}
# 3.mongodb.conf 配置文件中的副本集名称和初始化副本集的名称不一样,rs.initiate 初始化名称要和副本集保持一致
{
"ok" : 0,
"errmsg" : "Rejecting initiate with a set name that differs from command line set name, initiate set name: re1, command line set name: repliSet1",
"code" : 93,
"codeName" : "InvalidReplicaSetConfig"
}
- 初始化副本集
# 新版本 使用 mongosh, 老版本使用 mongo, 在文件mongodb/bin目录下面
# 登录 mongodb
# 方式一 (不能用127.0.0.1 本地IP,这样副本集会配置失败)
rs.initiate({_id:"repliSet1",members:[_id:0,host:"ip/域名:端口"]})
# 方式二 (先定义配置,在注入到初始化方法中)
> config = {_id:"repliSet1", members:[{_id:0,host:"120.77.27.xxx:27017"}]}
> rs.initiate(config)
# 后续添加节点
> rs.add("192.167.10.1:27017")
# 添加仲裁节点
> rs.addArb("ip:port")
# 删除节点
> rs.remove("192.167.10.1:27017")
# 替换节点
> replace = {_id:"repliSet1",members:[{_id:0,host:"192.167.10.1:27017"}]}
> rs.reconfig(replace)
# 查询副本集状态
> rs.status()
# 查看当前是否是主节
> rs.isMaster()
# 查看副本集的配置信息:
> rs.config()
- 查看副本集状态
repliSet1:PRIMARY> rs.status()
{
"set" : "repliSet1",
"date" : ISODate("2024-11-15T03:56:11.995Z"),
"myState" : 1,
"term" : NumberLong(1),
"syncSourceHost" : "",
"syncSourceId" : -1,
"heartbeatIntervalMillis" : NumberLong(2000),
"majorityVoteCount" : 2,
"writeMajorityCount" : 2,
"votingMembersCount" : 2,
"writableVotingMembersCount" : 2,
"optimes" : {
"lastCommittedOpTime" : {
"ts" : Timestamp(1731642965, 2),
"t" : NumberLong(1)
},
"lastCommittedWallTime" : ISODate("2024-11-15T03:56:05.290Z"),
"readConcernMajorityOpTime" : {
"ts" : Timestamp(1731642965, 2),
"t" : NumberLong(1)
},
"appliedOpTime" : {
"ts" : Timestamp(1731642965, 2),
"t" : NumberLong(1)
},
"durableOpTime" : {
"ts" : Timestamp(1731642965, 2),
"t" : NumberLong(1)
},
"lastAppliedWallTime" : ISODate("2024-11-15T03:56:05.290Z"),
"lastDurableWallTime" : ISODate("2024-11-15T03:56:05.290Z")
},
"lastStableRecoveryTimestamp" : Timestamp(1731642921, 1),
"electionCandidateMetrics" : {
"lastElectionReason" : "electionTimeout",
"lastElectionDate" : ISODate("2024-11-15T02:52:21.041Z"),
"electionTerm" : NumberLong(1),
"lastCommittedOpTimeAtElection" : {
"ts" : Timestamp(1731639141, 1),
"t" : NumberLong(-1)
},
"lastSeenOpTimeAtElection" : {
"ts" : Timestamp(1731639141, 1),
"t" : NumberLong(-1)
},
"numVotesNeeded" : 1,
"priorityAtElection" : 1,
"electionTimeoutMillis" : NumberLong(10000),
"newTermStartDate" : ISODate("2024-11-15T02:52:21.066Z"),
"wMajorityWriteAvailabilityDate" : ISODate("2024-11-15T02:52:21.080Z")
},
"members" : [
{
"_id" : 0,
"name" : "120.77.27.xxx:27017",
"health" : 1,
"state" : 1,
"stateStr" : "PRIMARY",
"uptime" : 4209,
"optime" : {
"ts" : Timestamp(1731642965, 2),
"t" : NumberLong(1)
},
"optimeDate" : ISODate("2024-11-15T03:56:05Z"),
"lastAppliedWallTime" : ISODate("2024-11-15T03:56:05.290Z"),
"lastDurableWallTime" : ISODate("2024-11-15T03:56:05.290Z"),
"syncSourceHost" : "",
"syncSourceId" : -1,
"infoMessage" : "",
"electionTime" : Timestamp(1731639141, 2),
"electionDate" : ISODate("2024-11-15T02:52:21Z"),
"configVersion" : 4,
"configTerm" : 1,
"self" : true,
"lastHeartbeatMessage" : ""
},
{
"_id" : 1,
"name" : "8.154.19.xxx:27017",
"health" : 1,
"state" : 2,
"stateStr" : "SECONDARY",
"uptime" : 1474,
"optime" : {
"ts" : Timestamp(1731642965, 2),
"t" : NumberLong(1)
},
"optimeDurable" : {
"ts" : Timestamp(1731642965, 2),
"t" : NumberLong(1)
},
"optimeDate" : ISODate("2024-11-15T03:56:05Z"),
"optimeDurableDate" : ISODate("2024-11-15T03:56:05Z"),
"lastAppliedWallTime" : ISODate("2024-11-15T03:56:05.290Z"),
"lastDurableWallTime" : ISODate("2024-11-15T03:56:05.290Z"),
"lastHeartbeat" : ISODate("2024-11-15T03:56:11.517Z"),
"lastHeartbeatRecv" : ISODate("2024-11-15T03:56:10.765Z"),
"pingMs" : NumberLong(27),
"lastHeartbeatMessage" : "",
"syncSourceHost" : "120.77.27.xxx:27017",
"syncSourceId" : 0,
"infoMessage" : "",
"configVersion" : 4,
"configTerm" : 1
}
],
"ok" : 1,
"$clusterTime" : {
"clusterTime" : Timestamp(1731642965, 2),
"signature" : {
"hash" : BinData(0,"TtCFYkNvf6ZcEgcDqxzGkViwCck="),
"keyId" : NumberLong("7437333479068532741")
}
},
"operationTime" : Timestamp(1731642965, 2)
}
- 模拟主节点掉线之后的投票选举
# 主节点关闭
repliSet1:PRIMARY> db.shutdownServer()
server should be down...
# 从节点登录查询,可以看出已经选出了新的主节点了
# 原来的主节点状态变为 "stateStr" : "(not reachable/healthy)",
repliSet1:PRIMARY> rs.status()
{
"set" : "repliSet1",
"date" : ISODate("2024-11-15T06:21:02.446Z"),
"myState" : 1,
"term" : NumberLong(2),
"syncSourceHost" : "",
"syncSourceId" : -1,
"heartbeatIntervalMillis" : NumberLong(2000),
"majorityVoteCount" : 2,
"writeMajorityCount" : 2,
"votingMembersCount" : 3,
"writableVotingMembersCount" : 3,
"optimes" : {
"lastCommittedOpTime" : {
"ts" : Timestamp(1731651660, 1),
"t" : NumberLong(2)
},
"lastCommittedWallTime" : ISODate("2024-11-15T06:21:00.615Z"),
"readConcernMajorityOpTime" : {
"ts" : Timestamp(1731651660, 1),
"t" : NumberLong(2)
},
"appliedOpTime" : {
"ts" : Timestamp(1731651660, 1),
"t" : NumberLong(2)
},
"durableOpTime" : {
"ts" : Timestamp(1731651660, 1),
"t" : NumberLong(2)
},
"lastAppliedWallTime" : ISODate("2024-11-15T06:21:00.615Z"),
"lastDurableWallTime" : ISODate("2024-11-15T06:21:00.615Z")
},
"lastStableRecoveryTimestamp" : Timestamp(1731651640, 1),
"electionCandidateMetrics" : {
"lastElectionReason" : "stepUpRequestSkipDryRun",
"lastElectionDate" : ISODate("2024-11-15T06:15:50.430Z"),
"electionTerm" : NumberLong(2),
"lastCommittedOpTimeAtElection" : {
"ts" : Timestamp(1731651341, 1),
"t" : NumberLong(1)
},
"lastSeenOpTimeAtElection" : {
"ts" : Timestamp(1731651341, 1),
"t" : NumberLong(1)
},
"numVotesNeeded" : 2,
"priorityAtElection" : 1,
"electionTimeoutMillis" : NumberLong(10000),
"priorPrimaryMemberId" : 0,
"numCatchUpOps" : NumberLong(0),
"newTermStartDate" : ISODate("2024-11-15T06:15:50.602Z"),
"wMajorityWriteAvailabilityDate" : ISODate("2024-11-15T06:15:50.607Z")
},
"members" : [
{
"_id" : 0,
"name" : "120.77.27.xxx: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"),
"lastAppliedWallTime" : ISODate("2024-11-15T06:16:00.607Z"),
"lastDurableWallTime" : ISODate("2024-11-15T06:16:00.607Z"),
"lastHeartbeat" : ISODate("2024-11-15T06:21:01.168Z"),
"lastHeartbeatRecv" : ISODate("2024-11-15T06:16:03.864Z"),
"pingMs" : NumberLong(25),
"lastHeartbeatMessage" : "Error connecting to 120.77.27.139:27017 :: caused by :: Connection refused",
"syncSourceHost" : "",
"syncSourceId" : -1,
"infoMessage" : "",
"configVersion" : 6,
"configTerm" : 1
},
{
"_id" : 1,
"name" : "8.154.19.xxx:27017",
"health" : 1,
"state" : 1,
"stateStr" : "PRIMARY",
"uptime" : 11059,
"optime" : {
"ts" : Timestamp(1731651660, 1),
"t" : NumberLong(2)
},
"optimeDate" : ISODate("2024-11-15T06:21:00Z"),
"lastAppliedWallTime" : ISODate("2024-11-15T06:21:00.615Z"),
"lastDurableWallTime" : ISODate("2024-11-15T06:21:00.615Z"),
"syncSourceHost" : "",
"syncSourceId" : -1,
"infoMessage" : "",
"electionTime" : Timestamp(1731651350, 1),
"electionDate" : ISODate("2024-11-15T06:15:50Z"),
"configVersion" : 6,
"configTerm" : 2,
"self" : true,
"lastHeartbeatMessage" : ""
},
{
"_id" : 2,
"name" : "121.199.168.xxx:27017",
"health" : 1,
"state" : 2,
"stateStr" : "SECONDARY",
"uptime" : 820,
"optime" : {
"ts" : Timestamp(1731651650, 1),
"t" : NumberLong(2)
},
"optimeDurable" : {
"ts" : Timestamp(1731651650, 1),
"t" : NumberLong(2)
},
"optimeDate" : ISODate("2024-11-15T06:20:50Z"),
"optimeDurableDate" : ISODate("2024-11-15T06:20:50Z"),
"lastAppliedWallTime" : ISODate("2024-11-15T06:21:00.615Z"),
"lastDurableWallTime" : ISODate("2024-11-15T06:21:00.615Z"),
"lastHeartbeat" : ISODate("2024-11-15T06:21:00.609Z"),
"lastHeartbeatRecv" : ISODate("2024-11-15T06:21:00.618Z"),
"pingMs" : NumberLong(0),
"lastHeartbeatMessage" : "",
"syncSourceHost" : "8.154.19.27:27017",
"syncSourceId" : 1,
"infoMessage" : "",
"configVersion" : 6,
"configTerm" : 2
}
],
"ok" : 1,
"$clusterTime" : {
"clusterTime" : Timestamp(1731651660, 1),
"signature" : {
"hash" : BinData(0,"d/VV17p6/8/yrj0qEwV4YzdfwI0="),
"keyId" : NumberLong("7437333479068532741")
}
},
"operationTime" : Timestamp(1731651660, 1)
}
- 旧主节点重启之后自动作为从节点加入
repliSet1:PRIMARY> rs.status()
{
"set" : "repliSet1",
...
"writableVotingMembersCount" : 3,
"members" : [
{
"_id" : 0,
"name" : "120.77.27.xxx:27017",
"health" : 1,
"state" : 2,
"stateStr" : "SECONDARY",
"uptime" : 25,
"syncSourceHost" : "8.154.19.xxx:27017",
"syncSourceId" : 1,
"infoMessage" : "",
"configVersion" : 6,
"configTerm" : 2
},
{
"_id" : 1,
"name" : "8.154.19.xxx:27017",
"health" : 1,
"state" : 1,
"stateStr" : "PRIMARY",
"uptime" : 11299,
"configVersion" : 6,
"configTerm" : 2,
"self" : true,
"lastHeartbeatMessage" : ""
},
{
"_id" : 2,
"name" : "121.199.168.xxx:27017",
"health" : 1,
"state" : 2,
"stateStr" : "SECONDARY",
"uptime" : 1061,
"lastHeartbeatMessage" : "",
"syncSourceHost" : "8.154.19.27:27017",
"syncSourceId" : 1,
"infoMessage" : "",
"configVersion" : 6,
"configTerm" : 2
}
],
}
2.4.5 springboot 副本集配置
在 Spring Boot 应用程序中连接 MongoDB 副本集时,不需要显式地连接所有的副本集成员。相反,你需要提供一个连接字符串,其中包含所有副本集成员的地址,Spring Boot 会自动处理与这些成员的连接和故障切换。
yaml配置如下
spring:
data:
mongodb:
uri: mongodb://192.168.1.1:27017,192.168.1.2:27017,192.168.1.3:27017/yourDatabase?replicaSet=replicaSet1
Spring Boot 使用 MongoDB 的驱动程序来管理连接。驱动程序会自动处理与副本集成员的连接和故障切换。这意味着:
- 读写操作:驱动程序会自动将读写操作路由到主节点。
- 故障切换:如果主节点宕机,驱动程序会自动检测并切换到新的主节点。
- 负载均衡:对于读操作,驱动程序可以根据副本集的读偏好设置,将请求路由到次节点,实现负载均衡。
读偏好设置
你还可以通过配置读偏好来控制读操作的行为。例如,你可以设置读操作优先从次节点读取数据:
spring:
data:
mongodb:
uri: mongodb://192.168.1.1:27017,192.168.1.2:27017,192.168.1.3:27017/yourDatabase?replicaSet=replicaSet1&readPreference=secondaryPreferred
三、分片集模式
当数据量比较大的时候,我们需要把数据分片保存并运行在不同的服务器中,以降低CPU、内存和IO的压力,Sharding分片集就是mongo数据库用于横向扩容的技术。
分片是数据跨多台机器存储,MongoDB使用分片来支持具有非常大的数据集和高吞吐量操作的部署。MongoDB分片技术类似MySQL的水平切分和垂直切分,mongo数据库实现Shardin分片集主要有两种方式:垂直扩展和横向切分(也叫水平扩展)
- 垂直扩展:进行计算机资源扩容,添加更多的CPU,内存,磁盘空间等。(原来一个分片存5G数据,扩到10G)
- 水平扩展:则是通过数据分片的方式将数据集分布在多个服务器上,通过集群统一提供服务。(原来一个服务器有两个分片只有一个服务器,再买一个服务器增加分片的数量)
3.1 分片集的简介
分片设计思想
分片为应对高吞吐量与大数据量提供了方法。使用分片减少了每个分片需要处理的请求数,因此通过水平扩展,集群可以提高自己的存储容量和吞吐量。举例来说,当插入一条数据时,项目应用只需要访问存储这条数据的分片.
3.2 分片集的好处
- 高吞吐量:分布式系统中,把请求分散到不同的服务器,可以提高整体的吞吐量。
- 大数据量:随着数据量的增加,单台服务器的存储是有上限的,并且处理速度也会下降,分片可以使数据分散到不同的服务器,可以使整体的存储容量扩大。
- 降低每个分片的请求数:数据分片之后,请求也会分流到不同的服务器,也就降低单台服务器的请求处理量。
- 水平扩展:当系统需要存储更多数据,处理更多请求时,可以通过增加更多的分片,实现水平扩展,动态的增加服务器的节点,提供系统的容量和性能。
- 局部性原理:分片通常基于某种键(用户ID、地理位置)分配数据,可以使得相关的数据存储同一分片中,这有助于提高数据访问的局部性。
- 容错性:一个分片故障,其他分片可以正常运行,(一般一个分片不是单个节点,而是采用副本集)
3.3 分片集的原理
3.3.1 分片集群架构
组件 | 说明 |
---|---|
Config Server | 配置服务器,是mongod实例,可以理解为真实数据的元数据,存储了集群的所有节点、分片数据路由等信息。默认需要配置3个Config Server节点。Config Server中存储的信息:所有存取数据的方式,所有shard节点的信息,分片功能的一些配置信息。 |
Mongos | 数据路由,作为与客户端打交道的模块,提供对外应用访问,所有操作均通过mongos执行。一般有多个mongos节点。数据迁移和数据自动平衡。Mongos本身并不持久化数据,Sharded cluster所有的元数据都会存储到Config Server,而用户的数据会议分散存储到各个shard。Mongos启动后,会从配置服务器加载元数据,开始提供服务,将用户的请求正确路由到对应的碎片 |
Mongod | 真正的数据存储位置,存储应用数据记录。一般有多个Mongod节点,以chunk为单位存数据,达到数据分片目的。 |
整体架构图:
详细架构图:
分片集群由以下3个服务组成:
Router Server
: 数据库集群的请求入口,所有请求都通过Router
(mongos
)进行协调,不需要在应用程序添加一个路由选择器,Router
(mongos
)就是一个请求分发中心它负责把应用程序的请求转发到对应的Shard
服务器上。Shards Server
: 每个shard
由一个或多个mongod
进程组成,用于存储数据。Config Server
: 配置服务器。存储所有数据库元信息(路由、分片)的配置。
3.3.2 片键(shard key)
MongoDB中数据的分片是以集合为基本单位的,集合中的数据通过片键(Shard key)被分成多部分。其实分片键就是在集合中选一个键(字段),用该键的值作为数据拆分的依据。
分片键必须是一个索引,通过sh.shardCollection
加会自动创建索引(前提是此集合不存在相同索引的情况下)。一个自增的分片键对写入和数据均匀分布就不是很好,因为自增的分片键总会在一个分片上写入,后续达到某个阀值可能会写到别的分片。但是按照分片键查询会非常高效。
随机分片键对数据的均匀分布效果很好。注意尽量避免在多个分片上进行查询。在所有分片上查询,mongos会对结果进行归并排序。
对集合进行分片时,你需要选择一个分片键,分片键是每条记录都必须包含的,且建立了索引的单个字段或复合字段,MongoDB按照分片键将数据划分到不同的数据块(chunk)中,并将数据块(chunk)均衡地分布到所有分片节点中。
为了按照分片键划分数据块,MongoDB使用基于范围或者基于哈希的分片方式。
Tip
- 分片键是不可变。
- 分片键必须有索引。
- 分片键大小限制512bytes。
- 分片键用于分片集中的路由查询(mongos)。
- MongoDB不接受已进行collection级分片的collection上插入无分片
- 键的文档(也不支持空值插入)
基于范围的分片(Range Sharding)
Sharded Cluster支持将单个集合的数据分散存储在多shard上,用户可以指定根据集合内文档的某个字段即shard key来进行范围分片(range sharding)。
选择分片键:首先,你需要选择一个字段作为分片键。这个字段的值将决定文档存储在哪个分片上。
范围划分:MongoDB会根据分片键的值的范围来分配文档。例如,如果分片键是年龄,那么可能的一个范围是 [0, 30),另一个范围是 [30, 60),等等。每个范围都会映射到一个分片。
优点:
- 查询效率:如果你的查询是基于范围的,比如 find({age: {$gte: 20, $lt: 30}}),那么基于范围的分片可以提供更好的性能,因为相关的文档很可能位于同一个分片上。
缺点:
- 查询效率:如果你的查询是基于范围的,比如 find({age: {$gte: 20, $lt: 30}}),那么基于范围的分片可以提供更好的性能,因为相关的文档很可能位于同一个分片上。
数据倾斜:如果分片键的值分布不均匀,某些分片可能会比其他分片拥有更多的文档,导致数据倾斜。
热点问题:如果某个范围内的文档经常受到查询,那么这个分片可能会变成一个热点,影响性能。查询:基于范围的分片可以提供高效的查询性能,特别是当查询范围与分片键的范围对齐时。如果查询的范围完全落在一个分片的范围内,查询不需要合并;如果查询范围跨越多个分片,那么就需要在多个分片上执行查询并将结果合并。
基于哈希的分片(Hashed Sharding)
对于基于哈希的分片,MongoDB计算一个字段的哈希值,并用这个哈希值来创建数据块。在使用基于哈希分片的系统中,拥有”相近”片键的文档很可能不会存储在同一个数据块中,因此数据的分离性更好一些。
计算哈希值:MongoDB会对分片键的值计算一个哈希值。这个哈希值是一个数字,它将被用来确定文档应该存储在哪个分片上。
分配文档:通过将哈希值映射到分片集合上,MongoDB可以确保文档在整个集群中均匀分布。例如,如果有4个分片,那么哈希值会被模4,结果为0的文档会被存储在分片1上,结果为1的文档会被存储在分片2上,以此类推。
优点:
- 数据均匀分布:基于哈希的分片通常能够提供更均匀的数据分布,因为哈希函数可以将键值均匀地分布在所有分片上。
- 减少热点:由于数据是随机分布的,所以不太可能出现某个分片因为特定查询而成为热点的情况。
缺点:
查询效率:对于范围查询,基于哈希的分片可能不如基于范围的分片效率高,因为相关的文档可能分布在不同的分片上,需要在多个分片上执行查询然后合并结果。
查询:基于哈希的分片提供了良好的数据分布和负载均衡,但是它通常需要在多个分片上执行查询并将结果合并,这可能会影响查询性能。因此,设计分片键和分片策略时,应该考虑到应用程序的查询模式,以优化性能。如果应用程序经常执行非分片键的查询,可能需要考虑使用基于范围的分片或其他策略,以减少跨分片查询的需求。
Hash分片与范围分片互补,能将文档随机的分散到各个chunk,充分的扩展写能力,弥补了范围分片的不足,但不能高效的服务范围查询,所有的范围查询要分发到后端所有的Shard才能找出满足条件的文档。
3.3.3 区块 (chunk)
chunk就是一个存储数据的单位,是为了方便分割(分裂)数据和迁移数据的。在一个shard server内部,MongoDB还是会把数据分为chunks,每个chunk代表这个shard server内部一部分数据。
Spliting(切割):当一个chunk的大小超过配置中的chunk size时,MongoDB的后台进程会把这个chunk切分成更小的chunk,从而避免chunk过大的情况
Balancing(迁移):在MongoDB中,balancer是一个后台进程,负责chunk(数据块)的迁移,从而均衡各个shard server的负载,系统初始化时默认只有1个chunk,chunk size默认值64M,生产环境中根据业务选择合适的chunk size是最好的。mongoDB会自动拆分和迁移chunks。
chunk size 的选择
chunk的分裂和迁移非常消耗IO资源;chunk分裂的时机:在插入和更新,读数据不会分裂。
- 小的chunksize:数据均衡是迁移速度快,数据分布更均匀。数据分裂频繁,路由节点消耗更多资源。
- 大的chunksize:数据分裂少。数据块迁移速度慢,迁移的时候就会比较集中消耗IO资源。
chunksize的值通过如果没有特殊的要求,可以设置为通常100-200M。
chunk 的分裂以及迁移
随着数据的增长,其中的数据大小超过了配置的chunksize,默认是64M,则这个chunk就会分裂成两个。数据的增长会让chunk分裂得越来越多。
这时候,各个shard 上的chunk数量就会不平衡。这时候,mongos中的一个组件balancer 就会执行自动平衡。把chunk从chunk数量最多的shard节点挪动到数量最少的节点。
Jumbo Chunk 问题
如果在分裂过程中,由于 chunkSize
设置得过小,某个分片键的值的分布非常不均匀,导致大量文档都落在同一个分片中,这个分片就会变得非常大,形成一个jumbo chunk,会出现以下问题:
- 无法分裂: 因为 chunkSize 太小,MongoDB无法找到合适的分裂点来分裂这个过大的分片。
如果 chunkSize 很小,那么分片会很快达到这个大小限制并触发分裂操作。但是,如果分片键的某些取值非常常见(某个取值出现频率很高),那么新插入的文档很可能会因为相同的分片键值而被分配到同一个分片中,导致这些文档只能放到一个 chunk 里,无法再分裂。 - 无法迁移: 因为jumbo chunk太大了,它不能被迁移到其他分片上,因为迁移过程需要先将chunk分裂成更小的部分。
解决方案:
增加 chunkSize: 临时增加 chunkSize 的值,允许更大的分片存在,然后尝试再次分裂和迁移。
手动分裂: 使用 splitChunk 命令手动指定一个分裂点,将jumbo chunk分裂成更小的分片。
优化分片键: 选择一个更分散的分片键,或者使用复合分片键,以避免数据过于集中。
数据重新分布: 删除或归档旧数据,或者重新设计数据模型,以减少jumbo chunk的形成。
3.4 分片集的部署
环境准备三台服务器
准备了三台阿里云的ESC按量计费的服务器:
120.77.xx.xx
121.199.xx.xx
8.154.xx.xx
架构如下:
配置服务
IP | 端口 |
---|---|
120.77.xx.xx | 27018 |
121.199.xx.xx | 27018 |
8.154.xx.xx | 27018 |
路由服务
IP | 端口 |
---|---|
120.77.xx.xx | 27020 |
121.199.xx.xx | 27020 |
8.154.xx.xx | 27020 |
分片服务
分片服务1 | 分片服务2 |
---|---|
120.77.xx.xx:27019 | 8.154.xx.xx:27019 |
121.199.xx.xx:27019 | 8.154.xx.xx:27029 |
120.77.xx.xx:27021 | 121.199.xx.xx:27021 |
出于成本考虑,一台服务器可以同时运行多个mongodb 的实例,通过指定不同的mongdb的配置文件以及启动端口,就可以在同一台服务器上,同时启动分片、配置、路由 mongodb的不同实例。
3.4.1 配置服务器
configsvr.conf 配置文件
创建 configsvr.conf 配置文件,配置如下
# where to write logging data.
systemLog:
destination: file
logAppend: true
# 指定config服务器的日志文件
path: /usr/local/mongodb/logs/configsvr.log
# Where and how to store data.
storage:
# 指定config服务器的数据目录,需要提前创建
dbPath: /data/new-mongodb/configsvr
journal:
enabled: true
engine: wiredTiger
wiredTiger:
engineConfig:
cacheSizeGB: 1
# how the process runs
processManagement:
fork: true
# 指定config服务器的进程号的pid文件
pidFilePath: /usr/local/mongodb/configsvr.pid
timeZoneInfo: /usr/share/zoneinfo
# network interfaces
# 指定不同的启动端口
net:
port: 27018
bindIp: 0.0.0.0
security:
authorization: enabled
keyFile: /usr/local/mongodb/keyfile
javascriptEnabled: false
# replication config
# 以副本集的方式对配置服务器集群配置
replication:
replSetName: config1ReplSet
# sharding config
# 指定了服务器是配置服务器的角色
sharding:
clusterRole: configsvr
启动脚本
创建启动脚本
#! /bin/bash
echo -e "\n"
echo "-------------------- CONFIGSVR MONGODB START --------------------"
echo -e "\n"
# 获取进程文件
pid_file="/usr/local/mongodb/configsvr.pid"
pid=""
if [ -n "$pid_file" ]
then
pid=$(cat $pid_file)
else
echo "${pid_file} is null"
fi
print="start"
if [ -n "$pid" ]
then
nohup /usr/local/mongodb/bin/mongod --shutdown --config /usr/local/mongodb/conf/configsvr.conf 2>/dev/null
echo "configsvr mongodb status ====> running"
print="restart"
else
echo "configsvr mongodb startup status ====> stopped"
fi
# 启动配置服务器
/usr/local/mongodb/bin/mongod -f /usr/local/mongodb/conf/configsvr.conf 2>/dev/null
sleep 2
echo "configsvr mongodb ${print} success!"
echo -e "\n"
echo "-------------------- CONFIGSVR MONGODB START --------------------"
echo -e "\n"
关闭脚本
#! /bin/bash
echo -e "\n"
echo "-------------------- CONFIGSVR MONGODB STOP --------------------"
echo -e "\n"
pid_file="/usr/local/mongodb/configsvr.pid"
pid=""
if [ -n "$pid_file" ]
then
pid=$(cat $pid_file)
else
echo "${pid_file} is null"
fi
if [ -n "$pid" ]
then
echo "configsvr mongodb startup status ====> running"
nohup /usr/local/mongodb/bin/mongod --shutdown --config /usr/local/mongodb/conf/configsvr.conf
rm -f ${pid_file}
else
echo "configsvr startup status ====> stopped"
fi
sleep 2
echo "configsvr stop success!"
echo -e "\n"
echo "-------------------- CONFIGSVR MONGODB STOP --------------------"
echo -e "\n"
创建配置服务器用户
路由服务器进行登录时需要使用
use admin
db.createUser({
user: "configUser",
pwd: "config123456",
roles: [
{ role: "userAdminAnyDatabase", db: "admin" },
{ role: "readWriteAnyDatabase", db: "admin" },
{ role: "dbAdminAnyDatabase", db: "admin" },
{ role: "clusterAdmin", db: "admin" }
]
})
内置角色
- userAdminAnyDatabase:
- 描述:允许用户在所有数据库中管理用户和角色。
- 权限:可以在所有数据库中创建、修改和删除用户和角色。
- readWriteAnyDatabase:
- 描述:允许用户在所有数据库中进行读写操作。
- 权限:可以在所有数据库中读取和写入数据。
- dbAdminAnyDatabase:
- 描述:允许用户在所有数据库中执行管理操作,如索引管理和性能监控。
- 权限:可以在所有数据库中执行管理操作,如创建和删除索引、查看性能统计信息等。
- clusterAdmin:
- 描述:允许用户管理整个集群的配置和状态。
- 权限:可以执行集群级别的管理操作,如分片管理、配置服务器管理等。
初始化副本集
使用启动脚本,分别启动三台配置服务器,开始初始化副本集
先初始配置副本集,然后再创建配置用户,进行登录,最后再添加其他从节点
# 任意使用配置副本集一个服务器登录
[root@linux-1 bin]# ./mongo --port 27018
MongoDB shell version v5.0.18
connecting to: mongodb://127.0.0.1:27018/?compressors=disabled&gssapiServiceName=mongodb
Implicit session: session { "id" : UUID("4221ac9e-29b4-4dc6-b498-ff97f482cd87") }
MongoDB server version: 5.0.18
================
Warning: the "mongo" shell has been superseded by "mongosh",
which delivers improved usability and compatibility.The "mongo" shell has been deprecated and will be removed in
an upcoming release.
For installation instructions, see
https://docs.mongodb.com/mongodb-shell/install/
================
>
# 查询当前配置副本集
> rs.status()
{
"ok" : 0,
"errmsg" : "no replset config has been received",
"code" : 94,
"codeName" : "NotYetInitialized",
"$gleStats" : {
"lastOpTime" : Timestamp(0, 0),
"electionId" : ObjectId("000000000000000000000000")
},
"lastCommittedOpTime" : Timestamp(0, 0)
}
# 初始化配置副本集
> rs.initiate({_id:'config1ReplSet',members:[{_id:0,host:'120.77.xx.xx:27018'}]})
{
"ok" : 1,
"$gleStats" : {
"lastOpTime" : Timestamp(1734329742, 1),
"electionId" : ObjectId("000000000000000000000000")
},
"lastCommittedOpTime" : Timestamp(1734329742, 1)
}
# 创建配置用户
config1ReplSet:PRIMARY> use admin
switched to db admin
config1ReplSet:PRIMARY> db.createUser({
... user: "configUser",
... pwd: "config123456",
... roles: [
... { role: "userAdminAnyDatabase", db: "admin" },
... { role: "readWriteAnyDatabase", db: "admin" },
... { role: "dbAdminAnyDatabase", db: "admin" },
... { role: "clusterAdmin", db: "admin" }
... ]
... })
# 登录
config1ReplSet:PRIMARY> use admin
switched to db admin
config1ReplSet:PRIMARY> db.auth("configUser","config123456")
1
# 添加从节点
config1ReplSet:PRIMARY> rs.add("121.199.xx.xx:27018")
config1ReplSet:PRIMARY> rs.add("8.154.xx.xx:27018")
# 查询配置集
config1ReplSet:PRIMARY> rs.config()
{
"_id" : "config1ReplSet",
"version" : 5,
"term" : 1,
"members" : [
{
"_id" : 0,
"host" : "120.77.xx.xx:27018",
},
{
"_id" : 1,
"host" : "121.199.xx.xx:27018",
},
{
"_id" : 2,
"host" : "8.154.xx.xx:27018",
}
],
"configsvr" : true,
}
“configsvr” : true, 表示当前副本集是配置服务器
3.4.2 分片服务器
shardsvr 节点
创建 shardsvr.conf 配置文件,配置如下
systemLog:
destination: file
logAppend: true
# 指定shard服务器的日志文件
path: /usr/local/mongodb/logs/shardsvr.log
# Where and how to store data.
storage:
# 指定shard服务器的数据目录,需要提前创建
dbPath: /data/new-mongodb/shardsvr
journal:
enabled: true
engine: wiredTiger
wiredTiger:
engineConfig:
cacheSizeGB: 2
# how the process runs
processManagement:
fork: true
# 指定shard服务器的进程号的pid文件
pidFilePath: /usr/local/mongodb/shardsvr.pid
timeZoneInfo: /usr/share/zoneinfo
# network interfaces
# 指定不同的启动端口
net:
port: 27019
bindIp: 0.0.0.0
security:
authorization: enabled
keyFile: /usr/local/mongodb/keyfile
javascriptEnabled: false
# replication config
# 以副本集的方式对分片服务器进行集群配置
replication:
replSetName: shard1Replset
# Sharding Configuration
# 指定了服务器是分片服务器的角色
sharding:
clusterRole: shardsvr
- 启动脚本
#! /bin/bash
echo -e "\n"
echo "-------------------- SHARDSVR MONGODB START --------------------"
echo -e "\n"
pid_file="/usr/local/mongodb/shardsvr.pid"
pid=""
if [ -n "$pid_file" ]
then
pid=$(cat $pid_file)
else
echo "${pid_file} is null"
fi
print="start"
if [ -n "$pid" ]
then
nohup /usr/local/mongodb/bin/mongod --shutdown --config /usr/local/mongodb/conf/shardsvr.conf 2>/dev/null
echo "shardsvr mongodb startup status ====> running"
print="restart"
else
echo "shardsvr mongodb startup status ====> stopped"
fi
/usr/local/mongodb/bin/mongod -f /usr/local/mongodb/conf/shardsvr.conf 2>/dev/null
sleep 2
echo "shardsvr mongodb ${print} success!"
echo -e "\n"
echo "-------------------- SHARDSVR MONGODB START --------------------"
echo -e "\n"
- 关闭脚本
#! /bin/bash
echo -e "\n"
echo "-------------------- SHARDSVR MONGODB STOP --------------------"
echo -e "\n"
pid_file="/usr/local/mongodb/shardsvr.pid"
pid=""
if [ -n "$pid_file" ]
then
pid=$(cat $pid_file)
else
echo "${pid_file} is null"
fi
if [ -n "$pid" ]
then
echo "shardsvr mongodb startup status ====> running"
nohup /usr/local/mongodb/bin/mongod --shutdown --config /usr/local/mongodb/conf/shardsvr.conf
rm -f ${pid_file}
else
echo "shardsvr startup status ====> stopped"
fi
sleep 2
echo "shardsvr stop success!"
echo -e "\n"
echo "-------------------- SHARDSVR MONGODB STOP --------------------"
echo -e "\n"
初始化副本集
- 配置两个分片副本集
#分片1:shard1ReplSet
120.77.xx.xx:27019
121.199.xx.xx:27019
120.77.xx.xx:27021
#分片2:shard2ReplSet
8.154.xx.xx:27019
8.154.xx.xx:27029
121.199.xx.xx:27021
按照标准配置,每个副本集至少包含3台服务器,可以自行启动多个mongodb实例进行模拟
- 登录分片服务器
[root@iZbp1dfulgjy4kd3ev4y7bZ bin]# ./mongo --port 27019
MongoDB shell version v5.0.18
connecting to: mongodb://127.0.0.1:27019/?compressors=disabled&gssapiServiceName=mongodb
Implicit session: session { "id" : UUID("8777fb4c-2679-4c7b-a12e-4fdc3c533029") }
MongoDB server version: 5.0.18
================
Warning: the "mongo" shell has been superseded by "mongosh",
which delivers improved usability and compatibility.The "mongo" shell has been deprecated and will be removed in
an upcoming release.
For installation instructions, see
https://docs.mongodb.com/mongodb-shell/install/
================
# 查看分片副本集状态
> rs.status()
{
"ok" : 0,
"errmsg" : "no replset config has been received",
"code" : 94,
"codeName" : "NotYetInitialized"
}
# 这里如果直接创建用户会报错,由于你在非主节点上尝试执行某些管理操作,比如创建用户
# 因为配置文件中包含了副本集配置,所以只有主节点才有权限创建用户
> use admin
switched to db admin
> db.createUser({user: "root",pwd: "mongodb213465",roles: ["root"]})
uncaught exception: Error: couldn't add user: not master :
_getErrorWithCode@src/mongo/shell/utils.js:25:13
DB.prototype.createUser@src/mongo/shell/db.js:1367:11
@(shell):1:1
>
# 先初始化分片副本集
> config = {_id:"shard2ReplSet", members:[{_id:0,host:"8.154.xx.xx:27019"}]}
{
"_id" : "shard2ReplSet",
"members" : [
{
"_id" : 0,
"host" : "8.154.xx.xx:27019"
}
]
}
> rs.initiate(config)
{ "ok" : 1 }
# 副本集初始化成功之后,如果直接添加从节点,也会报错
# 因为主节点已经初始化了,需要授权登录才能继续操作
shard2ReplSet:PRIMARY> rs.add("xx:ip")
uncaught exception: Error: count failed: {
"ok" : 0,
"errmsg" : "not authorized on local to execute command { count: \"system.replset\", query: {}, lsid: { id: UUID(\"8777fb4c-2679-4c7b-a12e-4fdc3c533029\") }, $db: \"local\" }",
"code" : 13,
"codeName" : "Unauthorized"
} :
_getErrorWithCode@src/mongo/shell/utils.js:25:13
DBCollection.prototype.count@src/mongo/shell/collection.js:1406:15
rs.add/<@src/mongo/shell/utils.js:1636:16
assert.soon@src/mongo/shell/assert.js:366:21
rs.add@src/mongo/shell/utils.js:1632:5
@(shell):1:1
# 创建用户,然后登录
shard2ReplSet:PRIMARY> db.createUser({user: "root",pwd: "mongodb213465",roles: ["root"]})
Successfully added user: { "user" : "root", "roles" : [ "root" ] }
shard2ReplSet:PRIMARY> use admin
switched to db admin
shard2ReplSet:PRIMARY> db.auth("root","mongodb213465")
1
# 添加从节点
shard2ReplSet:PRIMARY> rs.add("ip:port")
# 添加仲裁节点
shard2ReplSet:PRIMARY> rs.addArb("ip:port")
arbiter 节点
创建 arbiter.conf 配置文件,配置如下
# where to write logging data.
systemLog:
destination: file
logAppend: true
path: /usr/local/mongodb/logs/arbiter.log
# where and how to store data.
storage:
dbPath: /data/new-mongodb/arbiter
journal:
enabled: true
engine: wiredTiger
wiredTiger:
engineConfig:
cacheSizeGB: 1
# how the process runs
processManagement:
fork: true
pidFilePath: /usr/local/mongodb/arbiter.pid
timeZoneInfo: /usr/share/zoneinfo
# network interfaces
net:
port: 27021
bindIp: 0.0.0.0
security:
authorization: enabled
keyFile: /usr/local/mongodb/keyfile
javascriptEnabled: false
# replication config
replication:
replSetName: shard1ReplSet
# Sharding Configuration
sharding:
clusterRole: shardsvr
- 启动脚本
#! /bin/bash
echo -e "\n"
echo "-------------------- ARBITER MONGODB START --------------------"
echo -e "\n"
pid_file="/usr/local/mongodb/arbiter.pid"
pid=""
if [ -n "$pid_file" ]
then
pid=$(cat $pid_file)
else
echo "${pid_file} is null"
fi
print="start"
if [ -n "$pid" ]
then
nohup /usr/local/mongodb/bin/mongod --shutdown --config /usr/local/mongodb/conf/arbiter.conf 2>/dev/null
echo "arbiter mongodb startup status ====> running"
print="restart"
else
echo "arbiter mongodb startup status ====> stopped"
fi
/usr/local/mongodb/bin/mongod -f /usr/local/mongodb/conf/arbiter.conf 2>/dev/null
sleep 2
echo "arbiter mongodb ${print} success!"
echo -e "\n"
echo "-------------------- ARBITER MONGODB START --------------------"
echo -e "\n"
- 关闭脚本
#! /bin/bash
echo -e "\n"
echo "-------------------- ARBITER MONGODB STOP --------------------"
echo -e "\n"
pid_file="/usr/local/mongodb/arbiter.pid"
pid=""
if [ -n "$pid_file" ]
then
pid=$(cat $pid_file)
else
echo "${pid_file} is null"
fi
if [ -n "$pid" ]
then
echo "arbiter mongodb startup status ====> running"
nohup /usr/local/mongodb/bin/mongod --shutdown --config /usr/local/mongodb/conf/arbiter.conf
rm -f ${pid_file}
else
echo "arbiter startup status ====> stopped"
fi
sleep 2
echo "arbiter stop success!"
echo -e "\n"
echo "-------------------- ARBITER MONGODB STOP --------------------"
echo -e "\n"
3.4.3 路由服务器
mongos.conf 配置文件
创建 mongos.conf 配置文件,配置如下
# where to write logging data.
systemLog:
destination: file
logAppend: true
path: /usr/local/mongodb/logs/mongos.log
# how the process runs
processManagement:
fork: true
pidFilePath: /usr/local/mongodb/mongos.pid
timeZoneInfo: /usr/share/zoneinfo
# network interfaces
net:
port: 27020
bindIp: 0.0.0.0
security:
keyFile: /usr/local/mongodb/keyfile
javascriptEnabled: false
# sharding config
sharding:
configDB: config1ReplSet/8.154.xx.xx:27018,121.199.xx.xx:27018,120.77.xx.xx:27018
- mongos启动异常
# 1.mongos 开启了security 安全配置
Unrecognized option: security.authorization
try '/usr/local/mongodb/bin/mongos --help' for more information
# 解决方案,去掉 mongos.conf 配置中的 security.authorization: enabled
# 2.mongos 缺少必要的认证信息
{"t":{"$date":"2024-11-22T09:37:17.017+08:00"},"s":"W", "c":"SHARDING", "id":23834, "ctx":"mongosMain","msg":"Error initializing sharding state, sleeping for 2 seconds and retrying","attr":{"error":{"code":13,"codeName":"Unauthorized","errmsg":"Error loading clusterID :: caused by :: command find requires authentication"}}}
# 解决方案 配置和配置服务器一样的 keyFile 文件
security:
keyFile: /usr/local/mongodb/keyfile
启动脚本
#! /bin/bash
echo -e "\n"
echo "-------------------- MONGOS MONGODB START --------------------"
echo -e "\n"
pid_file="/usr/local/mongodb/mongos.pid"
pid=""
if [ -n "$pid_file" ]
then
pid=$(cat $pid_file)
else
echo "${pid_file} is null"
fi
print="start"
if [ -n "$pid" ]
then
nohup /usr/local/mongodb/bin/mongod --shutdown --config /usr/local/mongodb/conf/mongos.conf 2>/dev/null
echo "mongos mongodb startup status ====> running"
print="restart"
else
echo "mongos mongodb startup status ====> stopped"
fi
/usr/local/mongodb/bin/mongos -f /usr/local/mongodb/conf/mongos.conf 2>/dev/null
sleep 2
echo "mongos mongodb ${print} success!"
echo -e "\n"
echo "-------------------- MONGOS MONGODB START --------------------"
echo -e "\n"
关闭脚本
#! /bin/bash
echo -e "\n"
echo "-------------------- MONGOS MONGODB STOP --------------------"
echo -e "\n"
# 认证信息,配置服务器的用户密码
username="configUser"
password="config123456"
auth_db="admin"
# 连接并执行 shutdownServer 命令,关闭 mongos 服务器
mongo --port 27020 -u ${username} -p ${password} --authenticationDatabase ${auth_db} <<EOF
use admin
db.shutdownServer()
EOF
sleep 5
# 验证 mongos 是否已停止
pid=$(ps aux | grep '/usr/local/mongodb/bin/mongos' | grep -v 'grep' | awk '{print $2}')
if [ -z "$pid" ]; then
echo "mongos stopped ====> success"
else
echo "mongos stopped ====> fail"
fi
echo -e "\n"
echo "-------------------- MONGOS MONGODB STOP --------------------"
echo -e "\n"
3.5 分片集相关操作
3.5.1 路由服务器初始化分片配置
- 登录mongos
# 登录mongos
[root@iZbp1dfulgjy4kd3ev4y7bZ bin]# mongo --port 27020
MongoDB shell version v5.0.18
connecting to: mongodb://127.0.0.1:27020/?compressors=disabled&gssapiServiceName=mongodb
Implicit session: session { "id" : UUID("9fe5bbb0-5e0c-4d2f-ad2b-e080d8e66735") }
MongoDB server version: 5.0.18
================
Warning: the "mongo" shell has been superseded by "mongosh",
which delivers improved usability and compatibility.The "mongo" shell has been deprecated and will be removed in
an upcoming release.
For installation instructions, see
https://docs.mongodb.com/mongodb-shell/install/
================
- 插入数据
# 创建test数据库,插入一条数据
mongos> use test
switched to db test
mongos> db.test.insert({"name":"test"})
# 插入失败需要登录授权
WriteCommandError({
"ok" : 0,
"errmsg" : "command insert requires authentication",
"code" : 13,
"codeName" : "Unauthorized",
"$clusterTime" : {
"clusterTime" : Timestamp(1732261386, 1),
"signature" : {
"hash" : BinData(0,"3zYGJ48WDrl+EvwkpqtqNZ94AT4="),
"keyId" : NumberLong("7439660539594145816")
}
},
"operationTime" : Timestamp(1732261386, 1)
})
# 使用配置服务器创建的用户进行登录操作
mongos> use admin
switched to db admin
mongos> db.auth("configUser","config123456")
1
# 创建test数据库,插入数据
mongos> use test
switched to db test
mongos> db.test.insert({"name":"test"})
# 插入失败,当前没有配置分片无法写入
WriteCommandError({
"ok" : 0,
"errmsg" : "Database test could not be created :: caused by :: No shards found",
"code" : 70,
"codeName" : "ShardNotFound",
"$clusterTime" : {
"clusterTime" : Timestamp(1732261564, 1),
"signature" : {
"hash" : BinData(0,"3LwHGbp8bpbiRsPhIHTEJQKwKs4="),
"keyId" : NumberLong("7439660539594145816")
}
},
"operationTime" : Timestamp(1732261564, 1)
})
路由服务器只是连接了配置服务器,还没有连接分片服务器,所以无法写入数据,需要配置分片服务器。
- 配置分片
# 添加分片分片副本集1 (shard1ReplSet)
mongos>sh.addShard("shard1ReplSet/120.77.xx.xx:27019,121.199.xx.xx:27019,120.77.xx.xx:27021")
{
"shardAdded" : "shard1ReplSet",
"ok" : 1,
"$clusterTime" : {
"clusterTime" : Timestamp(1732262059, 4),
"signature" : {
"hash" : BinData(0,"kpxxN75Lm2l7St1pxc1mJjPg4Fk="),
"keyId" : NumberLong("7439660539594145816")
}
},
"operationTime" : Timestamp(1732262059, 4)
}
# 添加分片分片副本集2 (shard2ReplSet)
mongos>sh.addShard("shard2ReplSet/8.154.xx.xx:27019,8.154.xx.xx:27029,121.199.xx.xx:27021")
# 查看分片状态
mongos> sh.status()
--- Sharding Status ---
...
shards:
{ "_id" : "shard1ReplSet", "host" : "shard1ReplSet/120.77.xx.xx:27019,121.199.xx.xx:27019", "state" : 1, "topologyTime" : Timestamp(1734331100, 2) }
{ "_id" : "shard2ReplSet", "host" : "shard2ReplSet/8.154.19.xx:27019,8.154.19.xx:27029", "state" : 1, "topologyTime" : Timestamp(1734331254, 1) }
databases:
{ "_id" : "config", "primary" : "config", "partitioned" : true }
# 执行插入数据成功!
mongos> db.test.insert({"name":"test"})
WriteResult({ "nInserted" : 1 })
- 移除分片
mongos> use admin
switched to db admin
mongos> db.runCommand({removeShard:"shard2ReplSet"})
{
"msg" : "draining started successfully",
"state" : "started",
"shard" : "shard2ReplSet",
"note" : "you need to drop or movePrimary these databases",
"dbsToMove" : [
"test"
],
"ok" : 1,
"$clusterTime" : {
"clusterTime" : Timestamp(1732263942, 3),
"signature" : {
"hash" : BinData(0,"YgQ6e5Np3y4qfCsMCwMT0iAFaSU="),
"keyId" : NumberLong("7439660539594145816")
}
},
"operationTime" : Timestamp(1732263942, 3)
}
# 这里需要删除之前创建的test数据库
Tip
移除分片是一个非常耗时的操作,尽量不要进行移除操作。
3.5.2 哈希分片
创建基于哈希的分片键步骤
- 插入数据
# 1.登录路由服务器
mongos> use admin
switched to db admin
mongos> db.auth("configUser","config123456")
# 2.创建 mydb 数据库
mongos> use mydb
switched to db mydb
# 3.创建 books 集合, 插入数据
mongos> db.books.insert({"bookName": "未来之路", "writer": "张三", "price": 60.00, "genre": "科幻", "publicationDate": "2023-01-15", "publisher": "未来出版社", "isbn": "978-7-5366-9058-7"})
WriteResult({ "nInserted" : 1 })
mongos> show collections
books
test
mongos> db.books.find()
{ "_id" : ObjectId("675bd6f98d1cc12177d2fc58"), "bookName" : "未来之路", "writer" : "张三", "price" : 60, "genre" : "科幻", "publicationDate" : "2023-01-15", "publisher" : "未来出版社", "isbn" : "978-7-5366-9058-7" }
- 启用数据库分片
# 1.对 mydb 数据库启动分片
mongos> sh.enableSharding("mydb")
{
"ok" : 1,
...
}
# 2.查看分片状态
mongos> sh.status()
...
databases:
{ "_id" : "mydb", "primary" : "shard2ReplSet", "partitioned" : true, "version" : { "uuid" : UUID("27e172b7-7e1b-4bf3-915e-91ed6ee88b14"), "timestamp" : Timestamp(1734331733, 30), "lastMod" : 1 } }
- 创建索引
# 1.创建哈希索引
mongos> db.books.createIndex({ "bookName": "hashed" })
{
...
"ok" : 1,
}
# 2.查看创建的索引
mongos> db.books.getIndexes()
[
{
"v" : 2,
"key" : {
"bookName" : "hashed"
},
"name" : "bookName_hashed"
}
]
- 创建片键
# 为 mydb.books 集合启用分片,使用 bookName 的哈希值作为分片键
mongos> sh.shardCollection("mydb.books",{"bookName":"hashed"})
{
"collectionsharded" : "mydb.books",
}
- 查看状态
mongos> sh.status()
databases:
...
{ "_id" : "mydb", "primary" : "shard1ReplSet", "partitioned" : true, "version" : { "uuid" : UUID("1a64b1e3-d4ad-4125-b860-a09dcd2f487c"), "timestamp" : Timestamp(1732266882, 1), "lastMod" : 1 } }
mydb.books
shard key: { "bookName" : "hashed" }
unique: false
balancing: true
chunks:
shard2ReplSet 1
{ "bookName" : { "$minKey" : 1 } } -->> { "bookName" : { "$maxKey" : 1 } } on : shard2ReplSet Timestamp(1, 0)
3.5.3 范围分片
创建基于范围的分片键步骤
- 插入数据
mongos> db.writer.insert( {"name": "张三", "age": 35, "nationality": "中国", "bio": "著名科幻小说作家,擅长描写未来世界。", "publishedBooks": ["未来之路", "星际旅行"]},)
WriteResult({ "nInserted" : 1 })
mongos> db.writer.find()
{ "_id" : ObjectId("675be70b8d1cc12177d2fc5a"), "name" : "张三", "age" : 35, "nationality" : "中国", "bio" : "著名科幻小说作家,擅长描写未来世界。", "publishedBooks" : [ "未来之路", "星际旅行" ] }
mongos>
- 启用数据库分片
# 1.对 mydb 数据库启动分片
mongos> sh.enableSharding("mydb")
- 创建索引
mongos> db.writer.createIndex({ "age": 1 })
{
...
"ok" : 1
}
- 创建片键
sh.shardCollection("mydb.writer",{"age":1})
{
"collectionsharded" : "mydb.writer",
"ok" : 1,
...
}
- 查看状态
mongos> sh.status()
databases:
...
mydb.books
shard key: { "bookName" : "hashed" }
unique: false
balancing: true
chunks:
shard2ReplSet 1
{ "bookName" : { "$minKey" : 1 } } -->> { "bookName" : { "$maxKey" : 1 } } on : shard2ReplSet Timestamp(1, 0)
mydb.writer
shard key: { "age" : 1 }
unique: false
balancing: true
chunks:
shard2ReplSet 1
{ "age" : { "$minKey" : 1 } } -->> { "age" : { "$maxKey" : 1 } } on : shard2ReplSet Timestamp(1, 0)