思考并回答以下问题:
今天我们看另外一个NoSQL数据库,也就是大名鼎鼎的MongoDB。
MongoDB是出现得比较早的文档型数据库,应该说早期我们谈到NoSQL的时候,第一个想到的就是MongoDB。到现在,很多公司内部都使用了MongoDB来保存一些偏向文档类型的数据。
今天我们就先来学习一下MongoDB的基本原理,并看看MongoDB是如何保证高可用的。
为什么用MongoDB?
在讨论之前,你要知道我们为什么要用MongoDB?因为在很多情况下用MongoDB能够解决的问题,MySQL同样也可以解决。
就我个人来说,使用MongoDB的决策理由第一个就是灵活性,其次是它的横向扩展能力。
- MongoDB是灵活的文档模型。也就是说,如果我预计我的数据可以被一个稳定的模型来描述,那么我会倾向于使用MySQL等关系型数据库。而一旦我认为我的数据模型会经常变动,比如说我很难预料到用户会输入什么数据,这种情况下我就更加倾向于使用MongoDB。
- MongoDB更容易进行横向扩展。虽然关系型数据库也可以通过分库分表来达成横向扩展的目标,但是比MongoDB要困难很多,后期运维也要复杂很多。而这一切在MongoDB里面都是自动的,你基本不需要操心。
当然,对于一般的中小型公司来说,MongoDB都不是必须使用的存储中间件,大部分时候使用关系型数据库也能应付一般的文档存储需求。
MongoDB的分片机制
当下,跟数据存储和检索有关的中间件基本上都会支持分片。或者说我们步入了分布式时代之后诞生的中间件,基本上都会考虑分片机制。MongoDB也不例外。
在MongoDB里,你可以使用所谓的分片集合(collection)。每一个分片集合都被分成若干个分片,如果按照关系型数据库分库分表的说法,那么集合就是逻辑表,而分片就是物理表。
每个分片又由多个块(chunk)组成。在最新版本的默认情况下,一个块的大小是128MB。
如果一个块满足了下面这两个条件里的任何一个,就会被拆分成两个块。
- 整个块的数据量过多了。比如说默认一个块是128MB,但是这个块上放的数据超过了128MB,那么就会拆分。
- 块上面放了太多文档,这个阈值是平均每个块包含的文档数量的1.3倍。也就是说,如果平均每个块可以放1000个文档,如果当前块上面放了超过1300个文档,那么这个块也会被切分。
这两个条件简单记忆就是数据太多和文档太多。
接下来就是讨论MongoDB的神奇的地方了。在分库分表里面,经常会遇到的一个问题是,不同的表之间数据并不均衡,有的多有的少。所以这就要求你在设计分库分表方案的时候,要尽可能准确预估每一个物理表的数据,确保均衡。
而在MongoDB里面,它会自动平衡不同分片的数据,尽量做到每个分片都有差不多的数据量,整个机制也叫做负载均衡。只不过一般意义上的负载均衡强调的是流量负载均衡,而这里强调的数据量负载均衡。
而发现数据不均匀之后,迁移数据的过程也叫做再平衡(rebalance)。再平衡过程本质上就是挪动块。
那么什么时候才会触发再平衡过程呢?MongoDB设定了一些阈值,超过了这个阈值就会触发再平衡的过程。你可以看一下具体的阈值表。
举个例子,如果一个集合里面最大的分片有9个块,而最少的集合有7个块,那么就会触发再平衡过程。
那块迁移究竟是怎么进行的呢?这里假设我们要迁移块A,这个过程就包含七个步骤。
1,平衡器发送moveChunk命令到源分片上。
2,源分片执行moveChunk命令,这个时候读写块A的操作都是源分片来负责的。
3,目标分片创建对应的索引。
4,目标分片开始同步块A的数据。
5,当块A最后一个文档都同步给目标分片之后,目标分片会启动一个同步过程,把迁移过程中的数据变更也同步过来。
6,整个数据同步完成之后,源分片更新元数据,告知块A已经迁移到了目标分片。
7,当源分片上的游标都关闭之后,它就可以删除块A了。
应该说,这个过程和别的中间件的数据迁移过程都差不多。
MongoDB的配置服务器
当引入了分片机制之后,MongoDB启用了配置服务器(config server)来存储元数据。这些元数据包括分片信息、权限控制信息,用来控制分布式锁。其中分片信息还会被负责执行查询mongos使用。
MongoDB的配置服务器有一个很大的优点,就是就算主节点崩溃了,但是它还是可以继续提供读服务。这和别的中间件不一样,大多数中间件的主从结构都是在主节点崩溃之后完全不可用,直到选举出了一个新的主节点。
但是不管怎么说,配置服务器在MongoDB里面是一个非常关键的组件。甚至可以说,一旦配置服务器有问题,就算只是轻微地性能抖动一下,对整个MongoDB集群的影响都是很大的。
MongoDB的复制机制
你也可以把MongoDB的复制机制理解成MongoDB的主从机制。简单来说,MongoDB的副本也是MongoDB实例,它们和主实例持有一样的数据。在MongoDB里面,用Primary来代表主实例,而用Secondary来代表副本实例。主从实例合并在一起,也叫做一个复制集(Replica Set)。
类似于数据库的读写分离机制,你也可以在MongoDB上进行读写分离。
既然有主从,那么肯定也有主从集群和数据同步。在MongoDB里面,主从之间的数据同步是通过所谓的oplog来实现的,从这一点来看,这个oplog很像MySQL的binlog。
不过在MongoDB里面,oplog是有一些缺陷的。第一个缺陷就是在一些特定的操作里,oplog可能会超乎想象地大。这主要是因为oplog是幂等的,所以任何操作都必须转化成幂等操作。
你可以简单一点儿来理解,任何对MongoDB里数据的操作,最后都会被转化成一个set操作。所以可以预计的是,就算你只是更新了数据的一小部分,但是生成的oplog还是set整个数据。
第二个缺陷是oplog是有期限的。或者说,MongoDB限制了oplog的大小。当oplog占据了太多的磁盘之后,就会被删除。就算某个从节点来不及同步,oplog也是会被删除的。这个时候,这个从节点就只能重新发起一次全量的数据同步了。
写入语义
MongoDB的写入语义和Kafka的写入语义非常像。也就是说你可以通过参数来控制写入数据究竟写到哪里。而写入语义对性能、可用性和数据可靠性有显著的影响。
在MongoDB里面,写入语义也叫做Write Concern,它由w、j和wtimeout三个参数控制。
对于w来说,它的取值是这样的:
- majority:要求写操作已经同步给大部分节点,也是MongoDB的默认取值。这个选项的可用性很强,但是写入的性能比较差。
- 数字N:如果N等于1,那么要求必须写入主节点;如果N大于1,那么就必须写入主节点,并且写入从节点,这些节点数量加在一起等于N;如果N等于0,那么就不用等任何节点写入。这种模式下性能很好,但是你也可以想象到,在这个模式下虽然客户端可能收到了成功的响应,但是数据也会丢失。
- 自定义写入节点策略:具体来说就是你可以给一些节点打上标签,然后要求写入的时候一定要写入带有这些标签的节点。这个在实践中用得比较少,但是如果你们公司采用了的话,非常适合用来面试。
j选项控制数据有没有被写到磁盘上。对于j来说它的取值就是true或者false。
最后一个参数wtimeout,就是指写入的超时时间,它只会在w大于1的时候生效。需要注意的是,在超时之后MongoDB就直接返回一个错误,但是在这种情况下,MongoDB还是可能写入数据成功了。
面试准备
除了上面这些基础知识,你还要在公司内部弄清楚和MongoDB有关的信息。
- 你负责的业务或者你们公司有没有使用MongoDB?主要是用来做什么?
- 为什么你要用MongoDB?用MySQL行不行?
- 你用MongoDB的时候,你的文档支持分片吗?如果支持分片,那么是按照什么来分片的?
- 你用MongoDB的业务有多少数据量,MongoDB上的并发有多高?
- 你们公司的MongoDB是怎么部署的,主从节点有多少?有没有多数据中心的部署方案?
- 你使用MongoDB的写入语义是什么?也就是说w和j这两个参数的取值是什么?
站在你个人成长的角度,我建议你在某个问题可以用MySQL来解决,但是看起来用MongoDB更好的时候,尽量选用MongoDB。尤其是你还没接触过MongoDB的话,正好趁着机会接触一下。
当面试官和你聊到了这些话题的时候,你也可以引导到这节课的内容上。
- Kafka的acks机制,那么你可以引申到MongoDB的写入语义上。
- 其他中间件的对等结构,或者主从结构,你可以引导到MongoDB的分片和主从机制上。
- Kafka的元数据,你可以结合MongoDB的元数据来一起回答。
另外,如果面试官问到了MongoDB数据不丢失的问题,你记得结合前置知识里面的写入语义,参考我在Kafka里分析的思路来回答。你在整个MongoDB的面试过程中,要注意和不同的中间件进行对比,凸显你在这方面的积累。
主从结构
MongoDB的高可用其实和别的中间件的高可用方案不能说一模一样,但是可以说是一脉相承。比如说在MySQL里面,你就接触过了分库分表和主从同步。在Redis里面,你也看到了Redis的主从结构;在Kafka里,分区也是有主从结构的。
所以你要先介绍你启用了主从同步。
我们这个系统有一个关键组件,就是MongoDB。但是在最开始的时候,MongoDB都还没有启用主从,也就是一个单节点的。因此每年总会有那么一两次,MongoDB崩溃不可用。所以我做了一件很简单的事情,就是把MongoDB改成了主从同步,最开始的时候业务量不多,为了节省成本,我们就用了推荐的配置一主两从。这种改变的好处就是当主节点崩溃之后,从节点可以选举出一个新的主节点。
当然在这个回答里面,你可以直接说你所在的公司,用了几个主从节点。要是面试官问到了主从同步,你就回答oplog的内容就可以了。
引入仲裁节点
另外一种面试思路是说你在公司引入了仲裁节点(Aribiter)。所谓的仲裁节点是指这个节点参与主从集群的主节点选举,但是只参与投票,就类似于Elasticsearch里的仅投票节点。
这种机制你在别的中间件里面也见过了。这一类节点的好处就在于它们只参与投票,也就是只关心主从选举,所以只需要很少的资源就可以运行起来。
最开始的时候,我们公司只是部署了一主两从,两个从节点都会同步数据。后面为了进一步提高可用性,我引入了仲裁节点。这些仲裁节点被部署在轻量级的服务器上,成本非常低。在引入了这些仲裁节点之后,就算有一个从节点崩溃了,整个集群也基本没什么影响,因为这个时候还是有足够的节点可以投票。
启用主从模式的配置服务器
在前置知识里面,你也知道了配置服务器是MongoDB中的关键组件,一旦出现问题,对整个集群的可用性都有很大的影响。那么配置服务器能使用主从结构吗?
答案是可以的。
实际上,MongoDB本身也是推荐使用主从结构的配置服务器。
我们MongoDB最开始部署的时候,配置服务器并没有启用主从模式,毕竟当时是想着节省资源。但是后面发现,配置服务器这个对集群的影响太大了,一旦不可用,整个集群就基本不可用了。在这种情况下,我们只好引入了主从结构的配置服务器。目前我们公司的配置服务器本身就有一主两从。
紧接着你可以补充这个做法的好处。
虽然主节点还是存在崩溃的可能,但是在主节点崩溃之后会有主从选举。更加重要的是,在主节点崩溃之后,整个配置服务集群还是可读的。而我们也知道,在一个MongoDB集群里面,元数据也是读多写少的。两者一结合,整个MongoDB集群的可用性就提高了。
多数据中心的主从结构
在主从结构这里,再深入一些的话,就需要聊一聊多数据中心的主从结构。MongoDB里面有一个推荐的架构,你可以看一下示意图。
不过正常来说,大部分公司没那么多资源部署,那么一个简化版本就是用两个数据中心,部署一主三从或者一主两从。
我们公司本身业务规模比较大,对MongoDB的依赖也很严重,所以我们还部署了多数据中心的主从结构。我们有两个数据中心(可以同城,可以异地),其中一个数据中心,部署了一主一从,另外一个数据中心部署了两个从节点。那么万一一个数据中心崩溃了,另外一个数据中心也还是可用的。
然后在主从选举的时候,我们也会倾向于选择和主节点在同一个数据中心的从节点,也就是图里面深黄色的从节点。因为正常来说同一个数据中心内部的从节点,数据会比较新。
同时为了保证在主从选举的时候优先选择同一个数据中心的节点,我们还调整了从节点的优先级。也就是说,我们先把同一个机房的节点都设置成比较高的优先级,那么主节点大概率就会从这个机房选出来。
当然,如果你们公司本身就使用了这种多数据中心的主从结构,那么就可以用你们公司的多数据中心主从结构来回答。
在整个主从结构都面完了之后,你进一步总结一下。
基本上目前主流的这种大型中间件,在提高可用性上用的方法无外乎就是分片和主从结构。除了MongoDB,类似的还有Redis、Elasticsearch、Kafka。
引入分片
那么第二个可以提高可用性的点,就是引入分片。从理论上来说,分片既可以提高性能,又可以提高可用性。
在MongoDB里面,引入分片要比关系型数据库简单多了。你可以直接说你在开发新业务的时候就启用了分片功能。
随着我们业务的增长,我们后面使用MongoDB的时候,都要求开启分片功能,来进一步提高可用性和性能。
另外一种面试思路就是为已有的数据添加分片功能。这个面试思路你要谨慎使用,因为通过我和小伙伴们的经历,我们都一致认为,这个部分比较容易遇到问题。如果你没实际操刀过,面试官问细节你就可能答不上来。
为了进一步提高可用性和性能,我还在我的业务上引入了分片。不过因为现在已经有数据了,所以这算是一个比较危险的操作。因此我在业务低峰期的时候,在运维的协助下,把集合改成了分片集合。
最后也要进一步总结升华一下。
目前来说,支持大数据高并发的中间件基本上也有类似的分片功能。或者说,这一类的中间件明面上都是对等结构,而对等结构里面的每一个“节点”又是一个主从集群。就算是关系型数据库的分库分表,也可以看作是这种对等结构+主从结构的模式。
调整写入语义
就像之前你在MySQL和Kafka里见到的,像这种写入语义一般都有两种调整思路,一个是朝着可用性的角度调整,另一个是朝着性能的角度调整。
这里我们以可用性为例。
最开始的时候,我们遇到过一个Bug,就是数据写入到MongoDB之后,偶尔会出现数据丢失的问题。因为之前我在Kafka上也遇到过类似的问题,所以我就怀疑是不是写入语义没做好。然后我就去排查,果然有发现,在这个数据丢失的场景下,Write Concern的w取值不是默认的majority,而是1,也就是说只需要主节点写入就可以。
很明显,在这种情况下,万一写入之后主节点崩溃了,那么从节点就算被提升成主节点,也没有这一条数据。所以,后面我就把这个改回了majority。同时我还去排查了一个j参数,确认设置成了true。这样一来,数据就不太可能丢失了。
面试思路总结
在前置知识部分,我们了解了用MongoDB的原因,主要是为了灵活的文档模型和易于横向扩展。其次了解了MongoDB在可用性上最基本的机制:分片和复制,还有支撑分片和复制机制的配置服务器。这部分内容很容易引申到数据可靠性的问题上,所以为了以防万一,我们详细讲解了写入语义的问题。最后通过引入主从机制、引入分片和调整写入语义三个主要措施构建了一个高可用方案。
在MongoDB的面试里面,你始终要记住一点,就是把MongoDB和别的中间件进行横向对比,最好的方式是自己总结出一张思维导图或者表格,更加一目了然。
思考题
- 我提到MongoDB的配置服务器,有一个显著特点是主节点崩溃了还可以继续提供读服务,你还见过别的中间件有类似设计吗?
- 现在你来准备一段话,介绍你在接手一个系统后,把原本存储在MySQL中但是更适合放在MongoDB的部分数据,迁移到了MongoDB,你会怎么介绍呢?