前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >青胜于蓝丨腾讯MongoDB百万库表探索之路

青胜于蓝丨腾讯MongoDB百万库表探索之路

作者头像
腾讯数据库技术
发布2021-07-29 15:40:25
8850
发布2021-07-29 15:40:25
举报

文章出处: 鹅厂架构师

导读

腾讯 MongoDB 百万库场景下的性能优化成果汇报

腾讯 MongoDB目前广泛应用于游戏、电商、ugc、物联网等场景,很多客户在使用过程中库表数量会大量增长,甚至达到百万级别,导致性能急剧下降,严重影响客户业务。腾讯数据库研发中心CMongo团队在进行深入性能分析之后,改造底层引擎为共享表空间架构,新架构在百万级库表的场景下,相比原生版本读写性能提升 1-2 个数量级,内存消耗显著降低,启动时间从原先小时级缩短到一分钟内。

探索之路正式开始,文章很长但干货满满,建议收藏品用——

问题初现,立项攻坚

腾讯数据库研发中心CMongo团队(简称 CMongo) 在运营过程中发现很多业务存在创建大量库表的需求,而随着业务库表数量的不断增长,客户反馈持续出现几秒到几十秒的慢查询,并伴随节点不可用的情况,严重影响到了客户的正常业务请求。通过监控观察发现,原生MongoDB在库表和索引数量达到百万量级场景下, MongoDB 实例在 CPU、磁盘等资源远没有达到瓶颈时,也会出现操作卡顿性能下降的问题。

从我们的运营观察来看,至少有以下 3 个非常严重的问题:

● 内存消耗增大,频繁出现 OOM

● 性能严重下降,慢查询变多

● 实例启动时间明显变长,可能达到小时级

针对上述问题,CMongo 团队基于 v4.0 版本,对原生场景下的百万库表场景进行了性能分析,并结合业界的解决方案进行架构优化,优化后的架构在百万级库表的场景下,将读写性能提升了 1-2 个数量级,有效降低了内存资源消耗,并将启动时间从原先的小时级缩短到 1 分钟内。本文将根据 MongoDB 原理,为大家介绍分析过程及MongoDB 架构优化方案。

知己知彼,游刃有余

MongoDB内核从3.2版本开始,采用了典型的插件式架构,可以简单理解为分为server层和存储引擎层,通过打点和日志调试,我们最终发现,所有问题都指向存储引擎层,因此,WiredTiger 引擎的分析成为了我们后续的重中之重。

WiredTiger 存储引擎简介

MongoDB 采用 WiredTiger (简称 WT) 作为默认存储引擎,整体架构如下图所示:

(WiredTiger 存储引擎整体架构)

用户在 MongoDB 层创建的每个表和索引,都对应各自独立的 WT 表。

数据读写经过以下 3 层:

1. WT Cache:通过 B+ 树缓存未压缩的库表数据,并通过自定义的淘汰算法确保内存占用在合理范围

2. OS Cache:由操作系统管理,缓存压缩后的库表数据

3. 数据库文件:存储压缩后的库表数据。每个 WT 表 对应一个独立的磁盘文件。磁盘文件划分成多个按照 4KB 对齐的 extent(offset+length),并通过 3 个链表来管理:  available list(可分配的extent列表) , discard list(废弃的extent列表,但是还不能马上重用,可能其他checkpoint还在引用) 和 allocate list(当前已分配的extent列表)

01

内存消耗分析

思考:如果用户不会在短时间内访问所有的表,必然有表长时间空闲,那为何非活跃表的数据长时间停留在内存?

假设:如果非活跃表占用的内存能够及时换出,那将有效提高一个普通规格的集群能够支持的最大表数量,从而避免频繁OOM。

探索过程——

我们在云上创建一个 2核4G 的副本集,不断创建表(每个表2个索引),每个表在插入数据创建完成之后不再访问。测试过程中发现current active dhandle  一直在上升,而 connection sweep dhandles closed 指标却很平缓。最终实例占用的内存也一直上升,在创建的表不足 1 万时,就触发了实例 OOM.

data handle & sweep thread

data handle(简称 dhandle) 可以简单理解为 wiredtiger 资源的专属句柄,类似系统的 fd,源码里简写为dhandle

在全局 WT_CONNECTION 对象中维护着全局的dhandle list, 每个 WT_SESSION 对象维护着指向 dhandle list 的 dhandle cache. 第一次访问 WT 表时,在全局 dhandle list 和 session dhandle cache 中没有对应的 dhandle,会为此表创建 dhandle, 并放到全局 dhandle list 中。

sweep thread 后台线程每10秒扫描 WT_CONNECTION中 的 dhandle list,标记当前没有使用的 dhandle. 如果打开的 btree 数量超过了close_handle_minimum(默认值250),则检查有哪些dhandle 在 close_idle_time 内一直处于 idle 状态,则关闭与此 dhandle 相关的 btree,释放一些资源(非全部资源),标记 dhandle 为 dead 以便引用它的 session 能够发现此dhandle 已经不能再访问。接下来还得判断是否有 session 引用这个 dead dhandle,如果没有则可从全局 list 中移除。

基于以上分析,我们有理由怀疑为何清理的效率这么差。

深入分析 MongoDB 代码可知,在初始化 WiredTiger 引擎时将 close_idle_time 设置成了 100000s(~28h). 这样设置导致的后果是 sweep 不够及时,不再访问的表仍然会长时间消耗内存。

验证得出结果——

为了快速验证我们的分析,将代码中 close_idle_time从原先的 1万秒调成 600 秒,运行相同测试程序,发现节点没有 OOM,1 万个 collection 成功插入,内存使用率也维持在较低值。

connection data handles currently active 在一开始就不再直线上升,而是有升有降,最终程序结束后,回归到 250,符合预期。

所以,对于线上业务表多、频繁 OOM、实例规格又小的集群,可以临时采取调整配置的方式来缓解。但是这样又会导致 dhandle 的缓存命中率非常低,带来比较严重的性能下降。

因此,如何降低 dhandle 个数减少内存消耗,同时又保证 dhandle 缓存命中率避免性能下降,成为了我们的重点优化目标之一。

02

读写性能分析

场景一:预热前性能分析

预热指顺序读取每个 collection 至少一条数据,以便让所有 collection 的 cursor 都进入到 session cursor cache. 

在整个测试过程中,每个线程随机选择 collection 进行测试,可以发现持续有慢查询。通过下面的监控展示可以看到,在 wait time 窗口,随着蓝色曲线(schema lock wait)飙高,read latency 也飙高。因此,有必要分析 schema lock 的使用逻辑。

(预热前随机查询监控图)

为什么简单的 CRUD 请求要直接或间接依赖 schema lock 呢?

在测试过程中生成火焰图,得到函数调用栈如下:

(预热前单点随机查询火焰图)

在 all 上方还有很多像conn10091 线程一样的火焰柱形,表明客户端处理线程都在抢 schema lock,而它是属于WT_CONNECTION 对象的全局锁。这样也就解释了为何会产生那么大的 schema lock wait

那为何连接线程都在执行 open_cursor 呢?

在执行 CRUD 操作时,MongoServer 线程需要选择一个 WT_SESSION, 再从 WT_SESSION 中获取表的 wtcursor,然后执行 findRecord, insertRecord, updateRecord 等操作。

为了保证性能,在 MongoServer 侧和 WT 引擎侧都会缓存 wtcursor. 但如果表是首次访问,此时 dhandle 和 wtcursor 还未生成,需要执行代价昂贵的创建操作,包括打开 file,建立 btree 结构等。

结合代码和火焰图中可知, open_cursor 获取 schema lock 之后在 get dhandle 阶段消耗了很多 CPU 时间。当系统刚启动未预热时,很显然会有大量的 open_cursor调用,从而造成 spinlock 长时间等待。可见 ,dhandle 越多,竞争也越大。若想减小这种 spinlock 竞争,就需减少 dhandle 数量,这样就能大大地加快预热速度。这也是我们后续优化的一个攻克方向。

场景二:预热后性能分析

持续读写的场景下,在经历了“数据预热”阶段之后,每个 WT 表的 data handle 都完成了 open 操作,此时前文描述 schema lock 已经不再成为性能瓶颈。但是我们发现和表少的场景相比,性能仍然相对低下。

为了进一步分析性能瓶颈,我们对读写请求的全链路各个阶段进行了分段统计和分析,发现在 data handle 缓存访问阶段耗时很长。

WT_CONNECTION 和 WT_SESSION 会缓存 data handle,并采用哈希链表加速查找,默认的 hash bucket 的个数为 512. 在百万级库表场景下,每个 list 会变得很长,查找效率剧烈下降,从而导致用户的读写请求变慢。具体可以参考 __wt_session_get_dhandle 的代码逻辑。

通过在 WT 代码中增加 data handle 缓存的访问性能统计,发现用户侧慢请求的个数和 data handle 访问慢操作的个数相关,而且时延分布也基本一致。如下所示:

慢操作次数统计

MongoDB用户侧慢请求个数

遍历data handle hash表慢操作个数

10-50ms

13432

19684

50-100ms

81840

75371

>100ms

12473

6905

我们在压测时尝试将 Bucket 个数提升 100 倍,使每个 Hash 链表的长度大幅变短,发现性能会有几倍甚至数量级的提升。

通过以上分析,可以得到的启示是:如果能够将 MongoDB 表共享到少量 WT 表空间中,能够降低 data handle 个数,提升 data handle 缓存的访问效率,从而提升性能。

03

启动速度分析

原生 MongoDB 在百万级库表场景下,启动 mongod 实例会耗时长达几十分钟,甚至超过 1 个小时。如果有多个节点在 OOM 之后不能被很快被拉起提供服务,则整体服务可用性将受到很大影响。

探索过程——

为了观察启动期间 MongoServer 和 WT 引擎的流程和耗时分布,我们对 MongoDB 和 WT 引擎日志进行了分析统计。

通过日志分析发现,耗时最长的部分为 WT 表的 reconfig 阶段

具体来说,在 mongod 启动时 mongoDbMain 的初始化阶段会初始化存储引擎,并执行 loadCatalog 初始化所有表的 WiredTigerRecordStore. 在构造 WiredTigerRecordStore 时,会根据表的 uri 执行 setTableLogging 配置底层 WT 表是否开启 WAL. 最后调用底层 WT 引擎的 schema_alter 流程执行配置,这里会涉及到多次 IO 操作(获取原有配置,再和新配置进行比较,最后可能执行配置更新)。 同理,mongoDbMain 也会初始化所有表的索引(WiredTigerIndex),也会执行对应的 setTableLogging 操作。

另外,以上流程都是串行操作,在库表索引变多时,整体耗时也会线性增长。实测发现在百万库表场景下超过 99% 的耗时都在这个阶段,成为了启动速度的瓶颈。

为什么启动时要对 WT 表进行 reconfig 呢?

MongoDB 会根据表的用途以及当前的配置,决定是否对某个表开启 WAL,具体可以参考 WiredTigerUtil::useTableLogging 和 WiredTigerUtil::setTableLogging 的实现逻辑。在 mongod 实例启动以及用户建表时都会进行相关配置。仔细分析这里的逻辑,可以得到以下规律:在表创建完成,对应的 WT uri 确定之后,这个表是否开启 WAL 是可以确定的,不会随着实例的运行发生改变。

通过以上分析,可以得到 2 点启示:

  1. 如果将开启 WAL 配置一致的 MongoDB 表都共享到少量 WT 表空间中,可以将 setTableLogging 的操作次数由百万级降低为到个位数,从而极大提升初始化速度。从我们的架构优化和测试也能证明,可以将小时级的启动时间优化到 1 分钟内。
  2. 减少 setTableLogging 操作之后,会避免 WT 引擎进行 schema_alter 操作时获取全局schema lock,从而给 MongoServer 的上层逻辑带来优化空间。通过将串行初始化优化成多线程并发初始化表和索引,能够进一步加快启动速度。

04

性能分析总结

WT 引擎在百万级库表场景下,会对应生成大量的 data handle 并导致性能下降,包括锁竞争、关键数据结构的缓存命中率低、部分串行化流程在百万级库表下带来的耗时放大等问题。

思考:结合前面的分析,如果 MongoServer 层能够共享表空间,只在 WT 引擎上存储数量较少的物理表,是否能避免上述性能瓶颈?

进阶优化,青胜于蓝

01

方案选型

在 MySQL 中,InnoDB 通过为每个表分配 space id实现共享表空间;在 MongoRocks 中, 每个表会分配唯一的 prefix,每条数据的 Key 都会带上 prefix 信息。

在原生 MongoDB 代码中,也遵循“前缀映射”的思路实现了部分基础代码。具体可以参考 KVPrefix 的定义,以及 GroupCollections 选项的相关代码和注释。但是这个功能并没有完全实现,只进行了基础的数据结构定义,因此也不能直接使用。我们通过邮件和作者进行了沟通,社区目前也没有实现该功能的后续计划。

MongoDB JIRA 相关信息如下:

问题

描述

状态

使WT cursor 支持 groupCollections

当开启了groupCollections后,数据和索引的key_format分别变成qq, qu,cursor 在迭代时要检测prefix是否匹配

fixed

兼容sampling cursor

当支持groupCollections时,为WT_CUROSR 提供一个新方法 range()以支持random cursor,目前没有此方法

open

兼容sampling cursor

与第2个问题相关,需要WT 层支持random cursor 正确返回匹配前缀的数据。这在为oplog 设置截断点时要用到。据原作者所说,有比较多的工作要做

closed

官方测试百万表

对groupCollections 特性进行压测的POC 程序

closed

官方groupCollections 设计文档,2017.4

工作较多,包括共享session等;google doc需要申请

底层表不存在时才创建

当开启groupCollections时,是先搜索是否存在兼容的table,只能在没有underlying table时才创建它;删除collection/indexes 要注意不要无条件删除了底层表; oplog 要单独放

closed

结合我们对原生多表场景下的测试和分析,认为共享表空间可以解决上述性能瓶颈,从而提升性能。因此我们决定基于“前缀映射”的思路进行共享表空间架构优化和验证。

02

架构设计

在方案设计初期,我们对于在哪个模块实现共享表空间进行了以下对比:

1. 改造 WT 存储引擎内部逻辑。多个逻辑 WT 表通过前缀进行区分,共享同一个物理 WT 表空间,共享 data handle, btree, block manager 等资源。但是这种方式涉及的代码改动量极大,开发周期太长。

2. 改造 MongoDB 中 KVEngine 抽象层的逻辑。在存储引擎上层通过前缀映射的方式,使多个 MongoDB 表共享同一个 WT 表空间。这种方式主要涉及 KVEngine 存储抽象层使用逻辑的改造,并兼容原生 WT 引擎对前缀操作支持不完备带来的问题。这种方式涉及的代码改动量相对较少,不涉及 WT 引擎内部架构的调整,稳定性和开发周期更加可控。

因此,优化工作主要集中在 “KVEngine抽象层”:通过前缀方式,将多个 MongoDB用户表 映射成为1 个 WT 表;MongoServer 层对指定库表的 CURD 操作,都会通过 key -> prefix+key 的转换之后,到 WT 引擎进行数据操作。上述映射关系通过 __mdb_catalog 存储。

整体架构图如下所示:

(优化后整体架构)

建立好映射关系之后,不论用户在上层创建多少个库表和索引,在 WT 引擎层都只会有 9 个 WT 表

1. WiredTiger.wt :存储 Checkpoint 元数据信息

2. WiredTigerLAS.wt :存储换出的 LAS 数据

3. _mdb_catalog.wt :存储上层 MongoDB 表 和 底层 WT 表 的映射关系

4. sizeStorer.wt :存储计数数据

5. oplog-collection.wt :存储 oplog 数据

6. collection.wt : 存储所有MongoDB 非 local 表数据

7. index.wt : 存储所有MongoDB 非 local 索引数据

8. local-collection.wt: 存储MongoDB local 表数据

9. local-index.wt:存储 MongoDB local 索引数据

为什么表和索引分开存储?

因为表和索引在 WT 引擎中的 schema 定义不同,比如表的 schema 是 qq -> u,索引的 schema 是 qu -> q (q表示int64, u 表示 string);也可以将 collection 和 index 的 schema 都统一成 qu -> u 的形式, 但是在数据读写时会带来一些额外的类型转换。

每一个表都对应一个 prefix,通过建立 (NS, Prefix, Ident) 三元关系来将多个表的数据共享到一个文件中,共享后的数据文件如下所示:

(共享后数据文件图)

经过架构调整之后,一些关键路径发生了变化:

  • 路径变化 1:建表和索引操作,会先生成唯一的 prefix,并记录到 mdb_catalog 中,而不再对 WT 引擎发起表创建操作(WT 引擎的共享表会在实例第 1 次启动时检查并创建完成)
  • 路径变化 2:删表和索引操作,会删除 mdb_catalog 中的记录,然后进行数据删除操作,但不会直接删除 WT 表文件
  • 路径变化 3:数据读写操作,会将 prefix 添加到访问的 key 头部,然后再去 WT 中执行数据操作。

通过建立 prefix 映射关系,不论 MongoDB 上层库表个数如何增长,底层 WT 表个数都不会增长。因此,上文分析的 data handle 过多导致内存使用率高,data handle open 操作导致的锁争抢,data handle cache 效率低下,以及 WT 表太多导致实例启动慢的问题都不会出现。极大提升了整体性能。

当然,通过 prefix 共享表空间之后也会带来一些新的使用限制,主要有:

  1. 用户删除表之后,空间不会释放,但是可以被重新使用
  2. 部分表操作的语义发生了变化。比如 compact 和 validate 操作需要放到全局实现,目前暂不支持
  3. 部分统计信息发生了变化。比如表和索引的 storageSize 都无法统计,只能统计出逻辑大小 (压缩前的大小)。由于原生 sizeStorer.wt 中只记录了表的逻辑大小,因此需要自己实现索引逻辑大小的统计。进行这个改造之后,show dbs (listDatabases 命令) 的速度提升了不少,从我们的测试结果来看可以从 11s 缩短到 0.8s, 主要得益于不需要对大量索引文件执行统计操作。

03

优化效果

我们在腾讯云上搭建的测试环境,具体的测试环境如下:

1. 数据量大小 500GB (500 000 000 000 B)(collection * recordNum * fieldLength)

2. 总体压测线程数量 200

3. 副本集的配置:8核,16G内存,2T磁盘

4. mongo driver 版本:go-driver [v1.5.2]

5. driver 的连接池大小为 200

6. 测试工具与 primary 在同一台机器上,直连 primary

测试结果

CMongo团队根据业务使用情况,挑选出 50w 表与 100w 表两种场景进行测试。在实际测试过程中发现,改造之前的集群无法写入 100w 表的数据。最后给出 50w 表的测试数据。

测试的大概步骤如下所示:

1. 默认构建 10 个库;

2. 每个库先创建 5w 空表;

3. 开始向表中写入固定数量的随机数据;

4.  执行 CRUD 的操作。

下面给出百分位延迟 对比结果图(由于数据差异较大,为了能显示对比,对所有的数据都做了 log10()处理)。从P99图可以看出,改造后在CRUD操作上的性能要优于改造前。下图展示了QPS对比结果:

(改造前后QPS对比图)

改造后的QPS也远优于改造前,原因是,改造后可以认为所有的数据同在一张表中,性能不会随着表的数量发生很大的改变;而改造前抢锁的激烈程度,data handle 的访问时间会随着表数量的增加而增加,由此导致性能很低。

下图给出了改造前后多表以及原生单表 query 操作的 QPS 对比图,改造后相对于原生单表的 QPS 最大降幅在 7% 以内,而改造前相对于原生单表的 QPS 最大降幅已经超过 90%。

(QPS变化图)

线上效果

原生 MongoDB 随着表数量的大量增长,资源消耗也会大幅增加,性能急剧下降。CMongo 团队在进行性能分析之后,使用了共享表空间思路,将用户创建的海量库表共享底层 WT 引擎的 1 个表空间。WT 引擎维护的表数量不随用户创建表和索引的操作线性增长,始终保证在个位数。

目前架构优化已经通过了各项功能测试和性能压测并顺利上线。云上的即时通信IM业务,替换了百万库表版本之后,有效的解决了客户表很多时造成的CRUD操作变慢的问题,系统内存消耗也明显降低。后面该内核特性也会全面开放到云上,欢迎大家体验。

步履不停,在路上

腾讯MongoDB(TencentDB for MongoDB) 是腾讯基于全球广受欢迎的文档数据库MongoDB 打造的高性能 NoSQL 数据库,100% 完全兼容 MongoDB 协议。相比原生版本,CMongo内核团队对原生内核做了大量优化和深度定制,包括百万TPS、物理备份、免密、无损加节点、rocksdb引擎等优化,同时也新开发了流控、审计、加密等企业级特性,为公司内外客户的核心业务提供了有力的支撑。

目前,腾讯MongoDB 已在稳定性、安全性、性能以及企业级特性上取得了显著突破。接下来我们还会继续在性能、新特性、成本等方面进行持续不断的打磨、改进,争取为公司内外用户提供更好的云MongoDB服务。

↓↓ 产品动态:【腾讯云官网】云数据库 TencentDB for MongoDB

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-07-29,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 腾讯数据库技术 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
云数据库 MongoDB
腾讯云数据库 MongoDB(TencentDB for MongoDB)是腾讯云基于全球广受欢迎的 MongoDB 打造的高性能 NoSQL 数据库,100%完全兼容 MongoDB 协议,支持跨文档事务,提供稳定丰富的监控管理,弹性可扩展、自动容灾,适用于文档型数据库场景,您无需自建灾备体系及控制管理系统。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档