杨亚洲,前滴滴出行专家工程师,现任OPPO文档数据库mongodb负责人,负责数万亿级数据量文档数据库mongodb内核研发、性能优化及运维工作,一直专注于分布式缓存、高性能服务端、数据库、中间件等相关研发。
某百亿级mongodb业务只保存近期7天的数据,由于数据量大、流量高,数据过期删除点比较集中,同时不能错峰方式解决问题,因此如何利用最小物理成本来满足业务需求就成为了本集群性能优化的难点。
通过几轮和业务配合调优,包括存储引擎调优、数据删除方式调优、业务错峰读写等,最终完美解决了业务痛点,达到ms级业务读写访问。
一、业务背景
线上某业务数据量约100亿,白天为写流量高峰期,峰值写约14W/s,如下图所示:
业务每天白天生产数据,同时凌晨批量拉取过去几天数据做大数据分析,整个集群只保存最近七天数据。
单条数据约800字节,如下所示:
1. {
2. "_id" : ObjectId("608592008bd3dad61675b491"),
3. "deviceSn" : "xxxxxxxxxxxxx",
4. "itemType" : 0,
5. "module" : "xxxxxx",
6. "userId" : "xxxxx",
7. "callTimes" : NumberLong(2),
8. "capacityAdd" : NumberLong(0),
9. "capacityDelete" : ”xxxxxxxx”,
10. "capacityDownload" :”xxxxxxxxxxxx”,
11. "capacityModify" : ”xxxxxxxxxxxx”,
12. "createTime" : NumberLong("1619366400003"),
13. "expireAt" : ISODate("2021-05-02T22:53:45.497Z"),
14. "numAdd" : NumberLong(2),
15. "numDelete" : NumberLong(345),
16. "numDownload" : NumberLong(43),
17. "numModify" : NumberLong(3),
18. "osVersion" : "xxxx",
19. "reversedUserId" : "xxxxx",
20. "updateTime" : NumberLong("1619366402106")
二、mongodb资源评估及部署架构
通过和业务对接梳理,该集群规模及业务需求总结如下:
数据量百亿级
单条数据800字节,100亿条预计7.5T数据
读写分离
所有数据只保留七天
分片数及存储节点套餐规格选定评估过程如下:
内存评估
我司都是容器化部署,以以往经验来看,mongodb对内存消耗不高,历史百亿级以上mongodb集群单个容器最大内存基本上都是64Gb,因此内存规格确定为64G。
分片评估
业务流量峰值10W/s多,预计需要3个分片支持读写。
磁盘评估
100亿数据7.5T,由于mongodb默认有高压缩,预计真实磁盘占用2.5~3T左右。三个分片,一个分片刚好1T。
CPU规格评估
由于容器调度套餐化限制,因此CPU只能限定为16CPU(实际上用不了这么多CPU)。
mongos代理及config server规格评估
此外,由于分片集群还有mongos代理和config server复制集,因此还需要评估mongos代理和config server节点规格。由于config server只主要存储路由相关元数据,因此对磁盘、CUP、MEM消耗都很低;mongos代理只做路由转发只消耗CPU,因此对内存和磁盘消耗都不高。最终,为了最大化节省成本,我们决定让一个代理和一个config server复用同一个容器,容器规格如下:
8CPU/8G内存/50G磁盘,一个代理和一个config server节点复用同一个容器。
分片及存储节点规格总结:4分片/16CPU、64G内存、1T磁盘。
mongos及config server规格总结:8CPU/8G内存/50G磁盘
节点类型 |
单个节点容器规格 |
说明 |
shardServer存储节点 |
16CPU、64G内存、1T磁盘 |
1个分片存储约30亿数据 3个分片预计存储100亿数据 |
mongos及config server节点 |
8CPU/8G内存/50G磁盘 |
一个代理和一个config server节点复用同一个容器 |
该业务数据不是很重要,为了节省成本,因此我们采用2+1模式部署,也就是:2mongod+1arbiter模式,同城机房部署,部署架构图如下图所示:
考虑到数据重要性不高,通过2mongod+1arbiter模式即可满足用户要求,同时可以最大化节省成本。
三、性能优化过程
该集群优化过程按照如下两个步骤优化:业务使用集群前的性能优化、业务使用过程中的性能优化。
业务提前建好查询对应的最优索引,同时创建过期索引:
db.dailyCloudOperateInfo.createIndex( { "createTime": 1 }, { expireAfterSeconds: 604800} )
和业务沟通确定,业务每条数据都携带有一个设备标识userId,同时业务查询更新等都是根据userId维度查询该设备下面的单条或者一批数据,因此片建选择userId。
分片方式
为了充分散列数据到3个分片,因此选择hash分片方式,这样数据可以最大化散列,同时可以满足同一个userId数据落到同一个分片,保证查询效率。
预分片
mongodb如果分片片建为hashed分片,则可以提前做预分片,这样就可以保证数据写进来的时候比较均衡的写入多个分片。预分片的好处可以规避非预分片情况下的chunk迁移问题,最大化提升写入性能。
sh.shardCollection("cloud_track.dailyCloudOperateInfo", {userId:"hashed"}, false, { numInitialChunks: 8192} )
就近读
客户端增加secondaryPreferred配置,优先读从节点。
禁用enable Majority Read Concern
禁用该功能后Read Concern majority将会报错,Read Concern majority功能注意是避免脏读,和业务沟通业务没该需求,因此可以直接关闭。
mongodb默认使能了enable Majority Read Concern,该功能开启对性能有一定影响,参考:
MongoDB readConcern 原理解析:
https://developer.aliyun.com/article/60553
OPPO百万级高并发MongoDB集群性能数十倍提升优化实践:
https://mongoing.com/archives/29934
存储引擎cacheSize规格选择
单个容器规格:16CPU、64G内存、7T磁盘,考虑到全量迁移过程中对内存压力,内存碎片等压力会比较大,为了避免OOM,设置cacheSize=42G。
1)第一轮优化:存储引擎优化
业务高峰期主要是数据写入和更新,内存脏数据较多,当脏数据比例达到一定比例后用户读写请求对应线程将会阻塞,用户线程也会去淘汰内存中的脏数据page,最终写性能下降明显。
wiredtiger存储引擎cache淘汰策略相关的几个配置如下:
由于业务全量迁移数据是持续性的大流量写,而不是突发性的大流量写,因此eviction_target、eviction_trigger、eviction_dirty_target、eviction_dirty_trigger几个配置用处不大,这几个参数阀值只是在短时间突发流量情况下调整才有用。
但是,在持续性长时间大流量写的情况下,我们可以通过提高wiredtiger存储引擎后台线程数来解决脏数据比例过高引起的用户请求阻塞问题,淘汰脏数据的任务最终交由evict模块后台线程来完成。
全量大流量持续性写存储引擎优化如下:
db.adminCommand( { setParameter : 1, "wiredTigerEngineRuntimeConfig" : "eviction=(threads_min=4, threads_max=20)"})
2)第一轮优化后存在的问题
经过存储引擎后台线程数的调优后,数据写入和更新瓶颈解决,写入和更新过程时延都很平滑。但是随着一周后开始数据过期,业务写开始大量抖动,如下所示:
从上图可以看出平均时延极端情况下甚至达到了几百ms,这是业务完全接受不了的。通过mongostat监控发现如下现象:
主节点mongostat监控统计
从上面的监控可以看出,三个分片的每个主节点只有4000左右的写更新操作(注意:实际上还有4万左右的delete操作,由于主节点过期delete不会统计,因此只能通过从节点查看,详见后面分析,实际单个分片主节点写和删除操作4.4W/s),写流量很低。但是,监控中的脏数据比例持续性的超过20%,超过20%后业务的请求就需要进行脏数据淘汰,最终造成业务请求阻塞抖动。
通过前面的分析可以看出,业务正常峰值写入3个分片差不多10W/s。一星期后,七天前的数据需要过期,这时候过期删除的ops也需要delete删除10w/S,由于这时候新数据同样按照10w/s写入,因此集群就需要支持20w/s的ops操作才能支持。
显然,3个分片支持不了持续性的20w/s左右的ops操作,因此如何不扩容情况下支撑业务需求将是一大难点。
为何ttl过期主节点没用delete统计
上图为mongodb单机模块架构图,主节点默认启用一个TTLMonitor线程,借助查询引起模块实时扫描过期索引,然后把满足条件的数据删除。整个删除过程没有走command命令处理模块,而命令计数操作只会在command模块计数,因此主节点的过期删除不会有delete操作。
命令处理模块处理流程及其计数统计详见:
mongodb内核源码模块化设计与实现专栏
更多mongodb模块化源码设计实现详见:
https://github.com/y123456yz/reading-and-annotate-mongodb-3.6
从前面的分析可以看出,我们三个分片支撑不了持续性20W/S的更新和删除操作,因此我们考虑业务改造使用方式,把过期删除确定到凌晨低峰期。
但是业务上线后出现其他的问题,由于业务凌晨会持续性的批量拉取分析过去几天的数据,如果过期和批量读数据叠加到一起,严重影响业务查询效率。最终,该方案不可行,如果不扩容,则需要其他方案。
为了不增加成本,同时3个分片又支撑不了20W/s的读写删除等导致,为了尽量通过3个分片不扩容条件下来满足用户需求,因此转变方式,通过删表的方式来避免过期,具体实现如下:
业务改造代码,以天为单位建表,每天的数据存到对应的表中。
建表后业务启用预分片功能,确保数据均衡分布到三个分片。
业务保存8天数据,第九天凌晨删除第一天的表
该方案业务时延统计对应收益如下:
如上图所示,通过过期方式的优化,最终问题彻底解决,并且读写时延控制在0.5ms-2ms。
四、优化收益总结
通过前面的一系列优化,最终没有扩容,并且解决了业务过期和大量数据写入更新引起的时延抖动问题,总体收益如下:
改造优化前时延:经常几百ms抖动
改造优化后时延:0.5-2ms
dbaplus社群欢迎广大技术人员投稿,投稿邮箱:editor@dbaplus.cn
如果字段的最大可能长度超过255字节,那么长度值可能…
只能说作者太用心了,优秀
感谢详解
一般干个7-8年(即30岁左右),能做到年入40w-50w;有…
230721