mongo-db笔记
[TOC]
一,复制集
复制集简介
Mongodb复制集由一组Mongod实例(进程)组成,包含一个Primary节点和多个Secondary节点,Mongodb Driver(客户端)的所有数据都写入Primary,Secondary从Primary同步写入的数据,以保持复制集内所有成员存储相同的数据集,提供数据的高可用。
下图(图片源于Mongodb官方文档)是一个典型的Mongdb复制集,包含一个Primary节点和2个Secondary节点。
Primary选举
复制集通过replSetInitiate命令(或mongo shell的rs.initiate())进行初始化,初始化后各个成员间开始发送心跳消息,并发起Priamry选举操作,获得『大多数』成员投票支持的节点,会成为Primary,其余节点成为Secondary。
初始化复制集
1 | config = { |
『大多数』的定义
假设复制集内投票成员(后续介绍)数量为N,则大多数为 N/2 + 1,当复制集内存活成员数量不足大多数时,整个复制集将无法选举出Primary,复制集将无法提供写服务,处于只读状态。
投票成员数 | 大多数 | 容忍失效数 |
---|---|---|
通常建议将复制集成员数量设置为奇数,从上表可以看出3个节点和4个节点的复制集都只能容忍1个节点失效,从『服务可用性』的角度看,其效果是一样的。(但无疑4个节点能提供更可靠的数据存储)
特殊的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节点的数据来恢复到之前的时间点。
数据同步
Primary与Secondary之间通过oplog来同步数据,Primary上的写操作完成后,会向特殊的local.oplog.rs特殊集合写入一条oplog,Secondary不断的从Primary取新的oplog并应用。
因oplog的数据会不断增加,local.oplog.rs被设置成为一个capped集合,当容量达到配置上限时,会将最旧的数据删除掉。另外考虑到oplog在Secondary上可能重复应用,oplog必须具有幂等性,即重复应用也会得到相同的结果。
如下oplog的格式,包含ts、h、op、ns、o等字段
1 | { |
上述oplog里各个字段的含义如下
-
ts: 操作时间,当前timestamp + 计数器,计数器每秒都被重置
-
h:操作的全局唯一标识
-
v:oplog版本信息
-
op:操作类型
-
- i:插入操作
- u:更新操作
- d:删除操作
- c:执行命令(如createDatabase,dropDatabase)
- n:空操作,特殊用途
-
ns:操作针对的集合
-
o:操作内容,如果是更新操作
-
o2:操作查询条件,仅update操作包含该字段
Secondary初次同步数据时,会先进行init sync,从Primary(或其他数据更新的Secondary)同步全量数据,然后不断通过tailable cursor从Primary的local.oplog.rs集合里查询最新的oplog并应用到自身。
init sync过程包含如下步骤
- T1时间,从Primary同步所有数据库的数据(local除外),通过listDatabases + listCollections + cloneCollection敏命令组合完成,假设T2时间完成所有操作。
- 从Primary应用[T1-T2]时间段内的所有oplog,可能部分操作已经包含在步骤1,但由于oplog的幂等性,可重复应用。
- 根据Primary各集合的index设置,在Secondary上为相应集合创建index。(每个集合_id的index已在步骤1中完成)。
oplog集合的大小应根据DB规模及应用写入需求合理配置,配置得太大,会造成存储空间的浪费;配置得太小,可能造成Secondary的init sync一直无法成功。比如在步骤1里由于DB数据太多、并且oplog配置太小,导致oplog不足以存储[T1, T2]时间内的所有oplog,这就Secondary无法从Primary上同步完整的数据集。
修改复制集配置
当需要修改复制集时,比如增加成员、删除成员、或者修改成员配置(如priorty、vote、hidden、delayed等属性),可通过replSetReconfig命令(rs.reconfig())对复制集进行重新配置。 比如将复制集的第2个成员Priority设置为2,可执行如下命令
1 | cfg = rs.conf(); |
细说Primary选举
Primary选举除了在复制集初始化时发生,还有如下场景
- 复制集被reconfig
- Secondary节点检测到Primary宕机时,会触发新Primary的选举
- 当有Primary节点主动stepDown(主动降级为Secondary)时,也会触发新的Primary选举
Primary的选举受节点间心跳、优先级、最新的oplog时间等多种因素影响。
节点间心跳
复制集成员间默认每2s会发送一次心跳信息,如果10s未收到某个节点的心跳,则认为该节点已宕机;如果宕机的节点为Primary,Secondary(前提是可被选为Primary)会发起新的Primary选举。
节点优先级
- 每个节点都倾向于投票给优先级最高的节点
- 优先级为0的节点不会主动发起Primary选举
- 当Primary发现有优先级更高Secondary,并且该Secondary的数据落后在10s内,则Primary会主动降级,让优先级更高的Secondary有成为Primary的机会。
Optime
拥有最新optime(最近一条oplog的时间戳)的节点才能被选为主。
网络分区
只有更大多数投票节点间保持网络连通,才有机会被选Primary;如果Primary与大多数的节点断开连接,Primary会主动降级为Secondary。当发生网络分区时,可能在短时间内出现多个Primary,故Driver在写入时,最好设置『大多数成功』的策略,这样即使出现多个Primary,也只有一个Primary能成功写入大多数。
复制集的读写设置
Read Preference
默认情况下,复制集的所有读请求都发到Primary,Driver可通过设置Read Preference来将读请求路由到其他的节点。
- primary: 默认规则,所有读请求发到Primary
- primaryPreferred: Primary优先,如果Primary不可达,请求Secondary
- secondary: 所有的读请求都发到secondary
- secondaryPreferred:Secondary优先,当所有Secondary不可达时,请求Primary
- nearest:读请求发送到最近的可达节点上(通过ping探测得出最近的节点)
Write Concern
默认情况下,Primary完成写操作即返回,Driver可通过设置[Write Concern(https://docs.mongodb.org/manual/core/write-concern/)来设置写成功的规则。
如下的write concern规则设置写必须在大多数节点上成功,超时时间为5s。
1 | db.products.insert( |
上面的设置方式是针对单个请求的,也可以修改副本集默认的write concern,这样就不用每个请求单独设置。
1 | cfg = rs.conf() |
异常处理(rollback)
当Primary宕机时,如果有数据未同步到Secondary,当Primary重新加入时,如果新的Primary上已经发生了写操作,则旧Primary需要回滚部分操作(未同步的数据),以保证数据集与新的Primary一致。 旧Primary将回滚的数据写到单独的rollback目录下,数据库管理员可根据需要使用mongorestore进行恢复。
搭建复制集
复制集文档地址: https://www.mongodb.com/docs/manual/
mongo配置文件详解地址: https://docs.mongodb.com/manual/reference/configuration-options/
dockerhub地址: https://registry.hub.docker.com/_/mongo?tab=description
-
创建对应mongo文件夹
mkdir mongo
mkdir mongo/conf
-
进入conf文件夹创建配置文件
cd mongo/conf
vim mongod.conf
配置文件详解参控链接
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32storage:
dbPath: /data/db
journal:
enabled: true
engine:
mmapv1:
wiredTiger:
systemLog:
destination: file
logAppend: true
path: /var/log/mongodb/mongod.log
where to write logging data.
network interfaces
net:
port: 27017
bindIp: 0.0.0.0
how the process runs
security:
开启认证
authorization: enabled
指定keyfile认证
clusterAuthMode: keyFile
keyFile: /etc/mongo/mongodb.keyfile
operationProfiling:
replication:
oplogSizeMB: 10240
复制集名称
replSetName: rs1 -
进入conf文件夹生成密钥
# 生成keyfile文件,keyfile文件只生成一次 拷贝到每个节点上使用
openssl rand -base64 736 > mongodb.keyfile
# mongodb.keyfile必须授权600和999,否则后续启动不了MongoDB容器
# 给m1的mongodb.keyfile文件授权
chmod 600 mongodb.keyfile
# 修改文件拥有者和组
chown 999 mongodb.keyfile
-
准备镜像
# 搜索mongo镜像
docker search mongo
# 拉取镜像
docker pull mongo:latest
# 查看镜像
docker images
-
启动镜像
如果想将mongo复制集独立在一个网段里,可创建docker network 然后指定network
docker run --name mongo1 -v /root/mongo/conf:/etc/mongo -d -p 27018:27017 mongo:latest --config /etc/mongo/mongod.conf
docker run --name mongo2 -v /root/mongo/conf:/etc/mongo -d -p 27019:27017 mongo:latest --config /etc/mongo/mongod.conf
docker run --name mongo3 -v /root/mongo/conf:/etc/mongo -d -p 27020:27017 mongo:latest --config /etc/mongo/mongod.conf
-
记录其他mongoIP地址
这里我们将mongo1作为主节点,其他作为从节点
# 查看容器详情
docker inspect mongo2
ip为字段"IPAddress"
这里我的ip信息为
mongo1: 172.18.0.8
mongo2: 172.18.0.9
mongo3: 172.18.0.10
-
进入mongo1容器
# 进入容器
docker exec -it mongo1 /bin/bash
-
进入mongoshll
# 登录
mongo admin
# 配置信息 ip与上方对应
myconf = {“_id”:“rs1”,“members”:[{“_id”:0,“host”:“172.18.0.8:27017”},{“_id”:1,“host”:“172.18.0.9:27017”},{“_id”:2,“host”:“172.18.0.10:27017”}]}
rs.initiate(myconf)
# 查看复制集状态
rs.status()
-
创建用户
mongodb内置角色详解: https://www.mongodb.com/docs/manual/reference/built-in-roles/
# 选择admin库
use admin
# 创建admin库操作用户
db.createUser({user: “admin”, pwd: “xxxxx”, roles: [{role: “userAdminAnyDatabase”, db: “admin”}]})
# 认证
db.auth(“admin”, “xxxx”)
db.createUser({user: “root”, pwd: “xxxx”, roles: [“root”]})
db.auth(“root”, " xxxx")
show dbs //显示库表示用户创建成功
-
测试复制集是否搭建成功
复制集之间的用户共享,也就是主节点创建用户同步至从节点
# 插入数据
use test
db.test.insert({a: 1})
# 进入mongo2容器内部
docker exec -it mongo2 /bin/bash
# 登录mongoshll 注意root用户才有操作所有库的权限
mongo -u root
# 查看
db.test.find()
# 此时查看会报错,“errmsg” : “not master and slaveOk=false”, 执行以下命令允许对 MongoDB 连接的辅助成员进行读取操作。
rs.secondaryOk()
db.test.find() //有数据则代表成功
二,文档设计
1,分桶设计
分桶设计原则
所谓分桶优化,就是与其对每一条数据创建一个文档,我们可以把某一个时间段内的测量数据聚合到一起放到一个文档内,利用MongoDB提供的内嵌式数组或子文档特性
我们知道许多传感器数据都是时间序列数据。例如:风传感器,潮汐监测以及位置追踪等采集数据的无非这种类型: Timestamp,采集器名称/ID,采集值。对于时序类型的数据,我们可以采用一种叫做时间分桶的优化策略。
时间序列数据
简单的说 时间序列就是各时间点上形成的数值序列,时间序列分析就是通过观察历史数据预测未来的值。采用分桶设计写入的数据集,元素更多的是采用时间作为排序元素,依次写入和读取。
官方有一篇翻译文章,专门叙述 分桶设计模式
使用场景描述
基础数据集如下
1 | { |
复制
改进后的文档集如下
1 | { |
复制
我们在程序写入文档时,可以做一些简单的计算和整理,按时间分段,根据业务需要,将一个时间断内的大量文档合并,避免数据使用时的随机聚合和查询。这样的时间段,可以理解为桶。
在处理时间序列数据时,知道2018年7月13日加利福尼亚州康宁市下午2:00至3:00的平均温度通常比知道下午2:03那一时刻的温度更有意义也更重要。通过用桶组织数据并进行预聚合,我们可以更轻松地提供这些信息。
2,行转列
问题
大文档,很多字段,很多索引
1 | { |
以上方式记录数据,假如需要查询一些国家的上映日期就需要建立很多索引。
1 | {release_USA: 1} |
解决
1 | { |
db.movies.createIndex({“releases.country”: 1, “releases.date”: 1})
通过建立country和date的联合索引减少了建立大量索引的问题。
小结
3,版本号
问题
mongodb的文档模型过于灵活,常常会导致文档之间的字段属性差距过大
2019.01版本1.0
1 | { |
2019.03版本2.0
1 | { |
解决
新增版本号
1 | { |
通过新增版本号的方式区分,同一模型文档之间字段属性不一致的问题
还可以新增Validation规则限制文档灵活变更
小结
4,近似计算
问题
统计网站点击流量
可以看到这个需求大部分操作在统计网站的计数统计,数据库写入操作将在统计结束有写入
同时网站的计数不需要特别精准
解决
每隔一段时间去写入数据库,减少写入的压力
小结
5,预聚合字段
问题
业绩排名,游戏排名,商品统计等精准统计
这种问题就没办法使用近似计算,需要精准统计
如果使用mongo的聚合计算来完成,但是mongo的聚合计算是一个扫描型的操作,比较消耗系统资源并且时间较长容易拖垮服务
解决
1 | { |
使用预聚合而非全文档更新
1 | db.inventory.update({_id: 123}, |
小结
三,事务
写操作事务
什么是 writeConcern ?
writeConcern 决定一个写操作落到多少个节点上才算成功。writeConcern 的取值包括:
• 0:发起写操作,不关心是否成功;
• 1~集群最大数据节点数:写操作需要被复制到指定节点数才算成功;
• majority:写操作需要被复制到大多数节点上才算成功。 发起写操作的程序将阻塞到写操作到达指定的节点数为止
默认行为
primary写入成功立刻返回。
对于重要的数据默认行为比较危险,当写入primary后,primary立马宕机其secondary节点还未同步数据,那么就会发生数据丢失问题。
当然又不是真的丢失了,primary会将数据存储在Rollback文件中
3 节点复制集不作任何特别设定(默认值):
w: “majority”
大多数节点确认模式
w: “all”
全部节点确认模式
该模式存在一些缺陷,当有个节点宕机或者网络分区延迟较长,会导致迟迟无法写入成功,但是其实数据是已经写入primary节点了,所以需要特别注意下
j:true
writeConcern 可以决定写操作到达多少个节点才算成功,journal 则定义如何才算成 功。取值包括:
• true: 写操作落到 journal 文件中才算成功;
• false: 写操作到达内存即算作成功。
journal
journal日志是数据库的crash recovery手段。通常的做法是把数据库内的数据块修改,提前用文件顺序写方式刷到盘上,然后再去真正的提交数据的修改。这样的目的是在服务器宕机的时候,内存中被丢失的数据可以在恢复过程中从journal 日志文件中读回来。
Oplog
Oplog也是记录的数据库的操作日志,但是记的是逻辑操作命令。主要的目的是用于节点之间复制数据,而不是上面journal主要是用来recover crash。
mongod.log
还有一种就是mongod.log,这个就是一个文本文件,记录数据库系统的正常运行和错误信息等等。
writeConcern 的意义
对于5个节点的复制集来说,写操作落到多少个节点上才算是安全的?
• 1
• 2
• 3 ✓
• 4 ✓
• 5 ✓
• majority ✓
writeConcern 实验
测试writeConcern命令
rs1:PRIMARY> db.test.insert({count: 1}, {writeConcern: {w: “majority”}})
WriteResult({ “nInserted” : 1 })
rs1:PRIMARY> db.test.insert({count: 2}, {writeConcern: {w: 3}})
WriteResult({ “nInserted” : 1 })
rs1:PRIMARY> db.test.insert({count: 2}, {writeConcern: {w: 4}})
WriteResult({
“nInserted” : 1,
“writeConcernError” : {
“code” : 100,
“codeName” : “UnsatisfiableWriteConcern”,
“errmsg” : “Not enough data-bearing nodes”,
“errInfo” : {
“writeConcern” : {
“w” : 4,
“wtimeout” : 0,
“provenance” : “clientSupplied”
}
}
}
})
配置延迟节点,模拟网络延迟
secondaryDelaySecs 同步数据延迟秒数
priority: 0 不成为primary节点(优先级为0)
rs1:PRIMARY> conf = rs.conf()
rs1:PRIMARY> conf.members[2].secondaryDelaySecs = 10
10
rs1:PRIMARY> conf.members[2].priority = 0
0
rs1:PRIMARY> rs.reconfig(conf)rs1:PRIMARY> rs.conf()
观察复制延迟下的写入,以及timeout参数
注意writeConcern只是要求secondary同步写入数据后返回同步成功与否,无法保证同步失败,写入数据撤回。可以看到最后虽然返回等待超时,但是find()
看到数据还是写入成功了
rs1:PRIMARY> db.test.insert({count: 3}, {writeConcern: {w: 3}})
WriteResult({ “nInserted” : 1 })
rs1:PRIMARY> db.test.insert({count: 3}, {writeConcern: {w: 3, wtimeout: 3000}})
WriteResult({
“nInserted” : 1,
“writeConcernError” : {
“code” : 64,
“codeName” : “WriteConcernFailed”,
“errmsg” : “waiting for replication timed out”,
“errInfo” : {
“wtimeout” : true,
“writeConcern” : {
“w” : 3,
“wtimeout” : 3000,
“provenance” : “clientSupplied”
}
}
}
})rs1:PRIMARY> db.test.find()
{ “_id” : ObjectId(“6277dbadf7816143b0f2a3fd”), “count” : 1 }
{ “_id” : ObjectId(“6277dbbbf7816143b0f2a3fe”), “count” : 2 }
{ “_id” : ObjectId(“6277dbbef7816143b0f2a3ff”), “count” : 2 }
{ “_id” : ObjectId(“6277df3949fa133810940d93”), “count” : 3 }
{ “_id” : ObjectId(“6277df5649fa133810940d94”), “count” : 3 }
注意事项
- 虽然多于半数的 writeConcern 都是安全的,但通常只会设置 majority,因为这是 等待写入延迟时间最短的选择;
- 不要设置 writeConcern 等于总节点数,因为一旦有一个节点故障,所有写操作都 将失败;
- writeConcern 虽然会增加写操作延迟时间,但并不会显著增加集群压力,因此无论 是否等待,写操作最终都会复制到所有节点上。设置 writeConcern 只是让写操作 等待复制后再返回而已;
- 应对重要数据应用 {w: “majority”},普通数据可以应用 {w: 1} 以确保最佳性能。
连接设置
在实际开发过程,每条命令加上writeConcern比较繁琐,此时就可以在连接mongo的url上加参数
mongodb://db0.example.com,db1.example.com,db2.example.com/?replicaSet=myRepl&w=majority&wtimeoutMS=5000
文档: https://docs.mongodb.com/manual/reference/connection-string/
读操作事务
什么是 readPreference?
readPreference 决定使用哪一个节点来满足 正在发起的读请求。可选值包括:
- primary: 只选择主节点;
- primaryPreferred:优先选择主节点,如果不可用则选择从节点;
- secondary:只选择从节点;
- secondaryPreferred:优先选择从节点, 如果从节点不可用则选择主节点;
- nearest:选择最近的节点;
readPreference 场景举例
- 用户下订单后马上将用户转到订单详情页——primary/primaryPreferred。因为此 时从节点可能还没复制到新订单;
- 用户查询自己下过的订单——secondary/secondaryPreferred。查询历史订单对 时效性通常没有太高要求;
- 生成报表——secondary。报表对时效性要求不高,但资源需求大,可以在从节点 单独处理,避免对线上用户造成影响;
- 将用户上传的图片分发到全世界,让各地用户能够就近读取——nearest。每个地区 的应用选择最近的节点读取数据。
readPreference 与 Tag
readPreference 只能控制使用一类节点。Tag 则可以将节点选择控制 到一个或几个节点。考虑以下场景:
- 一个 5 个节点的复制集;
- 3 个节点硬件较好,专用于服务线上客户;
- 2 个节点硬件较差,专用于生成报表; 可以使用 Tag 来达到这样的控制目的:
- 为 3 个较好的节点打上 {purpose: “online”};
- 为 2 个较差的节点打上 {purpose: “analyse”};
- 在线应用读取时指定 online,报表读取时指定 reporting。
readPreference 配置
注意以下测试一定要开四个窗口
通过 MongoDB 的连接串参数
mongodb://host1:27107,host2:27107,host3:27017/?replicaSet=rs&readPre ference=secondary
通过 MongoDB 驱动程序 API:
MongoCollection.withReadPreference(ReadPreference readPref)
Mongo Shell:
db.collection.find({}).readPref( “secondary” )
readPreference 实验: 从节点读
1, 先清除上一个writeConcern实现的延迟节点
rs1:PRIMARY> conf = rs.conf()
rs1:PRIMARY> conf.members[2].secondaryDelaySecs = 0
rs1:PRIMARY> rs.reconfig(conf)
rs1:PRIMARY> db.test.drop()
2, 主节点写入 {x:1}, 观察该条数据在各个节点均可见
rs1:PRIMARY> db.test.insert({r: 1})
WriteResult({ “nInserted” : 1 })
rs1:SECONDARY> db.test.find().pretty()
{ “_id” : ObjectId(“6279198497720b7b20d419cf”), “r” : 1 }
3, 在两个从节点分别执行 db.fsyncLock() 来锁定写入(同步)
rs1:SECONDARY> db.fsyncLock()
{
“info” : “now locked against writes, use db.fsyncUnlock() to unlock”,
“lockCount” : NumberLong(1),
“seeAlso” : “http://dochub.mongodb.org/core/fsynccommand”,
“ok” : 1,
“$clusterTime” : {
“clusterTime” : Timestamp(1652103721, 1),
“signature” : {
“hash” : BinData(0,“c0tNopmyabuUWNU1MPlPDM925gQ=”),
“keyId” : NumberLong(“7094149400091426820”)
}
},
“operationTime” : Timestamp(1652103711, 1)
}
4, 主节点写入
shll当中无法显示出特性,还是会查出{r: 2},但是这个是有效的通过代码或者连接工具测试该实验即可
rs1:PRIMARY> db.test.insert({r: 2}.{writeConcern: {w: 1}})
rs1:PRIMARY> db.test.find().readPref(““secondary””)
注意事项
- 指定 readPreference 时也应注意高可用问题。例如将 readPreference 指定 primary,则发生 故障转移不存在 primary 期间将没有节点可读。如果业务允许,则应选择 primaryPreferred;
- 使用 Tag 时也会遇到同样的问题,如果只有一个节点拥有一个特定 Tag,则在这个节点失效时 将无节点可读。这在有时候是期望的结果,有时候不是。例如:
- 如果报表使用的节点失效,即使不生成报表,通常也不希望将报表负载转移到其他节点上,此时只有一个 节点有报表 Tag 是合理的选择;
- 如果线上节点失效,通常希望有替代节点,所以应该保持多个节点有同样的 Tag;
- Tag 有时需要与优先级、选举权综合考虑。例如做报表的节点通常不会希望它成为主节点,则 优先级应为 0。
什么是 readConcern?
在 readPreference 选择了指定的节点后,readConcern 决定这个节点上的数据哪些 是可读的,类似于关系数据库的隔离级别。可选值包括:
- available:读取所有可用的数据;
- local:读取所有可用且属于当前分片的数据;
- majority:读取在大多数节点上提交完成的数据;
- linearizable:可线性化读取文档;
- snapshot:读取最近快照中的数据;
readConcern: local 和 available
在复制集中 local 和 available 是没有区别的。两者的区别主要体现在分片集上。考虑以下场景:
- 一个 chunk x 正在从 shard1 向 shard2 迁移;
- 整个迁移过程中 chunk x 中的部分数据会在 shard1 和 shard2 中同时存在,但源分片 shard1仍然是 chunk x 的负责方:
- 所有对 chunk x 的读写操作仍然进入 shard1;
- config 中记录的信息 chunk x 仍然属于 shard1;
- 此时如果读 shard2,则会体现出 local 和 available 的区别:
- local:只取应该由 shard2 负责的数据(不包括 x);
- available:shard2 上有什么就读什么(包括 x);
注意事项:
- 虽然看上去总是应该选择 local,但毕竟对结果集进行过滤会造成额外消耗。在一些 无关紧要的场景(例如统计)下,也可以考虑 available;
- MongoDB <=3.6 不支持对从节点使用 {readConcern: “local”};
- 从主节点读取数据时默认 readConcern 是 local,从从节点读取数据时默认 readConcern 是 available(向前兼容原因)。
readConcern: majority
只读取大多数据节点上都提交了的数据。考虑如下场景:
- 集合中原有文档 {x: 0};
- 将x值更新为 1;
如果在各节点上应用 {readConcern: “majority”} 来读取数据:
一个读线程,使用 readConcern:majority,在t1 的时候读S1节点,S1是否要返回最新的一个写入 x=1 还是要等到t5?
首先理解,这个读线程只能读 一个被大多数节点确认的写入(满足write majority),这才可以满足readConcern: majority的条件。
在t1的时候,x=1实际上已经到达两个节点(majority),但是判断这个事实的是主节点。在主节点没有知道并确认x=1这个操作已经被多数节点写入的时候,这条数据是不够资格被发给 readConcern: majority 读线程的。
这个是3个节点,想象一下5个节点 - 那S1是肯定不知道数据已经写入到多数节点,只能等primary 告诉他。
这里是官方的文档: https://docs.mongodb.com/manual/reference/read-concern-majority/
总结: 判断一个数据是否被大多数节点同步是只能通过primary 告知,secondary是无法感知到数据有没有被大多数节点同步(只和primary交流)
readConcern: majority 的实现方式
考虑 t3 时刻的 Secondary1,此时:
- 对于要求 majority 的读操作,它将返回 x=0;
- 对于不要求 majority 的读操作,它将返回 x=1;
如何实现?
节点上维护多个 x 版本,MVCC 机制 MongoDB 通过维护多个快照来链接不同的版本:
- 每个被大多数节点确认过的版本都将是一个快照;
- 快照持续到没有人使用为止才被删除;
实验: readConcern : ”majority” vs “local”
-
安装 3 节点复制集。
-
注意配置文件内 server 参数 enableMajorityReadConcern
配置文件路径: /etc/mongo/mongod.conf
-
将复制集中的两个从节点使用 db.fsyncLock() 锁住写入(模拟同步延迟)
-
执行
rs1:PRIMARY> db.test.find().readConcern(“local”)
{ “_id” : ObjectId(“627a6dd10bd1286f6aa11a78”), “r” : 1 }
{ “_id” : ObjectId(“627a6def0bd1286f6aa11a79”), “r” : 2 }
rs1:PRIMARY> db.test.find().readConcern(“available”)
{ “_id” : ObjectId(“627a6dd10bd1286f6aa11a78”), “r” : 1 }
{ “_id” : ObjectId(“627a6def0bd1286f6aa11a79”), “r” : 2 }
rs1:PRIMARY> db.test.find().readConcern(“majority”)
{ “_id” : ObjectId(“627a6dd10bd1286f6aa11a78”), “r” : 1 } -
结论:
- 使用 local 参数,则可以直接查询到写入数据
- 使用 majority,只能查询到已经被多数节点确认过的数据
- update 与 remove 与上同理。
readConcern: majority 与脏读
MongoDB 中的回滚:
- 写操作到达大多数节点之前都是不安全的,一旦主节点崩溃,而从节还没复制到该 次操作,刚才的写操作就丢失了;
- 把一次写操作视为一个事务,从事务的角度,可以认为事务被回滚了。
所以从分布式系统的角度来看,事务的提交被提升到了分布式集群的多个节点级别的 “提交”,而不再是单个节点上的“提交”。 在可能发生回滚的前提下考虑脏读问题:
- 如果在一次写操作到达大多数节点前读取了这个写操作,然后因为系统故障该操作 回滚了,则发生了脏读问题;
使用 {readConcern: “majority”} 可以有效避免脏读
readConcern具有事务的隔离性,隔离级别有提交读(默认)和可重复读(snapshot)
readConcern: 如何实现安全的读写分离
考虑如下场景:
向主节点写入一条数据; 立即从从节点读取这条数据。 如何保证自己能够读到刚刚写入的数据?
下述方式有可能读不到刚写入的订单
使用 writeConcern + readConcern majority 来解决
readConcern: linearizable
readConcern: majority不一定是完全安全的,比如以下情景
- 旧主节点的网络分区失联
- 一条数据更新至新主节点
- 旧主节点就收到读取操作,此时旧主节点因为网络分区的缘故未同步到新的x变更,同时旧主节点任然认为x:1是被大多数节点承认的数据并返回(旧数据)
只读取大多数节点确认过的数据。和 majority 最大差别是保证绝对的操作线性顺序 – 在写操作自然时间后面的发生的读,一定可以读到之前的写
- 只对读取单个文档时有效;
- 可能导致非常慢的读,因此总是建议配合使用 maxTimeMS;
readConcern: snapshot
{readConcern: “snapshot”} 只在多文档事务中生效。将一个事务的 readConcern 设置为 snapshot,将保证在事务中的读:
- 不出现脏读;
- 不出现不可重复读;
- 不出现幻读。
因为所有的读都将使用同一个快照,直到事务提交为止该快照才被释放。
小结
- available:读取所有可用的数据
- local:读取所有可用且属于当前分片的数据,默认设置
- majority:数据读一致性的充分保证,可能你最需要关注的
- linearizable:增强处理 majority 情况下主节点失联时候的例外情况
- snapshot:最高隔离级别,接近于 Seriazable
多文档事务支持
事务属性 | 支持程度 |
---|---|
Atomocity 原子性 | 单表单文档 : 1.x 就支持 复制集多表多行:4.0 复制集 分片集群多表多行4.2 |
Consistency 一致性 | writeConcern, readConcern (3.2) |
Isolation 隔离性 | readConcern (3.2) |
Durability 持久性 | Journal and Replication |
使用
MongoDB 多文档事务的使用方式与关系数据库非常相似:
1 | try (ClientSession clientSession = client.startSession()) { |
事务的隔离级别
- 事务完成前,事务外的操作对该事务所做的修改不可访问
- 如果事务内使用 {readConcern: “snapshot”},则可以达到可重复读 Repeatable Read
实验:启用事务后的隔离性
隔离级别: read commit
rs1:PRIMARY> db.test.find()
{ “_id” : ObjectId(“627a7911bc799d083af44177”), “x” : 1, “y” : 1 }
rs1:PRIMARY> var session = db.getMongo().startSession()
rs1:PRIMARY> session.startTransaction()
rs1:PRIMARY> var coll = session.getDatabase(“test”).getCollection(“test”)
rs1:PRIMARY> coll.update({x: 1}, {$set: {y: 2}})
WriteResult({ “nMatched” : 1, “nUpserted” : 0, “nModified” : 1 })
rs1:PRIMARY> db.test.find()
{ “_id” : ObjectId(“627a7911bc799d083af44177”), “x” : 1, “y” : 1 }
rs1:PRIMARY> coll.find()
{ “_id” : ObjectId(“627a7911bc799d083af44177”), “x” : 1, “y” : 2 }
实验:可重复读 Repeatable Read
隔离性: Repeatable Read
rs1:PRIMARY> var session = db.getMongo().startSession()
rs1:PRIMARY> session.startTransaction({ readConcern: {level: “snapshot”}, writeConcern: {w: “majority”} })
rs1:PRIMARY> var coll = session.getDatabase(“test”).getCollection(“test”)
rs1:PRIMARY> coll.find()
{ “_id” : ObjectId(“627a7911bc799d083af44177”), “x” : 1, “y” : 0 }
rs1:PRIMARY> db.test.update({x: 1}, {$set: {y: 100}})
WriteResult({ “nMatched” : 1, “nUpserted” : 0, “nModified” : 1 })
rs1:PRIMARY> db.test.find()
{ “_id” : ObjectId(“627a7911bc799d083af44177”), “x” : 1, “y” : 100 }
rs1:PRIMARY> coll.find() //重复读
{ “_id” : ObjectId(“627a7911bc799d083af44177”), “x” : 1, “y” : 0 }
rs1:PRIMARY> session.commitTransaction()
rs1:PRIMARY> coll.find()
{ “_id” : ObjectId(“627a7911bc799d083af44177”), “x” : 1, “y” : 100 }
事务写机制
MongoDB 的事务错误处理机制不同于关系数据库:
- 当一个事务开始后,如果事务要修改的文档在事务外部被修改过,则事务修改这个 文档时会触发 Abort 错误,因为此时的修改冲突了;
- 这种情况下,只需要简单地重做事务就可以了;
- 如果一个事务已经开始修改一个文档,在事务以外尝试修改同一个文档,则事务以 外的修改会等待事务完成才能继续进行(write-wait.md实验)。
实验:写冲突
rs1:PRIMARY> var session = db.getMongo().startSession()
rs1:PRIMARY> session.startTransaction({ readConcern: {level: “snapshot”}, writeConcern: {w: “majority”} })
rs1:PRIMARY> var coll = session.getDatabase(“test”).getCollection(“test”)
shell1
rs1:PRIMARY> coll.update({x: 1}, {$set: {y: “t1”}})
shell2
rs1:PRIMARY> coll.update({x: 1}, {$set: {y: “t2”}})
WriteCommandError({
“errorLabels” : [
“TransientTransactionError”
],
“ok” : 0,
“errmsg” : “WriteConflict error: this operation conflicted with another operation. Please retry your operation or multi-document transaction.”,
“code” : 112,
“codeName” : “WriteConflict”,
“$clusterTime” : {
“clusterTime” : Timestamp(1652195653, 1),
“signature” : {
“hash” : BinData(0,“pUVVvbUoAhBTofVrNR20K5ham7Y=”),
“keyId” : NumberLong(“7094149400091426820”)
}
},
“operationTime” : Timestamp(1652195653, 1)
})
实验:写冲突 (续)
shell1
rs1:PRIMARY> var session = db.getMongo().startSession()
rs1:PRIMARY> session.startTransaction({ readConcern: {level: “snapshot”}, writeConcern: {w: “majority”} })
rs1:PRIMARY> var coll = session.getDatabase(“test”).getCollection(“test”)
rs1:PRIMARY> coll.update({x: 1}, {$set: {y: “t3”}})
WriteResult({ “nMatched” : 1, “nUpserted” : 0, “nModified” : 1 })
shell2
rs1:PRIMARY> db.test.update({x: 1}, {$set: {y: “t2”}})
//长时间等待直至前一个事务提交
rs1:PRIMARY> db.test.find()
{ “_id” : ObjectId(“627a7911bc799d083af44177”), “x” : 1, “y” : “t2” }
注意事项
感觉mongo的事务得一个一个来,无法多事务提交
- 可以实现和关系型数据库类似的事务场景
- 必须使用与 MongoDB 4.2 兼容的驱动;
- 事务默认必须在 60 秒(可调)内完成,否则将被取消;
- 涉及事务的分片不能使用仲裁节点;
- 事务会影响 chunk 迁移效率。正在迁移的 chunk 也可能造成事务提交失败(重试 即可);
- 多文档事务中的读操作必须使用主节点读;
- readConcern 只应该在事务级别设置,不能设置在每次读写操作上。
Change Stream
什么是 Change Stream
Change Stream 是 MongoDB 用于实现变更追踪的解决方案,类似于关系数据库的触 发器,但原理不完全相同:
Change Stream | 触发器 | |
---|---|---|
触发方式 | 异步 | 同步(事务保证) |
触发位置 | 应用回调事件 | 数据库触发器 |
触发次数 | 每个订阅事件的客户端 | 1次(触发器) |
故障恢复 | 从上次断点重新触发 | 事务回滚 |
Change Stream 的实现原理
Change Stream 是基于 oplog 实现的。它在 oplog 上开启一个 tailable cursor 来追踪所有复制集上的变更操作,最终调用应用中定义的回调函数。 被追踪的变更事件主要包括:
- insert/update/delete:插入、更新、删除;
- drop:集合被删除; • rename:集合被重命名;
- dropDatabase:数据库被删除;
- invalidate:drop/rename/dropDatabase 将导致 invalidate 被触发, 并关闭 change stream;
Change Stream 与可重复读
Change Stream 只推送已经在大多数节点上提交的变更操作。即“可重复读”的变更。 这个验证是通过 {readConcern: “majority”} 实现的。因此:
- 未开启 majority readConcern 的集群无法使用 Change Stream;
- 当集群无法满足 {w: “majority”} 时,不会触发 Change Stream(例如 PSA 架构 中的 S 因故障宕机)。
Change Stream 变更过滤
如果只对某些类型的变更事件感兴趣,可以使用使用聚合管道的过滤步骤过滤事件。 例如
1 | var cs = db.collection.watch([{ |
示例
修改配置文件
在使用Change Stream 之前我们需要确保我们的复制集开启了MajorityReadConcern
查看配置文件 /etc/mongo/mongod.conf
修改完成后重启mongo
docker restart mongo1
docker restart mongo2
docker restart mongo3
使用Change Stream
shll1
rs1:SECONDARY> db.test.watch([],{ maxAwaitTimeMS: 30000}).pretty()
shll2
rs1:PRIMARY> db.test.insert({x: 2, y: “t3”})
此时shll1就会接收到事件
rs1:SECONDARY> db.test.watch([],{ maxAwaitTimeMS: 30000}).pretty()
{
“_id” : {
“_data” : “82628507AC000000012B022C0100296E5A1004A3307A058820417C80561971FDFCB33346645F69640064628507AC161C35E8E0BA199F0004”
},
“operationType” : “insert”,
“clusterTime” : Timestamp(1652885420, 1),
“fullDocument” : {
“_id” : ObjectId(“628507ac161c35e8e0ba199f”),
“x” : 2,
“y” : “t3”
},
“ns” : {
“db” : “test”,
“coll” : “test”
},
“documentKey” : {
“_id” : ObjectId(“628507ac161c35e8e0ba199f”)
}
}
Change Stream 故障恢复
假设在一系列写入操作的过程中,订阅 Change Stream 的应用在接收到“写3”之后 于 t0 时刻崩溃,重启后后续的变更怎么办?
想要从上次中断的地方继续获取变更流,只需要保留上次 变更通知中的 _id
即可。 右侧所示是一次 Change Stream 回调所返回的数据。每 条这样的数据都带有一个 _id
,这个 _id
可以用于断点恢 复。例如:
var cs = db.collection.watch([], {resumeAfter: <_id>})
即可从上一条通知中断处继续获取后续的变更通知。
Change Stream 使用场景
- 跨集群的变更复制——在源集群中订阅 Change Stream,一旦得到任何变更立即写 入目标集群。
- 微服务联动——当一个微服务变更数据库时,其他微服务得到通知并做出相应的变 更。
- 其他任何需要系统联动的场景。
注意事项
- Change Stream 依赖于 oplog,因此中断时间不可超过 oplog 回收的最大时间窗; (意思是oplog的大小有限,如果上一次的变更id已经从oplog中抹去,那么也就无法恢复Change Stream了)
- 在执行 update 操作时,如果只更新了部分数据,那么 Change Stream 通知的也 是增量部分;
- 同理,删除数据时通知的仅是删除数据的 _id。
四,开发最佳实践
连接到 MongoDB
-
关于驱动程序:总是选择与所用之 MongoDB 相兼容的驱动程序。这可以很容易地从驱动 兼容对照表中查到;
-
如果使用第三方框架(如 Spring Data),则还需要考虑框架版本与驱动的兼容性;
-
关于连接对象 MongoClient:使用 MongoClient 对象连接到 MongoDB 实例时总是应该 保证它单例,并且在整个生命周期中都从它获取其他操作对象。
-
关于连接字符串:连接字符串中可以配置大部分连接选项,建议总是在连接字符串中配置 这些选项;
-
// 连接到复制集 mongodb://节点1,节点2,节点3…/database?[options]
-
// 连接到分片集 mongodb://mongos1,mongos2,mongos3…/database?[options]
-
常见连接字符串参数
-
maxPoolSize
连接池大小
-
Max Wait Time
建议设置,自动杀掉太慢的查询
-
Write Concern
建议 majority 保证数据安全
-
Read Concern
对于数据一致性要求高的场景适当使用
连接字符串节点和地址
- 无论对于复制集或分片集,连接字符串中都应尽可能多地提供节点地址,建议全部 列出;
- 复制集利用这些地址可以更有效地发现集群成员;
- 分片集利用这些地址可以更有效地分散负载;
- 连接字符串中尽可能使用与复制集内部配置相同的域名或 IP;
使用域名连接集群
在配置集群时使用域名可以为集群变更时提供一层额外的保护。例如需要将集群整体 迁移到新网段,直接修改域名解析即可。 另外,MongoDB 提供的 mongodb+srv:// 协议可以提供额外一层的保护。该协议允 许通过域名解析得到所有 mongos 或节点的地址,而不是写在连接字符串中。
mongodb+srv://server.example.com/ Record TTL Class Priority Weight Port Target _mongodb._tcp.server.example.com. 86400 IN SRV 0 5 27317 mongodb1.example.com. _mongodb._tcp.server.example.com. 86400 IN SRV 0 5 27017 mongodb2.example.com.
不使用负载均衡
- 基于前面提到的原因,驱动已经知晓在不同的 mongos 之间实现负载均衡,而复制集 则需要根据节点的角色来选择发送请求的目标。如果在 mongos 或复制集上层部署负 载均衡:
- 驱动会无法探测具体哪个节点存活,从而无法完成自动故障恢复;
- 驱动会无法判断游标是在哪个节点创建的,从而遍历游标时出错;
结论:不要在 mongos 或复制集上层放置负载均衡器,让驱动处理负载均衡和自动故 障恢复。
游标使用
如果一个游标已经遍历完,则会自动关闭;如果没有遍历完,则需要手动调用 close() 方法,否则该游标将在服务器上存在 10 分钟(默认值)后超时释放,造成不必要的资 源浪费。 但是,如果不能遍历完一个游标,通常意味着查询条件太宽泛,更应该考虑的问题是 如何将条件收紧。
关于查询及索引
- 每一个查询都必须要有对应的索引
- 尽量使用覆盖索引 Covered Indexes (可以避免读数据文件)
- 使用 projection 来减少返回到客户端的的文档的内容(减少网络传输压力)
关于写入
- 在 update 语句里只包括需要更新的字段 (减少网络带宽)
- 尽可能使用批量插入来提升写入性能
- 使用TTL自动过期日志类型的数据
关于文档结构
- 防止使用太长的字段名(浪费空间)
- 防止使用太深的数组嵌套(超过2层操作比较复杂)
- 不使用中文,标点符号等非拉丁字母作为字段名
处理分页问题
– 避免使用 count
尽可能不要计算总页数,特别是数据量大和查询条件不能完整命中索引时。 考虑以下场景:假设集合总共有 1000w 条数据,在没有索引的情况下考虑以下查询: db.coll.find({x: 100}).limit(50); db.coll.count({x: 100});
- 前者只需要遍历前 n 条,直到找到 50 条队伍 x=100 的文档即可结束;
- 后者需要遍历完 1000w 条找到所有符合要求的文档才能得到结果。
为了计算总页数而进行的 count() 往往是拖慢页面整体加载速度的原因
– 巧分页
避免使用skip/limit形式的分页,特别是数据量大的时候; 替代方案:使用查询条件+唯一排序条件; 例如:
第一页:db.posts.find({}).sort({_id: 1}).limit(20);
第二页:db.posts.find({_id: {$gt: <第一页最后一个_id>}}).sort({_id: 1}).limit(20);
第三页:db.posts.find({_id: {$gt: <第二页最后一个_id>}}).sort({_id: 1}).limit(20);
关于事务
使用事务的原则:
- 无论何时,事务的使用总是能避免则避免;
- 模型设计先于事务,尽可能用模型设计规避事务;
- 不要使用过大的事务(尽量控制在 1000 个文档更新以内);
- 当必须使用事务时,尽可能让涉及事务的文档分布在同一个分片上,这 将有效地提高效率;
五,分片集群
简介
MongoDB 常见部署架构
为什么要使用分片集群?
- 数据容量日益增大,访问性能日渐降低,怎么破?
- 新品上线异常火爆,如何支撑更多的并发用户?
- 单库已有 10TB 数据,恢复需要1-2天,如何加速?
- 地理分布数据
完整的分片集群
路由节点 mongos
提供集群单一入口 转发应用端请求 选择合适数据节点进行读写 合并多个数据节点的返回 无状态 建议至少2个
配置节点 mongod
配置(目录)节点 提供集群元数据存储 分片数据分布的映射
普通复制集架构
Lower | Uppwer | Shard |
---|---|---|
0 | 1000 | Shard0 |
1001 | 2000 | Shard1 |
数据节点 mongod
以复制集为单位 横向扩展 最大1024分片 分片之间数据不重复 所有分片在一起才可 完整工作
分片集群特点
- 应用全透明,无特殊处理
- 数据自动均衡 • 动态扩容,无须下线
- 提供三种分片方式
数据分布方式
基于范围
基于哈希
自定义Zone
集群设计
分片大小
分片的基本标准:
- 关于数据:数据量不超过3TB,尽可能保持在2TB一个片;
- 关于索引:常用索引必须容纳进内存;
按照以上标准初步确定分片后,还需要考虑业务压力,随着压力增大,CPU、RAM、 磁盘中的任何一项出现瓶颈时,都可以通过添加更多分片来解决。
需要多少个分片?
分片概念
- 片键 shard key:文档中的一个字段
- 文档 doc :包含 shard key 的一行数据
- 块 Chunk :包含 n 个文档
- 分片 Shard:包含 n 个 chunk
- 集群 Cluster: 包含 n 个分片
合适片键
影响片键效率的主要因素:
- 取值基数(Cardinality);
- 取值分布; • 分散写,集中读;
- 被尽可能多的业务场景用到;
- 避免单调递增或递减的片键;
选择基数大的片键
对于小基数的片键:
- 因为备选值有限,那么块的总数量就有限;
- 随着数据增多,块的大小会越来越大;
- 水平扩展时移动块会非常困难;
例如:存储一个高中的师生数据,以年龄(假设年龄范围为15~65岁)作为片键, 那么:
15<=年龄<=65,且只为整数
最多只会有51个 chunk
选择分布均匀的片键
对于分布不均匀的片键
- 造成某些块的数据量急剧增大
- 这些块压力随之增大
- 数据均衡以 chunk 为单位,所以系统无能为力
例如:存储一个学校的师生数据,以年龄(假设年龄范围为15~65岁)作为片键, 那么:
15<=年龄<=65,且只为整数
大部分人的年龄范围为15~18岁(学生)
15、16、17、18四个 chunk 的数据量、访问压力远大于其他 chunk
例子
一个 email 系统的片键例子
1 | { |
片键: { _id: 1}
片键: { _id: ”hashed”}
片键: { user_id: 1
片键: { user_id: 1, time:1 }
足够的资源
- mongos 与 config 通常消耗很少的资源,可以选择低规格虚拟机;
- 资源的重点在于 shard 服务器:
- 需要足以容纳热数据索引的内存;
- 正确创建索引后 CPU 通常不会成为瓶颈,除非涉及非常多的计算;
- 磁盘尽量选用 SSD;
- 最后,实际测试是最好的检验,来看你的资源配置是否完备。
即使项目初期已经具备了足够的资源,仍然需要考虑在合适的时候扩展。建议监控 各项资源使用情况,无论哪一项达到60%以上,则开始考虑扩展,因为:
- 扩展需要新的资源,申请新资源需要时间;
- 扩展后数据需要均衡,均衡需要时间。应保证新数据入库速度慢于均衡速度
- 均衡需要资源,如果资源即将或已经耗尽,均衡也是会很低效的。
搭建及扩容
以下是记录多机版的分片集群,单机版的分片集群可参考: https://blog.csdn.net/one2threexm/article/details/117822683
目标及流程
- 目标:学习如何搭建一个2分片的分片集群
- 环境:3台 Linux 虚拟机, 4 Core 8 GB
步骤:
- 配置域名解析
- 准备分片目录
- 创建第一个分片复制集并初始化
- 创建 config 复制集并初始化
- 初始化分片集群,加入第一个分片 创建分片表 加入第二个分片
实验架构
注意
注意本次实验皆在一台虚拟机上完成,使用docker部署,节点之间使用ip访问,由于mongo5.0
以上的不在支持节点之间使用ip访问,理由:
为避免由于 IP 地址更改而导致配置更新,请使用 DNS 主机名而不是 IP 地址。在配置副本集成员或分片集群成员时,使用 DNS 主机名而不是 IP 地址尤为重要。
使用主机名而不是 IP 地址来配置跨网络分割的集群。从 MongDB 5.0 开始,仅配置了 IP 地址的节点将无法启动验证并且不会启动。
文档: https://www.mongodb.com/docs/manual/tutorial/deploy-shard-cluster/
所以mongo的镜像请选择5.0以下
花了两个晚上才发现的坑
我发现我的docker启动mongo是总是起不起来,并且不打印任何日志,搞得我非常困惑,不打印日志就不知道哪里出来问题,只能是一个一个排除法,
首先正常不加任何参数的启动 —— 正常
然后是配置文件,我从之前的复制集中复制了配置文件启动——启动不了
但是对比配置文件,完全一样,但是就是起不起来,奇怪的很。
然后是参数等一个一个试还是起不起来。
最后的最后,对比了一下mongodb.keyfile
首先里面的内容是完全一致的,但是我突然注意到文件的用户组和修改时间不一样,普通文件的修改时间不一样没关系但是对于密钥文件就不一样了,密钥的生成涉及到时间,所以在复制密钥匙一定要使用命令:
cp -avxp
-p 保留原有文件的属性
前置准备
创建mongo文件夹
mkdir mongo && cd mongo
pwd
/root/mongo
生成ssh密钥
# 生成keyfile文件,keyfile文件只生成一次 拷贝到每个节点上使用
openssl rand -base64 736 > mongodb.keyfile
# mongodb.keyfile必须授权600和999,否则后续启动不了MongoDB容器
# 给m1的mongodb.keyfile文件授权
chmod 600 mongodb.keyfile
# 修改文件拥有者和组
chown 999 mongodb.keyfile
拉取镜像
docker pull mongo:4.4.14
mongod
创建第一个mongod复制集 —— shard1
注意第一个mongod复制集端口分别为27010
、27020
、27030
创建文件夹,配置配置文件
mkdir shard1 && vim shard1/shard1.conf
# 将密钥复制至shard1里面
cp -avxp mongodb.keyfile shard1/mongodb.keyfile
1 | storage: |
启动命令
docker run -v /root/mongo/shard1:/etc/mongo -p 27010:27017 --name mongo1-1 -d mongo:4.4.14 --config /etc/mongo/mongod.conf --shardsvr --wiredTigerCacheSizeGB 0.3
docker run -v /root/mongo/shard1:/etc/mongo -p 27020:27017 --name mongo1-2 -d mongo:4.4.14 --config /etc/mongo/mongod.conf --shardsvr --wiredTigerCacheSizeGB 0.3
docker run -v /root/mongo/shard1:/etc/mongo -p 27030:27017 --name mongo1-3 -d mongo:4.4.14 --config /etc/mongo/mongod.conf --shardsvr --wiredTigerCacheSizeGB 0.3
参数:
-
–shardsvr
指名分片角色 ,https://www.mongodb.com/docs/v4.4/release-notes/3.4-compatibility
-
–wiredTigerCacheSizeGB
缓存大小 测试开的小一点
分别启动mongo1-1
、mongo1-2
、mongo1-3
,地址是分别
172.17.0.2
、172.17.0.3
、172.17.0.4
进入mongo1-1
容器
docker exec -it mongo1-1 /bin/bash
mongo admin
# 注意端口对于容器之间而言是27107
myconf = {“_id”:“shard1”,“members”:[{“_id”:0,“host”:“172.17.0.2:27017”},{“_id”:1,“host”:“172.17.0.3:27017”},{“_id”:2,“host”:“172.17.0.4:27017”}]}
rs.initiate(myconf)
# 查看那台服务变成了PRIMARY
rs.status()
mongo-config
创建mongo配置服务
创建文件夹和复制密钥文件以及配置文件
mkdir config && cp -avxp shard1/mongodb.keyfile config/mongodb.keyfile
vim mongod.conf
1 | storage: |
配置基本相同,就改了复制集的名字
启动docker容器
docker run -v /root/mongo/config:/etc/mongo -p 27018:27017 --name mongo-conf-1 -d mongo:4.4.14 --config /etc/mongo/mongod.conf --configsvr --wiredTigerCacheSizeGB 0.3
docker run -v /root/mongo/config:/etc/mongo -p 27028:27017 --name mongo-conf-2 -d mongo:4.4.14 --config /etc/mongo/mongod.conf --configsvr --wiredTigerCacheSizeGB 0.3
docker run -v /root/mongo/config:/etc/mongo -p 27038:27017 --name mongo-conf-3 -d mongo:4.4.14 --config /etc/mongo/mongod.conf --configsvr --wiredTigerCacheSizeGB 0.3
参数:
-
–configsvr
指名分片角色 ,https://www.mongodb.com/docs/v4.4/release-notes/3.4-compatibility
-
–wiredTigerCacheSizeGB
缓存大小 测试开的小一点
分别启动mongo-conf-1
、mongo-conf-2
、mongo-conf-3
,地址是分别
172.17.0.5
、172.17.0.6
、172.17.0.7
进入mongo-conf-1
容器
docker exec -it mongo-conf-1 /bin/bash
mongo admin
# 注意端口对于容器之间而言是27107
myconf = {“_id”:“config”,“members”:[{“_id”:0,“host”:“172.17.0.5:27017”},{“_id”:1,“host”:“172.17.0.6:27017”},{“_id”:2,“host”:“172.17.0.7:27017”}]}
rs.initiate(myconf)
# 查看那台服务变成了PRIMARY
rs.status()
mongos
复制配置文件
cp -avxp config/ mongos/
修改配置文件
1 | net: |
**注意:**mongos的配置文件中需要指定配置服务地址
运行命令
docker run -v /root/mongo/mongos:/etc/mongo -p 27017:27017 --name mongos-1 -d mongo:4.4.14 mongos --config /etc/mongo/mongod.conf
mongos作为路由不用搭建复制集,但是可以多起几台,保证高可用
添加Shard
#加入mongoshll
mongo
#添加第一个复制集
sh.addShard(“shard1/172.17.0.2:27017,172.17.0.3:27017,172.17.0.4:27017”)
#创建用户
use admin
db.createUser({user: “root”, pwd: “XXXX”, roles: [“root”]})
sh.status()
分片
#开启分片数据库
sh.enableSharding(“foo”)
#开启分片集合以及分键的策略
sh.shardCollection(“foo.bar”, {_id: “hashed”})
#查看状态
sh.status()
如果要测试分片效果的话可以再添加一个复制集,
插入测试数据
1 | for(var i = 0; i < 100000; i++) { |
查看数据发布状态
db.bar.getShardDistribution()
六,监控
serverStatus
serverStatus() 主要信息
-
connections: 关于连接数的信息;
-
locks: 关于 MongoDB 使用的锁情况;
-
network: 网络使用情况统计;
-
opcounters: CRUD 的执行次数统计;
-
repl: 复制集配置信息;
-
wiredTiger: 包含大量 WirdTiger 执行情况的信息:
- block-manager: WT 数据块的读写情况;
- session: session 使用数量;
- concurrentTransactions: Ticket 使用情况;
-
mem: 内存使用情况;
-
metrics: 一系列性能指标统计信息;
-
更多指标介绍请参考:serverStatus
建议监控指标
指标 | 意义 | 获取 |
---|---|---|
opcounters(操作计数器) | 查询、更新、插入、删除、getmore 和其 他命令的的数量 | db.serverStatus().opcounters |
tickets(令牌) | 对 WiredTiger 存储引擎的读/写令牌数量。令牌数量表示了可以进入存储引擎的并 发操作数量。 | db.serverStatus().wiredTiger.c oncurrentTransactions |
replication lag(复制 延迟) | 这个指标代表了写操作到达从结点所需要 的最小时间。过高的 replication lag 会 减小从结点的价值并且不利于配置了写关 注 w>1 的那些操作。 | db.adminCommand({‘replSet GetStatus’: 1) |
oplog window (复制时间窗) | 这个指标代表oplog可以容纳多长时间的写 操作。它表示了一个从结点可以离线多长时 间仍能够追上主节点。通常建议该值应大于 24小时为佳。 | db.oplog.rs.find().sort({$natura l: -1}).limit(1).next().ts - db.oplog.rs.find().sort({$natura l: 1}).limit(1).next().ts |
connections(连接数) | 连接数应作为监控指标的一部分,因为每个 连接都将消耗资源。应该计算低峰/正常/高 峰时间的连接数,并制定合理的报警阈值范 围。 | db.serverStatus().connections |
Query targeting (查询专注度) | 索引键/文档扫描数量比返回的文档数量, 按秒平均。如果该值比较高表示查询系需要 进行很多低效的扫描来满足查询。这个情况 通常代表了索引不当或缺少索引来支持查询 。 | var status = db.serverStatus() status.metrics.queryExecutor.scanned / status.metrics.document.returned status.metrics.queryExecutor.scannedO bjects / status.metrics.document.returned |
Scan and Order(扫描 和排序) | 每秒内内存排序操作所占的平均比例。内存 排序可能会十分昂贵,因为它们通常要求缓 冲大量数据。如果有适当索引的情况下,内 存排序是可以避免的。 | var status = db.serverStatus() status.metrics.operation.scanAndOrder / status.opcounters.query |
节点状态 | 每个节点的运行状态。如果节点状态不是 PRIMARY、SECONDARY、ARBITER 中的 一个,或无法执行上述命令则报警 | db.runCommand(“isMaster”) |
dataSize(数据大小) | 整个实例数据总量(压缩前) | 每个 DB 执行 db.stats(); |
StorageSize(磁盘空间 大小) | 已使用的磁盘空间占总空间的百分比。 |
七,备份和恢复
为何备份
- 防止硬件故障引起的数据丢失
- 防止人为错误误删数据
- 时间回溯
- 监管要求
备份机制
- 延迟节点备份
- 全量备份 + Oplog 增量
方案一:延迟节点备份
安全范围内的任意时间点状态 = 延迟从节点当前状态 + 定量重放 oplog
注意:
主节点的 oplog 时间窗t应满足:t >= 延迟时间 + 48小时
留给自己48小时的处理时间,避免oplog丢弃一些日志
方案二:全量备份加 oplog
- 最近的 oplog 已经在 oplog.rs 集合中,因此可以在定期从集合中导出便得到了 oplog;
- 如果主节点上的 oplog.rs 集合足够大,全量备份足够密集,自然也可以不用备份 oplog;
- 只要有覆盖整个时间段的 oplog,就可以结合全量备份得到任意时间点的备份。
注意事项
复制数据库文件
- 必须先关闭节点才能复制,否则复制到的文件无效;
- 也可以选择 db.fsyncLock() 锁定节点,但完成后不要忘记 db.fsyncUnlock() 解锁;
- 可以且应该在从节点上完成;
- 该方法实际上会暂时宕机一个从节点,所以整个过程中应注意投票节点总数。
文件系统快照
- MongoDB 支持使用文件系统快照直接获取数据文件在某一时刻的镜像;
- 照过程中可以不用停机;
- 数据文件和 Journal 必须在同一个卷上;
- 快照完成后请尽快复制文件并删除快照;
Mongodump
- 使用 mongodump 备份最灵活,但速度上也是最慢的;
- mongodump 出来的数据不能表示某个个时间点,只是某个时间段(边dump边写)
幂等性
实操
dump
https://www.mongodb.com/docs/database-tools/mongodump/#examples
mongodump --oplog --username=root
注意–oplog只在复制机上有效果,单体没有这个命令,会报错!
#Failed: error getting oplog start: error getting recent oplog entry: mongo: no documents in result
restore
https://www.mongodb.com/docs/database-tools/mongorestore/
mongorestore --oplogReplay --username=root dump
复杂的重放 oplog
假设全量备份已经恢复到数据库中(无论使用快照、mongodump 或复制数据文件的方式),要重放一部分增量怎么办?
-
导出主节点上的 oplog:
- mongodump --host 127.0.0.1 -d local -c oplog.rs
- 可以通过—query 参数添加时间范围
-
使用 bsondump 查看导出的 oplog,找到需要截止的时间点:
{ “ts” : Timestamp(1577355175, 1), “t” : NumberLong(23), “h” : NumberLong(0), “v” : 2, “op” : “c”, “ns” : “foo.$cmd”, “ui” : UUID(“767b3a2b-a1cd-4db8-a74a-71ce9711f368”), “o2” : { “numRecords” : 1 }, “wall” : ISODate(“2019-12-26T10:12:55.436Z”), “o” : { “drop” : “employees” } }
-
恢复到指定时间点
- 利用–oplogLimit指定恢复到这条记录之前
- mongorestore -h 127.0.0.1 --oplogLimit “1577355175:1” --oplogFile dump/local/oplog.rs <空文件夹>
-
bsondump手册: https://www.mongodb.com/docs/database-tools/bsondump/
八,索引机制
大部分直接看文档吧: https://www.mongodb.com/docs/manual/indexes/
$explain
文档: https://www.mongodb.com/docs/v5.0/reference/explain-results/#mongodb-data-explain.executionStats
1 | { explainVersion: '1', |
stage
是对操作的描述;例如
COLLSCAN
收集扫描IXSCAN
用于扫描索引键FETCH
用于检索文档SHARD_MERGE
用于合并分片的结果SHARDING_FILTER
用于从分片中过滤掉孤立文档
索引执行计划
queryPlanner
queryPlanner
信息详细说明了查询优化器选择的计划。
1 | "queryPlanner" : { |
索引执行计划的选择
索引执行情况
executionStats
获胜计划的执行情况
1 | "executionStats" : { |
重要指标
- stage
- 执行阶段
- nReturned
- 返回结果数量
- executionTimeMillisEstimate
- 执行时间
- totalKeysExamined
- 检查的索引键数量
- totalDocsExamined
- 检查的文档数量
索引类型
- 单键索引
- 组合索引
- 多值索引
- 地理位置索引
- 全文索引
- TTL索引
- 部分索引
- 哈希索引
组合索引-Compound Index
**ESR原则: **https://www.mongodb.com/docs/manual/tutorial/equality-sort-range-rule
简单来说:
E: 建立复合索引时,第一个应当是精确匹配的字段,这样就可以排除大量的数据
S:需要查询排序的字段排在E字段后面,如果不建立该索引,将会在内存当中排序(相当昂贵)
R:需要返回范围查询的字段放在最后,避免找到大量索引数据,徒增耗时
建立索引的准则就是尽量的就将基数大的字段排前面,减少检索的数据量
其他索引就不记录了,再记也没文档详细全面
九、性能机制
应用端
一次数据库请求过程中发生了什么?
选择节点
对于复制集读操作,选择哪个节点是由 readPreference(读取偏好)决定的:
- primary/primaryPreferred
- secondary/secondaryPreferred
- nearest
如果不希望一个远距离节点被选择,应做到以下 之一:
- 将它设置为隐藏节点;
- 通过标签(Tag)控制可选的节点;
- 使用 nearest 方式;
排队等待
排队等待连接是如何发生的?
总连接数大于允许的最大连接数maxPoolSize;
如何解决这个问题?
- 加大最大连接数(不一定有用,本质上还是查的不够快,cpu和缓存资源就那么多,盲目的加连接数没用)
- 优化查询性能
连接与认证
如果一个请求需要等待创建新连接和进行认证,相比直接从连接池获取连接,它将 耗费更长时间。
- 设置 minPoolSize(最小连接数)一次性创建足够的连接; •
- 避免突发的大量请求;
数据库端
排队等待
由 ticket 不足引起的排队等待,问题往往不在 ticket 本身,而在于为什么正在执行 的操作会长时间占用 ticket。
ticket
像令牌一样,想要查询先拿令牌,避免无脑查询压垮服务
- 优化 CRUD 性能可以减少 ticket 占用时间;
- zlib 压缩方式也可能引起 ticket 不足,因为 zlib 算法本身在进行压缩、解压时需要的时 间比较长,从而造成长时间的 ticket 占用;
执行请求(读)
不能命中索引的搜索和内存排序是导致性能问题的最主要原因
执行请求(写)
journal日志
是将写入缓存的数据刷入磁盘持久化
磁盘速度必须比写入速度要快才能保持缓存水位
合并结果
在mongos当中查询没有片键的数据时需要合并查询结果
- 如果顺序不重要则不要排序
- 尽可能使用带片键的查询条件以减少参与查询的分片数
网络的考量
网络访问要快,尽量内网啦
性能瓶颈总结
应用端 | 服务端 | 网络 |
---|---|---|
选择访问入口节点 | 排队等待ticket | 应用/驱动 - mongos |
等待数据库连接 | 执行请求 | mongos - 片 |
创建连接和完成认证 | 合并执行结果 |
性能排查工具
mongostat
展示实例状态的快速概览
文档: https://www.mongodb.com/docs/database-tools/mongostat
mongostat --username=xxx --password=‘xxx’ --authenticationDatabase=admin
-
dirty
journal待刷入磁盘的数据量,不宜超过20%,一旦宕机内存数据就消失了
-
used
将文档从磁盘抓取至内存的数据量,mongo所占的内存大概是系统内存的60%,超过95%后就要开始回收内存了,使用LUR最近最少使用
-
qrw
等待排队请求的数量
-
conn
连接数
mongotop
提供每个集合级别的统计信息
文档: https://www.mongodb.com/docs/database-tools/mongotop/
mongotop 7 --username=xxx–password=‘xxx’ --authenticationDatabase=admin
数字表示每7秒刷新一次
展示了每个集合的读取和写入时间,方便排查哪个集合有问题
mongod 日志
进入mongoshll
查看当前库的慢查询状态
db.getProfilingStatus()
1 | { "was" : 0, "slowms" : 100, "sampleRate" : 1 } |
描述: https://www.mongodb.com/docs/v5.0/reference/command/profile/#mongodb-dbcommand-dbcmd.profile
-
was
0 不采集
1 采集执行时间超过slowms
3 采集所有
-
slowms
默认值:100,单位毫秒,运行时间超过此阈值的操作被认为是缓慢的。
设置慢查询级别和参数
db.setProfilingLevel(1,1000)
文档: https://www.mongodb.com/docs/v5.0/reference/method/db.setProfilingLeve
查看日志
db.system.profile.find()
慢慢分析吧
mtools
十、上线及升级
性能测试
模拟真实压力,对集群完成充分的性能测试,了解集群概况
性能测试的输出:
- 压测过程中各项指标表现,例如 CRUD 达到多少,连接数达到多少等。
- 根据正常指标范围配置监控阈值;
- 根据压测结果按需调整硬件资源;
环境检查
按照最佳实践要求对生产环境所使用的操作系统进行检查和调整。最常见的需要调 整的参数包括:
- 禁用 NUMA,否则在某些情况下会引起突发大量swap交换;
- 禁用 Transparent Huge Page,否则会影响数据库效率;
- tcp_keepalive_time 调整为120秒,避免一些网络问题;
- ulimit -n,避免打开文件句柄不足的情况;
- 关闭 atime,提高数据文件访问效率;
更多检查项,请参考文档:https://www.mongodb.com/docs/manual/administration/production-notes/
主版本升级流程
单机升级流程
复制集升级流程
分片集群升级流程
升级过程中虽然会发生主从节点切换,存在短时间不可用,但是:
- 3.6版本开始支持自动写重试可以自动恢复主从切换引起的集群暂时不可写;
- 4.2开始支持的自动读重试则提供了包括主从切换在内的读问题的自动恢复;
升级需要逐版本完成,不可以跳版本:
- 正确:3.2->3.4->3.6->4.0->4.2
- 错误:3.2->4.2
- 原因:
- MongoDB复制集仅仅允许相邻版本共存
- 有一些升级内部数据格式如密码加密字段,需要在升级过程中由mongo进行转换
十一、数据迁移
方案
如何迁移已有数据到 MongoDB?
导出导入
- 停止现有的基于 RDBMS 的应用
- 使用 RDBMS 的数据库导出工具,将数据库表导出到 CSV 或者 JSON(如 mysqldump)
- 使用 mongoimport 将 CSV 或者 JSON 文件导入 MongoDB 数据库
- 启动新的 MongoDB 应用
备注:
- 适用于一次性数据迁移
- 需要应用/数据库下线,较长的下线时间
命令
mysqldump
-u root -p xxl_job -T /var/lib/dump/
如果出现The MySQL server is running with the --secure-file-priv option so it cannot execute this statement when executing 'SELECT INTO OUTFILE'
异常请修改my.cnf
配置文件
参考:
docker cp mysql8:/etc/mysql/my.cnf /environment/docker/mysql8/conf/my.cnf
vim /environment/docker/mysql8/conf/my.cnf
1
2
3 ....
/var/lib/dump =
...docker cp /environment/docker/mysql8/conf/my.cnf mysql8:/etc/mysql/my.cnf
docker restart mysql8
将csv复制到mongo去
mkdir -p /environment/docker/mongo/data/csv/
docker cp mysql8:/var/lib/dump/ /environment/docker/mongo/data/csv/
docker cp /environment/docker/mongo/data/csv/dump mongo:/data/mysql/dump
导入
mongoimport --username=XXX–password=XXX --authenticationDatabase=admin -d xxl_job -c xxl_job_group --type=csv --headerline xxl_job_group.txt
TapData
旗下的TapdataCloud支持异构数据库迁移,国内产品文档特别详细直接参考文档