MongoDB 是当前比较流行的文档型数据库,其拥有易使用、易扩展、功能丰富、性能卓越等特性。MongoDB 本身就拥有高可用及分区的解决方案,分别为副本集(Replica Set)和分片(sharding),下面我们主要看这两个特性。

1. 副本集

有人说 MongoDB 副本集至少需要三个节点,但其实这句是有问题的,因为副本集中节点最少可以是一台,3.0之前最多12个节点,3.0开始节点数量能够达到50个。但节点数1个或者2个的时候,MongoDB 就无法发挥副本集特有的优势,因此我们一般建议节点数大于3个。


MongoDB高可用方案原理解析_第1张图片


首先,我们看一下 MongoDB 副本集的各种角色。

  • Primary:主服务器,只有一组,处理客户端的请求,一般是读写

  • Secondary:从服务器,有多组,保存主服务器的数据副本,主服务器出问题时其中一个从服务器可提升为新主服务器,可提供只读服务

  • Hidden:一般只用于备份节点,不处理客户端的读请求

  • Secondary-Only:不能成为 primary 节点,只能作为 secondary 副本节点,防止一些性能不高的节点成为主节点

  • Delayed:slaveDelay 来设置,为不处理客户端请求,一般需要隐藏

  • Non-Voting:没有选举权的 secondary 节点,纯粹的备份数据节点。

  • Arbiter:仲裁节点,不存数据,只参与选举,可用可不用


然后我们思考一下 MongoDB 副本集是通过什么方式去进行同步数据的,我们了解 Oracle 的 DataGuar 同步模式,我们也了解 MySQL 主从同步模式,他们都是传输日志到备库然后应用的方法,那么不难想象,MongoDB 的副本集基本也是这个路子,这里就不得不提到同步所依赖的核心 Oplog。Oplog 其实就像 MySQL 的 Binlog 一样,记录着主节点上执行的每一个操作,而 Secondary 通过复制 Oplog 并应用的方式来进行数据同步。Oplog 的大小是固定的,默认分配5%的可用空间(64位),当然我们也可以用    –oplogSize 选项指定具体大小,设置合适的大小在生产应用中是非常重要的一个环节,大家可能疑惑为什么?这是因为 Oplog 和 MySQL Binary 不同,它是循环复用的,它又和 Oracle 的日志不同,没有多组重做日志,也没有归档日志。Oplog 就是一个大小固定、循环复用的日志文件,当 Secondary 落后 Primary 很多,直到 oplog 被复写,那只能重新全量同步,而拉取全量同步代价特别高,直接影响 Primary 的读写性能。


大家还可能会问 MongoDB 副本集是实时同步吗?这其实也是在问数据库一致性的问题。MySQL 的半同步复制模式保证数据库的强一致,Oracle DataGuard 的最大保护模式也能够保证数据库的强一致,而 MongoDB 可以通过 getLastError 命令来保证写入的安全,但其毕竟不是事务操作,无法做到数据的强一致。


MongoDB 副本集 Secondary 通常会落后几毫秒,如果有加载问题、配置错误、网络故障等原因,延迟可能会更大。


MongoDB高可用方案原理解析_第2张图片


MongoDB 副本集本身就持有故障切换(Failover)、手动切换(Switchover)以及读写分离的功能,大家可能会关心 MongoDB 副本集如何选举、如何防止脑裂等等问题,这个先别着急,放到下面去说。MongoDB 副本集默认是把读写压力都请求到 Primary 节点上,但我们可以通过设置 setSlaveOk 来把读压力放在各个 Secondary 上,MongoDB 驱动还提供五种读取策略(Read Preferences),如下:


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

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

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

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

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


下面看一下 MongoDB 副本集选举的方法,选举我们可以简单理解为如何从集群节点中选择合适的节点提升为 Primary 的过程。跟很多 NoSQL 数据库一样,MongoDB 副本集采用的是 Bully 算法,具体说明请见 wiki 文档。


大致思想是集群中每个成员都能够声明自己是主节点并通知到其他节点,被其他节点接受的节点才能成为主节点。MongoDB 副本集有着“大多数”的概念,在进行选举时必须遵循”大多数”规则,节点在得到大多数支持时才能成为主节点,而副本集中节点存活数量必须大于“大多数”的数量。


MongoDB高可用方案原理解析_第3张图片


MongoDB 到了3.0之后副本集成员个数突破到了50个,但12个以上节点开始“大多数”都为7。


MongoDB 在下面几个条件触发之下进行选举:


  • 初始化副本集时;

  • 备份节点无法和主节点通讯时(可能主节点宕或网络原因);

  • Primary 手动降级,rs.stepDown(sec),默认 60s。


接着看下选举的步骤:


  • 得到每个服务器节点的最后操作时间戳。每个 mongodb 都有 oplog 机制会记录本机的操作,方便和主服务器进行对比数据是否同步还可以用于错误恢复;

  • 如果集群中大部分服务器宕机了,保留活着的节点都为 secondary 状态并停止选举;

  • 如果集群中选举出来的主节点或者所有从节点最后一次同步时间看起来很旧了,停止选举等待人工操作;

  • 如果上面都没有问题就选择最后操作时间戳最新(保证数据最新)的服务器节点作为主节点。


有些人可能在设计 MongoDB 副本集架构过程中会产生成员节点必须是奇数个的误区,MongoDB 副本集成员节点数量为偶数个会有问题吗?


MongoDB高可用方案原理解析_第4张图片


上图中我们很清晰的看到,在单机房内不管副本集成员节点数为偶数还是奇数都是没有问题的,但如果是两个机房,每个机房的成员节点数量一致,在两个机房之间心跳中断时,整个集群就会出现无法选举 Primary 的问题,这就是 MongoDB 副本集中的脑裂。那如何防止脑裂?从架构角度去看的话,我们如下推荐:


MongoDB高可用方案原理解析_第5张图片


左图为“大多数”成员都在一个数据中心

需求:副本集的 Primary 总在主数据中心

缺点:如果主数据中心挂了,没有可用 Primary 节点


右图为两个数据中心成员数量相同,第三个地方放一个用于决定胜负的副本节点(可是仲裁节点)

需求:跨机房容灾

缺点:额外需要第三个机房


所以说,MongoDB 副本集成员数量奇数个的说法其实针对的是多机房部署的场景下。


另外,在设计 MongoDB 副本集的过程中,我们还需要考虑过载的问题,因为过载导致 MongoDB 数据库性能极差。因此,一定衡量好读取的量,充分考虑读写节点宕机的可能性。


MongoDB高可用方案原理解析_第6张图片


MongoDB副本集还有些同步、心跳、回滚等概念,我简单整理了下。


同步

初始化同步:从副本集中其他节点全量同步一次,触发条件:


  • Secondary 节点首次加入时;

  • Secondary 节点落后 oplog 大小以上的数据时;

  • 回滚失败时。


保持同步:初始化同步之后的增量同步

注:同步源并非是 Primary 节点,MongoDB 根据 Ping 时间选择同步源。选择同步源时,会选择一个离自己比较近的而且数据比自己新的成员。


心跳


  • Primary 节点是? 哪个节点宕了?哪个节点可以作为同步源? — 心跳来解决;

  • 每个节点每2s向其他节点发送心跳请求,根据其结果来维护自己的状态视图;

  • Primary 节点通过心跳来知道自己是否满足”大多数”条件,如果不满足,它就会退位变成 Secondary。


回滚


Primary 执行了写请求之后宕机,Secondary 节点还没来得及复制本次的写操作,也就意味着新选举的 Primary 上没有这个写操作。这时候原 Primary 恢复并成为 Secondary 时,需要回滚这个写操作以能够重新进行同步。回滚数据量大于 300M 或者需要回滚的时间超过30分钟,回滚就会失败,必须重新全量同步。


2.分片


分片(sharding)其实就是数据拆分,把数据分散在多个节点上,也就是水平拆分。MongoDB 支持自动分片,无论自动分片有多优点或缺点,MongoDB 依然拥有该特性而引以为傲。


MongoDB 分片适用于如下几个场景:


  • 单个服务器无法承受压力时,压力包括负载、频繁写、吞吐量等;

  • 服务器磁盘空间不足时;

  • 增加可用内存大小,以更多的数据在内存中访问。


MongoDB高可用方案原理解析_第7张图片


如上图所示,MongoDB分片共有三个组件,介绍如下:


mongos:数据库集群请求的入口,起到路由作用,它负责把对应的数据请求请求转发到对应的 shard 服务器上。生产环境中需要多个 mongos。

config server:保存集群和分片的元数据,mongos 启动时会加载配置服务器上的配置信息,以后如果配置服务器信息变化会通知到所有的 mongos 更新自己的状态。生产环境需要多个 配置服务器。也需要定期备份。

shard server:实际存储数据的分片。生产环境要求是副本集。


下面我简单画了一下分片的过程:



在分片之前,可以把一个集合看成是单一整块儿,所有文档都包含在这个块当中。


MongoDB高可用方案原理解析_第8张图片


在选择片键进行分片之后,集合被拆分成为多个数据块儿,这时候第一个块和最后一个块儿中将出现 $minKey 和 $maxKey,分别表示负无穷和正无穷,当然这都是 MongoDB 分片内部使用的,我们只要了解即可。


MongoDB高可用方案原理解析_第9张图片


接下来拆分出来的数据块儿将会均匀分布到各个节点上。


那有些人可能会疑问块是怎么拆分的?我依然画了4个图来解释。


MongoDB高可用方案原理解析_第10张图片


  • mongos 记录每个块中的数据量,达到某个阈值,就检查是否需要拆分块;

  • 如拆分块,mongos 更新 config server 的块元数据;

  • config server 诞生新块,修改旧块的范围(拆分点);

  • 拆分完毕,mongos 重置原始块的追踪以及新建新块追踪;


注:这里的块(chunk)是逻辑的概念,一个 chunk 并不是实际存储的一个页或者一个文件之类,而是仅仅在 config 节点中的元数据中体现。也就是说,拆分块只修改元数据,并不进行数据移动。


拆分块儿的过程也是有隐患的,比如找不到拆分点而导致产生特大块儿,还有配置服务器不可达导致拆分风暴等。


MongoDB高可用方案原理解析_第11张图片


选择片键不合理:mongos 发现块达到阈值,然后请求分片拆分块,但分片却找不到拆分点,这样导致块越来越大。


连锁反应:片键不合理–>特大块(无法拆开)–>块无法移动–>造成数据分布不均衡–>进而数据写入不均衡–>进一步加剧了数据分布不均衡

预防:正确选择片键


config server 不可达:mongos 进行拆分时无法和配置服务器通讯,也就无法更新元数据,这导致一个循环的现象:尝试拆分和拆分失败之间来回切换,进而会影响 mongos 和当前分片的性能。这种不断重复发起拆分请求却无法进行拆分的过程,叫做拆分风暴(split storm)。


预防:

1) 保证配置服务器的可用状态

2) 重启 mongos,重置写入计数器


说了这么多,我们还不知道怎么创建分片,分为两种:



  • 从零开始创建分片:一般是新业务上线,架构设计初就选用分片;

  • 将副本集转换为分片:服务运行一段时间,单个副本集已无法满足需求,需要转换为分片;


第一种从零开始创建分片没什么可说的,选择好片键尤为关键,第二种副本集转换为分片,有如下过程:


  1. 部署好 config server 和 mongos;

  2. 连接 mongos,将原有的副本集添加到集群,该副本集将会成为第一个分片;

  3. 部署好其他副本集,也添加到集群中;

  4. 修改客户端配置,所有访问入口改为 mongos;

  5. 选择片键,启用分片。


注:在已存在的集合中进行分片,需保证片键上有索引,如果没有,需要先创建。


MongoDB 分片中有个非常重要的组件叫均衡器(balancer),实际上是由 mongos 去扮演这个角色的。均衡器负责块(chunk)的迁移,它会周期性检查分片之间块的均衡情况,如不均衡,就开始块迁移。块的迁移并不影响应用程序的访问与使用,在迁移之前,读写都会请求到旧的块儿上。如果元数据更新完成,那所有试图访问旧位置数据的 mongos 进程都会得到一个错误,这些错误对客户端是无感知的,mongos 会静默处理掉这些错误,在新的分片上重演一次。


这里大家可能会产生一个误区 — 分片依据数据大小,请记住,分片间衡量均衡的标准是块的数量,并非是数据的大小。


有些场景下块迁移也会导致影响性能,比如使用热点片键时,因为所有的新块都在热点上创建,系统就需要处理源源不断写入到热点分片上的数据;再比如向集群添加新的分片时,均衡器触发一系列迁移过程。


说半天我们还不知道块迁移是怎么做的,简单整理过程如下:


  1. 均衡器的进程向源分片发送 moveChunk 指令;

  2. 源分片开始移动块,期间在此块上的所有操作都路由到源分片上;

  3. 目标分片创建源分片上所有的索引,除非目标分片上已有索引;

  4. 目标分片开始请求块中的文档并接收数据副本;

  5. 在接收完最后一条文档之后,目标分片开始同步移动块期间产生的所有变化;

  6. 当完全同步之后,目标分片更新配置服务器的元数据(块的新地址);

  7. 更新完元数据,确认块儿上没有打开的游标,源分片就会删除数据副本。


上面介绍了 MongoDB 架构及分片的过程,但其实 MongoDB 分片中最重要的环节就是正确选择片键。何为片键?集合中选择一或两个字段进行数据的拆分,这个键叫作片键。我们应该分片初期选好片键,运行之后修改片键非常难。


如何选好片键?我们首先从数据分发的角度去分析一下片键,数据分发常用方式:


  • 升序片键:随着时间稳定增长的键,比如 date、或 ObjectId。


** MongoDB 本身没有自增主键。

现象:新增数据集中在某一个分片上

弊端:MongoDB 忙于处理数据的均衡


  • 随机分发的片键:数据集中没有规律的键,如用户名、MD5 值、邮件地址、UUID 等


现象:各分片增长速度基本相同,减少迁移的次数

弊端:随机请求数据超出可用内存大小时效率不高


  • 基于位置的片键:此处”位置”是抽象的概念,如 IP 地址、经纬度或者地址。


现象:所有与该键值接近的文档都会保存在同一范围的块中。


我们还可以根据应用类型不同选择合适的片键,其策略如下:


  • 散列片键(Hashed Shard Key):随机分发。


应用类型:追求数据加载速度快,在大量查询中使用升序键,同时也希望写入数据随机分发

弊端:无法通过散列片键做指定目标的范围查询


注:不能使用 unique 选项,不能使用数组字段,浮点型的值会先被取整


  • GridFS 的散列片键:GridFS 非常适合做分片,因为可以包含大量的文件数据

  • 流水策略:集群中某服务器性能更加(如 SSD),使用标签+升序片键方案让该服务器处理更多的负载


弊端:如果请求超出了强大服务器的处理能力,想要负载均衡到其他服务器并不简单


OK,关于片键我们研究到这里,总而言之,如果选用 MongoDB 分片,从分片初期就根据你的应用类型正确选择片键,这样才能让分片发挥最佳性能,进而你的应用拥有出色的表现能力。


MongoDB 分片的讲解也接近尾声了,最后我们简单看一下自动分片(auto-sharding)和手动分片(pre-spliting)。


auto-sharding

虽然官方说数据迁移操作对读写影响很小,但是在这个过程中可能会把内存中的热数据挤出去,这也就增大了 IO 压力。因此可以考虑平时关闭自动平衡,选择压力小的时间去进行。


并且 mongos 移动 Chunk 是单线程的,单个 mongos 每次只能移动一个块。


pre-spliting

通常也叫 manual-sharding,需要提前关闭 auto balance。在这种场景下我们需要充分了解自己的数据分布情况,对数据进行预先划分,也就是为每个分片划分出合适大小的片键范围,然后配合手动 move chunk 来实现手动分片。


自动分片是一个非常理想的选择,但自动分片在真实的应用场景中还是会有很多的坑,除非我们在这条路上不断踩坑并不断填坑,拥有足够的实力、足够的经验,掌控好其每个细节,那我们不妨可以选择自动分片。但很多公司还是避开这条路,选择手动分片方式,其最大原因就是手动分片可控能力强。


转自:https://mp.weixin.qq.com/s?__biz=MjM5MDAxOTk2MQ==&mid=401869989&idx=1&sn=c46b53ab7066b4728bebd06995ed7e23&scene=2&srcid=0128NFyNwJmymTIumU8UywJj&from=timeline&isappinstalled=0#wechat_redirect