背景知识

副本集(Replica Set,replSet,复制集)具有自动故障恢复的功能。主从集群和副本集最大的区别就是副本集没有固定的主节点,整个集群会选出一个主节点,当其挂掉后,会在剩下的副节点中选中一个作为主节点,副本集中总有一个活跃节点(primary)和多个备份节点(secondary)。

MongoDB官方已经不建议使用主从模式,代替的方案是采用副本集的模式。

主从模式 其实就是一个单副本的应用,没有很好的扩展性和容错性,而MongoDB副本集具有多个副本保证了容错性,即使一个副本挂掉了还有多个副本存在,主节点挂掉后,整个集群内会实现自动切换

副本集工作原理

客户端连接到整个MongoDB副本集,不关心具体哪一台节点是否挂掉。主节点负责整个副本集的读写,副本集定期同步数据备份,一旦主节点挂掉,副节点就会选举一个新的主节点,而这一切对于应用服务器不需要任何关心。

副本集中副节点在主节点挂掉后通过心跳机制检测到后,就会在副本集内发起主节点的选举机制,自动选举一位新的主节点。

官方推荐的MongoDB副本集机器数量为至少 3 个,即:一主两副

环境准备

因为主机数量有限,这里只用两个副本集部署演示,两台主机:Centos

ip地址主机名角色
192.168.198.140mongo-master副本集主节点
192.168.198.142mongo-slave副本集副节点

关闭防火墙:

systemctl stop firewalld
systemctl disable firewalld

安装MongoDB

安装

wget http://downloads.mongodb.org/linux/mongodb-linux-x86_64-rhel70-4.2.8.tgz
tar -zvxf mongodb-linux-x86_64-rhel70-4.2.8.tgz
mv mongodb-linux-x86_64-rhel70-4.2.8 /usr/local/mongodb
cd /usr/local/mongodb
mkdir conf data logs
touch conf/mongodb.conf
touch logs/mongodb.log
mkdir -p data/db
  • 编写MongoDB配置文件mongodb.conf
#数据存储路径
dbpath=/usr/local/mongodb/data/db
#日志存储文件
logpath=/usr/local/mongodb/logs/mongodb.log
#运行端口
port= 27017
#守护进程的方式运行MongoDB,创建服务器进程
fork=true
#使用追加的方式写日志
logappend=true
# 绑定服务IP,若绑定127.0.0.1,则只能本机访问,不指定默认本地所有IP
bind_ip= 192 .168.198.
#使用此设置来配置复制副本集。指定一个副本集名称作为参数,所有主机都必须有相同的名称作为同一个副本集
replSet=repset

启动

[root@mongo-master mongodb]# ./bin/mongod -f conf/mongodb.conf
about to fork child process, waiting until server is ready for connections.
forked process: 2238
child process started successfully, parent exiting
#出现上述显示则表示启动成功

# 查看进程是否启动
ps -aux | grep mongodb

初始化副本集

分别启动两台机器的MongoDB服务,选择一台主机作为主节点,比如 140

#登录MongoDB
[root@mongo-master mongodb]# ./bin/mongo 192.168.198.140:
#使用admin数据库
> use admin
#定义副本集配置变量,这里的 _id:”repset” 和mongodb.conf中replSet要保持一样。
> config = { _id:"repset", members:[{_id:0,host:"192.168.198.140:27017"},
{_id:1,host:"192.168.198.142:27017"}]}
#初始化副本集配置
> rs.initiate(config)
{
"ok" : 1 ,
"$clusterTime" : {
"clusterTime" : Timestamp(1608199737, 1 ),
"signature" : {
"hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
"keyId" : NumberLong(0)
}
},
"operationTime" : Timestamp(1608199737, 1 )
}
#查看集群节点的状态
> rs.status()
# 部分显示如下:
{
"_id" : 0 ,
"name" : "192.168.198.140:27017",
"health" : 1 ,
"state" : 1 ,
"stateStr" : "PRIMARY",
"uptime" : 47 ,
"optime" : {
"ts" : Timestamp(1608261017, 1 ),
"t" : NumberLong(6)
}
{
"_id" : 1 ,
"name" : "192.168.198.142:27017",
"health" : 1 ,
"state" : 2 ,
"stateStr" : "SECONDARY",
"uptime" : 34 ,
"optime" : {
"ts" : Timestamp(1608261017, 1 ),
"t" : NumberLong(6)
}
# 以上输出得知192.168.198.140为主节点;192.168.198.142为副节点,且都正常运行

节点状态字段说明:

  • health表示副本集中该节点是否正常, 0 表示不正常, 1 表示正常;
  • state表示节点的身份, 2 表示非主节点, 1 表示主节点;
  • stateStr用于对节点身份进行字符描述,PRIMARY表示主节点,SECONDARY表示副节点;
  • name是副本集节点的ip和端口信息

MongoDB副本集数据复制功能

设置副本节点可读

#登录所有节点mongodb,设置所有节点可读 db.getMongo().setSlaveOk();
#因为mongodb默认是从主节点读写数据的,副本节点上不允许读,需要设置副本节点可以读,在所有节点中设置可读
repset:SECONDARY> db.getMongo().setSlaveOk();

测试复制功能

#在主节点连接到终端
[root@mongo-master mongodb]# ./bin/mongo 192.168.198.140:
#新建数据库test2,插入数据
repset:PRIMARY> use test
switched to db test
repset:PRIMARY> db.testdb.insert({"test1":"testval1"})
WriteResult({ "nInserted" : 1 })

#在副节点上登录mongodb
[root@mongo-slave mongodb]# ./bin/mongo 192.168.198.142:
repset:SECONDARY> use test
switched to db test
repset:SECONDARY> show tables;
testdb
repset:SECONDARY> db.testdb.find();
{ "_id" : ObjectId("5fdc1fa6f1b5b7686d113ff4"), "test1" : "testval1" }
repset:SECONDARY>

如上发现已经在副本节点上发现了测试数据,即已经从主节点复制过来了

测试副本集故障转移功能

先停掉主节点192.168.198.140,查看mongodb副本集状态,可以看到经过一系列的投票选择操作,192.168.198.142当选主节点,192.168.198.140从192.168.198.142同步数据过来

[root@mongo-master mongodb]# ps -aux | grep mongodb
root 2383  0 .8 13 .3 1916136 132808? Sl 11 :09 0 :12 ./bin/mongod -
f conf/mongodb.conf

root 2503  0 .0 0 .0 112824 980 pts/0 R+ 11 :34 0 :00 grep --
color=auto mongodb
[root@mongo-master mongodb]# kill -9 2383
[root@mongo-master mongodb]# ps -aux | grep mongodb
root 2505  0 .0 0 .0 112824 980 pts/0 R+ 11 :35 0 :00 grep --
color=auto mongodb

#停止服务后重新启动
[root@mongo-master mongodb]# ./bin/mongod -f conf/mongodb.conf
about to fork child process, waiting until server is ready for connections.
forked process: 2509
child process started successfully, parent exiting
# 重新登录副节点192.168.198.142,查看副本集状态
> rs.status()
# 以下显示部分输出
{
"_id" : 0 ,
"name" : "192.168.198.140:27017",
"health" : 1 ,
"state" : 2 ,
"stateStr" : "SECONDARY",
"uptime" : 138 ,
"optime" : {
"ts" : Timestamp(1608262692, 1 ),
"t" : NumberLong(7)
}
{
"_id" : 1 ,
"name" : "192.168.198.142:27017",
"health" : 1 ,
"state" : 1 ,
"stateStr" : "PRIMARY",
"uptime" : 1709 ,
"optime" : {
"ts" : Timestamp(1608262692, 1 ),
"t" : NumberLong(7)
}

由以上输出得知192.168.198.142变成了PRIMARY主节点,而192.168.198.140由以前的主节点变为副节点。

由于这里只有两个副本集,所以192.168.198.142主机就变成了主节点,若副节点存在多个则会通过内部选举一个副节点成为主节点,数据复制功能也能正常运行

读写分离

在副节点永久设置setSlaveOk

#找到.mongorc.js
sudo find / -name .mongorc.js
#在.mongorc.js中添加rs.slaveOk();

注:一般这个文件都是空的,直接加上去。保存退出。之后退出mongo在进去就可以了 尽量每台服务器都添加上去

python代码

from pymongo import MongoClient
from pymongo.read_preferences import ReadPreference
import time

if __name__ == '__main__':
node_list = ['192.168.198.140:27017', '192.168.198.142:27017']
client = MongoClient(node_list,read_preference=ReadPreference.SECONDARY_PREFERRED)
coll = client.test.testdb
row = {'id': '20201218', 'name': 'Jordan', 'age': 20 , 'gender': 'male',
'time': int(time.time())}
ret = coll.insert_one(row)
print(ret.inserted_id)
# print(coll.find_one(filter={'name': 'Jordan'}))

ReadPreference属性说明:

  • PRIMARY:默认参数,只从主节点上进行读取操作;

  • PRIMARY_PREFERRED:大部分从主节点上读取数据,只有主节点不可用时从secondary节点读取数据。

  • SECONDARY:只从secondary节点上进行读取操作,存在的问题是secondary节点的数据会比primary节点数据“旧”。

  • SECONDARY_PREFERRED:优先从secondary节点进行读取操作,secondary节点不可用时从主节点读取数据;

  • NEAREST:不管是主节点、secondary节点,从网络延迟最低的节点上读取数据。

配置MongoDB服务开机启动

  • 编写service文件
  • vim /lib/systemd/system/mongodb.service
[Unit]
Description=mongodb
After=network.target remote-fs.target nss-lookup.target
[Service]
Type=forking
ExecStart=/usr/local/mongodb/bin/mongod --config
/usr/local/mongodb/conf/mongodb.conf
ExecReload=/bin/kill -s HUP $MAINPID
ExecStop=/usr/local/mongodb/bin/mongod --shutdown --config
/usr/local/mongodb/conf/mongodb.conf
PrivateTmp=true
[Install]
WantedBy=multi-user.target
  • 使之生效
chmod +x mongodb.service
# 关闭mongodb服务
[root@mongo-master system]# ps -ef | grep mongodb
root 6946  1  0 15 :46? 00 :00:33 ./bin/mongod -f
conf/mongodb.conf
root 10328 1240  0 16 :51 pts/0 00 :00:00 grep --color=auto mongodb
[root@mongo-master system]# kill -9 6946
[root@mongo-master system]# ps -ef | grep mongodb
root 10340 1240  0 16 :51 pts/0 00 :00:00 grep --color=auto mongodb
#启动服务
systemctl start mongodb
#开机自启
systemctl enable mongodb.service
systemctl status mongodb

添加环境变量

export PATH=$PATH:/usr/local/mongodb/bin
在/etc/profile文件中添加 export PATH="$PATH:/usr/local/mongodb/bin"
# 重新加载/etc/profile文件
source /etc/profile
在/root/.bashrc文件中添加 export PATH="$PATH:/usr/local/mongodb/bin"
#到此环境变量配置完成

在终端中输入mongo 192.168.198.140:27017即能进入mongodb

增加安全认证机制KeyFile

主库配置用户

#创建管理员账户
db.createUser( {user:"admin",pwd:"admin",roles:
[{role:"userAdminAnyDatabase",db:"admin"}]});

db.createUser( {user:"root",pwd:"root",roles:[{role: "root", db: "admin"}]});

集群之间的安全认证

#生成key
touch /usr/local/mongodb/usercenter/mongodb-keyfile
openssl rand -base64 745 > /usr/local/mongodb/usercenter/mongodb-keyfile
chmod 600 /usr/local/mongodb/usercenter/mongodb-keyfile

将该key放到集群中机器的每一台上,记住必须保持一致,权限设置成600,修改mongodb.conf,并重新启用所有服务器

#新增安全机制校验
keyFile=/usr/local/mongodb/usercenter/mongodb-keyfile

重新启动mongodb:记住重新启动时候,keyfile的指定如果没有在配置文件中配置,就必须启动时候使用参数keyfile指定

关闭顺序注意:mongodb集群有自动切换主库功能,如果先关主库,主库就切换到其它上面去 了,这里预防主库变更,从库关闭后再关闭主库

[root@mongo-master ~]# mongo 192.168.198.140:27017/admin -u admin -p
MongoDB shell version v4.2.
Enter password:
connecting to: mongodb://192.168.198.140:27017/admin?
compressors=disabled&gssapiServiceName=mongodb
Implicit session: session { "id" : UUID("0de7d81d-8985-46af-965a-f008c3cdcf85")
}
MongoDB server version: 4 .2.
test:PRIMARY> use admin
switched to db admin
test:PRIMARY> db.auth("root","root")
1
#重新查看从库和集群状态都是正常

创建用户和用户数据库

#开启了安全认证就可以开始对每个数据库进行安全认证了,首先给用户创建一个数据库

test:PRIMARY> use test
switched to db test
test:PRIMARY> db.test.insertOne({"name":"xxx"})
{
"acknowledged" : true,
"insertedId" : ObjectId("5fdffb32334ffe271fc5cf10")
}
#创建用户数据库的用户
test:PRIMARY> db.createUser({user:"test3",pwd:"test3",roles:
[{role:"readWrite",db:"test3"}]})
Successfully added user: {
"user" : "test3",
"roles" : [
{
"role" : "readWrite",
"db" : "test3"
}
]
}

#测试登录
[root@mongo-master ~]# mongo 192.168.198.140:27017/test3 -u test3 -p
MongoDB shell version v4.2.
Enter password:
connecting to: mongodb://192.168.198.140:27017/test3?
compressors=disabled&gssapiServiceName=mongodb
Implicit session: session { "id" : UUID("6aeed3b7-a230-4d52-87e5-a27f0e30ec74")
}
MongoDB server version: 4 .2.
test:PRIMARY> use test
switched to db test
test:PRIMARY> db.test.find()
{ "_id" : ObjectId("5fdffb32334ffe271fc5cf10"), "name" : "xxx" }
test:PRIMARY>

新增认证python代码


from pymongo import MongoClient
from pymongo.read_preferences import ReadPreference
import time
if __name__ == '__main__':
node_list = ['192.168.198.140:27017', '192.168.198.142:27017']
client = MongoClient(node_list,
read_preference=ReadPreference.SECONDARY_PREFERRED)
client.test3.authenticate("test3", "test3", mechanism='SCRAM-SHA-1')
coll = client.test3.test
row = {'id': '20201218', 'name': 'Jordan', 'age': 20 , 'gender': 'male',
'time': int(time.time())}
ret = coll.insert_one(row)
# print(ret.inserted_id)
print(coll.find_one(filter={'name': 'Jordan'}))
#{'_id': ObjectId('5fdffca10f07d7bba313f025'), 'id': '20201218', 'name':
'Jordan', 'age': 20, 'gender': 'male', 'time': 1608514721}