“2015 年 MongoDB 在年初发布了 3.0,标志一个新的里程碑,分离 Server 层和 Storage 层,并引入了 WiredTiger 存储引擎,基于此,我们也才敢再把 MongoDB 捡起来用。” —— 毕洪宇

存储引擎的发展

MongoDB 拆分后的架构图如下

下面先从存储引擎层开始介绍一下一些改进点,现在已知支持的存储引擎包括:MMAP,WiredTiger,RocksDB,Memory 以及 Encryption 等。

MMAP

在 3.0.x 的时候依然还是默认的存储引擎,新的改进就是在并发方面,由原来的 database-level 锁转为 collection-level 锁,不过在 3.2 以后默认的存储引擎替换为 WiredTiger。

WiredTiger

应该是 MongoDB 2015 年最大的亮点了,内部架构图:

那么它有哪些比较突出的 feature 呢?

Document-level concurrency control

MongoDB 早期的锁粒度是简单粗暴的,instance-level,database-level 这也是一直被吐槽的地方;有一些场景为了适应这么粗粒度锁拆了一堆数据库出来,如果没有自动化管理配合的话,运维压力非常大。而 document-level 并发控制的引入使得 MongoDB 的并发能力得到极大的释放。

这里的数据也是存多版本的,有些类似 RDBMS 中 MVCC ( multi version concurrency control ),来实现同一行的读写之间的并发访问,进一步提高系统的并发访问能力。

Compression

WiredTiger 支持两种压缩方式 snappy ( 默认 ) 和 zlib;

官方的测试效果:

我们一个业务场景之前在 2.6 上,1.18 Billion 记录数占用磁盘空间 3.12 TB,而升级到 3.0.x 后空间占用降到了 270 GB,使得 PCIE/SSD 的使用更划算。

我们在 MongoDB 3.0 正式生产之前使用 YCSB 进行了简单的压测:

Write-only 测试,standalone 模式

同样的 workload,enable 复制集

以及 read-only 场景

在压测过程中,也遇到了一些问题,比如会有短暂的 stall,以及 replication enable 后 Primary 的写入吞吐量会降低非常多,这些都是 MongoDB 在和 WiredTiger 融合后存在的一个不足之处。

WiredTiger 在 eviction,checkpoint 和 capped collection 的处理算法还在持续改善中,感兴趣的可以在 JIRA(WT-1788,SERVER-16736)上跟进。我们也在使用过程中也发现一些问题,也算是接下来对 MongoDB 2016 的一个展望。

另外,关于 WiredTiger 内部也有很多 internal 参数可以调整,这部分基本上在压测时没有调整,只是把 eviction worker min,max 都设置为 4。

RocksDB

该存储引擎是 facebook 开源的,写入强劲 NoSQL Storage ( http://rocksdb.org/ ),不过对于 MongoDB 来说是非官方 built-in 支持的存储引擎 ( https://github.com/mongodb-partners/mongo-rocks ),有朋友在测试写入性能比较野性。

但是 WiredTiger 本身也支持 LSM option (默认是 btree ),可以通过 internal 的参数在创建表时指定,我简单测了一下 LSM 方式的写入也是很强,感兴趣的朋友可以深入 benchmark 下。

不过,关于 WiredTiger 的 LSM 特性和 MongoDB 的结合还在进行中,所以现在使用是修改 undocument parameter,并且也存在比如 memory leak 的 Bug,这里只是 preview test,并没有生产环境使用。

Memory

企业版存储引擎,非开源,有些类似于 MySQL 的 memory engine,也是表级锁;这里简单提下感兴趣的朋友请参考 SERVER-1153。

复制集改进

Replication protocol 在 3.2 之后改进了 Replication Election/Consensus 算法,通过配置协议版本来指定。主要改进点:

引入 election ID 来加速 election progress,在这之前每轮选举无法给两个节点投票,并且每投票一次需要等待 30 秒才能进行下一轮,而引入 election ID 后可以加速这个进程,从而降低 MTTR( mean time to recovery )。简单来说 election ID 在每次“election attemp”的时候递增,用来区分每一轮的选举。 利用已有的复制通道完成心跳检测。在这之前复制集的每个节点默认每 2 秒会给集群中的每个节点发送心跳,这显然是不 scalable 的,因为这样会随着集群的增加导致“heartbeat storm”增加集群的 overhead。而采用新的算法每个节点只需要和上下游节点通过在 oplog 中写入额外的元数据来进行心跳检测,从而提高检测速度。 引入参数 election timeout。定义为:Node calls for an election when it hasn’t heard from the primary within the election timeout。针对具体的网络环境来做 trade-off,如果网络环境比较差,可以提高该值减少 false detection,反之减小。

通过以上,可以进一步保证 MongoDB 的 MTTR。关于复制集另外一个改进,加入了 read concern。

Read Concern

在 3.2 之前复制集只支持 Write Concern,因此如果想做到强一致读的话只能将 Read_Preferanence 设置为 Primary,否则依然会导致 stale read。而 read concern 的引入弥补了这个不足。类似 CAP 中 W + R > N。

自动分片机制

对于自动分片的话,最大的一个改进是 Config server 支持复制集模式。Config Server 在 3.2 开始支持复制集模式,在这之前 Config Server 彼此是无感知的,不但可能会出现一致性问题,扩容和迁移都是非常痛苦,尤其是更换 hostname 的情况下。而在 3.2 之后 Config Server 完全可以利用复制集的特性了,Write Concern 是 Majority 同时 Read_Preference 是 Primary 来保证一致性和 HA。

其他新特性介绍

下面介绍一些其他特性

Batch Index

在 MongoDB 中使用 createIndex 创建索引,如果想在一个表批量创建多个索引 MongoDB 提供了一个方便的命令:createIndexes (https://docs.mongodb.org/manual/reference/command/createIndexes#dbcmd.createIndexes)。不过在 3.0 之前如果通过该命令给一个表创建多个索引的话,每个索引都需要进行一次FullTableScan 这样显然是不高效的;在 3.0 之后,该命令只需要进行一次 FullTableScan 即可完成对所有索引的创建。

Partial Index

假设简单场景:一个流单系统表里包含两个字段 ( isDone default 0, add_time ),已经处理过的 isDone 会被更新为 1,在流单取数的时候会跑如下 query : {isDone : 1, add_time : {$ge : xx, $le : xx}} 并且 isDone = 1 的记录总占比是小于1%的,因此创建索引 {isDone : 1, add_time : 1} 可以使得取数避免全表扫描,但是确浪费了存储大量 isDone = 0 的索引空间。在 3.2 之后 MongoDB 支持在创建索引时带一个过滤表达式 partialFilterExpression 来实现 partial index 的功能。在该场景创建索引如下

createIndex ( {isDone : 1, add_time : 1}, { partialFilterExpression : {isDone : 1} } )

这样索引就只会索引 isDone = 1 的记录了 。

Document Validator
MongoDB 的 schema free 是 MongoDB 刚推出时的一个卖点,不过估计已经被吐槽过无数次了。不一致的 schema,奇形怪状的数据等等,因此 data validaton的事情被推送到程序端来负责,使得这部分就变得复杂。

所以在 3.2 MongoDB 推出了 Validator。可以通过 $type ( 限制字段类型 )和 $exists ( 限制字段必须存在 )即可简单达成 schema 的限制,字段类型等。这个特性有些类似于 RDBMS 中的 check constraint。

文档链接:https://docs.mongodb.org/manual/core/document-validation/

Vadidator 带有 2 个参数 validationAction 和 validationLevel 来控制在违背约束时的行为

Join

这个特性有一个小八卦,这个特性在 3.2 release 的时候是被宣布为 enterprise feature,然后社区就开始吐槽了:

包括上面的 memory engine 被告知 enterprise feature 后也是有各种吐槽的,可以去看下 jira 和 mail list。

在 3.2 release 两周后,MongoDB CTO 宣布该 feature 开源,也算是比较折腾的了,闲言少叙功能描述如下:

本质就是 left outer join

更详细的使用请查看文档 https://docs.mongodb.org/manual/reference/operator/aggregation/lookup/

Q & A

1、在一些涉及到地理位置的场景,例如陌陌附近的用户(基于地理位置和用户活跃时间排序),MongoDB 对这块也有支持,相较于 PostgreSQL 和 Solr 有什么优势么,该如何选择?
GIS 这块我个人是没有使用经验的,包括 MongoDB 的 GFS feature。我的看法还是团队对于哪个方案更熟悉,可控。网上关于这些的对比很多,不过最终还是得多 benchmark,多看看 maillist 的吐槽,看看能不能接受副作用。我们对于 MongoDB 的使用主要是看重他在非事务场景下的性能和可用性。

2、MongoDB 相比 mysql 优势有哪些?未来可能剃掉关系型数据库吗?
相对优势有 bulit-in failover 支持、可配置的读写分离模式(这里主要指 read_preference,以及 tag-awareness,包括 read concern)、ddl-free(这里避免提schema-free,因为很多场景是需要schema的),MySQL 的大表 DDL 还是问题,当然也是慢慢有解的、auto-sharding 也是比较方面的扩展。至于是否可以替代的话,其实今天我们回顾发现,都在不断的进化,比如 document validator/join 等 feature,包括 queryplan cache 等。而 MySQL 也会不断的改进自身的缺陷。所以我觉得不存在替换的可能。还是需要结合场景。说白了,还是 NoSQL,在事务方面想做到像 RDBMS 这样,还有很长的路要走。

3、MongoDB 做适合的场景是什么? 2016 将会有哪些方向突破?
简单来说非常适合读多写少;因为如果想扩展写入的能力需要引入 auto-sharding,我个人觉得 MongoDB 在 auto-sharding 这块比较重。LVS->proxy->datanode 经典的三层架构运维比较痛。而复制集模式是比较好用的,并且基本上 driver 都支持 topology auto discovery,所以从易用,易运维的角度我更推荐读多写少。
至于 2016 年的方向突破的话,也和 10gen 的聊过,他们有想去掉 mongos 的打算,做到类似 cassandra/redis-cluster 那种无中心的架构。我对此还是有些期待,起码在架构层面上简单一些,当然了,肯定一开始坑是不少的。另外就是 WiredTiger 和 MongoDB 的结合的稳定性还是有待提高的,我觉得性能还是会有更稳定的输出。

4、讲 MongoDB 和 Redis 同属 NoSQL,二者差异怎样,先选型上主要考虑哪些?
稳定的 latency 输出我会选择 Redis,MongoDB 当前还是会存在 spike 点。如果业务层面可以接受,或者通过架构层面来解决,那建议用 MongoDB。因为 MongoDB 天然支持 auto-failover 和读写分离,而 Redis 需要额外的工作。当然 redis-cluster 也可以支持 HA,不过生产使用还是慢慢在踩坑阶段,这方面还没有 MongoDB 成熟。

另外就是功能层面,MongoDB 支持的 API 和 Redis 支持的数据结构的差异。比如 mongodb 的 findAndModify,比如二级索引。而 Redis 支持的比如 HLL, zset,也是用 MongoDB 很难直接替代的。还有就是服务端的线程模型,Redis 是单线程而 MongoDB 是 one-thread-per-connection,这点也需要考虑进去,比如读热数据。

5、对这几年新兴的 newsql 怎么看?
NewSQL 我还是有些关注的,比如 voltdb/nuodb/tiDB 看着都眼馋,不过比较谨慎。太新了,存储一般还是比较重要。对于非核心场景感兴趣的可以试试,我还是持观望学习状态。

6、感觉这个新的存储引擎有些像关系数据库了,MongoDB 是怎么保证比关系数据库性能高的?
简单来说就是做的少,就做得快。我记得 Oracle 大牛 Tom 说过一句话翻译的大概意思是:如果想做一件事儿很快,最快的方法就是不做。当时的场景是讲 SQL parse 的。放到这样一样的道理。毕竟 MongoDB 相对 RDBMS 去掉了事务的支持,也就去掉太多事情了。快是应该的,但是现在和 WiredTiger 还是在不断磨合阶段,稳定输出有待提高。

7、有没有什么 MongoDB 和 ZooKeeper 结合的方案?
MongoDB 和 ZooKeeper 我们还没有结合在一起使用过。因为他的 failover 是基于内部协议的,没有通过 zk 来协调。比如 HBase 包括 Kafka 都在慢慢去掉对 zk 的依赖,使用内部的协议来完成可用性及拓扑发现的 feature。

转载自: 高可用架构 公众微信号