当前位置: 首页 > 科技观察

MongoDB综合索引引发的“生产事故”灾难_0

时间:2023-03-14 08:17:21 科技观察

上期总结11月底,我司商品业务MongoDB主库出现严重抖动和频繁锁死的情况。由于很多业务有插入MongoDB立即查询等逻辑,项目没有开启读写分离。最终定位问题是由于:服务器自身磁盘+大量慢查询。基于以上情况,运维同学将重点加强对MongoDB慢查询的监控和告警。好在缓存过期时间刚好在事故发生前完成。Upgradeand过期时间为1个月,C端查询全部落在缓存上,所以没有造成P0级事故,只是阻塞了部分B端逻辑事故。我公司各项监控比较到位。高报警通知,于是我和同事赶紧登录Zabbix监控,如下图,截图是正常状态,出事时忘记留图了。可以想象,那时候的数据曲线无论如何应该是很低的。,应该是低和高。Zabbix分布式监控系统官网:https://www.zabbix.com/开始分析是我们研发没有控制服务器的权限,于是委托运维同学帮我们抓取一些查询记录,如下:---------------------------------------------------------------------------------------------+操作|持续时间|查询-----------------------------------------------------------------------------------------------------------------+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"}...查询是veryslow也就是说,所有的研发首先想到的就是索引的使用,所以我立马查看了索引,如下图:###当时的索引db.sku_main.ensureIndex({"orgCode":1,"_id":-1},{background:true});db.sku_main.ensureIndex({"orgCode":1,"upcCode":1},{background:true});....我屏蔽了干扰项,反正可以很明显这个查询完全可以命中索引,所以我们要面对第一个问题:上面查询记录中排名第一的慢查询是不是源有问题吗?我的判断是:应该不是数据库整体慢的根本原因,第一,它的查询条件简单暴力到完全命中索引,上面只有少数其他查询条件的索引。二、查询记录中也有结构相同、条件不同的查询,耗时很短当运维同学继续查看查询日志时,又发现了一个令人震惊的查询,如下:###当时的场景日志查询:{$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}},数据库:{acquireCount:{r:293502}},集合:{acquireCount3:{2}r:29}}#Time-consuming179530ms#耗时180秒,从查询的执行计划来看,使用了_id_索引,进行了全表扫描。扫描数据总量为:37567133,所以不慢。快速解决定位问题后,没有办法立即修改。第一要务是:止损组合当时比较晚,所以我们发布公告,禁止上述查询功能,并暂时停止部分业务。过了一会儿,进行了主从切换,然后又去看了zabbix监控,一切正常。为了分析根本原因,让我们回顾一下查询语句和我们预期的索引,如下所示:###OriginalQuerydb.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});乍一看,一切似乎都很好,字段orgCode等效查询,字段_id按照索引创建方向倒序排序,怎么这么慢?不过重点是$lt上的知识点1:索引、方向和排序在MongoDB中,排序操作通过按照索引的顺序从索引中获取文档来保证结果的顺序。如果MongoDB的查询规划器无法从索引中获取排序顺序,那么它需要在内存中对结果进行排序。注意:当内存超过32MB时,无索引排序操作会终止,也就是说MongoDB只能支持32MB以内的无索引排序,如果排序方向和索引方向相反,只需要从另一端开始遍历,如下所示:#indexdb.records.createIndex({a:1});#querydb.records.find().sort({a:-1});#索引是升序的,但是我的查询是降序的,只需要从右端开始遍历就可以满足要求,反之亦然MIN01234567MAXMongoDB的复合索引结构官方介绍:MongoDB支持复合索引,其中一个是单索引结构保存对集合文档中多个字段的引用。复合索引结构示意图如下:索引和我们讨论的完全一样,userid的顺序和score的倒序。我们需要面对第二个问题:使用复合索引需要关心方向吗?假设有两个查询条件:#queryonedb.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的单列索引。仔细阅读根本原因后,回顾网上的查询语句,如下:###OriginalQuerydb.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);###expectedindexdb.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]=>timeout(超时10s)#优化后Query[TEMPINDEX]=>gt:{"limit":1000,"queryObject":{"_id":{"$gt":2039180008},"categoryId":23372,"orgCode":351414,"fixedStatus":{"$in":[1,2]}},"restrictedTypes":[],"skip":0,"sortObject":{"_id":-1}}#Time-优化后消耗[TEMPGT]=>消耗Time:383ms,ListSize:999总结分析了2000个字符,其实变化只有两个字符。Of当然,真正的改变需要考虑业务的需求,但是既然问题已经定位和修改任何内容都不难。回顾以上内容总结如下:学习数据库知识时,可以类推,但需要格外注意其区别(MySQL、MongoDB索引、索引方向)MongoDB数据库单列索引可以不用关心方向。比如需要控制数据级别(32M)对非索引字段进行排序。在使用MongoDB数据库的复合索引时,一定要注意它的方向,充分理解它的逻辑,避免索引失效。》,可以通过以下二维码关注和转载本文,请联系Kerwin公众号。