写点什么

mongodb 内核源码实现、性能调优、最佳运维实践系列 - 百万级高并发 mongodb 集群性能数十倍提升优化实践 (上篇)

发布于: 2020 年 10 月 19 日
mongodb内核源码实现、性能调优、最佳运维实践系列-百万级高并发mongodb集群性能数十倍提升优化实践(上篇)

说明

     本文是 oppo 互联网某百亿级数据量/百万级高并发 mongodb 集群线上真实优化案例,荣获 mongodb 中文社区 2019 年度一等奖。

关于作者

前滴滴出行技术专家,现任 OPPO 文档数据库 mongodb 负责人,负责 oppo 千万级峰值 TPS/十万亿级数据量文档数据库 mongodb 研发和运维工作,一直专注于分布式缓存、高性能服务端、数据库、中间件等相关研发。后续持续分享《MongoDB 内核源码设计、性能优化、最佳运维实践》,Github 账号地址:https://github.com/y123456yz

1. 背景

线上某集群峰值 TPS 超过 100 万/秒左右(主要为写流量,读流量很低),峰值 tps 几乎已经到达集群上限,同时平均时延也超过 100ms,随着读写流量的进一步增加,时延抖动严重影响业务可用性。该集群采用 mongodb 天然的分片模式架构,数据均衡的分布于各个分片中,添加片键启用分片功能后实现完美的负载均衡。集群每个节点流量监控如下图所示:


   从上图可以看出集群流量比较大,峰值已经突破 120 万/秒,其中 delete 过期删除的流量不算在总流量里面(delete 由主触发删除,但是主上面不会显示,只会在从节点拉取 oplog 的时候显示)。如果算上主节点的 delete 流量,总 tps 超过 150 万/秒。

2. 软件优化

在不增加服务器资源的情况下,首先做了如下软件层面的优化,并取得了理想的数倍性能提升:

1. 业务层面优化

2. Mongodb 配置优化

3. 存储引擎优化

2.1 业务层面优化

该集群总文档数百亿条,每条文档记录默认保存三天,业务随机散列数据到三天后任意时间点随机过期淘汰。由于文档数目很多,白天平峰监控可以发现从节点经常有大量 delete 操作,甚至部分时间点 delete 删除操作数已经超过了业务方读写流量,因此考虑把 delete 过期操作放入夜间进行,过期索引添加方法如下:

Db.collection.createIndex( { "expireAt": 1 }, { expireAfterSeconds: 0 } )

上面的过期索引中 expireAfterSeconds=0,代表 collection 集合中的文档的过期时间点在 expireAt 时间点过期,例如:


     db.collection.insert( {

   //表示该文档在夜间凌晨 1 点这个时间点将会被过期删除

   "expireAt": new Date('July 22, 2019 01:00:00'),    

   "logEvent": 2,

   "logMessage": "Success!"

 } )

通过随机散列 expireAt 在三天后的凌晨任意时间点,即可规避白天高峰期触发过期索引引入的集群大量 delete,从而降低了高峰期集群负载,最终减少业务平均时延及抖动。

 

Delete 过期 Tips1: expireAfterSeconds 含义

1.在 expireAt 指定的绝对时间点过期,也就是 12.22 日凌晨 2:01 过期

Db.collection.createIndex( { "expireAt": 1 }, { expireAfterSeconds: 0 } )

db.log_events.insert( { "expireAt": new Date(Dec 22, 2019 02:01:00'),"logEvent": 2,"logMessage": "Success!"})

 

2. 在 expireAt 指定的时间往后推迟 expireAfterSeconds 秒过期,也就是当前时间往后推迟 60 秒过期

    db.log_events.insert( {"createdAt": new Date(),"logEvent": 2,"logMessage": "Success!"} )

Db.collection.createIndex( { "expireAt": 1 }, { expireAfterSeconds: 60 } )

 

Delete 过期 Tips2:为何 mongostat 只能监控到从节点有 delete 操作,主节点没有?

原因是过期索引只在 master 主节点触发,触发后主节点会直接删除调用对应 wiredtiger 存储引擎接口做删除操作,不会走正常的客户端链接处理流程,因此主节点上看不到 delete 统计。

主节点过期 delete 后会生存对于的 delete oplog 信息,从节点通过拉取主节点 oplog 然后模拟对于 client 回放,这样就保证了主数据删除的同时从数据也得以删除,保证数据最终一致性。从节点模拟 client 回放过程将会走正常的 client 链接过程,因此会记录 delete count 统计,详见如下代码:

官方参考如下: https://docs.mongodb.com/manual/tutorial/expire-data/

2.2 Mongodb 配置优化(网络 IO 复用,网络 IO 和磁盘 IO 做分离)

由于集群 tps 高,同时整点有大量推送,因此整点并发会更高,mongodb 默认的一个请求一个线程这种模式将会严重影响系统负载,该默认配置不适合高并发的读写应用场景。官方介绍如下:

2.2.1 Mongodb 内部网络线程模型实现原理

mongodb 默认网络模型架构是一个客户端链接,mongodb 会创建一个线程处理该链接 fd 的所有读写请求及磁盘 IO 操作。

Mongodb 默认网络线程模型不适合高并发读写原因如下:

1.在高并发的情况下,瞬间就会创建大量的线程,例如线上的这个集群,连接数会瞬间增加到 1 万左右,也就是操作系统需要瞬间创建 1 万个线程,这样系统 load 负载就会很高。

2.此外,当链接请求处理完,进入流量低峰期的时候,客户端连接池回收链接,这时候 mongodb 服务端就需要销毁线程,这样进一步加剧了系统负载,同时进一步增加了数据库的抖动,特别是在 PHP 这种短链接业务中更加明显,频繁的创建线程销毁线程造成系统高负债。

    3.一个链接一个线程,该线程除了负责网络收发外,还负责写数据到存储引擎,整个网络 I/O 处理和磁盘 I/O 处理都由同一个线程负责,本身架构设计就是一个缺陷。

2.2.2 网络线程模型优化方法

     为了适应高并发的读写场景,mongodb-3.6 开始引入 serviceExecutor: adaptive 配置,该配置根据请求数动态调整网络线程数,并尽量做到网络 IO 复用来降低线程创建消耗引起的系统高负载问题。此外,加上 serviceExecutor: adaptive 配置后,借助 boost:asio 网络模块实现网络 IO 复用,同时实现网络 IO 和磁盘 IO 分离。这样高并发情况下,通过网络链接 IO 复用和 mongodb 的锁操作来控制磁盘 IO 访问线程数,最终降低了大量线程创建和消耗带来的高系统负载,最终通过该方式提升高并发读写性能。

2.2.3 网络线程模型优化前后性能对比

在该大流量集群中增加 serviceExecutor: adaptive 配置实现网络 IO 复用及网络 IO 与磁盘 IO 做分离后,该大流量集群时延大幅度降低,同时系统负载和慢日志也减少很多,具体如下:

2.2.3.1 优化前后系统负载对比

验证方式:

1. 该集群有多个分片,其中一个分片配置优化后的主节点和同一时刻未优化配置的主节点 load 负载比较:


 未优化配置的 load

 优化配置的 load

2.2.3.2 优化前后慢日志对比

验证方式:

该集群有多个分片,其中一个分片配置优化后的主节点和同一时刻未优化配置的主节点慢日志数比较:

同一时间的慢日志数统计:

未优化配置的慢日志数(19621)

     优化配置后的慢日志数(5222):

2.2.3.3 优化前后平均时延对比

验证方式:

该集群所有节点加上网络 IO 复用配置后与默认配置的平均时延对比如下:

    从上图可以看出,网络 IO 复用后时延降低了 1-2 倍。

2.3 wiredtiger 存储引擎优化

从上一节可以看出平均时延从 200ms 降低到了平均 80ms 左右,很显然平均时延还是很高,如何进一步提升性能降低时延?继续分析集群,我们发现磁盘 IO 一会儿为 0,一会儿持续性 100%,并且有跌 0 现象,现象如下:

从图中可以看出,I/O 写入一次性到 2G,后面几秒钟内 I/O 会持续性阻塞,读写 I/O 完全跌 0,avgqu-sz、awit 巨大,util 次序性 100%,在这个 I/O 跌 0 的过程中,业务方反应的 TPS 同时跌 0。

此外,在大量写入 IO 后很长一段时间 util 又持续为 0%,现象如下:

总体 IO 负载曲线如下:

从图中可以看出 IO 很长一段时间持续为 0%,然后又飙涨到 100%持续很长时间,当 IO util 达到 100%后,分析日志发现又大量满日志,同时 mongostat 监控流量发现如下现象:


从上可以看出我们定时通过 mongostat 获取某个节点的状态的时候,经常超时,超时的时候刚好是 io util=100%的时候,这时候 IO 跟不上客户端写入速度造成阻塞。

有了以上现象,我们可以确定问题是由于 IO 跟不上客户端写入速度引起,第 2 章我们已经做了 mongodb 服务层的优化,现在我们开始着手 wiredtiger 存储引擎层面的优化,主要通过以下几个方面:

1. cachesize 调整

2. 脏数据淘汰比例调整

3. checkpoint 优化

2.3.1 cachesize 调整优化(为何 cacheSize 越大性能越差)

前面的 IO 分析可以看出,超时时间点和 I/O 阻塞跌 0 的时间点一致,因此如何解决 I/O 跌 0 成为了解决改问题的关键所在。

找个集群平峰期(总 tps50 万/s)查看当时该节点的 TPS,发现 TPS 不是很高,单个分片也就 3-4 万左右,为何会有大量的刷盘,瞬间能够达到 10G/S,造成 IO util 持续性跌 0(因为 IO 跟不上写入速度)。继续分析 wiredtiger 存储引擎刷盘实现原理,wiredtiger 存储引擎是一种 B+树存储引擎,mongodb 文档首先转换为 KV 写入 wiredtiger,在写入过程中,内存会越来越大,当内存中脏数据和内存总占用率达到一定比例,就开始刷盘。同时当达到 checkpoint 限制也会触发刷盘操作,查看任意一个 mongod 节点进程状态,发现消耗的内存过多,达到 110G,如下图所示:

于是查看 mongod.conf 配置文件,发现配置文件中配置的 cacheSizeGB: 110G,可以看出,存储引擎中 KV 总量几乎已经达到 110G,按照 5%脏页开始刷盘的比例,峰值情况下 cachesSize 设置得越大,里面得脏数据就会越多,而磁盘 IO 能力跟不上脏数据得产生速度,这种情况很可能就是造成磁盘 I/O 瓶颈写满,并引起 I/O 跌 0 的原因。

此外,查看该机器的内存,可以看到内存总大小为 190G,其中已经使用 110G 左右,几乎是 mongod 的存储引起占用,这样会造成内核态的 page cache 减少,大量写入的时候内核 cache 不足就会引起磁盘缺页中断,引起大量的写盘。

解决办法:通过上面的分析问题可能是大量写入的场景,脏数据太多容易造成一次性大量 I/O 写入,于是我们可以考虑把存储引起 cacheSize 调小到 50G,来减少同一时刻 I/O 写入的量,从而规避峰值情况下一次性大量写入的磁盘 I/O 打满阻塞问题。

 

2.3.2 存储引擎 dirty 脏数据淘汰优化

调整 cachesize 大小解决了 5s 请求超时问题,对应告警也消失了,但是问题还是存在,5S 超时消失了,1s 超时问题还是偶尔会出现。

     因此如何在调整 cacheSize 的情况下进一步规避 I/O 大量写的问题成为了问题解决的关键,进一步分析存储引擎原理,如何解决内存和 I/O 的平衡关系成为了问题解决的关键,mongodb 默认存储因为 wiredtiger 的 cache 淘汰策略相关的几个配置如下:

   调整 cacheSize 从 120G 到 50G 后,如果脏数据比例达到 5%,则极端情况下如果淘汰速度跟不上客户端写入速度,这样还是容易引起 I/O 瓶颈,最终造成阻塞。

 

解决办法:如何进一步减少持续性 I/O 写入,也就是如何平衡 cache 内存和磁盘 I/O 的关系成为问题关键所在。从上表中可以看出,如果脏数据及总内占用存达到一定比例,后台线程开始选择 page 进行淘汰写盘,如果脏数据及内存占用比例进一步增加,那么用户线程就会开始做 page 淘汰,这是个非常危险的阻塞过程,造成用户请求验证阻塞。平衡 cache 和 I/O 的方法:调整淘汰策略,让后台线程尽早淘汰数据,避免大量刷盘,同时降低用户线程阀值,避免用户线程进行 page 淘汰引起阻塞。优化调整存储引起配置如下:

  eviction_target: 75%

  eviction_trigger:97%

  eviction_dirty_target: %3

  eviction_dirty_trigger:25%

  evict.threads_min:8

  evict.threads_min:12

总体思想是让后台 evict 尽量早点淘汰脏页 page 到磁盘,同时调整 evict 淘汰线程数来加快脏数据淘汰,调整后 mongostat 及客户端超时现象进一步缓解。

2.3.3 存储引擎 checkpoint 优化调整

存储引擎得 checkpoint 检测点,实际上就是做快照,把当前存储引擎的脏数据全部记录到磁盘。触发 checkpoint 的条件默认又两个,触发条件如下:

1. 固定周期做一次 checkpoint 快照,默认 60s

2. 增量的 redo log(也就是 journal 日志)达到 2G

当 journal 日志达到 2G 或者 redo log 没有达到 2G 并且距离上一次时间间隔达到 60s,wiredtiger 将会触发 checkpoint,如果在两次 checkpoint 的时间间隔类 evict 淘汰线程淘汰的 dirty page 越少,那么积压的脏数据就会越多,也就是 checkpoint 的时候脏数据就会越多,造成 checkpoint 的时候大量的 IO 写盘操作。如果我们把 checkpoint 的周期缩短,那么两个 checkpoint 期间的脏数据相应的也就会减少,磁盘 IO 100%持续的时间也就会缩短。

checkpoint 调整后的值如下:

checkpoint=(wait=25,log_size=1GB)

2.3.4 存储引擎优化前后 IO 对比

通过上面三个方面的存储引擎优化后,磁盘 IO 开始平均到各个不同的时间点,iostat 监控优化后的 IO 负载如下:

从上面的 io 负载图可以看出,之前的 IO 一会儿为 0%,一会儿 100%现象有所缓解,总结如下图所示:

2.3.5 存储引擎优化前后时延对比

优化前后时延对比如下(注:该集群有几个业务同时使用,优化前后时延对比如下):





从上图可以看出,存储引擎优化后时间延迟进一步降低并趋于平稳,从平均 80ms 到平均 20ms 左右,但是还是不完美,有抖动。

3 服务器系统磁盘 IO 问题解决

3.1 服务器 IO 硬件问题背景

如第 3 节所述,当 wiredtiger 大量淘汰数据后,发现只要每秒磁盘写入量超过 500M/s,接下来的几秒钟内 util 就会持续 100%,w/s 几乎跌 0,于是开始怀疑磁盘硬件存在缺陷。


从上图可以看出磁盘为 nvMe 的 ssd 盘,查看相关数据可以看出该盘 IO 性能很好,支持每秒 2G 写入,iops 能达到 2.5W/S,而我们线上的盘只能每秒写入最多 500M。

3.2 服务器 IO 硬件问题解决后性能对比

于是考虑把该分片集群的主节点全部迁移到另一款服务器,该服务器也是 ssd 盘,io 性能达到 2G/s 写入(注意:只迁移了主节点,从节点还是在之前的 IO-500M/s 的服务器)。 迁移完成后,发现性能得到了进一步提升,时延迟降低到 2-4ms/s,三个不同业务层面看到的时延监控如下图所示:



从上图时延可以看出,迁移主节点到 IO 能力更好的机器后,时延进一步降低到平均 2-4ms。

虽然时延降低到了平均 2-4ms,但是还是有很多几十 ms 的尖刺,鉴于篇幅将在下一期分享大家原因,最终保存所有时延控制在 5ms 以内,并消除几十 ms 的尖刺。

此外,nvme 的 ssd io 瓶颈问题原因,经过和厂商确认分析,最终定位到是 linux 内核版本不匹配引起,如果大家 nvme ssd 盘有同样问题,记得升级 linux 版本到 3.10.0-957.27.2.el7.x86_64 版本,升级后 nvme ssd 的 IO 能力达到 2G/s 以上写入。

4 总结及遗留问题

通过 mongodb 服务层配置优化、存储引擎优化、硬件 IO 提升三方面的优化后,该大流量写入集群的平均时延从之前的平均数百 ms 降低到了平均 2-4ms,整体性能提升数十倍,效果明显。

但是,从 4.2 章节优化后的时延可以看出,集群偶尔还是会有抖动,鉴于篇幅,下期会分享如果消除 4.2 章节中的时延抖动,最终保持时间完全延迟控制在 2-4ms,并且无任何超过 10ms 的抖动,敬请期待,下篇会更加精彩。

此外,在集群优化过程中采了一些坑,下期会继续分析大流量集群采坑记。

 

注意:文章中的一些优化方法并不是一定适用于所有 mongodb 场景,请根据实际业务场景和硬件资源能力进行优化,而不是按部就班。


发布于: 2020 年 10 月 19 日阅读数: 1209
用户头像

万亿级mongodb集群性能优化实践 2020.10.13 加入

Qcon、Gdevops、dbaplus等讲师,滴滴出行专家工程师/OPPO-mongodb负责人,负责数万亿级mongodb内核研发、性能优化及运维工作。持续分享《MongoDB内核源码设计、性能优化、最佳实践》,https://github.com/y123456yz

评论 (10 条评论)

发布
用户头像
终于打开了,牛逼啊,难得的大牛,已关注,期待更多文章
2020 年 10 月 20 日 19:11
回复
用户头像
抓狂,好卡
2020 年 10 月 20 日 19:11
回复
用户头像
好卡,打不开
2020 年 10 月 20 日 19:06
回复
用户头像
看上去主要做了以下几件事:
1、优化mongodb服务层配置
2、优化网络模型,多线程同步阻塞模型->多路复用模型(不清楚是改用的select还是epoll模型)
3、优化磁盘IO,旧磁盘->SSD ,提高磁盘吞吐量
2020 年 10 月 20 日 09:51
回复
同步线程模型,也就是一个链接一个线程,这时候时同步读。adaptive动态线程模型,借助epoll模型异步读哈
2020 年 10 月 20 日 11:01
回复
wiredtiger存储引擎优化也是一大以后点
2020 年 10 月 20 日 11:01
回复
wiredtiger存储引擎优化也是一大优化点
2020 年 10 月 20 日 11:01
回复
查看更多回复
用户头像
后续持续 千万级峰值TPS/十万亿级数据量文档数据库mongodb:《MongoDB内核源码设计、性能优化、最佳运维实践》
2020 年 10 月 19 日 14:07
回复
用户头像
百万级高并发 mongodb 集群性能数十倍提升优化实践 (下篇)地址:
https://xie.infoq.cn/article/329cd8f770f445d87e26637bc
2020 年 10 月 19 日 14:06
回复
没有更多了
mongodb内核源码实现、性能调优、最佳运维实践系列-百万级高并发mongodb集群性能数十倍提升优化实践(上篇)