「生產事故」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. 再來看看最近幾篇的「查漏補缺」系列吧,該系列會持續輸出~

相關文章
相關標籤/搜索