MongoDB 副本集与分片

MongoDB 副本集与分片

副本集 (Replica sets)

  • 提供数据的冗余备份, 在多个服务器上存储数据副本, 提高了数据的可用性, 并可以保证数据的安全性

  • 副本集包含多个数据节点和一个仲裁节点(可选)

    • 在数据节点中,只有一个成员被视为主要节点 (primary),而其他节点则被视为次要节点 (secondary)
    • 仲裁者 (Arbiter) 不保存数据, 当 primary 不可用时投票选举切换 primary
    • 一个副本集最多可以有 50 个成员,但只有 7 个投票成员
  • 选举时避免出现平票的情况,部署一般采用基数节点

Primary

image-20200917202406186

  • 主节点是副本集中接收写操作的唯一成员
    • MongoDB 在主数据库上应用写操作,然后在主数据库的操作日志 (oplog) 上记录该操作
    • 次要成员复制此日志,并将操作应用于其数据集
  • 副本集的所有成员都可以接受读取操作
    • 默认情况下,应用程序将其读取操作定向到 Primary

Secondary

image-20200917202714712

  • 次要节点数据的复制过程是异步的

Arbiter

  • 仲裁节点使用最小的资源并且不要求硬件设备
  • 不能将 Arbiter 部署在同一个数据集节点中

分片 (Sharding)

1578455248157007595.jpg

  • 分片集群由以下三个组件构成
    • mongos
      • 数据库集群请求的入口,负责把对应的数据请求请求转发到对应的 shard 服务器上
      • 在生产环境通常有多 mongos 作为请求的入口,防止其中一个挂掉所有的 mongodb 请求都没有办法操作
    • config server
      • 存储所有数据库元信息(路由、分片)的配置
      • mongos 本身没有物理存储分片服务器和数据路由信息,只是缓存在内存里,配置服务器则实际存储这些数据
      • MongoDB 3.4 开始必须将配置服务器部署为副本集 (CSRS, Config Servers Replica Set),防止数据丢失
    • shard
      • 分片是指将数据库拆分,将其分散在不同的机器上的过程,每个分片是整体数据的子集,且都可以部署为副本集
        • MongoDB 3.6 开始必须将分片部署为副本集
      • 将数据分散到不同的机器上,不需要功能强大的服务器就可以存储更多的数据和处理更大的负载
      • 基本思想就是将集合切成小块,这些块分散到若干片里,每个片只负责总数据的一部分,最后通过一个均衡器来对各个分片进行均衡(数据迁移)
      • 如果集合没有分片,那么该集合数据都存储在数据库的 Primary Shard 中

分片机制

分片键 (Shared Key)

  • MongoDB 通过定义分片键从而对整个集合进行分片,分片键的好坏直接影响到整个集群的性能

  • 一个集合只有且只能有一个分片键,一旦分片键确定好之后就不能更改

  • 分片方式

    • 基于范围
      • 优势:分片键范围查询性能较好,读性能较好
      • 劣势:数据分布可能不均匀,存在热点
    • 基于 Hash
      • 优势:数据分布均匀,写性能较好,适用于日志、物联网等高并发场景
      • 劣势:范围查询效率较低
    • 基于 zone/tag
      • 若数据具备一些天然的区分,如基于地域、时间等标签,数据可以基于标签来做区分
      • 优势:数据分布较为合理
  • 分片键的选择, 选择合适的片键对 sharding 效率影响很大

    • 取值基数
      • 取值基数建议尽可能大,如果用小基数的片键,因为备选值有限,那么块的总数量就有限,随着数据增多,块的大小会越来越大,导致水平扩展时移动块会非常困难
    • 取值分布
      • 取值分布建议尽量均匀,分布不均匀的片键会造成某些块的数据量非常大,同样有上面数据分布不均匀,性能瓶颈的问题
    • 查询带分片
      • 查询时建议带上分片,使用分片键进行条件查询时,mongos 可以直接定位到具体分片,否则 mongos 需要将查询分发到所有分片,再等待响应返回
    • 避免单调底层或递减
      • 单调递增的 sharding key,数据文件挪动小,但写入会集中,导致最后一篇的数据量持续增大,不断发生迁移,递减同理

Chunk 块

  • chunk(块)是均衡器迁移数据的最小单元,默认大小为 64MB,取值范围为 1-1024MB
  • 一个 chunk 只存在于一个分片,每个 chunk 由片键特定范围内的文档组成,范围为 [start,end)
  • 一个文档属于且只属于一个 chunk,当一个 chunk 增加到特定大小的时候,会通过拆分点(split point)被拆分成 2 个较小的块
  • 在有些情况下,chunk 会持续增长,超过 ChunkSize,官方称为 jumbo chunk,该块无法被拆分和迁移,所以会导致 chunk 在分片服务器上分布不均匀,从而成为性能瓶颈
Chunk 的拆分
  • mongos 会记录每个块中有多少数据,一旦达到了阈值就会检查是否需要对其进行拆分,如果确实需要拆分则可以在配置服务器上更新这个块的相关元信息

  • chunk 的拆分过程如下:

    • mongos 接收到客户端发起的写请求后会检查当前块的拆分阈值点
    • 如果需要拆分,mongos 则会像分片服务器发起一个拆分请求
    • 分片服务器会做拆分工作,然后将信息返回 mongos
Chunk 的迁移
  1. 均衡器进程发送 moveChunk 命令到源分片
  2. 源分片使用内部 moveChunk 命令,在迁移过程,对该块的操作还是会路由到源分片
  3. 目标分片构建索引
  4. 目标分片开始进行数据复制
  5. 复制完成后会同步在迁移过程中该块的更改
  6. 同步完成后源分片会连接到配置服务器,使用块的新位置更新集群元数据
  7. 源分片完成元数据更新后,一旦块上没有打开的游标,源分片将删除其文档副本
  • 迁移过程可确保一致性,并在平衡期间最大化块的可用性
迁移的阈值
  • 对于数据的不均衡是根据两个分片上的 Chunk 个数差异来判定的,阈值对应表如下:

    • Number of Chunks Migration Threshold
      Fewer than 20 2
      20-79 4
      80 and greater 8
修改 Chunk Size 的注意事项
  • chunk 的自动拆分操作仅发生在插入或更新的时候。

  • 如果减少 chunk size,将会耗费一些时间将原有的 chunk 拆分到新 chunk,并且此操作不可逆

  • 如果新增 chunk size,已存在的 chunk 只会等到新的插入或更新操作将其扩充至新的大小

  • chunk size 的可调整范围为 1-1024MB

Balancer 均衡器

  • MongoDB 的 balancer(均衡器)是监视每个分片的 chunk 数的一个后台进程

  • 当分片上的 chunk 数达到特定迁移阈值时,均衡器会尝试在分片之间自动迁移块,使得每个分片的块的数量达到平衡

  • 分片群集的平衡过程对用户和应用程序层完全透明,但在执行过程时可能会对性能产生一些影响

  • 从 MongoDB 3.4 开始,balancer 在配置服务器副本集(CSRS)的主服务器上运行

    • 在 3.4 版本中,当平衡器进程处于活动状态时,主配置服务器的的 locks 集合通过修改 _id: "balancer" 文档会获取一个 balancer lock,该 balancer lock 不会被释放,是为了保证只有一个 mongos 实例能够在分片集群中执行管理任务
    • 从 3.6 版本开始,均衡器不再需要 balancer lock
  • 均衡器可以动态的开启和关闭,也可以针对指定的集合开启和关闭,还可以手动控制均衡器迁移 chunk 的时间,避免在业务高峰期的时候迁移 chunk 从而影响集群性能

事务

  • MongoDB 很早就有事务的概念,但是这个事务只能是针对单文档的,即单个文档的操作是有原子性保证的

  • 在 4.0 版本之后,MongoDB 开始支持多文档的事务:

    • 4.0 版本支持副本集范围的多文档事务
    • 4.2 版本支持跨分片的多文档事务 (基于两阶段提交)
  • 在事务的隔离性上,MongoDB 支持快照(snapshot)的隔离级别,可以避免脏读、不可重复读和幻读

  • 尽管有了真正意义上的事务功能,但多文档事务对于性能有一定的影响,应用应该在充分评估后再做选用

相关操作 (Mongo shell)

添加分片键

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
mongos> sh.enableSharding("test");
{
"ok" : 1,
"operationTime" : Timestamp(1556535000, 8),
"$clusterTime" : {
"clusterTime" : Timestamp(1556535000, 8),
"signature" : {
"hash" : BinData(0,"84KefOzN8tKmsPfr6IrnBUxF9NM="),
"keyId" : NumberLong("6685242219722440730")
}
}
}

mongos> sh.shardCollection("test.testcoll", {"myfield": 1});
{
"collectionsharded" : "test.testcoll",
"collectionUUID" : UUID("68ff9452-40bb-41a2-b35a-405132f90cd3"),
"ok" : 1,
"operationTime" : Timestamp(1556535010, 8),
"$clusterTime" : {
"clusterTime" : Timestamp(1556535010, 8),
"signature" : {
"hash" : BinData(0,"IgVzMa8qE4UBzjc2gOZJX5kZ3T4="),
"keyId" : NumberLong("6685242219722440730")
}
}
}

mongos> use test;
switched to db test

mongos> db.testcoll.insert({"myfield": "a", "otherfield": "b"});
WriteResult({ "nInserted" : 1 })

mongos> db.testcoll.insert({"myfield": "c", "otherfield": "d", "kube" : "db" });
WriteResult({ "nInserted" : 1 })

mongos> db.testcoll.find();
{ "_id" : ObjectId("5cc6d6f656a9ddd30be2c12a"), "myfield" : "a", "otherfield" : "b" }
{ "_id" : ObjectId("5cc6d71e56a9ddd30be2c12b"), "myfield" : "c", "otherfield" : "d", "kube" : "db" }
  • 这里设置 test 数据库的 testcoll 表需要分片,根据 myfield 自动分片

查看集群状态 sh.status()

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
mongos> sh.status();
--- Sharding Status ---
sharding version: {
"_id" : 1,
"minCompatibleVersion" : 5,
"currentVersion" : 6,
"clusterId" : ObjectId("5cc6c061f439d076e04d737b")
}
shards:
{ "_id" : "shard0", "host" : "shard0/mongo-sh-shard0-0.mongo-sh-shard0-gvr.demo.svc.cluster.local:27017,mongo-sh-shard0-1.mongo-sh-shard0-gvr.demo.svc.cluster.local:27017,mongo-sh-shard0-2.mongo-sh-shard0-gvr.demo.svc.cluster.local:27017", "state" : 1 }
{ "_id" : "shard1", "host" : "shard1/mongo-sh-shard1-0.mongo-sh-shard1-gvr.demo.svc.cluster.local:27017,mongo-sh-shard1-1.mongo-sh-shard1-gvr.demo.svc.cluster.local:27017,mongo-sh-shard1-2.mongo-sh-shard1-gvr.demo.svc.cluster.local:27017", "state" : 1 }
{ "_id" : "shard2", "host" : "shard2/mongo-sh-shard2-0.mongo-sh-shard2-gvr.demo.svc.cluster.local:27017,mongo-sh-shard2-1.mongo-sh-shard2-gvr.demo.svc.cluster.local:27017,mongo-sh-shard2-2.mongo-sh-shard2-gvr.demo.svc.cluster.local:27017", "state" : 1 }
active mongoses:
"3.6.12" : 2
autosplit:
Currently enabled: yes
balancer:
Currently enabled: yes
Currently running: no
Failed balancer rounds in last 5 attempts: 0
Migration Results for the last 24 hours:
No recent migrations
databases:
{ "_id" : "config", "primary" : "config", "partitioned" : true }
config.system.sessions
shard key: { "_id" : 1 }
unique: false
balancing: true
chunks:
shard0 1
{ "_id" : { "$minKey" : 1 } } -->> { "_id" : { "$maxKey" : 1 } } on : shard0 Timestamp(1, 0)
{ "_id" : "test", "primary" : "shard1", "partitioned" : true }
test.testcoll
shard key: { "myfield" : 1 }
unique: false
balancing: true
chunks:
shard1 1
{ "myfield" : { "$minKey" : 1 } } -->> { "myfield" : { "$maxKey" : 1 } } on : shard1 Timestamp(1, 0)

删除片键

1
2
3
4
5
6
7
8
9
10
mongos> db.collections.remove({_id:"test.testcoll"})
WriteResult({ "nRemoved" : 1 })
mongos> db.chunks.remove({ns:"test.testcoll"})
WriteResult({ "nRemoved" : 38 })
mongos> db.locks.remove({_id:"test.testcoll"})
WriteResult({ "nRemoved" : 1 })
mongos> use admin
switched to db admin
mongos> db.adminCommand("flushRouterConfig") ##刷新路由配置
{ "flushed" : true, "ok" : 1 }

查看 mongo 集群是否开启了 balance

1
2
mongos> sh.getBalancerState()
true
  • 也可通过执行 sh.status() 查看 balance 状态。

查看是否正在有数据的迁移

1
2
mongos> sh.isBalancerRunning()
false

设置 balance 窗口

  • 将均衡器的迁移 chunk 时间控制在凌晨 02 点至凌晨 06 点:

    1
    2
    3
    4
    5
    6
    use config
    db.settings.update(
    { _id: "balancer" },
    { $set: { activeWindow : { start : "02:00", stop : "06:00" } } },
    { upsert: true }
    )
  • 删除 balance 窗口:

    1
    2
    use config
    db.settings.update({ _id : "balancer" }, { $unset : { activeWindow : true } })

关闭 balance

  • 默认 balance 的运行可以在任何时间,迁移只需要迁移的 chunk,如需关闭 balance,可执行下列命令:

    1
    2
    sh.stopBalancer()
    sh.getBalancerState()
  • 停止 balace 后,查看是否有迁移进程正在执行,可执行下列命令:

    1
    2
    3
    4
    5
    use config
    while( sh.isBalancerRunning() ) {
    print("waiting...");
    sleep(1000);
    }

打开 balance

  • 如您需要准备重新打开 balance,可执行下列命令:

    1
    sh.setBalancerState(true)
  • 当驱动版本不支持 sh.startBalancer() 时,可执行下列命令来重新打开 balance:

    1
    2
    use config
    db.settings.update( { _id: "balancer" }, { $set : { stopped: false } } , { upsert: true } )

集合的 balance

  • 关闭某个集合的 balance:

    1
    sh.disableBalancing("students.grades")
  • 打开某个集合的 balance:

    1
    sh.enableBalancing("students.grades")
  • 查看某个集合是否开启了 balance:

    1
    db.getSiblingDB("config").collections.findOne({_id : "students.grades"}).noBalance

相关问题

  • 分片建的值不允许更新
  • 在有分片键的存在的集合中,必须根据分片键或者是主键这种 mongos 可以确定唯一的方式来进行单条更新
  • 如果设置 {upsert:true} 在进行单条更新的时候, 必须根据分片键进行更新
    • 因为如果没有 shard key,mongos 既不能在所有 shard 实例上执行这条语句 (可能会导致每个shard都插入数据), 也无法选择在某个 shard 上执行这条语句
    • 虽然 mongos 知道 _id 是唯一的, 但是 _id 不是分片键, mongos 不清楚 _id 落在哪个分片上
    • 可以使用批量更新操作解决此问题