是什麼造成了數據庫的卡頓
- 2019 年 10 月 20 日
- 筆記
聲明:本文同步發表於 MongoDB 中文社區,傳送門:
http://www.mongoing.com/archives/26201
一、背景
MongoDB 提供了非常強大的性能分析及監控的命令,諸如 mongostat、mongotop 可以讓我們對數據庫的運行態性能了如指掌。
然而,這次我們在性能環境上就遇到一個非常棘手的問題:
某服務接口在 1-5分鐘內偶現超時導致業務失敗!
在接口調用上返回超時屬於前端的判斷,通常是設置的一個閾值(比如 3s)。
由於問題是偶現,且沒辦法發現有明顯的規律,很難直接判斷出原因。
而平台在做了微服務拆分之後,問題定位的難度加大了不少,且當前的調用鏈功能也不夠完善。
二、問題定界
業務診斷
在一番分析後,梳理出接口調用的關係圖如下:
其中,服務A 通過 RPC調用服務B的接口,而服務B 又通過 MongoDB 進行數據讀寫。
MongoManager 是 用於管理 MongoDB 的一個代理服務,主要是實現一些數據備份、監控的任務。
在採集了一些數據之後,我們基本把問題範圍鎖定到了 MongoDB 數據庫上面,這些手段包括:
- 通過對服務A、服務B的接口監控進行觀測
- 通過wiredshark 抓包,分析 DB讀寫上的響應包時延
- 通過CommandListener,將1s 以上的慢操作指標進行輸出
從接口監控及 wiredshark 抓包結果中確認到,DB 操作的響應時間都出現了偶現的超長(3s以上)。
而通過 CommandListener 將慢操作輸出統計後,得到的圖表如下:
其中典型的慢操作語句如:
update app.BussinessData query: { snId: "c1190522-6b6f-9192-b40a-6665ba2b2tta" } update: { $set: { taskInfo.state: "Running", lastModifiedTime: new Date(1531857361579) } }
然而,這些慢操作並沒有明顯的問題嫌疑,除了以寫操作為主之外,索引的命中也沒有問題。
數據庫診斷
接下來,我們將焦點集中到了數據庫上,檢查了 cpu、內存、網絡、磁盤這些常規指標後並沒有發現什麼異常。
通過 mongostat 觀察到的如下圖:
圖- mongostat
其中的一個異常點是 netout 會有偶現的積壓情況。
然後是嘗試通過 db.currentOp() 這個命令來看看能不能揪出慢操作背後的元兇。
currentOp 還是比較好用的,尤其是在定位一些長時間慢操作問題上
然而,我的想法落空了,這個辦法並沒有任何發現! 因為問題屬於偶現,所以執行currentOp 需要一些好的運氣..
儘管如此,我們還是大概能判定,在出現慢操作的時候,數據庫出現了命令積壓(卡頓)的情況
基於上面的這些診斷信息,我們還是沒辦法直接得出結論,但是大家都做出來一致的猜測:
"可能存在某個定時器的鎖,對業務操作產生了阻塞!"
那麼,鎖從哪裡來? 我們將目光移向了 MongoManager,的確這個程序承載了許多管理上的功能,包括監控、備份、升級這些瑣事..
其中,執行數據庫信息採集的監控定時器存在最大的嫌疑!,那麼問題又來了,
"如果是定時器導致的卡頓,為什麼慢操作卻沒有定時產生的規律呢?"
這個問題在前面也對我們產生了很大的困擾,但一個比較合理的解釋是:
"MongoManager 是多節點的,而其中定時器是按照 時間間隔來運作的,而不是整點觸發。"
這樣就能解釋,為什麼慢操作通常都是在1-5分鐘內不規律的出現了。
為了證實這個想法,我們直接將 MongoManager 逐個關閉到僅剩下一個,最終通過CommandListener收集到的慢操作圖表如下:
看,這次的慢操作非常的規律,基本每5分鐘就會出現一次卡頓!
然後我們將全部的 MongoManager 關閉,業務的慢操作完全消失了。
三、找出元兇
經過前面的問題定位,我們已經能確定是MongoManager的定時器搞的鬼了。
接下來走讀代碼,發現有下面這樣的一段實現:
public void testDbOperation() { try { MongoClient client = getClient(); MongoDatabase database = client.getDatabase("thisdb"); List<String> allCollections = new ArrayList<>(); //get all collections MongoCursor<String> iCollection = database.listCollectionNames().iterator(); while (iCollection.hasNext()) { allCollections.add(iCollection.next()); } if (allCollections.isEmpty()) { return; } //test a random collection String randomCollection = allCollections.get((int) (allCollections.size() * Math.random())); //issue find operation database.getCollection(randomCollection).find().first(); } catch (Exception e) { throw new DBMonitException("the db find test failed..", e); } }
為了便於理解,上述的代碼做了比較多的簡化,大致的步驟是:
- 通過 listCollections 獲取數據庫的全部集合;
- 隨機選取一個集合,執行findOne操作;
- 一旦發現操作失敗,產生異常(告警)
上述的代碼由定時器在5分鐘觸發一次,跟出現慢操作的條件是吻合的。
其中 listCollections 會獲取到一個集合的列表,我們猜測,這個操作可能會阻塞數據庫的操作。
通過搜索官方文檔,我們發現該操作使用了一個共享讀鎖(S):
圖-listCollection鎖
MongoDB 鎖機制
為了說明阻塞的產生,這裡需要解釋下MongoDB的鎖機制:
在數據庫內部有下面這幾種鎖:
- 寫鎖(X),對某個文檔或數據庫對象進行寫時加鎖
- 讀鎖(S),對某個文檔或數據庫對象進行讀取時加鎖
- 意向寫鎖(IX),對文檔寫操作時,對集合及數據庫產生意向寫鎖
- 意向讀鎖(IS),對文檔讀操作時,對集合及數據庫產生意向讀鎖
意向鎖提供了數據庫系統的"多粒度鎖"的能力,是提升並發能力的關鍵手段, WiredTiger 也是基於此來實現行級鎖的。
幾種鎖的互斥關係如下表所示:
鎖類型 | S | X | IS | IX |
---|---|---|---|---|
S | T | F | T | F |
X | F | F | F | F |
IS | T | F | T | T |
IX | F | F | T | T |
基於此,我們可以得出這樣的結論:
由定時器產生 的 listCollections 操作會對數據庫產生讀鎖(S),從而對文檔寫操作(數據庫的意向寫鎖IX)產生了阻塞。
那麼,listCollections 從監控的意義上來看是不應該對數據庫產生阻塞的。
我們認為這應該是 MongoDB 3.4 版本一個Bug,而SERVER-34243 這裡提交的一個Issue已經得到解決。
在最新的 4.x版本文檔中,可以發現 listCollections 的權限已經變更成了 意向讀鎖(IS)。
通過 4.0 版本的 ReleaseNotes 可以確認這點:
The command listCollections takes Intent Shared lock on the database. In previous versions, the command takes Shared lock on the database.
鏈接: https://docs.mongodb.com/manual/release-notes/4.0/index.html
四、解決思路
在了解了事情的來龍去脈之後,我們可以確定這是 MongoDB 3.4 版本的一個不嚴謹的實現導致的問題。
由於無法直接升級整個數據庫版本(代價太大), 我們在監控程序上做了優化,即將 listCollections 結果進行了緩存,避免定時器每次都去操作這個命令,而問題最終得到了解決。
"監控不是銀彈,濫用也會有坑",至少從這次的事件中,我們得到了一個教訓!
而要在這個問題上舉一反三的話,那就是需要警惕一些數據庫操作潛在的鎖問題了,比如:
- 創建索引(默認Foreground模式),會對數據庫產生寫鎖(X),所以一定要用Background模式
- 刪除集合,dropCollection,會對數據庫產生寫鎖(X),謹慎!
- MapReduce操作,會對數據庫產生讀鎖(S)和寫鎖(X),謹慎!
- 連接鑒權,db.auth(),會對admin庫產生讀鎖,而admin是庫級鎖。
以上的這些事情,你 Get 到了嗎?