MongoDB是当前比较流行的文档型数据库,其拥有易使用、易扩展、功能丰富、性能卓越等特性。MongoDB本身就拥有高可用及分区的解决方案,分别为副本集(Replica Set)和分片(sharding),下面我们主要看这两个特性。
1.副本集
有人说MongoDB副本集至少需要三个节点,但其实这句是有问题的,因为副本集中节点最少可以是一台,3.0之前最多12个节点,3.0开始节点数量能够达到50个。但节点数1个或者2个的时候,MongoDB就无法发挥副本集特有的优势,因此我们一般建议节点数大于3个。
首先,我们看一下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副本集本身就持有故障切换(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.0之后副本集成员个数突破到了50个,但12个以上节点开始“大多数”都为7。
MongoDB在下面几个条件触发之下进行选举:
- 初始化副本集时;
- 备份节点无法和主节点通讯时(可能主节点宕或网络原因);
- Primary手动降级,rs.stepDown(sec),默认60s。
接着看下选举的步骤:
- 得到每个服务器节点的最后操作时间戳。每个mongodb都有oplog机制会记录本机的操作,方便和主服务器进行对比数据是否同步还可以用于错误恢复;
- 如果集群中大部分服务器宕机了,保留活着的节点都为 secondary状态并停止选举;
- 如果集群中选举出来的主节点或者所有从节点最后一次同步时间看起来很旧了,停止选举等待人工操作;
- 如果上面都没有问题就选择最后操作时间戳最新(保证数据最新)的服务器节点作为主节点。
有些人可能在设计MongoDB副本集架构过程中会产生成员节点必须是奇数个的误区,MongoDB副本集成员节点数量为偶数个会有问题吗?
上图中我们很清晰的看到,在单机房内不管副本集成员节点数为偶数还是奇数都是没有问题的,但如果是两个机房,每个机房的成员节点数量一致,在两个机房之间心跳中断时,整个集群就会出现无法选举Primary的问题,这就是MongoDB副本集中的脑裂。那如何防止脑裂?从架构角度去看的话,我们如下推荐:
左图为”大多数”成员都在一个数据中心
需求:副本集的Primary总在主数据中心
缺点:如果主数据中心挂了,没有可用Primary节点
右图为两个数据中心成员数量相同,第三个地方放一个用于决定胜负的副本节点(可是仲裁节点)
需求:跨机房容灾
缺点:额外需要第三个机房
所以说,MongoDB副本集成员数量奇数个的说法其实针对的是多机房部署的场景下。
另外,在设计MongoDB副本集的过程中,我们还需要考虑过载的问题,因为过载导致MongoDB数据库性能极差。因此,一定衡量好读取的量,充分考虑读写节点宕机的可能性。
MongoDB副本集还有些同步、心跳、回滚等概念,我简单整理了下。
同步
初始化同步:从副本集中其他节点全量同步一次,触发条件:
- Secondary节点首次加入时;
- Secondary节点落后oplog大小以上的数据时;
- 回滚失败时。
保持同步:初始化同步之后的增量同步
注:同步源并非是Primary节点,MongoDB根据Ping时间选择同步源。选择同步源时,会选择一个离自己比较近的而且数据比自己新的成员。
心跳
- Primary节点是? 哪个节点宕了?哪个节点可以作为同步源? — 心跳来解决;
- 每个节点每2s向其他节点发送心跳请求,根据其结果来维护自己的状态视图;
- Primary节点通过心跳来知道自己是否满足”大多数”条件,如果不满足,它就会退位变成Secondary。
回滚
Primary执行了写请求之后宕机,Secondary节点还没来得及复制本次的写操作,也就意味着新选举的Primary上没有这个写操作。这时候原Primary恢复并成为Secondary时,需要回滚这个写操作以能够重新进行同步。回滚数据量大于300M或者需要回滚的时间超过30分钟,回滚就会失败,必须重新全量同步。
2.分片
分片(sharding)其实就是数据拆分,把数据分散在多个节点上,也就是水平拆分。MongoDB支持自动分片,无论自动分片有多优点或缺点,MongoDB依然拥有该特性而引以为傲。
MongoDB分片适用于如下几个场景:
- 单个服务器无法承受压力时,压力包括负载、频繁写、吞吐量等;
- 服务器磁盘空间不足时;
- 增加可用内存大小,以更多的数据在内存中访问。
如上图所示,MongoDB分片共有三个组件,介绍如下:
mongos:数据库集群请求的入口,起到路由作用,它负责把对应的数据请求请求转发到对应的shard服务器上。生产环境中需要多个mongos。
config server:保存集群和分片的元数据,mongos启动时会加载配置服务器上的配置信息,以后如果配置服务器信息变化会通知到所有的 mongos 更新自己的状态,。生产环境需要多个 配置服务器。也需要定期备份。
shard server:实际存储数据的分片。生产环境要求是副本集。
下面我简单画了一下分片的过程:
在分片之前,可以把一个集合看成是单一整块儿,所有文档都包含在这个块当中。
在选择片键进行分片之后,集合被拆分成为多个数据块儿,这时候第一个块和最后一个块儿中将出现$minKey和$maxKey,分别表示负无穷和正无穷,当然这都是MongoDB分片内部使用的,我们只要了解即可。
接下来拆分出来的数据块儿将会均匀分布到各个节点上。
那有些人可能会疑问块是怎么拆分的?我依然画了4个图来解释。
- mongos记录每个块中的数据量,达到某个阈值,就检查是否需要拆分块;
- 如拆分块,mongos更新config server的块元数据;
- config server诞生新块,修改旧块的范围(拆分点);
- 拆分完毕,mongos重置原始块的追踪以及新建新块追踪;
注:这里的块(chunk)是逻辑的概念,一个 chunk 并不是实际存储的一个页或者一个文件之类,而是仅仅在 config 节点中的元数据中体现。也就是说,拆分块只修改元数据,并不进行数据移动。
拆分块儿的过程也是有隐患的,比如找不到拆分点而导致产生特大块儿,还有配置服务器不可达导致拆分风暴等。
选择片键不合理:mongos发现块达到阈值,然后请求分片拆分块,但分片却找不到拆分点,这样导致块越来越大。
连锁反应:片键不合理–>特大块(无法拆开)–>块无法移动–>造成数据分布不均衡–>进而数据写入不均衡–>进一步加剧了数据分布不均衡
预防:正确选择片键
config server不可达:mongos进行拆分时无法和配置服务器通讯,也就无法更新元数据,这导致一个循环的现象:尝试拆分和拆分失败之间来回切换,进而会影响mongos和当前分片的性能。这种不断重复发起拆分请求却无法进行拆分的过程,叫做拆分风暴(split storm)。
预防:
1) 保证配置服务器的可用状态
2) 重启mongos,重置写入计数器
说了这么多,我们还不知道怎么创建分片,分为两种:
- 从零开始创建分片:一般是新业务上线,架构设计初就选用分片;
- 将副本集转换为分片:服务运行一段时间,单个副本集已无法满足需求,需要转换为分片;
第一种从零开始创建分片没什么可说的,选择好片键尤为关键,第二种副本集转换为分片,有如下过程:
- 部署好config server和mongos;
- 连接mongos,将原有的副本集添加到集群,该副本集将会成为第一个分片;
- 部署好其他副本集,也添加到集群中;
- 修改客户端配置,所有访问入口改为mongos;
- 选择片键,启用分片。
注:在已存在的集合中进行分片,需保证片键上有索引,如果没有,需要先创建。
MongoDB分片中有个非常重要的组件叫均衡器(balancer),实际上是由mongos去扮演这个角色的。均衡器负责块(chunk)的迁移,它会周期性检查分片之间块的均衡情况,如不均衡,就开始块迁移。块的迁移并不影响应用程序的访问与使用,在迁移之前,读写都会请求到旧的块儿上。如果元数据更新完成,那所有试图访问旧位置数据的mongos进程都会得到一个错误,这些错误对客户端是无感知的,mongos会静默处理掉这些错误,在新的分片上重演一次。
这里大家可能会产生一个误区 — 分片依据数据大小,请记住,分片间衡量均衡的标准是块的数量,并非是数据的大小。
有些场景下块迁移也会导致影响性能,比如使用热点片键时,因为所有的新块都在热点上创建,系统就需要处理源源不断写入到热点分片上的数据;再比如向集群添加新的分片时,均衡器触发一系列迁移过程。
说半天我们还不知道块迁移是怎么做的,简单整理过程如下:
- 均衡器的进程向源分片发送moveChunk指令;
- 源分片开始移动块,期间在此块上的所有操作都路由到源分片上;
- 目标分片创建源分片上所有的索引,除非目标分片上已有索引;
- 目标分片开始请求块中的文档并接收数据副本;
- 在接收完最后一条文档之后,目标分片开始同步移动块期间产生的所有变化;
- 当完全同步之后,目标分片更新配置服务器的元数据(块的新地址);
- 更新完元数据,确认块儿上没有打开的游标,源分片就会删除数据副本。
上面介绍了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来实现手动分片。
自动分片是一个非常理想的选择,但自动分片在真实的应用场景中还是会有很多的坑,除非我们在这条路上不断踩坑并不断填坑,拥有足够的实力、足够的经验,掌控好其每个细节,那我们不妨可以选择自动分片。但很多公司还是避开这条路,选择手动分片方式,其最大原因就是手动分片可控能力强。