mongodb学习系列5:可复制集和分片集群

mongodb,nosql,主从模式,可复制集,分片

Posted by Chris on November 9, 2019

单机的mongod服务相比于分布式的服务,存在比较多的问题。

  • 数据容量问题
    单机的mongodb服务无法存放海量的数据。即使磁盘可以存放,但是CPU、网络IO和磁盘IO可能无法支持。因此,注定了单机的容量是有限制的。
  • 价格问题
    如果单机想要支持海量数据存储,一般的做法是购买价格昂贵的机器,这相比分布式存储还说是比较昂贵的。
  • 数据可靠性
    单机的存储服务存在可靠性问题。例如,单个机器磁盘损坏,数据可能就丢失了。
  • 性能问题
    在海量数据的时候,单机在线程池、CPU、网络IO和磁盘IO都存在性能问题。
  • 可用性
    系统的可用性局限于单台机器。如果单台机器受损,服务就不可用了。
  • 可扩展性
    单机的可扩展性局限于垂直扩展,即购买更加昂贵性能更好的机器,而且垂直扩展的能力是有限的。

在互联网海量数据的场景下,以及更高的可靠性、可用性和性能要求下,采用分布式存储,是通用的做法。
mongodb提供了主从模式、可复制集和分片集群三种方式来支持分布式存储。

1 主从模式

主从模式是mongodb 3.2版本之前提供的分布式方式。从3.2版本之后就废弃了,不支持使用。下面我们看看这种方式的特点。

这种方式下,一个主节点可以有多个从节点。这种构架主要用于缓解主节点的读压力。
主节点:负责所有的写请求,负责部分读请求
从节点:不负责写请求,只负责读请求。从节点会同步主节点的数据。

这种主从模式,在主节点出现故障时,需要手动恢复。已经不推荐使用。

2 可复制集(replica set)

可复制集是mongodb提供的另一种部署模式,可以认为是用来代替主从模式的。一个可复制集里面有一个主节点和多个从节点,有时候还可以有裁判节点。

  • 主节点:常见的情况下,负责集群中所有的写请求,也可以负责读请求。当主节点不可用时,就会有从节点升级为新的主节点。原来的主节点恢复后,就变成了一个从节点。
  • 从节点:常见的情况下,负责集群中的读请求。当集群中的主节点不可用时,就会有从节点升级(通过选举的方式)到主节点。
  • 裁判节点:不负责任何读请求和写请求,不存储数据,只参与集群中的主节点选举。比如,我们为了节省资源,只有2台可供使用的存储节点,那么,就可以引入一个裁判节点,凑够奇数个节点,这样就可以方便集群中选举了。

下图表示了拥有1个主节点和2个从节点情况下,集群的节点间的复制情况和心跳情况。每次客户端向主节点写入数据时,都会写入一个oplog(operation log)。从节点通过读取该oplog,从而同步主节点的数据。每个节点之间都会有心跳检测,当某个从节点发现主节点不可用时,就会选举自己当选为新的主节点,从而发起一轮选举。

下图表示了1个主节点、1个从节点和1个裁判节点的情况。裁判节点只是参与选举过程,并不复制主节点的数据。

除了主从高可用和引入裁判节点,可复制集还可以做到自动故障转移(automatic failover)、设置读写偏好、变更流的订阅和多文档的事务。
自动故障转移:当主节点与其他节点失去联系超过一点时间时(默认配置为10s),其他从节点就会发起选举,选出一个新的主节点。在选举出新的主节点之前,集群不能处理写请求,但是可以根据配置决定是否处理读请求。
设置读写偏好:默认情况下,客户端从主节点读取数据。客户端可以设置read preference参数,将读请求发送给从节点。如下图所示。

变更流的订阅:从3.6版本开始,可复制集和分片集群提供了变更流,用于应用程序监听集合的数据变更。当数据库中有任何数据发生变化,应用端都可以及时得到通知。
多文档事务:从4.0版本开始,在可复制集范围内支持事务,但只支持对现有collections的增删查改操作及一些基本的信息查询操作。

3 分片集群

mongodb通过分片的方式来支持水平扩展。分片集群的结构如下图所示。

shard:分片用于存储数据,每个分派只是存储一部分数据。分片可以部署为一个可复制集。
mongos:mongos起着路由服务的作用,用于路由应用客户端的读写请求到具体的分片中。
config server:配置服务器,存储了集群的元数据和配置信息。从3.4版本开始,config server必须部署为一个可复制集。

先约定一个概念,下文中分片键值指文档的分片键所对应的的值。例如文档为{"userId":1001,"username":"chris"},分片键选择为userId,那么分片键值就是1001。

分片键

为了将文档分布到某个shard,需要使用到分片键。分片键可以是文档的某个键或者是多个键(分片键为多个键的时候,叫做组合键)。分片键的选择,关系到该文档被分布到哪个分片中。分片键选择得好,则文档被均匀分布到各个分片中,此时分片集群的性能和扩展性都比较好。如果选择得不好,则文档可能会只分布到某一个分片中,其他的分片无法起到作用,这样就达不到分片集群的效果。

一旦选择了文档的分片键,则后面无法更改。例如,选择了用户的userId来作为分片键,则后面一直得用userId来作为分片键。因此,分片键的选择得特别注意。

注意:虽然我们不能更改分片键,但是,4.2之后的版本,允许我们更改文档的分片键的值(除非分片键是不可变的_id),从而让该文档重新分布,来达到均匀的效果。

chunk

mongodb将分片划分为多个chunk,每个chunk都有分片键的最小值和最大值。如下图所示。

chunk的默认大小为64M。当存储的数据大小超过64M之后,则分裂为2个chunk。如下图所示。64.2M的chunk超过了64M,则分裂为2个更小的chunk。这个过程叫做分割

为了让每个分片包含的chunk数量大致相等,mongodb有个后台运行的均衡器(balancer),用于在分片之间迁移分片。如下图所示,shard C只有一个chunk,而shard A和B都拥有3个chunk,则从shard B迁移一个chunk到shard C。这个过程叫做迁移

分割和迁移是保持shard集群平衡的重要机制。由于分割和迁移都是会影响性能的,因此,尽量让数据分布均匀来减小分割和迁移的几率则显得很重要。接下来我们讲分片策略是怎么将文档分布到shard或者chunk的。

分片策略

分片策略决定了某个文档是存储到chunk 1还是chunk 2。mongodb支持两种分片策略:哈希分片策略和范围分片策略。

哈希分片策略
哈希分片策略是指,计算每个分片键值的哈希值,然后根据哈希值的大小将文档放入对应的chunk中。如下图所示。分片键x对应的值分别为25,26和27。根据哈希函数处理之后,分别进入chunk1,chunk4和chunk3。

哈希分片策略能够将哈希键值很接近的文档分布到不同的chunk中,可以做到很均匀的分布文档。但是,如果我们希望根据哈希键进行范围查询,比如查询条件为25<= x <= 27,则mongodb不知道哪个chunk存储有对应的文档,它不得不查询所有的chunk。这叫做广播操作(broadcast operation)。广播操作的性能很显然没有直接查询单个chunk的性能好。

范围分片策略
范围分片策略是指,将分片键值接近的文档,尽量分布到相同的chunk中。如下图所示,key为26、27和28的文档都分布到了chunk2中。

范围分片策略可以将分片键值接近的文档分布到同一个chunk中,对于根据分片键查询的范围查询很友好。但是,如果分片键值的频率分布不均匀,或者分片键值是单调递增的,则会导致数据总是读写某几个chunk,从而导致性能问题。 范围分片策略同时满足如下条件时效果较好:

  • 分片键值的基数尽量大。例如有[1,2,3]比只有[1,2]的效果要好。
  • 分片键值的分布尽量均匀。例如[1,2,3]出现的频率是相同的
  • 非单调变化的键值。如果将要插入的文档的分片键值分别是[1,2,3],则它们都会写入同一个chunk,从而导致性能问题。

4 如何选择分片键和分片策略

分片键和分片策略的选择,对于数据分布到哪个chunk显得非常关键,因此,我们需要讨论一下如何选择分片键和分片策略。下面看看选择分片键会碰到的3个问题。

热点chunk的问题

糟糕的分片键会导致所有的读或写操作都在单个chunk上,这会导致该chunk所在的服务器不堪重负,而其他服务器则闲置一旁。很显然,这会造成读写的性能问题。
例如,我们选择分片键为_id字段,{"_id": 1},选择范围分片策略。由于_id字段是ObjectId类型,是严格递增的,因此,每次写入数据的时候,都会写入同一个chunk。下图中分片键值为26、27、28的文档都写入了相同的chunk中。

chunk不可分割的问题(分片键相同的问题)

当很多文档都有相同的分片键时,会导致这些文档都存在于一个chunk中,相同的分片键值导致该chunk无法被分割,该chunk只会一直无限增大。很显然,这会限制mongodb均匀分布chunk的能力。
例如,我们选择customerId作为分片键,{"customerId":1},选择某个分片策略。假设我们30%的数据都是某个大客户产生的,那么,该大客户的数据都会放到一个chunk下面。因为该chunk下面分片键值都相同,所以无法进行chunk的分割,会影响chunk的迁移,影响mongodb均匀分布数据的能力。
下图中,chunk1已经达到了2048M,但是,由于分片键值都相同,所以无法进行chunk的分割。极端情况下,数据全部由chunk1来支持读写,就变成单机的情况了。

定位糟糕的问题

如果我们的查询条件与分片键没有关联,则会导致该查询变为广播操作,即该次查询会查询所有的分片然后汇总数据。很显然,这会影响性能。
例如,我们选择某个完全随机的分片键A,写数据时,可以做到均匀分布到chunk中,也不会有分片键值大部分相同的问题。这种方式对于写请求很好。但是,如果我们根据另一个键B来查询数据数据时,则不得不进行广播操作了。 如果写操作要求性能,读操作不要求性能,例如我们需要保存一批传感器数据,后期异步批量进行处理,则可以忽略定位糟糕的问题。

理想的分片键

同时考虑到以上3个因素:热点chunk问题、chunk不可分割问题和定位糟糕问题,一种解决方式是考虑使用组合键。
例如经常用于查询条件的键是customerId,那么,组合键的第一个键是customerId。为了防止大客户的数据都落入某个chunk中,考虑加上第二个键_id。这样分片键就变为{"customerId":1, "_id":1}。由于customerId不是递增的,因此,不会导致所有的读写请求都落入一个chunk中。

实际上,不存在一劳永逸的分片键,分片键的选择得根据具体的场景来分析和选择。以上提到的3个问题是比较常见的,选择分片键时一定要考虑到。

分片策略可以认为是对于分片键的预处理,然后分布到chunk中。范围分片策略没有对原始的分片键值进行处理;哈希分片策略先用哈希函数对分片键值进行处理然后分布到chunk中,哈希函数的作用是让处理后的分片键值更加均匀。在实际选择分片策略的时候,需要根据具体的业务场景,考虑数据分布均匀和范围查询的影响,来选择分片策略。

5 参考

master-slave
用大白话聊聊分布式系统
MongoDB 4.0 多文档事务相关
transactions
Data Partitioning with Chunks
MongoDB实战