热点 | MongoDB最全面的增强版本 4.4 新特性曝光
MongoDB 在今年正式发布了新的 4.4 大版本,这次的发布包含众多的增强 Feature,可以称之为是一个维护性的版本,而且是一个用户期待已久的维护性版本,MongoDB 官方也把这次发布称为「User-Driven Engineering」,说明新版本主要是针对用户呼声最高的一些痛点,重点进行了改进。
阿里云作为 MongoDB 官方的全球战略合作伙伴,将全网独家上线 4.4 新版本。接下来,数据库 MongoDB 团队针对一些用户关注度比较高的 Feature ,进行深度解读。
可用性和容错性增强
Mirrored Reads
在服务阿里云 MongoDB 客户的过程中,笔者观察到有很多的客户虽然购买的是三节点的副本集,但是实际在使用过程中读写都是在 Primary 节点,其中一个可见的 Secondary 并未承载任何的读流量。
那么在偶尔的宕机切换之后,客户能明显的感受到业务的访问延迟会有抖动,经过一段时间后才会恢复到之前的水平,抖动原因就在于,新选举出的主库之前从未提供过读服务,并不了解业务的访问特征,没有针对性的对数据做缓存,所以在突然提供服务后,读操作会出现大量的「Cache Miss」,需要从磁盘重新加载数据,造成访问延迟上升。在大内存实例的情况下,这个问题更为明显。
在 4.4 中,MongoDB 针对上述问题实现了「Mirrored Reads」功能,即,主库会按一定的比例把读流量复制到备库上执行,来帮助备库预热缓存。这个执行是一个「Fire and Forgot」的行为,不会对主库的性能产生任何实质性的影响,但是备库负载会有一定程度的上升。
流量复制的比例是可动态配置的,通过 mirrorReads 参数设置,默认复制 1% 的流量。
db.adminCommand( { setParameter: 1, mirrorReads: { samplingRate: 0.10 } } )
此外,可以通过db.serverStatus( { mirroredReads: 1 } )来查看 Mirrored Reads 相关的统计信息,
SECONDARY> db.serverStatus( { mirroredReads: 1 } ).mirroredReads
{ "seen" : NumberLong(2), "sent" : NumberLong(0) }
Resumable Initial Sync
在 4.4 之前的版本中,如果备库在做全量同步,出现网络抖动而导致连接闪断,那么备库是需要从头开始全量同步的,导致之前的工作全部白费,这个情况在数据量比较大时,比如 TB 级别,更加让人崩溃。
而在 4.4 中,MongoDB 提供了因网络异常导致全量同步中断情况下,从中断位置恢复全量同步的能力。在尝试恢复一段时间后,如果仍然不成功,那么会重新选择一个同步源进行新的全量同步。这个尝试的超时时间默认是 24 小时,可以通过
replication.initialSyncTransientErrorRetryPeriodSeconds 在进程启动时更改。
需要注意的是,对于全量同步过程中遇到的非网络异常导致的中断,仍然需要重新发起全量同步。
Time-Based Oplog Retention
我们知道,MongoDB 中的 Oplog 集合记录了所有的数据变更操作,除了用于复制,还可用于增量备份,数据迁移,数据订阅等场景,是 MongoDB 数据生态的重要基础设施。
Oplog 是作为 Capped Collection 来实现的,虽然从 3.6 开始,MongoDB 支持通过 replSetResizeOplog 命令动态修改 Oplog 集合的大小,但是大小往往并不能准确反映下游对 Oplog 增量数据的需求,考虑如下场景:
- 计划在凌晨的 2 - 4 点对某个 Secondary 节点进行停机维护,应避免上游 Oplog 被清理而触发全量同步。
- 下游的数据订阅组件可能会因为一些异常情况而停止服务,但是最慢会在 3 个小时之内恢复服务并继续进行增量拉取,也应当避免上游的增量缺失。
所以,在真实的应用场景下,很多时候是需要保留最近一个时间段内的 Oplog,这个时间段内产生多少的 Oplog 往往是很难确定的。
在4.4中,MongoDB支持storage.oplogMinRetentionHours参数定义最少保留的 Oplog 时长,也可以通过 replSetResizeOplog命令在线修改这个值,如下,
// First, show current configured value
db.getSiblingDB("admin").serverStatus().oplogTruncation.oplogMinRetentionHours
// Modify
db.adminCommand({
"replSetResizeOplog" : 1,
"minRetentionHours" : 2
})
扩展性和性能增强
Hidden Indexes
Hidden Index 是阿里云 MongoDB 和 MongoDB 官方达成战略合作后共建的一个 Feature。我们都知道数据库维护太多的索引会导致写性能的下降,但是往往业务上的复杂性决定了运维 MongoDB 的同学不敢轻易的删除一个潜在的低效率索引,担心错误的删除会带来业务性能的抖动,而重建索引往往代价也非常大。
Hidden Index 正是为了解决 DBA 同学面临的上述困境,它支持通过 collMod 命令对现有的索引进行隐藏,保证后续的 Query 都不会利用到该索引,在观察一段时间后,确定业务没有异常,可以放心地删除该索引。
db.runCommand( {
collMod: 'testcoll',
index: {
keyPattern: 'key_1',
hidden: false
}
} )
需要注意的是,索引被隐藏之后只是对 MongoDB 的执行计划器不可见,并不会改变索引本身的一些特殊行为,比如唯一键约束,TTL 淘汰等。
索引在隐藏期间,如果新的写入,也是会被更新的,所以也可以通过取消隐藏,很方便的让索引立刻变的可用。
Refinable Shard Keys
当使用 MongoDB 分片集群时,相信大家都知道选择一个好的 Shard key 是多么的重要,因为它决定了分片集群在指定的 Workload 下是否有良好的扩展性。但是在实际使用 MongoDB 的过程中,即使我们事先仔细斟酌了要选择的 Shard Key,也会因为 Workload 的变化而导致出现 Jumbo Chunk,或者业务流量都打向单一 Shard 的情况。
在 4.0 及之前的版本中,集合选定的 Shard Key 及其对应的 Value 都是不能更改的,在 4.2 版本,虽然可以修改 Shard Key 的 Value,但是数据的跨 Shard 迁移以及基于分布式事务的实现机制导致性能开销很大,而且并不能完全解决 Jumbo Chunk 或访问热点的问题。比如,现在有一个订单表,Shard Key 为 {customer_id:1},在业务初期每个客户不会有很多的订单,这样的 Shard Key 完全可以满足需求,但是随着业务的发展,某个大客户累积的订单越来越多,进而对这个客户订单的访问成为某个单一 Shard 的热点,由于订单和customer_id天然的关联关系,修改customer_id并不能改善访问不均的情况。
针对上述类似场景,在 4.4 中,你可以通过 refineCollectionShardKey 命令给现有的 Shard Key 增加一个或多个 Suffix Field 来改善现有的文档在 Chunk 上的分布问题。比如,在上面描述的订单业务场景中,通过refineCollectionShardKey命令把 Shard key 更改为{customer_id:1, order_id:1},即可避免单一 Shard 上的访问热点问题。
需要了解的是,
refineCollectionShardKey 命令性能开销非常低,只是更改 Config Server 上的元数据,不需要任何形式的数据迁移(因为单纯的添加 Suffix 并不会改变数据在现有chunk 上的分布),数据的打散仍然是在后续正常的 Chunk 自动分裂和迁移的流程中逐步进行的。此外,Shard Key 需要有对应的 Index 来支撑,所以refineCollectionShardKey 要求提前创建新 Shard Key 对应的 Index。
因为并不是所有的文档都存在新增的 Suffix Field(s),所以在 4.4 中实际上隐含支持了「Missing Shard Key」的功能,即新插入的文档可以不包含指定的 Shard Key Field。但是,笔者不建议这么做,很容易产生 Jumbo Chunk。
Compound Hashed Shard Keys
在 4.4 之前的版本中,只能指定单字段的哈希片键,原因是此时 MongoDB 不支持复合哈希索引,这样导致的结果是,很容易出现集合数据在分片上分布不均。
而在 4.4 中支持了复合哈希索引,即,可以在复合索引中指定单个哈希字段,位置不限,可以作为前缀,也可以作为后缀,进而也就提供了对复合哈希片键的支持,
sh.shardCollection(
"examples.compoundHashedCollection",
{ "region_id" : 1, "city_id": 1, field1" : "hashed" }
)
sh.shardCollection(
"examples.compoundHashedCollection",
{ "_id" : "hashed", "fieldA" : 1}
)
有这个新功能之后,会带来很多好处,比如在如下两个场景下,
- 因为法律法规的要求,需要使用 MongoDB 的 zone sharding 功能,把数据尽量均匀打散在某个地域的多个分片上。
- 集合指定的片键的值是递增的,比如在上文中举的例子,{customer_id:1, order_id:1} 这个片键,如果customer_id 是递增的,而业务也总是访问最新的顾客的数据,导致的结果是大部分的流量总是访问单一分片。
在没有「复合哈希片键」支持的情况下,只能由业务对需要的字段提前计算哈希值,存储到文档中的某个特殊字段中,然后再通过「范围分片」的方式指定这个预先计算出哈希值的特殊字段及其他字段作为片键来解决上述问题。
而在 4.4 中直接把需要的字段指定为为哈希的方式即可轻松解决上述问题,比如,对于上文描述的第二个问题场景,片键设置为{customer_id:'hashed', order_id:1} 即可,大大简化了业务逻辑的复杂性。
Hedged Reads
访问延迟的升高可能会带来直接的经济损失,Google 有一个研究报告表明,如果网页的加载时间超过 3 秒,用户的跳出率会增加 50%。所以,在 4.4 中 MongoDB 提供了 Hedged Reads 的功能,即在分片集群场景下,mongos 会把一个读请求同时发送到某个分片的两个副本集成员,然后选择最快的返回结果回复客户端,来减少业务上的 P95 和 P99 延迟。
Hedged Reads 功能是作为 Read Preference 的一部分来提供的, 所以可以是在 Operation 粒度上做配置,当 Read Preference 指定 nearest 时,默认启用 Hedged Reads 功能,当指定为 primary 时,不支持 Hedged Reads 功能,当指定为其他时,需要显示的指定 hedgeOptions,如下,
db.collection.find({ }).readPref(
"secondary", // mode
[ { "datacenter": "B" }, { } ], // tag set
{ enabled: true } // hedge options
)
此外,Hedged Reads 也需要 mongos 开启支持,配置 readHedgingMode 参数为 on,默认 mongos 开启该功能支持。
db.adminCommand( { setParameter: 1, readHedgingMode: "on" } )
降低复制延迟
主备复制的延迟对 MongoDB 的读写有非常大的影响,一方面,在一些特定的场景下,读写需要等待,备库需要及时的复制并应用主库的增量更新,读写才能继续,另一方面,更低的复制延迟,也会带来备库读时更好的一致性体验。
Streaming Replication
在 4.4 之前的版本中,备库通过不断的轮询主库来获取增量更新操作。每次轮询时,备库主动给主库发送一个 getMore 命令读取其上的 Oplog 集合,如果有数据,返回一个最大 16MB 的 Batch,如果没有数据,备库也会通过 awaitData 选项来控制备库无谓的 getMore 开销,同时能够在有新的增量更新时,第一时间获取到对应的 Oplog。
拉取是由单个 OplogFetcher 线程来完成,每个 Batch 的获取都需要经历一个完整的 RTT,在副本集网络状况不好的情况下,复制的性能就严重受限于网络延迟。所以,在 4.4 中,增量的 Oplog 是不断的“流向”备库的,而不是依靠备库主动轮询,相比于之前的方式,至少在 Oplog 获取上节省了一半的 RTT。
当用户的写操作指定了 “majority” writeConcern 的时候,写操作需要等待足够多的备库返回复制成功的确认,MongoDB 内部的一个测试表明,在新的复制机制下,在高延迟的网络环境中,可以平均提升 50% 的 majority 写性能。
另外一个场景是用户使用了Causal Consistency,为了保证可以在备库读到自己的写操作(Read Your Write),同样强依赖备库对主库 Oplog 的及时复制。
Simultaneous Indexing
在 4.4 之前的版本中,索引创建需要在主库完成之后,才会复制到备库上执行。备库上的创建动作,在不同的版本中,因为创建机制和创建方式(前台、后台)的不同,对备库 Oplog 的应用影响也大为不同。
但是,即使在 4.2 中,统一了前后台索引创建机制,使用了相当细粒度的加锁机制——只在索引创建的开始和结束阶段对集合加独占锁,也会因为索引创建本身的性能开销(CPU、IO),导致复制延迟,或者因为一些特殊操作,比如 collMod 命令修改集合元信息,而导致 Oplog 的应用阻塞,甚至会因为主库历史 Oplog 被覆盖掉而进入 Recovering 状态。
在 4.4 中,主库和备库上的索引创建操作是同时进行的,可以大幅减少因为上述情况所带来的主备延迟,尽量保证即使在索引创建过程中,备库读也可以访问到最新的数据。
此外,新的索引创建机制是在 majority 的具备投票权限的数据承载节点返回成功后,索引才会真正生效。所以,也可以减轻在读写分离场景下,因为索引不同而导致的性能差异。
查询能力和易用性增强
传统的关系型数据库(RDBMS)普遍以 SQL 语言为接口,客户端可以在本地编写融入部分业务逻辑的复杂 SQL 语句,来实现强大的查询能力。MongoDB 作为一个新型的文档数据库系统,也有自定义的 MQL 语言,复杂查询能力主要借助于 Aggregation Pipeline 来实现,虽弱于 RDBMS,但在最近的几个大版本中也在持续不断的打磨,最终的目的是使用户在享受到 MongoDB 灵活性和扩展性的同时,也能享受到丰富的功能性。
Union
在多表联合查询能力上,4.4 之前只提供了一个 $lookup stage 用于实现类似于 SQL 中的「left outer join」功能,在 4.4 中新增的 $unionWith stage 又提供了类似 SQL 中的「union all」功能,用户把两个集合中的数据聚合到一个结果集中,然后做指定的查询和过滤。区别于 $lookup stage 的是,$unionWith stage 支持分片集合。当在 Aggregate Pipeline 中使用了多个 $unionWith stage 的时候,可以对多个集合数据做聚合,使用方式如下,
{ $unionWith: { coll: "<collection>", pipeline: [ <stage1>, ... ] } }
可以在 pipeline 参数中指定不同的 stage,用于在对集合数据聚合前,先进行一定的过滤,使用起来非常灵活,下面举一个简单的例子,比如业务上对订单数据按表拆分存储到不同的集合,第二季度有如下数据(演示目的),
db.orders_april.insertMany([
{ _id:1, item: "A", quantity: 100 },
{ _id:2, item: "B", quantity: 30 },
]);
db.orders_may.insertMany([
{ _id:1, item: "C", quantity: 20 },
{ _id:2, item: "A", quantity: 50 },
]);
db.orders_june.insertMany([
{ _id:1, item: "C", quantity: 100 },
{ _id:2, item: "D", quantity: 10 },
]);
现在假设业务上需要知道,二季度不同产品的销量,在 4.4 之前,可能需要业务自己把数据都读出来,然后在应用层面做聚合才能解决这个问题,或者依赖某种数据仓库产品来做分析,但是需要有某种数据的同步机制。
而在 4.4 中只需要如下一条 Aggregate 语句即可解决问题,
db.orders_april.aggregate( [
{ $unionWith: "orders_may" },
{ $unionWith: "orders_june" },
{ $group: { _id: "$item", total: { $sum: "$quantity" } } },
{ $sort: { total: -1 }}
] )
Custom Aggregation Expressions
4.4 之前的版本中可以通过 find 命令中的 $where operator 或者 MapReduce 功能来实现在 Server 端执行自定义的 JavaScript 脚本,进而提供更为复杂的查询能力,但是这两个功能并没有做到和 Aggregation Pipeline 在使用上的统一。
所以,在 4.4 中,MongoDB 提供了两个新的 Aggregation Pipeline Operator,$accumulator 和 $function 用来取代 $where operator 和 MapReduce,借助于「Server Side JavaScript」来实现自定义的 Aggregation Expression,这样做到复杂查询的功能接口都集中到 Aggregation Pipeline 中,完善接口统一性和用户体验的同时,也可以把Aggregation Pipeline 本身的执行模型利用上,实现所谓 「1+1 > 2」 的效果。
$accumulator 和 MapReduce 功能有些相似,会先通过init 函数定义一个初始的状态,然后对于每一个输入的文档,根据指定的 accumate 函数更新状态,然后会根据需要决定是否执行 merge 函数,比如,如果在分片集合上使用了 $accumulator operator,那么最后需要把不同分片上执行完成的结果做 merge,最后,如果指定了 finalize 函数,在所有输入文档处理完成后,会根据该函数把状态转换为一个最终的输出。
$function 和 $where operator 在功能上基本一致,但是强大之处是可以和其他的 Aggregation Pipeline Operator 配合使用,此外也可以在 find 命令中借助于 $expr operator 来使用 $function operator,等价于之前的 $where operator,MongoDB 官方在文档中也建议优先使用 $function operator。
其他易用性增强
Some Other New Aggregation Operators and Expressions
除了上述的 $accumulator 和 $function operator,4.4 中还新增了其他多个 Aggregation Pipeline Operator,比如做字符串处理的,获取数组收尾元素的,还有用来获取文档或二进制串大小的操作符,具体见如下列表,
Connection Monitoring and Pooling
4.4 的 Driver 中增加了对客户端连接池的行为监控和自定义配置,通过标准的 API 来订阅和连接池相关的事件,包括连接的关闭和打开,连接池的清理。也可以通过 API 来配置连接池的一些行为,比如,拥有的最大/最小连接数,每个连接的最大空闲时间,线程等待可用连接时的超时时间,具体可以参考 MongoDB 官方的设计文档。
Global Read and Write Concerns
在 4.4 之前的版本中,如果操作的执行没有显式指定 readConcern 或者 writeConcern,也会有默认行为,比如readConcern 默认是 local,而 writeConcern 默认是 {w: 1}。但是,这个默认行为并不可以变更,如果用户想让所有的 insert 操作的 writeConcern 默认都是是 {w: majority},那么只能所有访问 MongoDB 的代码都显式去指定该值。
在 4.4 中可以通过 setDefaultRWConcern 命令来配置全局默认的 readConcern 和 writeConcern,如下,
db.adminCommand({
"setDefaultRWConcern" : 1,
"defaultWriteConcern" : {
"w" : "majority"
},
"defaultReadConcern" : { "level" : "majority" }
})
也可以通过 getDefaultRWConcern 命令获取当前默认的readConcern 和 writeConcern。
此外,这次 MongoDB 做的更加贴心,在记录慢日志或诊断日志的时候,会记录当前操作的 readConcern 或者 writeConcern 设置的来源,二者相同的来源定义有如下三种,
对于 writeConcern 来说,还有如下一种来源,
New MongoDB Shell (beta)
对于运维 MongoDB 的同学来说,使用最多的工具可能就是 mongo shell,4.4 提供了新版本的 mongo shell,增加了像代码高亮,命令自动补全,更加可读的错误信息等非常人性化的功能,不过,目前还是 beta 版本,很多命令还不支持,仅供尝鲜。
其他
这次的 4.4 发布,前面讲了主要是一个维护性的版本,所以除了上述解读,还有很多其他小的优化,像 $indexStats 优化,TCP Fast Open 支持优化建连,索引删除优化等等,还有一些相对大的增强,像新的结构化日志LogV2,新的安全机制支持等,这些可能不是用户最优先去关注的,在这里就不一一描述了,感兴趣的读者可以自行参考官方的 Release Notes。
文章转自公众号:阿里云数据库