「生产事故」MongoDB复合索引引起的灾难

前情提要

  1. 11月末我司商品服务MongoDB主库曾出现过严重抖动、频繁锁库等状况。
  2. 因为诸多业务存在插入MongoDB、而后当即查询等逻辑,所以项目并未开启读写分离。
  3. 最终定位问题是因为:服务器自身磁盘 + 大量慢查询致使
  4. 基于上述状况,运维同窗后续着重加强了对MongoDB慢查询的监控和告警
幸运的一点:在出事故以前恰好完成了缓存过时时间的升级且过时时间为一个月, C端查询都落在缓存上,所以没有形成 P0级事故,仅仅阻塞了部分 B端逻辑

<br/>数据库

事故回放

我司的各类监控作的比较到位,当天忽然收到了数据库服务器负载较高的告警通知,因而我和同事们就赶忙登陆了Zabbix监控,以下图所示,截图的时候是正常状态,当时事故期间忘记留图了,能够想象当时的数据曲线反正是该高的很低,该低的很高就是了。缓存

Zabbix 分布式监控系统官网: https://www.zabbix.com/

<br/>bash

开始分析

咱们研发是没有操控服务器权限的,所以委托运维同窗帮助咱们抓取了部分查询记录,以下所示:服务器

---------------------------------------------------------------------------------------------------------------------------+
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});
....

我屏蔽了干扰项,反正能很明显的看出来,这个查询是彻底能够命中索引的,因此就须要直面第一个问题:分布式

<font color="red">上述查询记录中排首位的慢查询究竟是不是出问题的根源?</font>post

个人判断是:它应该不是数据库总体缓慢的根源,由于第一它的查询条件足够简单暴力,彻底命中索引,在索引之上有一点其余的查询条件而已,第二在查询记录中也存在相同结构不一样条件的查询,耗时很是短。学习

在运维同窗继续排查查询日志时,发现了另外一个比较惊爆的查询,以下:优化

### 当时场景日志

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,不慢才怪。ui

<br/>

迅速解决

定位到问题后,没办法当即修改,第一要务是:止损

结合当时的时间也比较晚了,所以咱们发了公告,禁止了上述查询的功能并短暂暂停了部分业务,,过了一会以后进行了主从切换,再去看Zabbix监控就一切安好了。

<br/>

分析根源

咱们回顾一下查询的语句和咱们预期的索引,以下所示:

### 原始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倒序

咱们须要直面第二个问题:<font color="red">复合索引在使用时需不须要在意方向?</font>

假设两个查询条件:

# 查询 一
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});

错误缘由以下:

  • <font color="red">因为score字段按照倒序排序,所以为了使用该索引,因此须要从左侧开始遍历</font>
  • <font color="red">从倒序顺序中找小于某个值的数据,势必会扫描不少无用数据,而后丢弃,当前场景下找大于某个值才是最佳方案</font>
  • <font color="red">因此MongoDB为了更多场景考虑,在该种状况下,放弃了复合索引,选用其余的索引,如 score 的单列索引</font>

针对性修改

仔细阅读了根源以后,再回顾线上的查询语句,以下:

### 原始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数据库复合索引在使用中必定要注意其方向,要彻底理解其逻辑,避免索引失效

最后

若是你以为这篇内容对你挺有帮助的话:

  1. 固然要点赞支持一下啦~
  2. 搜索并关注公众号「是Kerwin啊」,一块儿唠唠嗑~
  3. 再来看看最近几篇的「查漏补缺」系列吧,该系列会持续输出~

相关文章
相关标签/搜索