MongoDB 占用CPU過高,如何解決?


十年河東,十年河西,莫欺少年窮

學無止境,精益求精

首先,先說下自己遇到的真實案例,如下:

我司主做掃碼換電業務,主要設備有換電櫃,智能鋰電池,充電樁等,針對的客戶群體為美團/餓了么等外賣跑腿小哥/小妹,他們通過換電業務,更換電動車的電池。

2021年7月22,公司客戶反饋掃碼換電非常卡,我首先查看了阿里雲消息隊列中的活躍消息是否有堆積,結果發現確實部分消息堆積。

消息怎么會堆積呢?我開了10個偵聽客戶端用於活躍消息的消費,並且每個客戶端都采用了異步多線程的模式進行處理。這使我百思不得其解...

 以上截圖便是阿里雲活躍消息個數,活躍消息代表未被消費的消息,我們的設備操作需要即時返回處理結果,因此:一旦堆積,則掃碼換電業務等於攤還...【上圖中的平均值1為正常情況,我們有一次堆積了近400條消息,汗不汗顏...】

阿里雲消息雖然有堆積,但肯定不是阿里雲的問題,畢竟阿里雲是中國乃至世界比較大的公司,雲服務世界上也是排名前幾的。

那么,只能在自己程序上找問題,於是乎,我遠程了偵聽客戶端的服務器,並沒有發現什么,再然后,我去了MongoDb的獨立服務器,通過監控,我發現 Mongodb 服務器的CPU一直高居100%,這尼瑪再多的偵聽客戶端【依賴MOngoDB服務】也無濟於事啊。

問題定位到了,那就是MongoDB服務器CPU消耗過高,是什么操作使一個16核,32G內存服務器CPU消耗這么高呢?

1、我在Robo 3T 1.3.1客戶端上執行如下指令

db.currentOp()

這條指令的作用是:查看數據庫當前正在執行的操作。

該命令輸出范例如下:

{
        "desc" : "conn632530",
        "threadId" : "140298196924160",
        "connectionId" : 632530,
        "client" : "11.192.159.236:57052",
        "active" : true,
        "opid" : 1008837885,
        "secs_running" : 0,
        "microsecs_running" : NumberLong(70),
        "op" : "update",
        "ns" : "mygame.players",
        "query" : {
            "uid" : NumberLong(31577677)
        },
        "numYields" : 0,
        "locks" : {
            "Global" : "w",
            "Database" : "w",
            "Collection" : "w"
        },
        ....
    },

您需要重點關注以下幾個字段。

 
字段 返回值說明
client 該請求是由哪個客戶端發起的。
opid 操作的唯一標識符。
 
說明 如果有需要,可以通過 db.killOp(opid)直接終止該操作。
secs_running 表示該操作已經執行的時間,單位為秒。如果該字段返回的值特別大,需要查看請求是否合理。
microsecs_running 表示該操作已經執行的時間,單位為微秒。如果該字段返回的值特別大,需要查看請求是否合理。
ns 該操作目標集合。
op 表示操作的類型。通常是查詢、插入、更新、刪除中的一種。
locks 跟鎖相關的信息,詳情請參見並發介紹
 
說明 db.currentOp文檔請參見 db.currentOp
通過 db.currentOp()查看正在執行的操作,分析是否有不正常耗時的請求正在執行。例如您的業務平時CPU使用率不高,運維管理人員連到MongoDB數據庫執行了一些需要全表掃描的操作導致CPU使用率非常高,業務響應緩慢,此時需要重點關注執行時間非常耗時的操作。

如果您的應用剛剛上線,MongoDB實例的CPU使用率馬上處於持續很高的狀態,執行db.currentOp(),在輸出結果中未發現異常請求,您可參見下述小節分析數據庫慢請求。

分析MongoDB數據庫的慢請求

雲數據庫MongoDB默認開啟了慢請求Profiling ,系統自動地將請求時間超過100ms的執行情況記錄到對應數據庫下的system.profile集合里。

  1. 通過Mongo Shell連接實例。

    詳情請參見Mongo Shell連接單節點實例Mongo Shell連接副本集實例Mongo Shell連接分片集群實例Mongo Shell連接Serverless實例

  2. 通過use <database>命令進入指定數據庫。
    use mongodbtest
  3. 執行如下命令,查看該數據下的慢請求日志。
    db.system.profile.find().pretty()
  4. 分析慢請求日志,查找引起MongoDB CPU使用率升高的原因。
    以下為某個慢請求日志示例,可查看到該請求進行了全表掃描,掃描了11000000個文檔,沒有通過索引進行查詢。
{
        "op" : "query",
        "ns" : "123.testCollection",
        "command" : {
                "find" : "testCollection",
                "filter" : {
                        "name" : "zhangsan"
                },
                "$db" : "123"
        },
        "keysExamined" : 0,
        "docsExamined" : 11000000,
        "cursorExhausted" : true,
        "numYield" : 85977,
        "nreturned" : 0,
        "locks" : {
                "Global" : {
                        "acquireCount" : {
                                "r" : NumberLong(85978)
                        }
                },
                "Database" : {
                        "acquireCount" : {
                                "r" : NumberLong(85978)
                        }
                },
                "Collection" : {
                        "acquireCount" : {
                                "r" : NumberLong(85978)
                        }
                }
        },
        "responseLength" : 232,
        "protocol" : "op_command",
        "millis" : 19428,
        "planSummary" : "COLLSCAN",
        "execStats" : {
                "stage" : "COLLSCAN",
                "filter" : {
                        "name" : {
                                "$eq" : "zhangsan"
                        }
                },
                "nReturned" : 0,
                "executionTimeMillisEstimate" : 18233,
                "works" : 11000002,
                "advanced" : 0,
                "needTime" : 11000001,
                "needYield" : 0,
                "saveState" : 85977,
                "restoreState" : 85977,
                "isEOF" : 1,
                "invalidates" : 0,
                "direction" : "forward",
....in"
                }
        ],
        "user" : "root@admin"
}

 

通常在慢請求日志中,您需要重點關注以下幾點。

  • 全表掃描(關鍵字: COLLSCAN、 docsExamined )
    • 全集合(表)掃描COLLSCAN 。
      當一個操作請求(如查詢、更新、刪除等)需要全表掃描時,將非常占用CPU資源。在查看慢請求日志時發現COLLSCAN關鍵字,很可能是這些查詢占用了CPU資源。
       
      說明 如果這種請求比較頻繁,建議對查詢的字段建立索引的方式來優化。
    • 通過查看docsExamined的值,可以查看到一個查詢掃描了多少文檔。該值越大,請求所占用的CPU開銷越大。
  • 不合理的索引(關鍵字: IXSCAN、keysExamined )
     
    說明
    • 索引不是越多越好,索引過多會影響寫入、更新的性能。
    • 如果您的應用偏向於寫操作,索引可能會影響性能。

    通過查看keysExamined字段,可以查看到一個使用了索引的查詢,掃描了多少條索引。該值越大,CPU開銷越大。

    如果索引建立的不太合理,或者是匹配的結果很多。這樣即使使用索引,請求開銷也不會優化很多,執行的速度也會很慢。

    如下所示,假設某個集合的數據,x字段取值的重復率很高(假設只有1、2),而y字段取值的重復率很低。

    { x: 1, y: 1 }
    { x: 1, y: 2 }
    { x: 1, y: 3 }
    ......
    { x: 1, y: 100000} 
    { x: 2, y: 1 }
    { x: 2, y: 2 }
    { x: 2, y: 3 }
    ......
    { x: 1, y: 100000}

    要實現 {x: 1, y: 2} 這樣的查詢。

    db.createIndex( {x: 1} )         效果不好,因為x相同取值太多
    db.createIndex( {x: 1, y: 1} )   效果不好,因為x相同取值太多
    db.createIndex( {y: 1 } )        效果好,因為y相同取值很少
    db.createIndex( {y: 1, x: 1 } )  效果好,因為y相同取值少

    關於{y: 1} 與 {y: 1, x: 1} 的區別,可參見MongoDB索引原理復合索引官方文檔

  • 大量數據排序(關鍵字: SORT、hasSortStage )
    當查詢請求里包含排序的時候, system.profile 集合里的hasSortStage字段會為 true 。如果排序無法通過索引滿足,MongoDB會在查詢結果中進行排序。而排序這個動作將非常消耗CPU資源,這種情況需要對經常排序的字段建立索引的方式進行優化。
     
    說明 當您在system.profile集合里發現SORT關鍵字時,可以考慮通過索引來優化排序。

其他還有諸如建立索引、aggregation(遍歷、查詢、更新、排序等動作的組合) 等操作也可能非常耗CPU資源,但本質上也是上述幾種場景.

上述文檔參考自:阿里雲。https://help.aliyun.com/document_detail/62224.html

最終助我解決問題的是這條指令:

db.currentOp()

這條指令的輸出結果指向的其中一張表的操作,於是,我通過排查全公司所有涉及到這張表的MOngodb操作,發現了一個查詢【每秒都會查】非常消耗CPU,通過代碼的改進,最終使Cpu降了下來。有圖為證,如下:

 最后,針對 db.currentOp() 指令的輸出,做一個詳細說明:

db.currentOp()

db.currentOp是個好東西,顧名思義,就是當前的操作。在mongodb中可以查看當前數據庫上此刻的操作語句信息,包括insert/query/update/remove/getmore/command等多種操作。直接執行

db.currentOp()一般返回一個空的數組,我們可以指定一個參數true,這樣就返回用戶connections與系統cmmand相關的操作。下面看個列子:

db.currentOp(true) 會返回很多信息:

db.currentOp()

查看數據庫當前執行什么操作。

用於查看長時間運行進程。

通過(執行時長、操作、鎖、等待鎖時長)等條件過濾。

重點關注以下幾個字段:

字段 說明
client 請求是由哪個客戶端發起的。
opid 操作的opid,有需要的話,可以通過db.killOp(opid) 直接終止該操作。
secs_running/microsecs_running 這個值重點關注,代表請求運行的時間,如果這個值特別大,請看看請求是否合理。
query/ns 這個字段能看出是對哪個集合正在執行什么操作。
lock* - 還有一些跟鎖相關的參數,需要了解可以看官網文檔,本文不做詳細介紹。
- db.currentOp文檔請參見:db.currentOp 

返回信息:

{
  "inprog": [
       {
         "host" : <string>,
         "desc" : <string>,
         "connectionId" : <number>,
         "client" : <string>,
         "appName" : <string>,
         "clientMetadata" : <document>,
         "active" : <boolean>,
         "currentOpTime" : <string>,
         "opid" : <number>,
         "secs_running" : <NumberLong()>,
         "microsecs_running" : <number>,
         "op" : <string>,
         "ns" : <string>,
         "command" : <document>,
         "originatingCommand" : <document>,
         "planSummary": <string>,
         "msg": <string>,
         "progress" : {
             "done" : <number>,
             "total" : <number>
         },
         "killPending" : <boolean>,
         "numYields" : <number>,
         "locks" : {
             "Global" : <string>,
             "MMAPV1Journal" : <string>,
             "Database" : <string>,
             "Collection" : <string>,
             "Metadata" : <string>,
             "oplog" : <string>
         },
         "waitingForLock" : <boolean>,
         "lockStats" : {
             "Global": {
                "acquireCount": {
                   "r": <NumberLong>,
                   "w": <NumberLong>,
                   "R": <NumberLong>,
                   "W": <NumberLong>
                },
                "acquireWaitCount": {
                   "r": <NumberLong>,
                   "w": <NumberLong>,
                   "R": <NumberLong>,
                   "W": <NumberLong>
                },
                "timeAcquiringMicros" : {
                   "r" : NumberLong(0),
                   "w" : NumberLong(0),
                   "R" : NumberLong(0),
                   "W" : NumberLong(0)
                },
                "deadlockCount" : {
                   "r" : NumberLong(0),
                   "w" : NumberLong(0),
                   "R" : NumberLong(0),
                   "W" : NumberLong(0)
                }
             },
             "MMAPV1Journal": {
                ...
             },
             "Database" : {
                ...
             },
             ...
         }
       },
       ...
   ],
   "fsyncLock": <boolean>,
   "info": <string>,
   "ok": 1
}
View Code

返回字段說明:

currentOp.host:運行該操作的主機的名稱。
currentOp.desc:客戶端的描述。這個字符串包括connectionId。
currentOp.connectionId:操作起源的連接的標識符。
currentOp.client:包含有關操作起源的信息的字符串。
對於多文檔事務,客戶機存儲要在事務中運行操作的最新客戶機的信息。
currentOp.appName:包含發出請求的客戶機類型信息的字符串。
currentOp.clientMetadata:關於客戶端的附加信息。
對於多文檔事務,客戶機存儲要在事務中運行操作的最新客戶機的信息。
currentOp.currentOpTime:操作的開始時間。新版本3.6。
currentOp.lsid:會話標識符。僅當操作與會話關聯時才顯示。新版本3.6。
currentOp.transaction:包含多文檔事務信息的文檔。僅當操作是事務的一部分時才出現。新版本4.0。
currentOp.transaction.parameters:包含多文檔事務信息的文檔。僅當操作是事務的一部分時才出現。新版本4.0。
currentOp.transaction.parameters.txnNumber:事務數量。僅當操作是事務的一部分時才出現。新版本4.0。
currentOp.transaction.parameters.autocommit:一個布爾標志,指示事務的自動提交是否打開。僅當操作是事務的一部分時才出現。新版本4.0.2。
currentOp.transaction.parameters.readConcern:事務的read關注點。多文檔事務支持讀取關注點“快照”、“本地”和“多數”。僅當操作是事務的一部分時才出現。新版本4.0.2。
currentOp.transaction.readTimestamp:事務中的操作正在讀取快照的時間戳。僅當操作是事務的一部分時才出現。新版本4.0.2。
currentOp.transaction.startWallClockTime:事務開始的日期和時間(帶有時區)。僅當操作是事務的一部分時才出現。新版本4.0.2。
currentOp.transaction.timeOpenMicros事務的持續時間(以微秒為單位)。添加到timeInactiveMicros的timeActiveMicros值應該等於timeOpenMicros。僅當操作是事務的一部分時才出現。新版本4.0.2。
currentOp.transaction.timeActiveMicros:交易活動的總時間;例如,當事務運行操作時。添加到timeInactiveMicros的timeActiveMicros值應該等於timeOpenMicros。僅當操作是事務的一部分時才出現。新版本4.0.2。
currentOp.transaction.timeInactiveMicros:該事務處於非活動狀態的總時間;例如,當事務沒有運行任何操作時。添加到timeActiveMicros的timeInactiveMicros值應該等於timeOpenMicros。僅當操作是事務的一部分時才出現。
currentOp.transaction.expiryTime:事務超時並中止的日期和時間(帶有時區)。
currentOp.transaction:呼氣時間等於current .transaction。startWallClockTime + transactionLifetimeLimitSeconds。有關更多信息,請參見事務的運行時限制。僅當操作是事務的一部分時才出現。新版本4.0.2。

參考:MongoDB 慢日志字段解析

currentOp.opid:操作的標識符。您可以將此值傳遞給mongo shell中的db.killOp()來終止操作。
警告
非常謹慎地終止運行操作。只使用db.killOp()終止客戶機發起的操作,而不終止內部數據庫操作。

currentOp.active:指定操作是否已啟動的布爾值。如果操作已經啟動,則值為true;如果操作是空閑的,則值為false,例如空閑連接或當前空閑的內部線程。一個操作可以是活動的,即使該操作已被另一個操作取代。在3.0版本中進行了更改:對於一些非活動的后台線程,例如非活動的signalProcessingThread, MongoDB會抑制各種空字段。
currentOp.secs_running:操作持續時間(以秒為單位)。MongoDB通過從操作開始時減去當前時間來計算這個值。僅當操作正在運行時才出現;也就是說,如果active為真。
currentOp.microsecs_running:操作持續時間(以微秒為單位)。MongoDB通過從操作開始時減去當前時間來計算這個值。僅當操作正在運行時才出現;也就是說,如果active為真。
currentOp.op:標識操作類型的字符串。可能的值是:

  • "none"
  • "update"
  • "insert"
  • "query"
  • "command"
  • "getmore"
  • "remove"
  • "killcursors"

query”操作包括讀取操作。
command”操作包括大多數命令,如createIndexes和findandmodify。
在3.0版本中更改:使用insert、update和delete命令的寫操作分別為op顯示“insert”、“update”和“remove”。以前的版本在“query”操作下包含這些寫命令。
修改版本3.2:大多數命令,包括createIndexes和findandmodify,都為op顯示“command”。以前版本的MongoDB在“query”操作下包含了這些命令。
currentOp.ns:操作目標的命名空間。名稱空間由數據庫名和集合名組成,集合名與點(.)連接;也就是說,“<數據庫>。<收藏>”。
currentOp.command:在3.6版中進行了更改。包含與此操作關聯的完整命令對象的文檔。如果命令文件超過1 kb,則文件的形式如下:

"command" : { "$truncated": <string>, "comment": <string> }


$truncated字段包含文檔的字符串摘要,如果存在,則不包括文檔的注釋字段。如果摘要仍然超過1 kb,則進一步截斷它,用字符串末尾的省略號(…)表示。
如果將注釋傳遞給操作,則會顯示注釋字段。
下面的示例輸出包含一個命令對象,用於在一個名為test的數據庫中對一個集合的命名項執行查找操作:

"command" : { "find" : "items", "filter" : { "sku" : 1403978 }, "$db" : "test" }

下面的示例輸出包含用於getMore操作的命令對象,getMore操作是由一個命令生成的,該命令的游標id為80336119321,用於一個名為test的數據庫中的集合命名項:

"command" : { "getMore" : NumberLong("80336119321"), "collection" : "items", "$db" : "test" }

currentOp.originatingCommand:版本3.6中的更改:對於從游標檢索下一批結果的“getmore”操作,originatingCommand字段包含最初創建該游標的完整命令對象(例如find或aggregate)。
currentOp.planSummary:包含查詢計划的字符串,用於幫助調試慢速查詢。
currentOp.client:IP地址(或主機名)和發起操作的客戶機連接的臨時端口。如果您的inprog數組有來自許多不同客戶端的操作,請使用此字符串將操作與客戶端關聯起來。
currentOp.appName:新版本3.4。運行該操作的客戶機應用程序的標識符。使用appName連接字符串選項為appName字段設置自定義值。
currentOp.locks:在3.0版本中進行了更改。locks文檔報告操作當前持有的鎖的類型和模式。可能的鎖類型如下:
鎖類型      描述
Global      表示全局鎖。
MMAPV1Journal  表示用於同步日志寫入的MMAPv1存儲引擎特定鎖;對於非mmapv1存儲引擎,MMAPV1Journal的模式為空。
Database      表示數據庫鎖。
Collection    表示集合鎖。
Metadata      表示元數據鎖。
oplog        表示oplog上的鎖。
可能的模式如下:
鎖定模式      描述
R          表示共享鎖。
W          表示排他(X)鎖。
r          表示共享的意圖(IS)鎖。
w          表示意圖獨占(IX)鎖。

currentOp.msg:msg提供一條消息,描述操作的狀態和進度。對於索引或mapReduce操作,字段報告完成百分比。
currentOp.progress:報告mapReduce或索引操作的進度。進度字段對應於msg字段中的完成百分比。進度說明了以下信息:
currentOp.progress.done:報告完成的數字。
currentOp.progress.total:報告總數。
currentOp.killPending:如果當前操作被標記為要終止,則返回true。當操作遇到下一個安全終止點時,操作將終止。
currentOp.numYields:numyield是一個計數器,它報告操作已經讓步多少次,以允許其他操作完成。
通常,當需要訪問MongoDB尚未完全讀入內存的數據時,操作會產生收益。這允許其他在內存中有數據的操作在MongoDB為生成操作讀入數據時快速完成。
currentOp.fsyncLock:指定當前是否鎖定fsync寫入/快照的數據庫。只有鎖定時才會出現;例如,如果fsyncLock為真。
currentOp.info:有關如何從db.fsyncLock()解鎖數據庫的信息。只有當fsyncLock為真時才會出現。
currentOp.lockStats:對於每種鎖類型和模式(參見currentOp)。),返回以下信息:
currentOp.lockStats.acquireCount:操作以指定模式獲取鎖的次數。
currentOp.lockStats.acquireWaitCount:操作必須等待acquirecallock獲取的次數,因為鎖處於沖突模式。acquireWaitCount小於或等於acquirecore。
currentOp.lockStats.timeAcquiringMicros:操作必須等待獲取鎖的累積時間(以微秒為單位)。時間獲取微s除以acquireWaitCount給出了特定鎖定模式的平均等待時間。
currentOp.lockStats.deadlockCount:在等待鎖獲取時,操作遇到死鎖的次數。

1:查看MongoDB當前正在做哪些操作?

db.currentOp()
等同於:
db.$cmd.sys.inprog.findOne()

2:查看當前有多少個連接?

db.serverStatus().connections

3:詳解currentOp()

gechongrepl:PRIMARY> db.currentOp()
{
    "inprog" : [
        {
            "opid" : 6222,   #進程號
            "active" : true, #是否活動狀態
            "secs_running" : 3,#操作運行了多少秒
            "microsecs_running" : NumberLong(3662328),#操作持續時間(以微秒為單位)。MongoDB通過從操作開始時減去當前時間來計算這個值。
            "op" : "getmore",#操作類型,包括(insert/query/update/remove/getmore/command)
            "ns" : "local.oplog.rs",#命名空間
            "query" : {#如果op是查詢操作,這里將顯示查詢內容;也有說這里顯示具體的操作語句的
                 
            },
            "client" : "192.168.91.132:45745",#連接的客戶端信息
            "desc" : "conn5",#數據庫的連接信息
            "threadId" : "0x7f1370cb4700",#線程ID
            "connectionId" : 5,#數據庫的連接ID
            "waitingForLock" : false,#是否等待獲取鎖
            "numYields" : 0,
            "lockStats" : {
                "timeLockedMicros" : {#持有的鎖時間微秒
                    "r" : NumberLong(141),#整個MongoDB實例的全局讀鎖
                    "w" : NumberLong(0)#整個MongoDB實例的全局寫鎖
                },
                "timeAcquiringMicros" : {#為了獲得鎖,等待的微秒時間
                    "r" : NumberLong(16),#整個MongoDB實例的全局讀鎖
                    "w" : NumberLong(0)#整個MongoDB實例的全局寫鎖
                }
            }
        }
    ]
}
View Code

@天才卧龍的博客

參考文檔:https://www.cnblogs.com/duanxz/p/4366603.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM