0%

mongo学习笔记

mongo-db笔记

[TOC]

一,复制集

复制集简介

Mongodb复制集由一组Mongod实例(进程)组成,包含一个Primary节点和多个Secondary节点,Mongodb Driver(客户端)的所有数据都写入Primary,Secondary从Primary同步写入的数据,以保持复制集内所有成员存储相同的数据集,提供数据的高可用。

下图(图片源于Mongodb官方文档)是一个典型的Mongdb复制集,包含一个Primary节点和2个Secondary节点。

img

Primary选举

复制集通过replSetInitiate命令(或mongo shell的rs.initiate())进行初始化,初始化后各个成员间开始发送心跳消息,并发起Priamry选举操作,获得『大多数』成员投票支持的节点,会成为Primary,其余节点成为Secondary。

初始化复制集

1
2
3
4
5
6
7
8
9
10
config = {
_id : "my_replica_set",
members : [
{_id : 0, host : "rs1.example.net:27017"},
{_id : 1, host : "rs2.example.net:27017"},
{_id : 2, host : "rs3.example.net:27017"},
]
}

rs.initiate(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
2
3
4
5
6
7
8
{
"ts" : Timestamp(1446011584, 2),
"h" : NumberLong("1687359108795812092"),
"v" : 2,
"op" : "i",
"ns" : "test.nosql",
"o" : { "_id" : ObjectId("563062c0b085733f34ab4129"), "name" : "mongodb", "score" : "100" }
}

上述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过程包含如下步骤

  1. T1时间,从Primary同步所有数据库的数据(local除外),通过listDatabases + listCollections + cloneCollection敏命令组合完成,假设T2时间完成所有操作。
  2. 从Primary应用[T1-T2]时间段内的所有oplog,可能部分操作已经包含在步骤1,但由于oplog的幂等性,可重复应用。
  3. 根据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
2
3
cfg = rs.conf();
cfg.members[1].priority = 2;
rs.reconfig(cfg);

细说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
2
3
4
db.products.insert(
{ item: "envelopes", qty : 100, type: "Clasp" },
{ writeConcern: { w: majority, wtimeout: 5000 } }
)

上面的设置方式是针对单个请求的,也可以修改副本集默认的write concern,这样就不用每个请求单独设置。

1
2
3
4
cfg = rs.conf()
cfg.settings = {}
cfg.settings.getLastErrorDefaults = { w: "majority", wtimeout: 5000 }
rs.reconfig(cfg)

异常处理(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

  1. 创建对应mongo文件夹

    mkdir mongo

    mkdir mongo/conf

  2. 进入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
    32
    storage:
    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

  3. 进入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

  4. 准备镜像

    # 搜索mongo镜像

    docker search mongo

    # 拉取镜像

    docker pull mongo:latest

    # 查看镜像

    docker images

  5. 启动镜像

    如果想将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

  6. 记录其他mongoIP地址

    这里我们将mongo1作为主节点,其他作为从节点

    # 查看容器详情

    docker inspect mongo2

    ip为字段"IPAddress"

    image-20220505143856596

    这里我的ip信息为

    mongo1: 172.18.0.8

    mongo2: 172.18.0.9

    mongo3: 172.18.0.10

  7. 进入mongo1容器

    # 进入容器

    docker exec -it mongo1 /bin/bash

  8. 进入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()

  9. 创建用户

    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 //显示库表示用户创建成功

  10. 测试复制集是否搭建成功

    复制集之间的用户共享,也就是主节点创建用户同步至从节点

    # 插入数据

    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
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
 {

sensor_id: 12345,

timestamp: ISODate("2019-01-31T10:00:00.000Z"),

temperature: 40

}

{

sensor_id: 12345,

timestamp: ISODate("2019-01-31T10:01:00.000Z"),

temperature: 40

}

{

sensor_id: 12345,

timestamp: ISODate("2019-01-31T10:02:00.000Z"),

temperature: 41

}

复制

改进后的文档集如下

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
32
33
34
35
36
37
38
39
{

sensor_id: 12345,

start_date: ISODate("2019-01-31T10:00:00.000Z"),

end_date: ISODate("2019-01-31T10:59:59.000Z"),

measurements: [

{

timestamp: ISODate("2019-01-31T10:00:00.000Z"),

temperature: 40

},

{

timestamp: ISODate("2019-01-31T10:01:00.000Z"),

temperature: 40

},


{

timestamp: ISODate("2019-01-31T10:42:00.000Z"),

temperature: 42

}

],
transaction_count: 42,
sum_temperature: 2413
}

复制

我们在程序写入文档时,可以做一些简单的计算和整理,按时间分段,根据业务需要,将一个时间断内的大量文档合并,避免数据使用时的随机聚合和查询。这样的时间段,可以理解为桶。

在处理时间序列数据时,知道2018年7月13日加利福尼亚州康宁市下午2:00至3:00的平均温度通常比知道下午2:03那一时刻的温度更有意义也更重要。通过用桶组织数据并进行预聚合,我们可以更轻松地提供这些信息。

2,行转列

问题

大文档,很多字段,很多索引

1
2
3
4
5
6
7
{
title: "Dunkirk",
...
release_USA: "2017/07/23",
release_UK: "2017/08/23",
...
}

以上方式记录数据,假如需要查询一些国家的上映日期就需要建立很多索引。

1
2
{release_USA: 1}
{release_UK: 1}

解决

1
2
3
4
5
6
7
8
9
{
title: "Dunkirk",
...
releases: [
{country: "USA", date: "2017/07/23"},
{country: "UK", date: "2017/08/23"},
],
...
}

db.movies.createIndex({“releases.country”: 1, “releases.date”: 1})

通过建立country和date的联合索引减少了建立大量索引的问题。

小结

image-20220508213114055

3,版本号

问题

mongodb的文档模型过于灵活,常常会导致文档之间的字段属性差距过大

2019.01版本1.0

1
2
3
4
{
id: 1,
name: "张三"
}

2019.03版本2.0

1
2
3
4
5
6
{
id: 1,
name: "张三",
age: 67,
address: "悟道龙场"
}

解决

新增版本号

1
2
3
4
5
6
7
{
id: 1,
name: "张三",
age: 67,
address: "悟道龙场",
schema_version: "v2.0"
}

通过新增版本号的方式区分,同一模型文档之间字段属性不一致的问题

还可以新增Validation规则限制文档灵活变更

小结

image-20220508213942493

4,近似计算

问题

统计网站点击流量

image-20220508214139165

可以看到这个需求大部分操作在统计网站的计数统计,数据库写入操作将在统计结束有写入

同时网站的计数不需要特别精准

解决

每隔一段时间去写入数据库,减少写入的压力

image-20220508214436216

小结

image-20220508214459732

5,预聚合字段

问题

业绩排名,游戏排名,商品统计等精准统计

这种问题就没办法使用近似计算,需要精准统计

如果使用mongo的聚合计算来完成,但是mongo的聚合计算是一个扫描型的操作,比较消耗系统资源并且时间较长容易拖垮服务

解决

1
2
3
4
5
6
7
8
{
product: "Bike",
sku: "abc123456",
quantitiy: 2039,
daily_sales: 40,
weekly_sales: 302,
minthly_sales: 1419
}

使用预聚合而非全文档更新

1
2
3
4
5
6
7
8
9
10
db.inventory.update({_id: 123},
{
$inc: {
quantitiy: -1,
daily_sales: 1,
weekly_sales: 1,
minthly_sales: 1
}
}
)

小结

image-20220508215440554

三,事务

写操作事务

什么是 writeConcern ?

writeConcern 决定一个写操作落到多少个节点上才算成功。writeConcern 的取值包括:

• 0:发起写操作,不关心是否成功;

• 1~集群最大数据节点数:写操作需要被复制到指定节点数才算成功;

• majority:写操作需要被复制到大多数节点上才算成功。 发起写操作的程序将阻塞到写操作到达指定的节点数为止

默认行为

primary写入成功立刻返回。

对于重要的数据默认行为比较危险,当写入primary后,primary立马宕机其secondary节点还未同步数据,那么就会发生数据丢失问题

当然又不是真的丢失了,primary会将数据存储在Rollback文件中

3 节点复制集不作任何特别设定(默认值):

image-20220508224018102

w: “majority”

大多数节点确认模式

image-20220508224044809

w: “all”

全部节点确认模式

该模式存在一些缺陷,当有个节点宕机或者网络分区延迟较长,会导致迟迟无法写入成功,但是其实数据是已经写入primary节点了,所以需要特别注意下

image-20220508224104241

j:true

writeConcern 可以决定写操作到达多少个节点才算成功,journal 则定义如何才算成 功。取值包括:

• true: 写操作落到 journal 文件中才算成功;

• false: 写操作到达内存即算作成功。

journal

journal日志是数据库的crash recovery手段。通常的做法是把数据库内的数据块修改,提前用文件顺序写方式刷到盘上,然后再去真正的提交数据的修改。这样的目的是在服务器宕机的时候,内存中被丢失的数据可以在恢复过程中从journal 日志文件中读回来。

Oplog

Oplog也是记录的数据库的操作日志,但是记的是逻辑操作命令。主要的目的是用于节点之间复制数据,而不是上面journal主要是用来recover crash。

mongod.log

还有一种就是mongod.log,这个就是一个文本文件,记录数据库系统的正常运行和错误信息等等。

image-20220508224142603

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:选择最近的节点;

image-20220509212628967

readPreference 场景举例

  • 用户下订单后马上将用户转到订单详情页——primary/primaryPreferred。因为此 时从节点可能还没复制到新订单;
  • 用户查询自己下过的订单——secondary/secondaryPreferred。查询历史订单对 时效性通常没有太高要求;
  • 生成报表——secondary。报表对时效性要求不高,但资源需求大,可以在从节点 单独处理,避免对线上用户造成影响;
  • 将用户上传的图片分发到全世界,让各地用户能够就近读取——nearest。每个地区 的应用选择最近的节点读取数据。

readPreference 与 Tag

readPreference 只能控制使用一类节点。Tag 则可以将节点选择控制 到一个或几个节点。考虑以下场景:

  • 一个 5 个节点的复制集;
  • 3 个节点硬件较好,专用于服务线上客户;
  • 2 个节点硬件较差,专用于生成报表; 可以使用 Tag 来达到这样的控制目的:
  • 为 3 个较好的节点打上 {purpose: “online”};
  • 为 2 个较差的节点打上 {purpose: “analyse”};
  • 在线应用读取时指定 online,报表读取时指定 reporting。
image-20220509213020516

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);

image-20220510211317866

注意事项:

  • 虽然看上去总是应该选择 local,但毕竟对结果集进行过滤会造成额外消耗。在一些 无关紧要的场景(例如统计)下,也可以考虑 available;
  • MongoDB <=3.6 不支持对从节点使用 {readConcern: “local”};
  • 从主节点读取数据时默认 readConcern 是 local,从从节点读取数据时默认 readConcern 是 available(向前兼容原因)。

readConcern: majority

只读取大多数据节点上都提交了的数据。考虑如下场景:

  • 集合中原有文档 {x: 0};
  • 将x值更新为 1;

image-20220510211537718

如果在各节点上应用 {readConcern: “majority”} 来读取数据:

image-20220510211551009

一个读线程,使用 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;

image-20220510212016434

如何实现?

节点上维护多个 x 版本,MVCC 机制 MongoDB 通过维护多个快照来链接不同的版本:

  • 每个被大多数节点确认过的版本都将是一个快照;
  • 快照持续到没有人使用为止才被删除;

实验: readConcern : ”majority” vs “local”

  • 安装 3 节点复制集。

  • 注意配置文件内 server 参数 enableMajorityReadConcern

    配置文件路径: /etc/mongo/mongod.conf

    image-20220510215821233

    image-20220510215907405

  • 将复制集中的两个从节点使用 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: 如何实现安全的读写分离

考虑如下场景:

向主节点写入一条数据; 立即从从节点读取这条数据。 如何保证自己能够读到刚刚写入的数据?

image-20220510220020811

下述方式有可能读不到刚写入的订单

image-20220510220040522

使用 writeConcern + readConcern majority 来解决

image-20220510220054631

readConcern: linearizable

readConcern: majority不一定是完全安全的,比如以下情景

  • 旧主节点的网络分区失联
  • 一条数据更新至新主节点
  • 旧主节点就收到读取操作,此时旧主节点因为网络分区的缘故未同步到新的x变更,同时旧主节点任然认为x:1是被大多数节点承认的数据并返回(旧数据)

image-20220510220217117

只读取大多数节点确认过的数据。和 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
2
3
4
5
6
try (ClientSession clientSession = client.startSession()) {
clientSession.startTransaction();
collection.insertOne(clientSession, docOne);
collection.insertOne(clientSession, docTwo);
clientSession.commitTransaction();
}

事务的隔离级别

  • 事务完成前,事务外的操作对该事务所做的修改不可访问
  • 如果事务内使用 {readConcern: “snapshot”},则可以达到可重复读 Repeatable Read

实验:启用事务后的隔离性

隔离级别: read commit

image-20220510224854164

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

image-20220510225827092

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实验)。

实验:写冲突

image-20220510231646202

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)
})

实验:写冲突 (续)

image-20220510231816767

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;

image-20220518221618568

Change Stream 与可重复读

Change Stream 只推送已经在大多数节点上提交的变更操作。即“可重复读”的变更。 这个验证是通过 {readConcern: “majority”} 实现的。因此:

  • 未开启 majority readConcern 的集群无法使用 Change Stream;
  • 当集群无法满足 {w: “majority”} 时,不会触发 Change Stream(例如 PSA 架构 中的 S 因故障宕机)。

Change Stream 变更过滤

如果只对某些类型的变更事件感兴趣,可以使用使用聚合管道的过滤步骤过滤事件。 例如

1
2
3
4
5
6
7
var cs = db.collection.watch([{
$match: {
operationType: {
$in: ['insert', 'delete']
}
}
}])

示例

修改配置文件

在使用Change Stream 之前我们需要确保我们的复制集开启了MajorityReadConcern

查看配置文件 /etc/mongo/mongod.conf

image-20220518224426636

修改完成后重启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 时刻崩溃,重启后后续的变更怎么办?

image-20220518225436209

想要从上次中断的地方继续获取变更流,只需要保留上次 变更通知中的 _id 即可。 右侧所示是一次 Change Stream 回调所返回的数据。每 条这样的数据都带有一个 _id,这个 _id 可以用于断点恢 复。例如:

var cs = db.collection.watch([], {resumeAfter: <_id>})

即可从上一条通知中断处继续获取后续的变更通知。

image-20220518225605304

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 常见部署架构

image-20220523211105843

为什么要使用分片集群?

  • 数据容量日益增大,访问性能日渐降低,怎么破?
  • 新品上线异常火爆,如何支撑更多的并发用户?
  • 单库已有 10TB 数据,恢复需要1-2天,如何加速?
  • 地理分布数据

完整的分片集群

image-20220523211242363

路由节点 mongos

提供集群单一入口 转发应用端请求 选择合适数据节点进行读写 合并多个数据节点的返回 无状态 建议至少2个

image-20220523211348828

配置节点 mongod

配置(目录)节点 提供集群元数据存储 分片数据分布的映射

普通复制集架构

Lower Uppwer Shard
0 1000 Shard0
1001 2000 Shard1

image-20220523211539258

数据节点 mongod

以复制集为单位 横向扩展 最大1024分片 分片之间数据不重复 所有分片在一起才可 完整工作

image-20220523211616339

分片集群特点

  • 应用全透明,无特殊处理
  • 数据自动均衡 • 动态扩容,无须下线
  • 提供三种分片方式

数据分布方式

基于范围

image-20220523211728097

基于哈希

image-20220523211749355

自定义Zone

image-20220523211806775

集群设计

image-20220523211945476

分片大小

分片的基本标准:

  • 关于数据:数据量不超过3TB,尽可能保持在2TB一个片;
  • 关于索引:常用索引必须容纳进内存;

按照以上标准初步确定分片后,还需要考虑业务压力,随着压力增大,CPU、RAM、 磁盘中的任何一项出现瓶颈时,都可以通过添加更多分片来解决。

需要多少个分片?

image-20220523212049755

分片概念

  • 片键 shard key:文档中的一个字段
  • 文档 doc :包含 shard key 的一行数据
  • 块 Chunk :包含 n 个文档
  • 分片 Shard:包含 n 个 chunk
  • 集群 Cluster: 包含 n 个分片

image-20220523212252679

合适片键

影响片键效率的主要因素:

  • 取值基数(Cardinality);
  • 取值分布; • 分散写,集中读;
  • 被尽可能多的业务场景用到;
  • 避免单调递增或递减的片键;

选择基数大的片键

对于小基数的片键:

  • 因为备选值有限,那么块的总数量就有限;
  • 随着数据增多,块的大小会越来越大;
  • 水平扩展时移动块会非常困难;

例如:存储一个高中的师生数据,以年龄(假设年龄范围为15~65岁)作为片键, 那么:

15<=年龄<=65,且只为整数

最多只会有51个 chunk

选择分布均匀的片键

对于分布不均匀的片键

  • 造成某些块的数据量急剧增大
  • 这些块压力随之增大
  • 数据均衡以 chunk 为单位,所以系统无能为力

例如:存储一个学校的师生数据,以年龄(假设年龄范围为15~65岁)作为片键, 那么:

15<=年龄<=65,且只为整数

大部分人的年龄范围为15~18岁(学生)

15、16、17、18四个 chunk 的数据量、访问压力远大于其他 chunk

例子

一个 email 系统的片键例子

1
2
3
4
5
6
7
8
9
{
_id: ObjectId(),
user: 123,
time: Date(),
subject: “...”,
recipients: [],
body: “...”,
attachments: []
}

片键: { _id: 1}

image-20220523212849065

片键: { _id: ”hashed”}

image-20220523212907423

片键: { user_id: 1

image-20220523213006124

片键: { user_id: 1, time:1 }

image-20220523213035968

足够的资源

  • mongos 与 config 通常消耗很少的资源,可以选择低规格虚拟机;
  • 资源的重点在于 shard 服务器:
    • 需要足以容纳热数据索引的内存;
    • 正确创建索引后 CPU 通常不会成为瓶颈,除非涉及非常多的计算;
    • 磁盘尽量选用 SSD;
  • 最后,实际测试是最好的检验,来看你的资源配置是否完备。

即使项目初期已经具备了足够的资源,仍然需要考虑在合适的时候扩展。建议监控 各项资源使用情况,无论哪一项达到60%以上,则开始考虑扩展,因为:

  • 扩展需要新的资源,申请新资源需要时间;
  • 扩展后数据需要均衡,均衡需要时间。应保证新数据入库速度慢于均衡速度
  • 均衡需要资源,如果资源即将或已经耗尽,均衡也是会很低效的。

搭建及扩容

以下是记录多机版的分片集群,单机版的分片集群可参考: https://blog.csdn.net/one2threexm/article/details/117822683

目标及流程

  • 目标:学习如何搭建一个2分片的分片集群
  • 环境:3台 Linux 虚拟机, 4 Core 8 GB

步骤:

  1. 配置域名解析
  2. 准备分片目录
  3. 创建第一个分片复制集并初始化
  4. 创建 config 复制集并初始化
  5. 初始化分片集群,加入第一个分片 创建分片表 加入第二个分片

实验架构

image-20220523213408030

image-20220523213422125

注意

注意本次实验皆在一台虚拟机上完成,使用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复制集端口分别为270102702027030

创建文件夹,配置配置文件

mkdir shard1 && vim shard1/shard1.conf

# 将密钥复制至shard1里面

cp -avxp mongodb.keyfile shard1/mongodb.keyfile

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
32
storage:
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
enableMajorityReadConcern: true
#复制集名称
replSetName: shard1

启动命令

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

参数:

分别启动mongo1-1mongo1-2mongo1-3,地址是分别

172.17.0.2172.17.0.3172.17.0.4

image-20220524223005788

进入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
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
32
storage:
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
enableMajorityReadConcern: true
#复制集名称
replSetName: config

配置基本相同,就改了复制集的名字

启动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

参数:

分别启动mongo-conf-1mongo-conf-2mongo-conf-3,地址是分别

172.17.0.5172.17.0.6172.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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
net:
port: 27017
bindIp: 0.0.0.0
bindIpAll: true


# how the process runs
security:
#开启认证 mongos不开启
#authorization: enabled
# 指定keyfile认证
clusterAuthMode: keyFile
keyFile: /etc/mongo/mongodb.keyfile


#mongos 配置服务集群地址
sharding:
configDB: config/172.17.0.5:27017,172.17.0.6:27017,172.17.0.7:27017

**注意:**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()

image-20220528230807921

如果要测试分片效果的话可以再添加一个复制集,

插入测试数据

1
2
3
for(var i = 0; i < 100000; i++) {
db.bar.insert({i: 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 增量

方案一:延迟节点备份

image-20220531224321744

image-20220531224338567

安全范围内的任意时间点状态 = 延迟从节点当前状态 + 定量重放 oplog

注意:

主节点的 oplog 时间窗t应满足:t >= 延迟时间 + 48小时

留给自己48小时的处理时间,避免oplog丢弃一些日志

image-20220531224451684

方案二:全量备份加 oplog

image-20220531224618928

  • 最近的 oplog 已经在 oplog.rs 集合中,因此可以在定期从集合中导出便得到了 oplog;
  • 如果主节点上的 oplog.rs 集合足够大,全量备份足够密集,自然也可以不用备份 oplog;
  • 只要有覆盖整个时间段的 oplog,就可以结合全量备份得到任意时间点的备份。

image-20220531224744471

注意事项

复制数据库文件

  • 必须先关闭节点才能复制,否则复制到的文件无效;
  • 也可以选择 db.fsyncLock() 锁定节点,但完成后不要忘记 db.fsyncUnlock() 解锁;
  • 可以且应该在从节点上完成;
  • 该方法实际上会暂时宕机一个从节点,所以整个过程中应注意投票节点总数。

文件系统快照

  • MongoDB 支持使用文件系统快照直接获取数据文件在某一时刻的镜像;
  • 照过程中可以不用停机;
  • 数据文件和 Journal 必须在同一个卷上;
  • 快照完成后请尽快复制文件并删除快照;

Mongodump

  • 使用 mongodump 备份最灵活,但速度上也是最慢的;
  • mongodump 出来的数据不能表示某个个时间点,只是某个时间段(边dump边写)

image-20220531225112755

幂等性

image-20220531225150996

image-20220531225237459

实操

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

image-20220531232356759

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
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
{ explainVersion: '1',
queryPlanner:
{ namespace: 'mock.form_template_test',
indexFilterSet: false,
parsedQuery: { t: { '$eq': '定制酒' } },
maxIndexedOrSolutionsReached: false,
maxIndexedAndSolutionsReached: false,
maxScansToExplodeReached: false,
winningPlan:
{ stage: 'FETCH',
inputStage:
{ stage: 'IXSCAN',
keyPattern: { t: 1 },
indexName: 't_1',
isMultiKey: false,
multiKeyPaths: { t: [] },
isUnique: true,
isSparse: false,
isPartial: false,
indexVersion: 2,
direction: 'forward',
indexBounds: { t: [ '["定制酒", "定制酒"]' ] } } },
rejectedPlans: [] },
executionStats:
{ executionSuccess: true,
nReturned: 0,
executionTimeMillis: 0,
totalKeysExamined: 0,
totalDocsExamined: 0,
executionStages:
{ stage: 'FETCH',
nReturned: 0,
executionTimeMillisEstimate: 0,
works: 1,
advanced: 0,
needTime: 0,
needYield: 0,
saveState: 0,
restoreState: 0,
isEOF: 1,
docsExamined: 0,
alreadyHasObj: 0,
inputStage:
{ stage: 'IXSCAN',
nReturned: 0,
executionTimeMillisEstimate: 0,
works: 1,
advanced: 0,
needTime: 0,
needYield: 0,
saveState: 0,
restoreState: 0,
isEOF: 1,
keyPattern: { t: 1 },
indexName: 't_1',
isMultiKey: false,
multiKeyPaths: { t: [] },
isUnique: true,
isSparse: false,
isPartial: false,
indexVersion: 2,
direction: 'forward',
indexBounds: { t: [ '["定制酒", "定制酒"]' ] },
keysExamined: 0,
seeks: 1,
dupsTested: 0,
dupsDropped: 0 } } },
command:
{ find: 'form_template_test',
filter: { t: '定制酒' },
'$db': 'mock' },
serverInfo:
{ host: '27943b9966b3',
port: 27017,
version: '5.0.5',
gitVersion: 'd65fd89df3fc039b5c55933c0f71d647a54510ae' },
serverParameters:
{ internalQueryFacetBufferSizeBytes: 104857600,
internalQueryFacetMaxOutputDocSizeBytes: 104857600,
internalLookupStageIntermediateDocumentMaxSizeBytes: 104857600,
internalDocumentSourceGroupMaxMemoryBytes: 104857600,
internalQueryMaxBlockingSortMemoryUsageBytes: 104857600,
internalQueryProhibitBlockingMergeOnMongoS: 0,
internalQueryMaxAddToSetBytes: 104857600,
internalDocumentSourceSetWindowFieldsMaxMemoryBytes: 104857600 },
ok: 1 }

stage是对操作的描述;例如

  • COLLSCAN收集扫描
  • IXSCAN用于扫描索引键
  • FETCH用于检索文档
  • SHARD_MERGE用于合并分片的结果
  • SHARDING_FILTER用于从分片中过滤掉孤立文档

索引执行计划

queryPlanner

queryPlanner信息详细说明了查询优化器选择的计划。

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
"queryPlanner" : {
"plannerVersion" : <int>,
"namespace" : <string>,
"indexFilterSet" : <boolean>,
"parsedQuery" : {
...
},
"queryHash" : <hexadecimal string>,
"planCacheKey" : <hexadecimal string>,
"optimizedPipeline" : <boolean>, // Starting in MongoDB 4.2, only appears if true
"winningPlan" : {
"stage" : <STAGE1>,
...
"inputStage" : {
"stage" : <STAGE2>,
...
"inputStage" : {
...
}
}
},
"rejectedPlans" : [
<candidate plan 1>,
...
]
}

索引执行计划的选择

image-20220601221424518

索引执行情况

executionStats

获胜计划的执行情况

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
"executionStats" : {
"executionSuccess" : <boolean>,
"nReturned" : <int>,
"executionTimeMillis" : <int>,
"totalKeysExamined" : <int>,
"totalDocsExamined" : <int>,
"executionStages" : {
"stage" : <STAGE1>
"nReturned" : <int>,
"executionTimeMillisEstimate" : <int>,
"works" : <int>,
"advanced" : <int>,
"needTime" : <int>,
"needYield" : <int>,
"saveState" : <int>,
"restoreState" : <int>,
"isEOF" : <boolean>,
...
"inputStage" : {
"stage" : <STAGE2>,
"nReturned" : <int>,
"executionTimeMillisEstimate" : <int>,
...
"inputStage" : {
...
}
}
},
"allPlansExecution" : [
{
"nReturned" : <int>,
"executionTimeMillisEstimate" : <int>,
"totalKeysExamined" : <int>,
"totalDocsExamined" :<int>,
"executionStages" : {
"stage" : <STAGEA>,
"nReturned" : <int>,
"executionTimeMillisEstimate" : <int>,
...
"inputStage" : {
"stage" : <STAGEB>,
...
"inputStage" : {
...
}
}
}
},
...
]
}

重要指标

  • stage
    • 执行阶段
  • nReturned
    • 返回结果数量
  • executionTimeMillisEstimate
    • 执行时间
  • totalKeysExamined
    • 检查的索引键数量
  • totalDocsExamined
    • 检查的文档数量

索引类型

  • 单键索引
  • 组合索引
  • 多值索引
  • 地理位置索引
  • 全文索引
  • TTL索引
  • 部分索引
  • 哈希索引

组合索引-Compound Index

image-20220601221953687

**ESR原则: **https://www.mongodb.com/docs/manual/tutorial/equality-sort-range-rule

简单来说:

E: 建立复合索引时,第一个应当是精确匹配的字段,这样就可以排除大量的数据

S:需要查询排序的字段排在E字段后面,如果不建立该索引,将会在内存当中排序(相当昂贵)

R:需要返回范围查询的字段放在最后,避免找到大量索引数据,徒增耗时

建立索引的准则就是尽量的就将基数大的字段排前面,减少检索的数据量

其他索引就不记录了,再记也没文档详细全面

九、性能机制

应用端

一次数据库请求过程中发生了什么?

image-20220601223042954

选择节点

对于复制集读操作,选择哪个节点是由 readPreference(读取偏好)决定的:

  • primary/primaryPreferred
  • secondary/secondaryPreferred
  • nearest

如果不希望一个远距离节点被选择,应做到以下 之一:

  • 将它设置为隐藏节点;
  • 通过标签(Tag)控制可选的节点;
  • 使用 nearest 方式;

image-20220601223221209

排队等待

排队等待连接是如何发生的?

总连接数大于允许的最大连接数maxPoolSize;

如何解决这个问题?

  • 加大最大连接数(不一定有用,本质上还是查的不够快,cpu和缓存资源就那么多,盲目的加连接数没用)
  • 优化查询性能

连接与认证

如果一个请求需要等待创建新连接和进行认证,相比直接从连接池获取连接,它将 耗费更长时间。

  • 设置 minPoolSize(最小连接数)一次性创建足够的连接; •
  • 避免突发的大量请求;

数据库端

image-20220601223551182

排队等待

由 ticket 不足引起的排队等待,问题往往不在 ticket 本身,而在于为什么正在执行 的操作会长时间占用 ticket。

ticket

像令牌一样,想要查询先拿令牌,避免无脑查询压垮服务

  • 优化 CRUD 性能可以减少 ticket 占用时间;
  • zlib 压缩方式也可能引起 ticket 不足,因为 zlib 算法本身在进行压缩、解压时需要的时 间比较长,从而造成长时间的 ticket 占用;

执行请求(读)

image-20220601223745444

不能命中索引的搜索和内存排序是导致性能问题的最主要原因

执行请求(写)

image-20220601223823028journal日志

是将写入缓存的数据刷入磁盘持久化

image-20220601223857022

磁盘速度必须比写入速度要快才能保持缓存水位

合并结果

在mongos当中查询没有片键的数据时需要合并查询结果

image-20220601224002449

  • 如果顺序不重要则不要排序
  • 尽可能使用带片键的查询条件以减少参与查询的分片数

网络的考量

网络访问要快,尽量内网啦

image-20220601224110191

性能瓶颈总结

应用端 服务端 网络
选择访问入口节点 排队等待ticket 应用/驱动 - mongos
等待数据库连接 执行请求 mongos - 片
创建连接和完成认证 合并执行结果

性能排查工具

mongostat

展示实例状态的快速概览

文档: https://www.mongodb.com/docs/database-tools/mongostat

mongostat --username=xxx --password=‘xxx’ --authenticationDatabase=admin

image-20220601225618397

  • 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秒刷新一次

image-20220601230718493

展示了每个集合的读取和写入时间,方便排查哪个集合有问题

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()

image-20220601233520274

慢慢分析吧

mtools

十、上线及升级

性能测试

模拟真实压力,对集群完成充分的性能测试,了解集群概况

性能测试的输出:

  • 压测过程中各项指标表现,例如 CRUD 达到多少,连接数达到多少等。
  • 根据正常指标范围配置监控阈值;
  • 根据压测结果按需调整硬件资源;

测压工具: POCDriver / YCSB

环境检查

按照最佳实践要求对生产环境所使用的操作系统进行检查和调整。最常见的需要调 整的参数包括:

  • 禁用 NUMA,否则在某些情况下会引起突发大量swap交换;
  • 禁用 Transparent Huge Page,否则会影响数据库效率;
  • tcp_keepalive_time 调整为120秒,避免一些网络问题;
  • ulimit -n,避免打开文件句柄不足的情况;
  • 关闭 atime,提高数据文件访问效率;

更多检查项,请参考文档:https://www.mongodb.com/docs/manual/administration/production-notes/

主版本升级流程

image-20220605162509463

单机升级流程

image-20220605162553035

复制集升级流程

image-20220605162658377

分片集群升级流程

image-20220605162718959

升级过程中虽然会发生主从节点切换,存在短时间不可用,但是:

  • 3.6版本开始支持自动写重试可以自动恢复主从切换引起的集群暂时不可写;
  • 4.2开始支持的自动读重试则提供了包括主从切换在内的读问题的自动恢复;

升级需要逐版本完成,不可以跳版本:

  • 正确:3.2->3.4->3.6->4.0->4.2
  • 错误:3.2->4.2
  • 原因:
    • MongoDB复制集仅仅允许相邻版本共存
    • 有一些升级内部数据格式如密码加密字段,需要在升级过程中由mongo进行转换

十一、数据迁移

方案

如何迁移已有数据到 MongoDB?

image-20220605223221255

导出导入

  • 停止现有的基于 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
....
secure-file-priv= /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支持异构数据库迁移,国内产品文档特别详细直接参考文档

文章作者:xpp011

发布时间:2022年09月14日 - 23:09

原始链接:http://xpp011.cn/2022/09/14/7302381.html

许可协议: 转载请保留原文链接及作者。