前情提要
- 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啊」,一起嘮嘮嗑~
- 再來看看最近幾篇的「查漏補缺」系列吧,該系列會持續輸出~