MongoDB复合索引引发的灾难是怎样的
这期内容当中小编将会给大家带来有关MongoDB复合索引引发的灾难是怎样的,文章内容丰富且以专业的角度为大家分析和叙述,阅读完这篇文章希望大家可以有所收获。
前情提要
11月末我司商品服务的MongoDB主库曾出现过严重抖动、频繁锁库等情况。
由于诸多业务存在插入MongoDB、然后立即查询等逻辑,因此项目并未开启读写分离。
最终定位问题是由于:服务器自身磁盘 + 大量慢查询导致
基于上述情况,运维同学后续着重增强了对MongoDB慢查询的监控和告警
幸运的一点:在出事故之前刚好完成了缓存过期时间的升级且过期时间为一个月,C端查询都落在缓存上,因此没有造成P0级事故,仅仅阻塞了部分B端逻辑
事故回放
我司的各种监控做的比较到位,当天突然收到了数据库服务器负载较高的告警通知,于是我和同事们就赶紧登录了Zabbix监控,如下图所示,截图的时候是正常状态,当时事故期间忘记留图了,可以想象当时的数据曲线反正是该高的很低,该低的很高就是了。
Zabbix 分布式监控系统官网:https://www.zabbix.com/
开始分析
我们研发是没有操控服务器权限的,因此委托运维同学帮助我们抓取了部分查询记录,如下所示:
---------------------------------------------------------------------------------------------------------------------------+Op|Duration|Query---------------------------------------------------------------------------------------------------------------------------+query|5s|{"filter":{"orgCode":350119,"fixedStatus":{"$in":[1,2]}},"sort":{"_id":-1},"find":"sku_main"}query|5s|{"filter":{"orgCode":350119,"fixedStatus":{"$in":[1,2]}},"sort":{"_id":-1},"find":"sku_main"}query|4s|{"filter":{"orgCode":346814,"fixedStatus":{"$in":[1,2]}},"sort":{"_id":-1},"find":"sku_main"}query|4s|{"filter":{"orgCode":346814,"fixedStatus":{"$in":[1,2]}},"sort":{"_id":-1},"find":"sku_main"}query|4s|{"filter":{"orgCode":346814,"fixedStatus":{"$in":[1,2]}},"sort":{"_id":-1},"find":"sku_main"}...
查询很慢的话所有研发应该第一时间想到的就是索引的使用问题,所以立即检查了一遍索引,如下所示:
###当时的索引db.sku_main.ensureIndex({"orgCode":1,"_id":-1},{background:true});db.sku_main.ensureIndex({"orgCode":1,"upcCode":1},{background:true});....
我屏蔽了干扰项,反正能很明显的看出来,这个查询是完全可以命中索引的,所以就需要直面第一个问题:
上述查询记录中排首位的慢查询到底是不是出问题的根源?
我的判断是:它应该不是数据库整体缓慢的根源,因为第一它的查询条件足够简单暴力,完全命中索引,在索引之上有一点其他的查询条件而已,第二在查询记录中也存在相同结构不同条件的查询,耗时非常短。
在运维同学继续排查查询日志时,发现了另一个比较惊爆的查询,如下:
###当时场景日志query:{$query:{shopCategories.0:{$exists:false},orgCode:337451,fixedStatus:{$in:[1,2]},_id:{$lt:2038092587}},$orderby:{_id:-1}}planSummary:IXSCAN{_id:1}ntoreturn:1000ntoskip:0keysExamined:37567133docsExamined:37567133cursorExhausted:1keyUpdates:0writeConflicts:0numYields:293501nreturned:659reslen:2469894locks:{Global:{acquireCount:{r:587004}},Database:{acquireCount:{r:293502}},Collection:{acquireCount:{r:293502}}}#耗时179530ms
# 耗时耗时180秒且基于查询的执行计划可以看出,它走的是_id_索引,进行了全表扫描,扫描的数据总量为:37567133,不慢才怪。
迅速解决
定位到问题后,没办法立即修改,第一要务是:止损
结合当时的时间也比较晚了,因此我们发了公告,禁止了上述查询的功能并短暂暂停了部分业务,,过了一会之后进行了主从切换,再去看Zabbix监控就一切安好了。
分析根源
我们回顾一下查询的语句和我们预期的索引,如下所示:
###原始Querydb.getCollection("sku_main").find({"orgCode":NumberLong(337451),"fixedStatus":{"$in":[1.0,2.0]},"shopCategories":{"$exists":false},"_id":{"$lt":NumberLong(2038092587)}}).sort({"_id":-1.0}).skip(1000).limit(1000);###期望的索引db.sku_main.ensureIndex({"orgCode":1,"_id":-1},{background:true});
乍一看,好像一切都很Nice啊,字段orgCode等值查询,字段_id按照创建索引的方向进行倒序排序,为啥会这么慢?
但是,关键的一点就在 $lt 上
知识点一:索引、方向及排序
在MongoDB中,排序操作可以通过从索引中按照索引的顺序获取文档的方式,来保证结果的有序性。
如果MongoDB的查询计划器没法从索引中得到排序顺序,那么它就需要在内存中对结果排序。
注意:不用索引的排序操作,会在内存超过32MB时终止,也就是说MongoDB只能支持32MB以内的非索引排序
知识点二:单列索引不在乎方向
无论是MongoDB还是MySQL都是用的树结构作为索引,如果排序方向和索引方向相反,只需要从另一头开始遍历即可,如下所示:
#索引db.records.createIndex({a:1});#查询db.records.find().sort({a:-1});#索引为升序,但是我查询要按降序,我只需要从右端开始遍历即可满足需求,反之亦然MIN01234567MAX
MongoDB的复合索引结构
官方介绍:MongoDB supports compound indexes, where a single index structure holds references to multiple fields within a collection’s documents.
复合索引结构示意图如下所示:
该索引刚好和我们讨论的是一样的,userid顺序,score倒序。
我们需要直面第二个问题:复合索引在使用时需不需要在乎方向?
假设两个查询条件:
#查询一db.getCollection("records").find({"userid":"ca2"}).sort({"score":-1.0});#查询二db.getCollection("records").find({"userid":"ca2"}).sort({"score":1.0});
上述的查询没有任何问题,因为受到score字段排序的影响,只是数据从左侧还是从右侧遍历的问题,那么下面的一个查询呢?
#错误示范db.getCollection("records").find({"userid":"ca2","score":{"$lt":NumberLong(2038092587)}}).sort({"score":-1.0});
错误原因如下:
由于score字段按照倒序排序,因此为了使用该索引,所以需要从左侧开始遍历
从倒序顺序中找小于某个值的数据,势必会扫描很多无用数据,然后丢弃,当前场景下找大于某个值才是最佳方案
所以MongoDB为了更多场景考虑,在该种情况下,放弃了复合索引,选用其他的索引,如 score 的单列索引
针对性修改
仔细阅读了根源之后,再回顾线上的查询语句,如下:
###原始Querydb.getCollection("sku_main").find({"orgCode":NumberLong(337451),"fixedStatus":{"$in":[1.0,2.0]},"shopCategories":{"$exists":false},"_id":{"$lt":NumberLong(2038092587)}}).sort({"_id":-1.0}).skip(1000).limit(1000);###期望的索引db.sku_main.ensureIndex({"orgCode":1,"_id":-1},{background:true});
犯的错误一模一样,所以MongoDB放弃了复合索引的使用,该为单列索引,因此进行针对性修改,把 $lt 条件改为 $gt 观察优化结果:
#原始查询[TEMPINDEX]=>lt:{"limit":1000,"queryObject":{"_id":{"$lt":2039180008},"categoryId":23372,"orgCode":351414,"fixedStatus":{"$in":[1,2]}},"restrictedTypes":[],"skip":0,"sortObject":{"_id":-1}}#原始耗时[TEMPLT]=>超时(超时时间10s)#优化后查询[TEMPINDEX]=>gt:{"limit":1000,"queryObject":{"_id":{"$gt":2039180008},"categoryId":23372,"orgCode":351414,"fixedStatus":{"$in":[1,2]}},"restrictedTypes":[],"skip":0,"sortObject":{"_id":-1}}#优化后耗时[TEMPGT]=>耗时:383ms,ListSize:999
分析了小2000字,其实改动就是两个字符而已,当然真正的改动需要考虑业务的需要,但是问题既然已经定位,修改什么的就不难了,
上述就是小编为大家分享的MongoDB复合索引引发的灾难是怎样的了,如果刚好有类似的疑惑,不妨参照上述分析进行理解。如果想知道更多相关知识,欢迎关注亿速云行业资讯频道。
声明:本站所有文章资源内容,如无特殊说明或标注,均为采集网络资源。如若本站内容侵犯了原著者的合法权益,可联系本站删除。