docker 部署mongoDB集群与读写分离
一.生成key文件
需要注意集群中所有机器都需要用同一个文件,否则会出现验证失败的情况
# 生成key
openssl rand -base64 756 > /data/volume/mongodb/configdb/mongo.key
# 设置访问权限
chmod 400 /data/volume/mongodb/configdb/mongo.key
二.启动MongoDB的docker 容器
docker run -di --name=mongo -p 27017:27017 -v /data/volume/mongodb/configdb/:/data/configdb/ -v /data/volume/mongodb/db/:/data/db/ mongo mongod --replSet mongos --keyFile /data/configdb/mongo.key
# -itd:其中,i是交互式操作,t是一个终端,d指的是在后台运行。
# --name mongo-test:容器名称
# -p 27017:27017 :映射容器服务的 27017 端口到宿主机的 27017 端口。外部可以直接通过 宿主机 ip:27017 访问到 mongo 的服务。
# --auth:需要密码才能访问容器服务。
# -v /data/volume/mongodb/configdb/:/data/configdb/ :映射配置文件路径
# -v /data/volume/mongodb/db/:/data/db/ :映射数据文件路径
# mongod 设置mongod配置
# --replSet mongos 设置集群名称 mongos
# --keyFile /data/configdb/mongo.key 指定秘钥文件路径
# --config /data/configdb/mongo.conf 指定配置文件路径
# --dbpath:指定存放 MongoDB 数据的目录,启动服务时必须指定。
# --logpath:默认日志是打印在命令行中的,使用该选项指定日志输出的文件。如果对此目录有写权限且文件不存在,则会自动创建该文件。如果日志文件已经存在,默认会覆盖掉该文件,并删除所有旧的日志。如果希望保留旧的日志,除了使用 --logpath 之外,还应该使用 --logappend 选项。
三.登录到mongo
docker exec -it mongo mongosh admin
docker exec -it mongo mongo admin #5.0以下版本使用
登录成功 显示 连接地址、版本信息等
四.初始化集群
注意_id要与“--replSet mongos”一致
rs.initiate({
_id:"mongos",
members:[
{_id:0,host:"192.168.230.128:27017"},
{_id:1,host:"192.168.230.129:27017"},
{_id:2,host:'192.168.230.130:27017',arbiterOnly:true}
]
})
# arbiterOnly:true 设为仲裁节点
修改使用:rs.reconfig(...)
五.创建root账号
Mongodb默认是不需要用户密码就可以连接的,如果使用命令报错"not authorized on admin to execute command ",则表示当前登陆用户不具备相应权限;
解决办法:通过创建一个用户,赋予用户授权管理集群权限(clusterAdmin)即可(在createUser之前先use admin切换一下)
添加用户权限成功之后,使用root用户登陆,再次使用命令即可成功,仅需要在primary上创建即可,读写分离完成后会同步到secondary节点
db.createUser(
{
user:'root',
pwd:'123456',
roles:[{ role:'userAdminAnyDatabase', db: 'admin'}, 'readWriteAnyDatabase','clusterAdmin']
});
# user:'root' :设置用户名为root
# pwd:'123456':设置密码为123456
# role:'userAdminAnyDatabase':只在admin数据库中可用,赋予用户所有数据库的userAdmin权限
# db: 'admin':可操作的数据库
# 'readWriteAnyDatabase':赋予用户对其他所有库读写权限
- 数据库用户角色:read、readWrite;
- 数据库管理角色:dbAdmin、dbOwner、userAdmin;
- 集群管理角色:clusterAdmin、clusterManager、4. clusterMonitor、hostManage;
- 备份恢复角色:backup、restore;
- 所有数据库角色:readAnyDatabase、readWriteAnyDatabase、userAdminAnyDatabase、dbAdminAnyDatabas
- 超级用户角色:root
- 内部角色:__system
若修改指定用户权限使用:
use admin # 切换到管理库
db.grantRolesToUser("root", ["clusterAdmin"]) # 为用户“root”授权 集群管理
六.登录root账号
七.查看集群配置
rs.conf()
八.查看集群状态
rs.status();
如下:
mongos [direct: primary] admin> rs.status();
{
set: 'mongos',
date: ISODate("2023-03-25T09:15:26.242Z"),
myState: 1,
term: Long("1"),
syncSourceHost: '',
syncSourceId: -1,
heartbeatIntervalMillis: Long("2000"),
majorityVoteCount: 2,
writeMajorityCount: 1,
votingMembersCount: 2,
writableVotingMembersCount: 1,
optimes: {
lastCommittedOpTime: { ts: Timestamp({ t: 1679735718, i: 1 }), t: Long("1") },
lastCommittedWallTime: ISODate("2023-03-25T09:15:18.494Z"),
readConcernMajorityOpTime: { ts: Timestamp({ t: 1679735718, i: 1 }), t: Long("1") },
appliedOpTime: { ts: Timestamp({ t: 1679735718, i: 1 }), t: Long("1") },
durableOpTime: { ts: Timestamp({ t: 1679735718, i: 1 }), t: Long("1") },
lastAppliedWallTime: ISODate("2023-03-25T09:15:18.494Z"),
lastDurableWallTime: ISODate("2023-03-25T09:15:18.494Z")
},
lastStableRecoveryTimestamp: Timestamp({ t: 1679735678, i: 1 }),
electionCandidateMetrics: {
lastElectionReason: 'electionTimeout',
lastElectionDate: ISODate("2023-03-25T08:17:58.236Z"),
electionTerm: Long("1"),
lastCommittedOpTimeAtElection: { ts: Timestamp({ t: 1679732267, i: 1 }), t: Long("-1") },
lastSeenOpTimeAtElection: { ts: Timestamp({ t: 1679732267, i: 1 }), t: Long("-1") },
numVotesNeeded: 2,
priorityAtElection: 1,
electionTimeoutMillis: Long("10000"),
numCatchUpOps: Long("0"),
newTermStartDate: ISODate("2023-03-25T08:17:58.307Z"),
wMajorityWriteAvailabilityDate: ISODate("2023-03-25T08:17:58.317Z")
},
members: [
{
_id: 0,
name: '192.168.230.128:27017',
health: 1,
state: 1,
stateStr: 'PRIMARY',
uptime: 4073,
optime: { ts: Timestamp({ t: 1679735718, i: 1 }), t: Long("1") },
optimeDate: ISODate("2023-03-25T09:15:18.000Z"),
lastAppliedWallTime: ISODate("2023-03-25T09:15:18.494Z"),
lastDurableWallTime: ISODate("2023-03-25T09:15:18.494Z"),
syncSourceHost: '',
syncSourceId: -1,
infoMessage: '',
electionTime: Timestamp({ t: 1679732278, i: 1 }),
electionDate: ISODate("2023-03-25T08:17:58.000Z"),
configVersion: 1,
configTerm: 1,
self: true,
lastHeartbeatMessage: ''
},
{
_id: 1,
name: '192.168.230.129:27017',
health: 1,
state: 7,
stateStr: 'ARBITER',
uptime: 3458,
lastHeartbeat: ISODate("2023-03-25T09:15:25.565Z"),
lastHeartbeatRecv: ISODate("2023-03-25T09:15:25.566Z"),
pingMs: Long("0"),
lastHeartbeatMessage: '',
syncSourceHost: '',
syncSourceId: -1,
infoMessage: '',
configVersion: 1,
configTerm: 1
}
],
ok: 1,
'$clusterTime': {
clusterTime: Timestamp({ t: 1679735718, i: 1 }),
signature: {
hash: Binary(Buffer.from("a880e39a0ace5a06ee9474d8c77a4b862d9cd46f", "hex"), 0),
keyId: Long("7214395200045580294")
}
},
operationTime: Timestamp({ t: 1679735718, i: 1 })
}
节点类型
MongoDB的节点类型有主节点(primary或称为Master),副本节点(Slave或者称为Secondary),arbiter仲裁节点,Secondary-Only节点,Hidden节点,Delayed节点和Non-Voting节点。
- primary:主节点,提供增删查改服务
- secondary:备节点,只提供读(需要执行 “db.getMongo().setReadPref('secondary')”)
- arbiter:仲裁节点不存储数据,只是用于投票。所以仲裁节点对于服务器负载很低。节点一旦以仲裁者的身份加入集群,他就只能是仲裁者,无法将仲裁者配置为非仲裁者。
- Secondary-Only:不能成为primary节点,只能作为secondary副本节点,防止一些性能不高的节点成为主节点。
- Hidden:这类节点是不能够被客户端制定IP引用,也不能被设置为主节点,但是可以投票,一般用于备份数据。
- Delayed:可以指定一个时间延迟从primary节点同步数据。主要用于备份数据,如果实时同步,误删除数据马上同步到从节点。所以延迟复制主要用于避免用户错误。
- Non-Voting:没有选举权的secondary节点,纯粹的备份数据节点。
副本集成员状态
副本集成员状态指的是rs.status()的stateStr字段。
- STARTUP:刚加入到复制集中,配置还未加载
- STARTUP2:配置已加载完,初始化状态
- RECOVERING:正在恢复,不适用读
- ARBITER: 仲裁者
- DOWN:节点不可到达
- UNKNOWN:未获取其他节点状态而不知是什么状态,一般发生在只有两个成员的架构
- REMOVED:移除复制集
- ROLLBACK:数据回滚,在回滚结束时,转移到RECOVERING或SECONDARY状态
- FATAL:出错。查看日志grep “replSet FATAL”找出错原因,重新做同步
- PRIMARY:主节点
- SECONDARY:备份节点。
九.修改集群配置
可以通过re.config(...)重新初始化配置修改集群
rs.reconfig({ _id: "mongos", members: [ { _id: 1, host: "192.168.230.131:27017" }, { _id: 2, host: '192.168.230.130:27017', arbiterOnly: true }] }, {force : true})
在primary主机上执行,如果都已经不存在primary主机的话,或以上不能执行,则任意进入该副本集的任意一台主机,使用下面方法进行操作:
use admin;
db.auth('root','123456');
rs.config(); #观察members中哪一部分需要更改,会影响下面members[n]中n的值
cfg = rs.config();
cfg.members[0].host = "192.168.1.3:2777";
rs.reconfig(cfg,{"force":true}); #如果不是primary主机,则会发生replSetReconfig should only be run on PRIMARY, but my state is REMOVED; use the "force" argument to override这个错误,那么则运行rs.reconfig(rsc,{"force":true});
rs.config(); #观察members中地址是否已经变更
exit
十.添加/移除一个节点
添加/移除节点需要先设置 setDefaultRWConcern 否者会提示
Reconfig attempted to install a config that would change the implicit default write concern. Use the setDefaultRWConcern command to set a cluster-wide write concern and try the reconfig again.
# 设置写关注WriteConcern
db.adminCommand({
"setDefaultRWConcern" : 2,
"defaultWriteConcern" : {
"w" : majority
}
})
写关注WriteConcern
指定了写入操作的确认级别,单机、副本集、分片集群都可以指定写关注级别。
可通过setDefaultRWConcern设置全局默认写关注级别(从mongoDB4.4开始)。
写关注需使用事务级的,不能在事务内为某个写操作单独指定写关注级别,会导致报错。
参数包括:
{ w: <value>, j: <boolean>, wtimeout: <number> }
w:数字—写入请求确认写入操作已传播到指定数量的节点(mongod实例)。
"majority"—请求请求确认写入操作已传播到计算出的多数节点。
其他自定义写关注名称—settings.getLastErrorModes中可自定义写关注条件。
j:指定写入请求是否确认已写入磁盘预写日志。true时会确保写入操作已经写入w指定数量节点的预写日志中。
wtimeout:指定写入操作超时时间(毫秒)。
需注意:到达wtimeout后无论写入最终能否成功都会返回错误,且已成功的写入修改不会回退;如果不指定wtimeout,且写关注级别无法满足,写入操作会一直阻塞。
## 具体使用方式参考:https://blog.csdn.net/Synup/article/details/123668468
https://www.mongodb.com/docs/manual/reference/command/setDefaultRWConcern/#mongodb-dbcommand-dbcmd.setDefaultRWConcern
添加节点:
rs.add('192.168.230.130:27017')
登录被添加的节点,查看命令提示符状态变化
添加仲裁节点
rs.addArb("192.168.230.130:27017")
移除节点
rs.remove('192.168.230.130:27017')
十一.设置为读写分离
Read Preference 描述了mongodb 如何将请求路由到副本集的节点,默认下,会路由到primary节点,如果primary节点读写压力过大,可以考虑读写分离的方案。
不过需要考虑一种场景,就是primary的写入压力非常的大,所以副本节点复制的写入压力同样很大。
这时副本节点如果读取压力也很大的话,根据MongoDB库级别读写锁的机制,
很可能复制写入程序拿不到写锁,从而导致副本节点与主节点有较大延迟。
如果进行读写分离,首先需要在副本节点声明其为secondary
db.getMongo().setReadPref('secondary')
其中的ReadRreference有几种设置:
- primary:默认参数,只从主节点上进行读取操作;
- primaryPreferred:大部分从主节点上读取数据,只有主节点不可用时从secondary节点读取数据。
- secondary:只从secondary节点上进行读取操作,存在的问题是secondary节点的数据会比primary节点数据“旧”。
- secondaryPreferred:优先从secondary节点进行读取操作,secondary节点不可用时从主节点读取数据;
- nearest:不管是主节点、secondary节点,从网络延迟最低的节点上读取数据。
十二.Springboot客户端连接
# MongoDB URI
spring.data.mongodb.uri=mongodb://testUser:pwd@192.168.230.130:27017,192.168.230.131:27017/testDb?replicaSet=mongos&readPreference=secondaryPreferred
# “test”:用户名
# “123456”:密码
# “192.168.230.130:27017,192.168.230.131:27017” :连接地址多个使用逗号隔开。
# “testDb”:数据库
# “replicaSet=mongos”:集群id
# “readPreference=secondaryPreferred” 将请求路由到副本集方式,这里设置为“优先从secondary节点进行读取操作,secondary节点不可用时从主节点读取数据”
这里不能通过, ROOT_USERNAME 来读取数据库(我们的 root 用户可用于创建新的数据库、集合和新用户),否者会报错:
Command failed with error 18 (AuthenticationFailed)
可以创建一个普通读写用户用于客户端连接
# 切换到管理库
use admin
# 添加一个用户,用于客户端连接
db.createUser({user: "testUser", pwd: "pwd", roles : [{role: "readWrite", db: "interviewTest"}]});
# 显示所有用户
db.system.users.find()
十三.验证
1.服务端数据复制验证:
- 在primary中插入数据
# 切换到用户库
use admin
# 用户登录
db.auth('root','123456')
# 切换到指定业务库
use testDb
# 插入一条数据
db.student.insert({"name":"张三",age:21})
- 在secondary中查询数据
# 切换到用户库
use admin
# 用户登录
db.auth('root','123456')
# 切换到指定业务库
use testDb
# 插入一条数据
db.student.find()
在primary插入的数据,secondary中可以查询得到,表示成功!
2.服务端故障测试
- 分别观察三台服务器,命令提示为:
192.168.230.130 -> mongos [direct: secondary] admin>_
192.168.230.131 -> mongos [direct: primary] admin>_
192.168.230.131 -> mongos [direct: arbiter] admin>_
- 直接停掉192.168.230.131,观察
192.168.230.130
是否变成为了primary
3.客户端连接测试
public static void main(String[] args) {
//创建一个mongo连接
MongoDatabaseFactory mongoDatabaseFactory = new SimpleMongoClientDatabaseFactory("mongodb://testUser:pwd@192.168.230.130:27017,192.168.230.131:27017/testDb?replicaSet=mongos&readPreference=secondaryPreferred");
MongoOperations mongoTemplate = new MongoTemplate(mongoDatabaseFactory);
//开启1000个线程进行循环读取
for (int i = 0; i < 1000; i++) {
new Thread(() -> {
while (true){
System.out.println(Thread.currentThread().getId());
List<TestModel> list = mongoTemplate.findAll(TestModel.class, "student");
System.out.println(JSON.toJSONString(list));
}
}).start();
}
}
通过调整连接字符串中的 readPreference 参数,观察服务器压力,判断查询落在哪个副本上
- primary 节点压力显示
- secondary节点压力显示