背景

中午12点半,接到了线上MongoDB 数据库异常的告警通报:

“CPU不间断飙升到百分百,业务也相应出现了抖动现象。”

通过排查数据库主节点的日志,发现了这样的一个慢语句:

2019-03-07T10:56:43.470+0000 I COMMAND [conn2544772] command nlp.ApplicationDevice appName: "nlp" command:
find { find: "ApplicationDevice", filter: { appId: "Gf93VvCfOdttrxSOemt_03ff", tags.tagName: "pipeline",
tags.tagValue: "multi", _id: { $gt: ObjectId('000000000000000000000000') } }, projection: { $sortKey: { $meta: "sortKey" } },
sort: { _id: 1 }, limit: 10, shardVersion: [ Timestamp 1000|1000, ObjectId('5c64f941c81e2b922e48e347') ] }
planSummary: IXSCAN { appId: 1, tags.tagName: 1, tags.tagValue: 1, _id: 1 }
keysExamined:1000002 docsExamined:1000001 hasSortStage:1 cursorExhausted:1 numYields:7829
nreturned:10 reslen:7102 locks:{ Global: { acquireCount: { r: 15660 } },
Database: { acquireCount: { r: 7830 } }, Collection: { acquireCount: { r: 7830 } } } protocol:op_command 4008ms

从语句中初步判断,"keysExamined"docsExamined 显示扫描了100W 条记录,其中也用到了下面的索引:

{ appId: 1, tags.tagName: 1, tags.tagValue: 1, _id: 1 }

跟研发兄弟确认过后,该查询的目的是 找到某些应用下带指定标签的设备信息,按ID分段去获取,每次只查询10条。
关于索引的设计也已经确认过是最优的了,而且此前在开发环境中一直没有出现过问题,不知道为什么这次就出问题了。

详细分析

接下来,看了下该集合的模型,大致是长下面的样子:

/* 1 */
{"appId" : "Gf93VvCfOdttrxSOemt_03ff","deviceId" : "bbc-lmc-03991933","nodeType" : "FACTORY","creationTime" : ISODate("2019-03-01T10:11:39.852Z"),"lastModifiedTime" : ISODate("2019-03-03T10:45:40.249Z"),"tags" : [ {"tagName" : "pipeline","tagValue" : "multi","tagType" : 1}],...
}

说明
除了其他的属性之外,tags字段采用了嵌套文档数组的结构;
每一个元素都对应了一个tag对象,包含 tagName/tagValue/tagType几个字段。

然后是查询的模式


//过滤条件
{ appId: "Gf93VvCfOdttrxSOemt_03ff", tags.tagName: "pipeline", tags.tagValue: "multi", _id: { $gt: ObjectId('000000000000000000000000')
}//排序
sort: { _id: 1 }//限制条数
limit: 10

这从索引的前缀匹配来看,是应该没有问题的,索引的定义如下所示:

{ appId: 1, tags.tagName: 1, tags.tagValue: 1, _id: 1 }

为了避免对线上环境造成影响,我们找了一个测试环境来做了尝试复现,执行:

db.ApplicationDevice.find({"appId":"Gf93VvCfOdttrxSOemt_03ff","tags.tagName":"pipeline","tags.tagValue":"multi",_id:{$gt:ObjectId("000000000000000000000000")}}).sort({"_id" : 1}).explain()

结果却跟线上的情况不大一样,这次选中的是**_id**索引!

"winningPlan" : {
"stage" : "LIMIT",
"limitAmount" : 10,
"inputStage" : {"stage" : "SHARDING_FILTER","inputStage" : {"stage" : "FETCH","filter" : {"$and" : [{"appId" : {"$eq" : "Gf93VvCfOdttrxSOemt_03ff"}},{"tags.tagName" : {"$eq" : "pipeline"}},{"tags.tagValue" : {"$eq" : "multi"}}]},"inputStage" : {"stage" : "IXSCAN","keyPattern" : {"_id" : 1},"indexName" : "_id_","isMultiKey" : false,"multiKeyPaths" : {"_id" : [ ]},"isUnique" : true,"isSparse" : false,"isPartial" : false,"indexVersion" : 2,"direction" : "forward","indexBounds" : {"_id" : ["(ObjectId('000000000000000000000000'), ObjectId('ffffffffffffffffffffffff')]"]}}}
}

而同样的是也扫描了100W+的记录数,于是大家认为可能索引的选择器出了问题,但就算是选择器的问题也仍然没办法解释线上出现的现象(线上的索引可是命中的)
为了一探究竟,我们使用 hint 强制让查询命中 **appId_1_tags.tagName_1_tags.tagValue_1__id_1**这个索引:

db.ApplicationDevice.find({"appId":"Gf93VvCfOdttrxSOemt_03ff","tags.tagName":"pipeline","tags.tagValue":"multi",_id:{$gt:ObjectId("000000000000000000000000")}}).sort({"_id" : 1}).limit(10).hint("appId_1_tags.tagName_1_tags.tagValue_1__id_1").explain("executionStats")

这一次的结果显示确实命中了对应的索引:

"winningPlan" : {
"stage" : "SORT",
"sortPattern" : {"_id" : 1.0
},
"limitAmount" : 10,
"inputStage" : {"stage" : "SORT_KEY_GENERATOR","inputStage" : {"stage" : "FETCH","filter" : {"tags.tagValue" : {"$eq" : "multi"}},"inputStage" : {"stage" : "IXSCAN","keyPattern" : {"appId" : 1.0,"tags.tagName" : 1.0,"tags.tagValue" : 1.0,"_id" : 1.0},"indexName" : "appId_1_tags.tagName_1_tags.tagValue_1__id_1"..."executionStats" : {"executionSuccess" : true,"nReturned" : 10,"executionTimeMillis" : 3665,"totalKeysExamined" : 1000002,"totalDocsExamined" : 1000001,"executionStages" : {"stage" : "SORT","nReturned" : 10,"executionTimeMillisEstimate" : 3513,"works" : 1000014,"sortPattern" : {"_id" : 1.0},"memUsage" : 6660,"memLimit" : 33554432,"limitAmount" : 10,"inputStage" : {"stage" : "SORT_KEY_GENERATOR","nReturned" : 500001,"executionTimeMillisEstimate" : 3333,"works" : 1000003,"advanced" : 500001,"needTime" : 500001,"inputStage" : {"stage" : "FETCH","filter" : {"tags.tagValue" : {"$eq" : "multi"}},"nReturned" : 500001,"executionTimeMillisEstimate" : 3087,"works" : 1000002,"advanced" : 500001,"needTime" : 500000,"docsExamined" : 1000001,"alreadyHasObj" : 0,"inputStage" : {"stage" : "IXSCAN","nReturned" : 1000001,"executionTimeMillisEstimate" : 1117,"works" : 1000002,"advanced" : 1000001,"keyPattern" : {"appId" : 1.0,"tags.tagName" : 1.0,"tags.tagValue" : 1.0,"_id" : 1.0},"indexName" : "appId_1_tags.tagName_1_tags.tagValue_1__id_1","isMultiKey" : true,"multiKeyPaths" : {"appId" : [],"tags.tagName" : [ "tags"],"tags.tagValue" : [ "tags"],"_id" : []},"indexBounds" : {"appId" : [ "[\"Gf93VvCfOdttrxSOemt_03ff\", \"Gf93VvCfOdttrxSOemt_03ff\"]"],"tags.tagName" : [ "[\"pipeline\", \"pipeline\"]"],"tags.tagValue" : [ "[MinKey, MaxKey]"],"_id" : [ "(ObjectId('000000000000000000000000'), ObjectId('ffffffffffffffffffffffff')]"]},"keysExamined" : 1000002,"dupsTested" : 1000001,}}}

然而,在整个执行过程中(executionStats),出现了内存排序(SORT)。
而且,从一开始命中** appId_1_tags.tagName_1_tags.tagValue_1__id_1 **这个索引的阶段中,就已经扫描了100W条记录,简直不可思议!

但同时,我们也从indexBounds的指示中找到了端倪:
appId、tags.tagName 都命中了单值,在 tags.tagValue 的路径节点上却覆盖了全部范围!
**由于中间索引节点出现了大范围覆盖,导致最终需要在内存中对大量的数据做 _id字段的排序**,这个就是导致慢操作的原因!

解决问题

既然从前面的分析中找到了问题的来源,我们的推论如下:

  • 既然索引的命中没有问题,那么导致大范围扫描的只可能是查询模式的问题。

再次拿出前面的查询条件:

{ appId: "Gf93VvCfOdttrxSOemt_03ff", tags.tagName: "pipeline", tags.tagValue: "multi", _id: { $gt: ObjectId('000000000000000000000000')
}

在索引的匹配中,只能单键命中tags.tagName: "pipeline" 这一个条件,那么由于 tags是一个嵌套文档的数组,
对于上面的查询,语义上是指那些 包含某个元素 可命中tagName,且包含某个元素 可命中 tagValue的文档,这里面并不要求 同一个元素同时命中tagName和tagValue。

但 MongoDB 在嵌套数组索引的构建上是按照同一个元素的字段组合去构建的。 关于这点,可以参考下面的地址:
https://docs.mongodb.com/manual/core/index-multikey/#multikey-embedded-documents

对于数组元素的条件匹配,应该使用 $elemMatch,其用法可参考这里

为此,我们构建了下面的查询:

db.ApplicationDevice.find({"appId":"Gf93VvCfOdttrxSOemt_03ff","tags": {$elemMatch: { "tagName":"pipeline","tagValue":"multi" }},_id:{$gt:ObjectId("000000000000000000000000")}}).sort({"_id" : 1}).limit(10).explain("executionStats")

执行后输出如下:

"winningPlan" : {"stage" : "LIMIT","limitAmount" : 10,"inputStage" : {"stage" : "IXSCAN","keyPattern" : {"appId" : 1.0,"tags.tagName" : 1.0,"tags.tagValue" : 1.0,"_id" : 1.0},"indexName" : "appId_1_tags.tagName_1_tags.tagValue_1__id_1",}
},
"executionStats" : {
"executionSuccess" : true,
"nReturned" : 10,
"executionTimeMillis" : 3,
"totalKeysExamined" : 10,
"totalDocsExamined" : 10,
"executionStages" : {"stage" : "LIMIT","nReturned" : 10,"inputStage" : {"stage" : "FETCH","filter" : {...},"nReturned" : 10,,"inputStage" : {"stage" : "IXSCAN","nReturned" : 10,"executionTimeMillisEstimate" : 0,"works" : 10,"advanced" : 10,"isEOF" : 0,"indexName" : "appId_1_tags.tagName_1_tags.tagValue_1__id_1","isMultiKey" : true,"indexBounds" : {"appId" : [ "[\"Gf93VvCfOdttrxSOemt_03ff\", \"Gf93VvCfOdttrxSOemt_03ff\"]"],"tags.tagName" : [ "[\"pipeline\", \"pipeline\"]"],"tags.tagValue" : [ "[\"multi\", \"multi\"]"],"_id" : [ "(ObjectId('000000000000000000000000'), ObjectId('ffffffffffffffffffffffff')]"]}...

这个结果是令人满意的,除了自动命中合适的索引之外,这个查询过程也达到了最优的路径匹配,扫描记录数才10条!
最后,根据该方案调整了查询模式,线上的问题得到恢复。

小结

看似很简单的一个查询语句,没想到会出现这么大的坑,其实无论是作为开发人员还是DBA,都应当谨慎对待你的SQL。
重要的事情说三遍!!! SQl查询上线前务必 explain、务必分析到位,这难道没有道理?

作者:美码师

线上一个数组查询遇到的坑相关推荐

  1. 记录一次线上超时异常查询

    线上事故复盘 前言 前一次上线,当时正常,第二天发现有部分超时报警,最终发现应为Dubbo接口一次传输数据量太大导致线程虚拟内存占用 线上问题排查过程 报警邮件中查询到有一部分接口超时量激增,查询定位 ...

  2. 记录上一个项目踩过的坑

    1.objecthtmldivelement对象 var avc = document.getElementById("div1"); alert(avc ); 为objectht ...

  3. 用 gson 替换 fastjson 引发的线上问题分析

    点击上方蓝色"程序猿DD",选择"设为星标" 回复"资源"获取独家整理的学习资料! 前言 Json 序列化框架存在的安全漏洞一直以来都是程序 ...

  4. Linux 截取线上日志

    按日期截取线上日志命令 (1)首先,需清楚日志输出的时间日期格式,具体命令如下: sed -n '/6-5 00:00:00/,/6-5 16:30:31/'p catalina.log > t ...

  5. 干货|以产品要素设计解读线上小微信贷

    小微信贷市场中经常收到客户的一些反馈,如:没有房子抵押.额度不够.利率太高.期限短等等,这些反馈的声音主要集中在产品要素上,客户拿到的贷款就是这些产品要素的组合,换句话讲,这些要素也正是一款产品的卖点 ...

  6. 海外直播软件 Bigo 的 TiDB 4.0 线上实践

    作者介绍:徐嘉埥,Bigo DBA,TUG 华南区大使. Bigo 于 2014 年成立,是一家高速发展的科技公司.Bigo 基于强大的音视频处理技术.全球音视频实时传输技术.人工智能技术.CDN 技 ...

  7. linux系统中怎么截取某一天的日志,Linux系统如何截取线上日志

    1.按香港云主机日期截取线上日志命令 (1)首先,需清楚日志输出的时间日期格式,具体命令如下: sed -n '/6-5 00:00:00/,/6-5 16:30:31/'p catalina.log ...

  8. 线上问题-kafka生产者发送消息总是失败

    问题解决过程 线上一个界面发现老是没有数据,排查下来时生产者没有成功发送消息所致,报错如下: org.springframework.kafka.core.KafkaProducerException ...

  9. 实习生跑路留了一个大坑,搞出2个线上问题,我被坑惨了

    小编今天被实习生坑惨了... 昨天线上问题 昨天出了一个线上问题,今天又出了一个线上问题. 昨天出的一个线上问题. 监控开始报大量的异常pvlost,接口访问过来报500,就报pvlost了. 小编赶 ...

最新文章

  1. 富士康c语言试卷答案,2015富士康笔试题目及答案
  2. MatLab GUI Load .mat File 导入mat文件
  3. 课堂练习--最大子数组和 环
  4. Python 可执行对象
  5. [Jarvis OJ - PWN]——[61dctf]fm
  6. Java-类加载器-类运行时结构-。。。。
  7. linux kears 切换后端,如何更改Keras后端(哪里是json文件)?
  8. 使用jsoncpp解析json文件
  9. 如何打造自己强大的气场?
  10. MTK MT6577 Android驱动设置方法资料分享
  11. 【大话云原生】微服务篇-五星级酒店的服务方式
  12. win10系统暴雪战网连不上服务器,win10系统暴雪战网无法登陆的解决方法
  13. 我是这样被电麻的:电容降压电路使用和注意事项
  14. DAY1古典概型和几何概型
  15. Dharma暴跌过度解读了吗?去中心化不足,模式难持续是关键
  16. 小程序数据框有重影_微信小程序input重影什么原因?怎么解决重影?
  17. QCC51XX---固件OTA升级指南
  18. 网页版连连看(JS jQuery)
  19. 如何抠图人像换背景?教你一个在线操作的方法
  20. 中国仓储业市场运营现状及前景趋势预测报告2021~2028年

热门文章

  1. php socket_create_listen(),socket_create_listen()
  2. 文献检索的正确姿势——新生培训
  3. tikz 折线 箭头_问一个关于latex中,使用tikz画图的问题,如何使箭头上方的权值不居中(因为居中会互相重叠)...
  4. blob数据类型_MySQL 8.0 基本操作步骤:3.字段和数据类型的选择
  5. vc6 设置静态文本框透明_微信还能这么玩?半透明的微信背景主题用起来!
  6. Java 导出 Excel 文件
  7. ASP .NET Controller返回类型
  8. springmvc-interceptor(拦截器)
  9. Activity-在ListFragment中为ListView增加空白视图
  10. [动态规划] uestc oj A - 男神的礼物