本文来自OPPO互联网技术团队,转载请注名做者。同时欢迎关注OPPO互联网技术团队的公众号:OPPO_tech,与你分享OPPO前沿互联网技术及活动。linux
线上某集群峰值TPS超过100万/秒左右(主要为写流量,读流量很低),峰值tps几乎已经到达集群上限,同时平均时延也超过100ms,随着读写流量的进一步增长,时延抖动严重影响业务可用性。该集群采用mongodb自然的分片模式架构,数据均衡的分布于各个分片中,添加片键启用分片功能后实现完美的负载均衡。集群每一个节点流量监控以下图所示:ios
从上图能够看出集群流量比较大,峰值已经突破120万/秒,其中delete过时删除的流量不算在总流量里面(delete由主触发删除,可是主上面不会显示,只会在从节点拉取oplog的时候显示)。若是算上主节点的delete流量,总tps超过150万/秒。mongodb
在不增长服务器资源的状况下,首先作了以下软件层面的优化,并取得了理想的数倍性能提高:数据库
业务层面优化性能优化
Mongodb配置优化bash
存储引擎优化服务器
该集群总文档近百亿条,每条文档记录默认保存三天,业务随机散列数据到三天后任意时间点随机过时淘汰。因为文档数目不少,白天平峰监控能够发现从节点常常有大量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 含义
Db.collection.createIndex( { "expireAt": 1}, { expireAfterSeconds: 0 })
db.log_events.insert( { "expireAt": new Date(Dec 22, 2019 02:01:00'),"logEvent": 2,"logMessage": "Success!"}) 复制代码
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统计。
官方参考以下:docs.mongodb.com/manual/tuto…
因为集群tps高,同时整点有大量推送,所以整点并发会更高,mongodb默认的一个请求一个线程这种模式将会严重影响系统负载,该默认配置不适合高并发的读写应用场景。官方介绍以下:
mongodb默认网络模型架构是一个客户端连接,mongodb会建立一个线程处理该连接fd的全部读写请求及磁盘IO操做。
Mongodb默认网络线程模型不适合高并发读写缘由以下:
在高并发的状况下,瞬间就会建立大量的线程,例如线上的这个集群,链接数会瞬间增长到1万左右,也就是操做系统须要瞬间建立1万个线程,这样系统load负载就会很高。
此外,当连接请求处理完,进入流量低峰期的时候,客户端链接池回收连接,这时候mongodb服务端就须要销毁线程,这样进一步加重了系统负载,同时进一步增长了数据库的抖动,特别是在PHP这种短连接业务中更加明显,频繁的建立线程销毁线程形成系统高负债。
一个连接一个线程,该线程除了负责网络收发外,还负责写数据到存储引擎,整个网络 I/O 处理和磁盘 I/O 处理都由同一个线程负责,自己架构设计就是一个缺陷。
为了适应高并发的读写场景,mongodb-3.6开始引入serviceExecutor: adaptive配置,该配置根据请求数动态调整网络线程数,并尽可能作到网络IO复用来下降线程建立消耗引发的系统高负载问题。
此外,加上serviceExecutor: adaptive配置后,借助boost:asio网络模块实现网络IO复用,同时实现网络IO和磁盘IO分离。这样高并发状况下,经过网络连接IO复用和mongodb的锁操做来控制磁盘IO访问线程数,最终下降了大量线程建立和消耗带来的高系统负载,最终经过该方式提高高并发读写性能。
在该大流量集群中增长serviceExecutor: adaptive配置实现网络IO复用及网络IO与磁盘IO作分离后,该大流量集群时延大幅度下降,同时系统负载和慢日志也减小不少,具体以下:
验证方式:
该集群有多个分片,其中一个分片配置优化后的主节点和同一时刻未优化配置的主节点load负载比较:
未优化配置的load
优化配置的load
验证方式:
该集群有多个分片,其中一个分片配置优化后的主节点和同一时刻未优化配置的主节点慢日志数比较:
同一时间的慢日志数统计:
未优化配置的慢日志数(19621):
优化配置后的慢日志数(5222):
验证方式:
该集群全部节点加上网络IO复用配置后与默认配置的平均时延对好比下:
从上图能够看出,网络IO复用后时延下降了1-2倍。
从上一节能够看出平均时延从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存储引擎层面的优化,主要经过如下几个方面:
cachesize调整
脏数据淘汰比例调整
checkpoint优化
前面的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打满阻塞问题。
调整cachesize大小解决了5s请求超时问题,对应告警也消失了,可是问题仍是存在,5S超时消失了,1s超时问题仍是偶尔会出现。
所以如何在调整cacheSize的状况下进一步规避I/O大量写的问题成为了问题解决的关键,进一步分析存储引擎原理,如何解决内存和I/O的平衡关系成为了问题解决的关键,mongodb默认存储由于wiredtiger的cache淘汰策略相关的几个配置以下:
wiredtiger淘汰相关配置 | 默认值 | 工做原理 |
---|---|---|
eviction_target | 80 | 当用掉的内存超过总内存的百分比达到eviction_target,后台evict线程开始淘汰 |
eviction_trigger | 95 | 当用掉的内存超过总内存的 eviction_trigger,用户线程也开始淘汰 |
eviction_dirty_target | 5 | 当cache中脏数据比例超过eviction_dirty_target,后台evict线程开始淘汰 |
eviction_dirty_trigger | 20 | 当cache中脏数据比例超过eviction_dirty_trigger, 用户线程也开始淘汰 |
evict.threads_min | 4 | 后台evict线程最小数 |
evict.threads_max | 4 | 后台evict线程最大数 |
调整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及客户端超时现象进一步缓解。
存储引擎得checkpoint检测点,实际上就是作快照,把当前存储引擎的脏数据所有记录到磁盘。触发checkpoint的条件默认又两个,触发条件以下:
当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)
复制代码
经过上面三个方面的存储引擎优化后,磁盘IO开始平均到各个不一样的时间点,iostat监控优化后的IO负载以下:
从上面的IO负载图能够看出,以前的IO一下子为0%,一下子100%现象有所缓解,总结以下图所示:
优化先后时延对好比下(注: 该集群有几个业务同时使用,优化先后时延对好比下):
从上图能够看出,存储引擎优化后时间延迟进一步下降并趋于平稳,从平均80ms到平均20ms左右,可是仍是不完美,有抖动。
如第3节所述,当wiredtiger大量淘汰数据后,发现只要每秒磁盘写入量超过500M/s,接下来的几秒钟内util就会持续100%,w/s几乎跌0,因而开始怀疑磁盘硬件存在缺陷。
从上图能够看出磁盘为nvMe的ssd盘,查看相关数据能够看出该盘IO性能很好,支持每秒2G写入,iops能达到2.5W/S,而咱们线上的盘只能每秒写入最多500M。
因而考虑把该分片集群的主节点所有迁移到另外一款服务器,该服务器也是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以上写入。
经过mongodb服务层配置优化、存储引擎优化、硬件IO提高三方面的优化后,该大流量写入集群的平均时延从以前的平均数百ms下降到了平均2-4ms,总体性能提高数十倍,效果明显。
可是,从4.2章节优化后的时延能够看出,集群偶尔仍是会有抖动,鉴于篇幅,下期会分享若是消除4.2章节中的时延抖动,最终保持时间彻底延迟控制在2-4ms,而且无任何超过10ms的抖动,敬请期待,下篇会更加精彩。
此外,在集群优化过程当中采了一些坑,下期会继续分析大流量集群采坑记。
注意:文章中的一些优化方法并非必定适用于全部mongodb场景,请根据实际业务场景和硬件资源能力进行优化,而不是循序渐进。
咱们近期还将继续分享以下主题,敬请关注:
百万级高并发MongoDB集群性能数十倍提高原理(下)
百万计高并发MongoDB集群性能优化采坑记
线上典型集群抖动、不可用等问题汇总分析
MongoDB文档数据库业务使用最佳案例分享
最后的最后,顺便打一个广告,OPPO互联网运维云存储团队急缺多个岗位:
若是对MongoDB内核源码、Wiredtiger存储引擎、RocksDB存储引擎、数据库机房多活、数据链路同步系统、中间件、数据库等有兴趣的同窗。欢迎加入OPPO你们庭,一块儿参与OPPO百万级高并发文档数据库研发。
工做地点:成都 / 深圳
邮箱:yangyazhou#oppo.com