「生产事故」MongoDB复合索引引发的灾难
前情提要
- 11月末
我司商品服务
的MongoDB主库
曾出现过严重抖动、频繁锁库等情况。 - 由于诸多业务存在插入
MongoDB
、然后立即查询等逻辑,因此项目并未开启读写分离。 - 最终定位问题是由于:服务器自身磁盘 + 大量
慢查询
导致 - 基于上述情况,运维同学后续着重增强了对
MongoDB慢查询
的监控和告警
幸运的一点:在出事故之前刚好完成了缓存过期时间的升级且过期时间为一个月,
C端查询
都落在缓存上,因此没有造成P0级
事故,仅仅阻塞了部分B端逻辑
事故回放
我司的各种监控做的比较到位,当天突然收到了数据库服务器负载较高的告警通知,于是我和同事们就赶紧登录了Zabbix监控
,如下图所示,截图的时候是正常状态,当时事故期间忘记留图了,可以想象当时的数据曲线反正是该高的很低,该低的很高就是了。
Zabbix 分布式监控系统官网:https://www.zabbix.com/
开始分析
我们研发是没有操控服务器权限的,因此委托运维同学帮助我们抓取了部分查询记录,如下所示:
---------------------------------------------------------------------------------------------------------------------------+
Op | Duration | Query ---------------------------------------------------------------------------------------------------------------------------+
query | 5 s | {"filter": {"orgCode": 350119, "fixedStatus": {"$in": [1, 2]}}, "sort": {"_id": -1}, "find": "sku_main"}
query | 5 s | {"filter": {"orgCode": 350119, "fixedStatus": {"$in": [1, 2]}}, "sort": {"_id": -1}, "find": "sku_main"} query | 4 s | {"filter": {"orgCode": 346814, "fixedStatus": {"$in": [1, 2]}}, "sort": {"_id": -1}, "find": "sku_main"} query | 4 s | {"filter": {"orgCode": 346814, "fixedStatus": {"$in": [1, 2]}}, "sort": {"_id": -1}, "find": "sku_main"} query | 4 s | {"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:1000 ntoskip:0 keysExamined:37567133 docsExamined:37567133 cursorExhausted:1 keyUpdates:0 writeConflicts:0 numYields:293501 nreturned:659 reslen:2469894 locks:{ Global: { acquireCount: { r: 587004 } }, Database: { acquireCount: { r: 293502 } }, Collection: { acquireCount: { r: 293502 } } }
# 耗时
179530ms
耗时180秒且基于查询的执行计划
可以看出,它走的是_id_
索引,进行了全表扫描,扫描的数据总量为:37567133,不慢才怪。
迅速解决
定位到问题后,没办法立即修改,第一要务是:止损
结合当时的时间也比较晚了,因此我们发了公告,禁止了上述查询的功能并短暂暂停了部分业务,,过了一会之后进行了主从切换
,再去看Zabbix监控
就一切安好了。
分析根源
我们回顾一下查询的语句和我们预期的索引,如下所示:
### 原始Query
db.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});
# 索引为升序,但是我查询要按降序,我只需要从右端开始遍历即可满足需求,反之亦然
MIN 0 1 2 3 4 5 6 7 MAX
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 的单列索引
针对性修改
仔细阅读了根源之后,再回顾线上的查询语句,如下:
### 原始Query
db.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
观察优化结果:
# 原始查询
[TEMP INDEX] => lt: {"limit":1000,"queryObject":{"_id":{"$lt":2039180008},"categoryId":23372,"orgCode":351414,"fixedStatus":{"$in":[1,2]}},"restrictedTypes":[],"skip":0,"sortObject":{"_id":-1}}
# 原始耗时
[TEMP LT] => 超时 (超时时间10s)
# 优化后查询
[TEMP INDEX] => gt: {"limit":1000,"queryObject":{"_id":{"$gt":2039180008},"categoryId":23372,"orgCode":351414,"fixedStatus":{"$in":[1,2]}},"restrictedTypes":[],"skip":0,"sortObject":{"_id":-1}}
# 优化后耗时
[TEMP GT] => 耗时: 383ms , List Size: 999
总结
分析了小2000字,其实改动就是两个字符而已,当然真正的改动需要考虑业务的需要,但是问题既然已经定位,修改什么的就不难了,回顾上述内容总结如下:
- 学习数据库知识的时候可以用类比的方式,但是需要额外注意其不同的地方(MySQL、MongoDB索引、索引的方向)
- MongoDB数据库单列索引可以不在乎方向,如对无索引字段排序需要控制数据量级(32M)
MongoDB数据库复合索引在使用中一定要注意其方向
,要完全理解其逻辑,避免索引失效
最后
如果你觉得这篇内容对你挺有帮助的话:
- 当然要点赞支持一下啦~
- 搜索并关注公众号「是Kerwin啊」,一起唠唠嗑~
- 再来看看最近几篇的「查漏补缺」系列吧,该系列会持续输出~
「生产事故」MongoDB复合索引引发的灾难的更多相关文章
- MongoDB复合索引详解
摘要: 对于MongoDB的多键查询,创建复合索引可以有效提高性能. 什么是复合索引? 复合索引,即Compound Index,指的是将多个键组合到一起创建索引,这样可以加速匹配多个键的查询.不妨通 ...
- 生产事故(MongoDB数据分布不均解决方案)
可以很明显可以看到我们这个集合的数据严重分布不均匀. 一共有8个分片,面对这个情况我首先想到的是手动拆分数据块,但这不是解决此问题的根本办法. 造成此次生产事故的首要原因就是片键选择上的问题,由于片键 ...
- MongoDB 复合索引结构
- 深入理解MongoDB的复合索引
更新时间:2018年03月26日 10:17:37 作者:Fundebug 我要评论 对于MongoDB的多键查询,创建复合索引可以有效提高性能.这篇文章主要给大家介绍了关于MongoDB复 ...
- 关于mongodb的复合索引新功能
最新在做一个项目,由于查询字段较多,且查询较频繁,所以我做了一个复合索引,将所有需要查询的字段都做到索引里,做了一个名为s_1_m_1_c_1_v_1_year_1_month_1_week_1_da ...
- 一次 select for update 的悲观锁使用引发的生产事故
1.事故描述 本月 8 日上午十点多,我们的基础应用发生生产事故.具体表象为系统出现假死无响应.查看事发时间段的基础应用 error 日志,没发现明显异常.查看基础应用业务日志,银行结果处理的部分普遍 ...
- mongodb AND查询遇到多个index时候可能会做交集——和复合索引不同
关于MongoDB中索引文档的一个问题? - To illustrate index intersection, consider a collection orders that has the f ...
- mongodb索引 复合索引
当我们的查询条件不只有一个时,就需要建立复合索引,比如插入一条{x:1,y:2,z:3}记录,按照我们之前建立的x为1的索引,可是使用x查询,现在想按照x与y的值查询,就需要创建如下的索引 创 ...
- 关于mongodb创建索引的一些经验总结(转)
查看语句执行计划: explain() 在mongodb3+版本后输出格式发生改变: 详情参见:https://docs.mongodb.com/v3.0/reference/method/curso ...
随机推荐
- mdp文件-Chapter4-MD.mdp
终于到了mdp系列的第四篇,最终MD模拟的mdp文件 先上代码,md.mdp 1 title = OPLS Lysozyme MD simulation 2 ; Run parameters 3 in ...
- tcp 输入 简析 转载
正常来说 TCP 收消息过程会涉及三个队列: Backlog Queue sk->sk_backlog Prequeue tp->ucopy.prequeue Receive Queue ...
- binary hacks读数笔记(装载)
1.地址空间 在linux系统中,每个进程拥有自己独立的虚拟地址空间,这个虚拟地址空间的大小是由计算机硬件决定的,具体地说,是由CPU的位数决定的.比如,32位硬件平台决定的虚拟地址空间大小:0--2 ...
- 在Linux下的安装mysql-5.7.28 心得总结
mysql-5.7.28 在Linux下的安装教程图解 这篇文章主要介绍了mysql-5.7.28 的Linux安装,本文通过图文并茂的形式给大家介绍的非常详细,具有一定的参考借鉴价值,希望给有需要的 ...
- Python博文_爬虫工程师是干什么的
程序员有时候很难和外行人讲明白自己的工作是什么,甚至有些时候,跟同行的人讲清楚"你是干什么的"也很困难.比如我自己,就对Daivd在搞的语义网一头雾水.所以我打算写一篇博客,讲一下 ...
- hive简单的项目实战
解压user.zip [root@hadoop1 test]# unzip user.zip -d /test/bigdatacase/dataset Archive: user.zip inflat ...
- matlab 向量操作作业
写出下列语句的计算结果及作用 clear 清除所有变量 clc 清屏 A = [2 5 7 1 3 4]; 创建行向量并赋值 odds = 1:2:length(A); 冒号操 ...
- IDEA 使用的一些快捷键记录
1,ctrl+tab 导航当前编辑打开的所有文件,按住Ctrl,使用backspace 可以关闭某个文件 2,ctrl+shift+alt+s 打开项目设置,alt+shift+s 打开所有设置 3, ...
- 应聘阿里,字节跳动,美团必须掌握的Spring IOC与工厂模式
Spring IOC与工厂模式 PS:本文内容较为硬核,需要对java的面向对象.反射.类加载器.泛型.properties.XML等基础知识有较深理解. (一)简单介绍 在讲Spring IOC之前 ...
- Android sensor架构分析
一.其主要框架如下图所示: 二.sensor的JNI层:android_hardware_SensorManager.cpp (frameworks\base\core\jni) 注册JN ...