跳过正文

Mongodb架构设计

·475 字·3 分钟
Chuck Chan
作者
Chuck Chan
分享技术、思考与生活

Mongodb
#

1. 简介
#

MongoDB 是一个基于分布式文件存储的数据库。它具有开源、高性能无模式等优点,由 C++ 语言编写。旨在为 WEB 应用提供可扩展的高性能数据存储解决方案。MongoDB 是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库当中功能最丰富,最像关系数据库的。MongoDB支持的数据结构类似于JSON,叫做 BSON,所以它既可以存储比较复杂的数据类型,又相当的灵活。

2. 关系型与非关系型数据库
#

281e605b476e424fba0d9f417e7545d5

3. 高可用架构
#

高可用性 HA(High Availability)指的是缩短因正常运维或者非预期故障而导致的停机时间,提高系统可用性。

3.1 Master-Slave 模式
#

Master-Slave 架构一般用于备份或者做读写分离,一般是一主一从设计和一主多从设计。在这种模式中,Master负责写数据,Slave负责读或者备份数据。

v2-00d63d121cb2ce50afa082a47e6a4d55_1440w

优点:

  • 实现简单
  • 从节点能分担读取数据的压力

缺点:

  • Master同步数据到Slave是一个异步的过程,存在主从延迟,不适合强一致性的场景
  • Master-Slave 的角色是静态配置的,不能自动切换角色,必须人为指定
  • 所有写请求都集中在Master上,容易造成Master压力过大,且如果Master挂掉,需要人为处理

MongoDB 3.6 起已不推荐使用主从模式,自 MongoDB 3.2 起,分片群集组件已弃用主从复制。因为 Master-Slave 其中 Master 宕机后不能自动恢复,只能靠人为操作,可靠性也差,操作不当就存在丢数据的风险。

3.2 Replica Set 副本集模式
#

Replica Set 是 mongod 的实例集合,包含三类节点角色

v2-daf3779600904a7f7f92ce48d602a57a_1440w

Primary( 主节点 )

只有 Primary 是可读可写的,Primary 接收所有的写请求,然后把数据同步到所有 Secondary 。一个 Replica Set 只有一个 Primary 节点,当 Primary 挂掉后,其他 Secondary 或者 Arbiter 节点会重新选举出来一个 Primary 节点,这样就又可以提供服务了。

读请求默认是发到 Primary 节点处理,如果需要故意转发到 Secondary 需要客户端修改一下配置(注意:是客户端配置,决策权在客户端)

那有人又会想了,这里也存在 Primary 和 Secondary 节点角色的分类,岂不是也存在单点问题?

这里和 Master-Slave 模式的最大区别在于,Primary 角色是通过整个集群共同选举出来的,人人都可能成为 Primary ,人人最开始只是 Secondary ,而这个选举过程完全自动,不需要人为参与。

Secondary( 副本节点 )

数据副本节点,当主节点挂掉的时候,参与选主。

思考一个问题:Secondary 和 Master-Slave 模式的 Slave 角色有什么区别?

最根本的一个不同在于:Secondary 相互有心跳,Secondary 可以作为数据源,Replica 可以是一种链式的复制模式。

Arbiter( 仲裁者 )

不存数据,不会被选为主,只进行选主投票。使用 Arbiter 可以减轻在减少数据的冗余备份,又能提供高可用的能力。

优点:

  • 数据多副本,在故障的时候,可以使用完的副本恢复服务。注意:这里是故障自动恢复
  • 读写分离,读的请求分流到副本上,减轻主(Primary)的读压力;
  • 节点直接互有心跳,可以感知集群的整体状态;

缺点:

  • 可用性大大增强,因为故障时自动恢复的,主节点故障,立马就能选出一个新的 Primary 节点。但是有一个要注意的点:每两个节点之间互有心跳,这种模式会导致节点的心跳几何倍数增大,单个 Replica Set 集群规模不能太大,一般来讲最大不要超过 50 个节点。
  • 其次,节点的数目必须为奇数,以防止脑裂的情况

3.3 Sharding 模式
#

按道理 Replica Set 模式已经非常好的解决了可用性问题,为什么还会往后演进呢?因为在当今大数据时代,有一个必须要考虑的问题:就是数据量。用户的数据量是永远都在增加的,理论是没有上限的,但 Replica Set 却是有上限的。所以Mongodb利用分布式技术来解决数据量无限增长的问题。Sharding 模式下按照层次划分可以分为 3 个大模块:

  1. 代理层:mongos
  2. 配置中心:副本集群(mongod)
  3. 数据层:Shard 集群
v2-41b9d157522f8da4f9d6febb658fd19e_1440w

代理层

代理层的组件也就是 mongos ,这是个无状态的组件,纯粹是路由功能。向上对接 Client ,收到 Client 写请求的时候,按照特定算法均衡散列到某一个 Shard 集群,然后数据就写到 Shard 集群了。收到读请求的时候,定位找到这个要读的对象在哪个 Shard 上,就把请求转发到这个 Shard 上,就能读到数据了。

数据层

数据层是啥?就是存储数据的地方。你会惊奇的发现,其实数据层就是由一个个 Replica Set 集群组成。在前面我们说过,单个 Replica Set 是有极限的,怎么办?那就搞多个 Replica Set ,这样的一个 Replica Set 我们就叫做 Shard 。理论上,Replica Set 的集群的个数是可以无限增长的。

配置中心

代理层是无状态的模块,数据层的每一个 Shard 是各自独立的,那总要有一个集群统配管理的地方,这个地方就是配置中心。里面记录的是什么呢?比如:有多少个 Shard,每个 Shard 集群又是由哪些节点组成的。每个 Shard 里大概存储了多少数据量(以便做均衡)。这些东西就是在配置中心的。配置中心存储的就是集群拓扑,管理的配置信息。这些信息也非常重要,所以也不能单点存储,怎么办?配置中心也是一个 Replica Set 集群,数据也是多副本的

详细架构图:

v2-d2887601cc252d4c8267ddbbd68402bd_1440w

Sharding 模式怎么存储数据?
#

我们说过,纵向优化是对硬件使用者最友好的,横向优化则对硬件使用者提出了更高的要求,也就是说软件架构要适配

单 Shard 集群是有限的,但 Shard 数量是无限的,Mongo 理论上能够提供近乎无限的空间,能够不断的横向扩容。那么现在唯一要解决的就是怎么去把用户数据存到这些 Shard 里?MongDB 是怎么做的?

首先,要选一个字段(或者多个字段组合也可以)用来做 Key,这个 Key 可以是你任意指定的一个字段。我们现在就是要使用这个 Key 来,通过某种策略算出发往哪个 Shard 上。这个策略叫做: Sharding Strategy ,也就是分片策略。

我们把 Sharding Key 作为输入,按照特点的 Sharding Strategy 计算出一个值,值的集合形成了一个值域,我们按照固定步长去切分这个值域,每一个片叫做 Chunk每个 Chunk 出生的时候就和某个 Shard 绑定起来,这个绑定关系存储在配置中心里。

所以,我们看到 MongoDB 的用 Chunk 再做了一层抽象层,隔离了用户数据和 Shard 的位置,用户数据先按照分片策略算出落在哪个 Chunk 上,由于 Chunk 某一时刻只属于某一个 Shard,所以自然就知道用户数据存到哪个 Shard 了。

Sharding 模式下数据写入过程:

Xnip2023-03-09_16-14-55

Sharding 模式下数据读取过程:

Xnip2023-03-09_16-18-00

通过上图我们也看出来了,mongos 作为路由模块其实就是寻路的组件,写的时候先算出用户 key 属于哪个 Chunk,然后找出这个 Chunk 属于哪个 Shard,最后把请求发给这个 Shard ,就能把数据写下去。读的时候也是类似,先算出用户 key 属于哪个 Chunk,然后找出这个 Chunk 属于哪个 Shard,最后把请求发给这个 Shard ,就能把数据读上来。

实际情况下,mongos 不需要每次都和 Config Server 交互,大部分情况下只需要把 Chunk 的映射表 cache 一份在 mongos 的内存,就能减少一次网络交互,提高性能。

为什么要多一层 Chunk 这个抽象?

为了灵活,因为一旦是用户数据直接映射到 Shard 上,那就相当于是用户数据和底下的物理位置绑定起来了,这个万一 Shard 空间已经满了,怎么办?

存储不了呀,又不能存储到其他地方去。有同学就会想了,那我可以把这个变化的映射记录下来呀,记录下来理论上行得通,但是每一个用户数据记录一条到 Shard 的映射,这个量级是非常大的,实际中没有可行性。

而现在多了一层 Chunk 空间,就灵活了。用户数据不再和物理位置绑定,而是只映射到 Chunk 上就可以了。如果某个 Shard 数据不均衡,那么可以把 Chunk 空间分裂开,迁走一半的数据到其他 Shard ,修改下 Chunk 到 Shard 的映射,Chunk 到 Shard 的映射条目很少,完全 Hold 住,并且这种均衡过程用户完全不感知。

讲回 Sharding Strategy 是什么?本质上 Sharding Strategy 是形成值域的策略而已,MongoDB 支持两种 Sharding Strategy:

  1. Hashed Sharding 的方式
  2. Range Sharding 的方式

Hashed Sharding

把 Key 作为输入,输入到一个 Hash 函数中,计算出一个整数值,值的集合形成了一个值域,我们按照固定步长去切分这个值域,每一个片叫做 Chunk ,这里的 Chunk 则就是整数的一段范围而已

Xnip2023-03-09_16-22-38

这种计算值域的方式有什么优缺点呢?

好处是:

  • 计算速度快
  • 均衡性好,纯随机

坏处是:

  • 正因为纯随机,排序列举的性能极差,比如你如果按照 name 这个字段去列举数据,你会发现几乎所有的 Shard 都要参与进来;

Range Sharding

Range 的方式本质上是直接用 Key 本身来做值,形成的 Key Space

Xnip2023-03-10_10-26-29

如上图例子,Sharding Key 选为 name 这个字段,对于 “test_0”,“test_1”,“test_2” 这样的 key 排序就是挨着的,所以就全都分配在一个 Chunk 里。

这 3 条 Docuement 大概率是在一个 Chunk 上,因为我们就是按照 Name 来排序的。这种方式有什么优缺点?

好处是:

  • 对排序列举场景非常友好,因为数据本来就是按照顺序依次放在 Shard 上的,排序列举的时候,顺序读即可,非常快速;

坏处是:

  • 容易导致热点,举个例子,如果 Sharding Key 都有相同前缀,那么大概率会分配到同一个 Shard 上,就盯着这个 Shard 写,其他 Shard 空闲的很,却帮不上忙;

可用性的进一步提升
#

为什么说 Sharding 模式不仅是容量问题得到解决,可用性也进一步提升?

因为 Shard(Replica Set)集群个数多了,即使一个或多个 Shard 不可用,Mongo 集群对外仍可以 提供读取和写入服务。因为每一个 Shard 都有一个 Primary 节点,都可以提供写服务,可用性进一步提升。

4. 推荐使用姿势
#

上面已经介绍了历史演进的 3 种高可用模式,Master-Slave 模式已经在不推荐了,Relicate Set 和 Sharding 模式都可以保证数据的高可靠和高可用,但是在我们实践过程中,发现客户端存在非常大的配置权限,也就是说如果用户在使用 MongoDB 的时候使用姿势不对,可能会导致达不到你的预期。

使用姿势一:怎么保证高可用?
#

如果是 Replicate Set 模式,那么客户端要主动感知主从切换。以前用过 Go 语言某个版本的 MongoDB client SDK,发现在主从切换的时候,并没有主动感知,导致请求还一直发到已经故障的节点,从而导致服务不可用。

所以针对这种形式要怎么做?有两个方案:

  1. 用 Sharding 模式,因为 Sharding 模式下,用户打交道的是 mongos ,这个是一个代理,帮你屏蔽了底层 Replica Set 的细节,主从切换由它帮你做好;
  2. 客户端自己感知,定期刷新(这种就相对麻烦);

使用姿势二:怎么保证数据的高可靠?
#

客户端配置写多数成功才算成功。没错,这个权限交由由客户端配置。如果没有配置写多数成功,那么很可能写一份数据成功就成功了,这个时候如果发生故障,或者切主,那么数据可能丢失或者被主节点 rollback ,也等同用户数据丢失。

mongodb 有完善的 rollback 及写入策略(WriteConcern)机制,但是也要使用得当。怎么保证高可靠?一定要写多数成功才算成功。

使用姿势三:怎么保证数据的强一致性?
#

客户端要配置两个东西:

  1. 写多数成功,才算成功;
  2. 读使用 strong 模式,也就是只从主节点读;

只有这两个配置一起上,才能保证用户数据的绝对安全,并且对外提供数据的强一致性。

5. 存储引擎
#

存储引擎(Storage Engine)是MongoDB的核心组件,负责管理数据如何存储在硬盘(Disk)和内存(Memory)上。从MongoDB 3.2 版本开始,MongoDB 支持多数据存储引擎(Storage Engine),MongoDB支持的存储引擎有:WiredTiger,MMAPv1和In-Memory。

从MongoDB 3.2 版本开始,WiredTiger成为MongDB默认的Storage Engine,用于将数据持久化存储到硬盘文件中,WiredTiger提供文档级别(Document-Level)的并发控制,检查点(CheckPoint),数据压缩和本地数据加密( Native Encryption)等功能。

MongoDB不仅能将数据持久化存储到硬盘文件中,而且还能将数据只保存到内存中;In-Memory存储引擎用于将数据只存储在内存中,只将少量的元数据和诊断日志(Diagnostic)存储到硬盘文件中,由于不需要Disk的IO操作,就能获取索取的数据,In-Memory存储引擎大幅度降低了数据查询的延迟(Latency)。