更多关于MongoDB的技术分享请关注公众号:mongodb_side
原创文章 部分内容取自官方Release Notes(3.0-rc8)
作者 shingo(6623662005@163.com)
原计划12底发布的MongoDB2.8直接改为在3月份发布3.0了,原因是WiredTiger团队的加入让MongoDB的各项性能指标有了质的飞跃,最大的改变就是引入了WiredTiger存储引擎并带来了行级锁。
MongoDB的CTO Eliot Horowitz连发3篇博客来说明为什么直接从2.6跳到3.0。大致意思是本来打算发布2.8版本,后来因为加入wiredTiger引擎,做着做着越来越觉得牛X,他认为这些牛X的东西配得上提升主版本号。
他在博客里说到了下面这句话,足以证明3.0是一个里程碑似的版本。
By drawing on both academic research and their extensive commercial experience, the WiredTiger team built a storage engine that can underpin the next 20 years of data storage applications. |
接下来详细介绍本次更新,有两个点配上了源码说明,受限于篇幅原因,以及手机上糟糕的代码阅读体验,有很多源码没有贴上来,将来会有专门的文章来介绍一些值得说的关键点。
主要更新
可拔插的存储引擎接口
MongoDB 3.0 对外提供可拔插的存储引擎接口,允许将第三方存储引引擎接入到MongoDB。
WiredTiger
MongoDB 3.0 引入了WiredTiger存储引擎,目前MongoDB原生包含两种存储引擎:
MMAPv1, 之前版本使用的存储引擎,也是唯一的存储引擎。
WiredTiger, 只支持MongoDB 3.0 64位版本。
WiredTiger用法
MongoDB 3.0仍然使用MMAPv1做为默认的存储引擎。WiredTiger支持所有的MongoDB特性,要切换到WiredTiger需要在启动mongod进程时声明, 如下:
mongod --storageEngine wiredtiger |
在同一套复制集环境或分版中,允许各成员使用不同的存储引擎,但是性能不能保证,不同的生产环境负载下表现都会不同。
使用3.0的mongo shell或官方最新的客户端驱动包来连接开启了WiredTiger存储引擎的库。目前支持的各语言第三方包版本如下:
语言 | 版本 |
C | 1.1.0 |
C++ | 1.0.0 |
C# | 1.10 |
Java | 2.13 |
Node.js | 1.4.18 |
Perl | 0.708.0.0 |
PHP | 1.5 |
Python | 3.0 |
Motor | 0.4 |
Ruby | 1.12 |
Scala | 3.0 |
WiredTiger在磁盘上存储的索引格式已经升级到3.0-rc5,如果你使用了rc5之前的候选版操作过数据,升级到最新存储格式只需在启动 mongod 进程时加上 –repair。
WiredTigern属性配置
可以在启动mongod进程时将wiredTiger相关的信息声明在命令行里,比如
mongod--storageEngine wiredtiger --wiredTigerJournalCompressor snappy --wiredTigerCacheSizeGB2 --wiredTigerCheckpointDelaySecs 50 |
上面启动命令的意思是使用wiredtiger,将journal文件用snappy压缩,为数据创建2G的缓存空间,数据刷到磁盘文件的时间间隔为50秒。
这些配置可以按照MongoDB推荐的做法写在一个配置文件中,启动mongod进程时指定配置文件。
关于上边的wiredTigerCacheSizeGB多说一句,当使用最近比较火的Docker或类似的容器来部署MongoDB时,这个值要低于容器可使用的内存大小。
WiredTiger 并发和压缩
WiredTiger存储引擎提供行级锁和数据压缩功能。
3.0之前的所有版本,锁的粒度最多到db级,行级锁把MongoDB带到了一个崭新的层面,这意味着可以并行的对同一张表里的N条数据做不同的读写操作,这在以前是不敢想像的,2.6版本时大家都在等待表级锁,wiredtiger直接将MongoDB带到了彼岸。
wiredtiger对表的数据默认使用snappy库进行压缩。而对于索引,默认使用前辍压缩。
MMAPv1存储引擎改进
MMAPv1从3.0版开始支持表级锁。
为了支持多种存储引擎并存,3.0版对一些MMAPv1的配置做了重命名,具体如下
旧属性名 | 新属性名 |
storage.journal.commitIntervalMs | storage.mmapv1.journal.commitIntervalMs |
storage.journal.debugFlags | storage.mmapv1.journal.debugFlags |
storage.nsSize | storage.mmapv1.nsSize |
storage.preallocDataFiles | storage.mmapv1.preallocDataFiles |
storage.quota.enforced | storage.mmapv1.quota.enforced |
storage.quota.maxFilesPerDB | storage.mmapv1.quota.maxFilesPerDB |
storage.smallFiles | storage.mmapv1.smallFiles |
storage.syncPeriodSecs | storage.mmapv1.syncPeriodSecs |
MMAPv1 空间分配策略修改
mmapv1默认的空间分配策略变更为powerOf2Sizes,之前为paddingfactor。如果不想采用任何padding策略可以在创建表时将表设置为noPadding。
powerOf2Size是MongoDB中非常重要的存储空间使用策略,关于powerOf2Size相关代码如下
// 先定义好一系列以2的n次方为结果的数据 // record_store_v1_base.h public: static const int Buckets = 26; static const int MaxAllowedAllocation =16*1024*1024 + 512*1024; static const int bucketSizes[]; enum UserFlags { Flag_UsePowerOf2Sizes = 1 <<0, Flag_NoPadding = 1 << 1, }; // (略去其它代码)… // record_store_v1_base.app const int RecordStoreV1Base::bucketSizes[] = { 0x20, 0x40, 0x80, 0x100, // 32, 64, 128, 256 0x200, 0x400, 0x800, 0x1000, // 512, 1K, 2K, 4K 0x2000, 0x4000, 0x8000, 0x10000, // 8K, 16K, 32K, 64K 0x20000, 0x40000, 0x80000, 0x100000, // 128K, 256K, 512K, 1M 0x200000, 0x400000, 0x600000, 0x800000, // 2M, 4M, 6M, 8M 0xA00000, 0xC00000, 0xE00000, // 10M, 12M, 14M, MaxAllowedAllocation, // 16.5M MaxAllowedAllocation + 1, // Only MaxAllowedAllocation sized records go here. INT_MAX, // "oversized" bucket for unused parts of extents. }; |
接下来截取分配空间的关键代码
// record_store_v1_simple.h // 在这里返回shouldPadInserets(),表示是否需要对空间做冗余处理 virtual bool shouldPadInserts() const { return !_details->isUserFlagSet(Flag_NoPadding); } // record_store_v1_base.app // 注意下边标红部分的关键代码 Status RecordStoreV1Base::setCustomOption( OperationContext* txn, const BSONElement& option, BSONObjBuilder* info ) { const StringData name = option.fieldNameStringData(); const int flag = (name == "usePowerOf2Sizes") ? Flag_UsePowerOf2Sizes : (name == "noPadding") ? Flag_NoPadding : 0; if (flag) { bool oldSetting = _details->isUserFlagSet(flag); bool newSetting = option.trueValue(); // (略去其它代码)… // record_store_v1_base.app int RecordStoreV1Base::quantizeAllocationSpace(int allocSize) { invariant(allocSize <= MaxAllowedAllocation); // 减2去掉两个高位 for ( int i = 0; i < Buckets - 2; i++ ) { if ( bucketSizes[i] >= allocSize ) { Return the size of the first bucket sized >= the requested size. return bucketSizes[i]; } } invariant(false); prior invariant means we should find something. } |
以上代码描述了powerOf2Size的逻辑,简而言之就是将insert来的数据和2的n次方比大小,最接近哪个值就分配多少空间,这种策略主要是防止数据修改引起的data move。
下面截取了Redis字符串空间扩展的代码做为对比,原理是:当发生Append操作导致字符串超过原有空间时,新开辟的空间大小为
n = (原长度 + 增加的长度)*2 + 1
// sds.c sds sdsMakeRoomFor(sds s, size_t addlen) { struct sdshdr *sh, *newsh; size_t free = sdsavail(s); size_t len, newlen; if (free >= addlen) return s; len = sdslen(s); sh = (void*) (s-(sizeof(struct sdshdr))); // 新空间长度为原长度加新增长度 newlen = (len+addlen); // 这个SDS_MAX_PREALLOC为定长1M 字符串长度超过1M后就按照2倍增加了,只增加1M if (newlen < SDS_MAX_PREALLOC) newlen *= 2; else newlen += SDS_MAX_PREALLOC; newsh = zrealloc(sh, sizeof(struct sdshdr)+newlen+1); if (newsh == NULL) return NULL; newsh->free = newlen - len; return newsh->buf; } |
复制集
增加复制集成员数量
3.0版的复制集最大可以支持50个成员,之前的版本不能超过12个成员,最多只能有7个可投票成员。
相关信息可以参见我之前的文章《MongoDB的限制和门槛标准》
以下驱动支持大复制集:
C# (.NET) Driver 1.10
Java Driver 2.13
Python Driver (PyMongo) 3.0+
Ruby Driver 2.0+
Node.JS Driver 2.0+
replSetStepDown操作更新
replSetStepDown命令用来将复制集中当前主节点降级为从节点,现在的replSetStepDown命令
db.runCommand( { replSetStepDown:<seconds> , secondaryCatchUpPeriodSecs: <seconds>, force:<true|false> } ) |
各参数说明如下
参数名 | 类型 | 描述 |
replSetStepDown | 数值 | 可选。这个值的单位是秒,意思是在指定的秒级时间内防止当前被replSetDown的节点重新竞选为主节点。如果未设置这个参数,默认为60秒。 |
force | 布尔 | 可选。2.0版之后的新参数,强制将主节点降级为从节点,即使当前没有合适的从节点(比如没有任何从节点数据同步完成) |
secondaryCatchupPeriodSecs | 数值 | 可选。在replSetDown命令发出后,可以容忍从节点赶上主节点数据进度的时间。如果时间截止时仍然没有任何从节点同步完最新数据,那么此次replSetDown失败,除非指定了force : true。 这个参数默认值为10,如果没有指定force:true,默认值为0。 |
另外,在replSetStepDown命令发出后,MongoDB会试图终止一些长时间执行的非系统操作,例如创建索引或map/reduce任务。
复制集的其它更改
初始化同步时创建索引更有效率,使用多线程批量同步oplog数据。
w: “majority”从原先的“大多数节点”更改为“大多数可投票节点”。
对Replica Set实行更加严格的配置,集中在一些细的属性调整上比如投票数,名称等。
2.6.6及以前的版本不能以3.0版的成员为初始化同步目标,即不要将2.6.6版本的节点接入到含3.0版成员的复制集中。
对于已经存在于子节点上的表, MongoDB 3.0不会再自动创建_id索引。
关于w:majority的源码解析在将来我会单写一篇来说明,MongoDB针对getLastError的响应主要用到了“链式同步”的方式。
篇幅有限,这里只贴一部分入口代码:
// write_concern.capp Status waitForWriteConcern( OperationContext* txn, const WriteConcernOptions& writeConcern, const OpTime& replOpTime, WriteConcernResult* result ) { // (略去部分代码)… result->syncMillis = syncTimer.millis(); Now wait for replication if (replOpTime.isNull()) { no write happened for this client yet return Status::OK(); } needed to avoid incrementing gleWtimeStats SERVER-9005 if (writeConcern.wNumNodes <= 1 && writeConcern.wMode.empty()) { no desired replication check return Status::OK(); } // 这里的awaitReplication方法处理writeConcern的信息,按指定个数返回。 repl::ReplicationCoordinator::StatusAndDuration replStatus = repl::getGlobalReplicationCoordinator()->awaitReplication(txn,replOpTime,writeConcern); // replication_coordinator_impl.cpp // _awaitReplication_inlock 通过一个while循环来处理复制集的子节点返回 // 这里只贴了方法签名,因篇幅原因,具体处理的代码太长,将来单写一篇具体描述复制集的GetLastError ReplicationCoordinator::StatusAndDuration ReplicationCoordinatorImpl::_awaitReplication_inlock( const Timer* timer, boost::unique_lock<boost::mutex>* lock, const OperationContext* txn, const OpTime& opTime, const WriteConcernOptions& writeConcern) |
分片集群
MongoDB 3.0 对于分片集群有以下几个方面的增强:
增加一个新的方法sh.removeTagRange() ,与原有的sh.addTagRange()方法一同来管理shard标签。
mongos在执行读操作时不会再将连接绑到复制集成员上,在每次操作完成后,连接会被归还到连接池中,所以每次读操作mongos都会根据当前的规则重新评定连接的目标机器。
为balancer、moveChunk、cleanupOrphaned命令加上write concern设置,因为在chunk迁移时在复制集环境会有写入和同步的问题需要考虑。.
sh.status()现在会包含一些balancer的状态信息,这些信息将有助于排查故障,比如常会出的一各分片不均衡。新增的与balcancer相关的信息如下:
balancer: Currently enabled: yes Currently running: yes Balancer lock taken at Wed Dec 10 2014 12:00:16 GMT+1100 (AEDT) by Pixl.local:27017:1418172757:16807:Balancer:282475249 Collections with active migrations: test.t2 started at Wed Dec 10 2014 11:54:51 GMT+1100 (AEDT) Failed balancer rounds in last 5 attempts: 1 Last reported error: tag ranges not valid for: test.t2 Time of Reported error: Wed Dec 10 2014 12:00:33 GMT+1100 (AEDT) Migration Results for the last 24 hours: 96 : Success 15 : Failed with error 'ns not found, should be impossible', from shard01 to shard02 |
安全改进
MongoDB 3.0包含以下安全改进:
增加SCRAM-SHA-1认证机制,这是一个混淆认证机制,它满足部署级别的challenge-response认证机制的需求。
为Localhost Exception增加限制,限制连接只能在admin库中增加第一个用户,而之前的版本可以做任何操作。
移除db.addUser()方法。
用—sslAllowConnectionsWithoutCertificates代替—sslWeakCertificateValidation,相应的,如果放将放置到配置文件中,用net.ssl.allowConnectionsWithoutCertificates代替net.ssl.weakCertificateValidation,老的名称依然有效,但为了长期的兼容性考虑,建议使用3.0之后的新参数名。
彻底不兼容2.4版本的用户模型,2.4的用户请升级到2.6或3.0。
3.0之前支持的认证机制如下
MONGODB-CR | MongoDB实现的challenge-response认证机制 |
MONGODB-X509 | MongoDB SSL证书认证 |
PLAIN | plain认证方式,只能在企业版使用 |
GSSAPI | gss-api,只能在企业版使用 |
其它改进
全新的db.collection.explain()方法
签名如下
db.collection.explain().<method(...)> |
用法举例
db.products.explain().remove( { category: "apparel" }, { justOne: true } ) |
explain的返回值中主要包括三大块"queryPlanner", "executionStats", 和 "allPlansExecution",这也是和以前版本不同的。
日志增强
为了提高故障诊断时日志的可用率,MongoDB按模块将日志分类,3.0版定义了日志的系统级别和信息显示级别。日志显示格式如下:
<timestamp> <severity> <component> [<context>] <message> |
第二个参数severity即系统日志级别,目前支持有:
级别 | 描述 |
F | Fatal |
E | Error |
W | Warning |
I | Info |
D | Debug |
举例说明
2014-11-03T18:28:32.450-0500 I NETWORK [initandlisten] waiting for connections on port 27017 |
工具增强
所有的MongoDB工具现在都用go语言来改写,并且做为单独的项目来维护。
更小的二进制文件。
mongorestore 可以使用一个新的参数–numParallelCollections来指定同一时间内操作的表的数量。
执行mongodump操作时,使用新参数-excludeCollection和--excludeCollectionsWithPrefix排除不需要的表。
mongorestore现在可以直接从标准输入中接收BSON格式的数据。
mongostat和mongotop可以通过加上—json参数来得到JSON格式的输出。
mongoimport、mongorestore和mongofiles现在也可以加上write concern设置,默认为majority。
索引
当dropDatabase、dropIndexes、drop发生时,后台创建索引不再会自动中断。
对于3.0版的mongod进程,如果在后台创建索引时进程终止,在mongod重新启动时这个后台创建索引的工作将会被拿到前台重新执行,这期间出了任何错误mongod进程都会退出。可以在命令行加上--noIndexBuildRetry来防止故障重启时重新创建索引。
如果createIndexes命令中包含多个索引,只会扫一次表,如果其中有某 一个索引在前台创建那所有这条命令中的其它索引都会在前台创建。
对于分片表,如果查询条件中包含shard key,索引可以覆盖查询。
查询增强
aggregate()框架 增加$dateToString运算符提供更方便的方式将日期转为字符串。
增加$eq查询操作符,用来判断相等的场景 。