閱讀554 返回首頁    go 技術社區[雲棲]


論MongoDB索引選擇的重要性

線上某業務,頻繁出現IOPS 使用率100%的(每秒4000IOPS)現象,每次持續接近1個小時,從慢請求的日誌發現是一個 getMore 請求耗時1個小時,導致IOPS高;深入調查之後,最終發現竟是一個索引選擇的問題。

IOPS

2017-11-01T15:04:17.498+0800 I COMMAND  [conn5735095] command db.mycoll command: getMore { getMore: 215174255789, collection: "mycoll" } cursorid:215174255789 keyUpdates:0 writeConflicts:0 numYields:161127 nreturned:8419 reslen:4194961 locks:{ Global: { acquireCount: { r: 322256 } }, Database: { acquireCount: { r: 161128 } }, Collection: { acquireCount: { r: 161128 } } } protocol:op_command 3651743ms

問題背景

業務每個整點開始,會把過去1小時的數據同步到另一個數據源,查詢時會按 _id 排序,2個主要查詢條件如下,先執行find命令,然後遍曆cursor,讀取所有滿足條件的文檔。

* created_at: { $gte: "2017-11-01 13:00:00", $lte: "2017-11-01 13:59:59" }
* sort: {_id: 1}

業務數據的特性

  • 每條數據插入時都帶上 created_at 字段,時間為當前時間戳,並建立了 {created_at: -1} 的索引
  • _id 字段為用戶自定義(並非mongodb默認的ObjectId),取值較隨機,無規律
  • 整個集合非常大,總文檔數超過1億條

MongoDB的find、getMore特性

  • find命令,會返回第一批滿足條件的batch(默認101條記錄)以及一個cursor
  • getMore 根據find返回的cursor繼續遍曆,每次遍曆默認返回不超過4MB的數據

索引的選擇

方案1:使用 created_at 索引

整個執行路徑為

  1. 通過 created_at 索引,快速定位到符合條件的文檔
  2. 讀出所有的滿足 created_at 查詢條件的文檔
  3. 對所有的文檔根據 _id 字段進行排序

如下是走這個索引的2條典型日誌,可以看出

  • 符合 created_at 條件的文檔大概有7w+,全部排序後,返回前101條,總共耗時約600ms;
  • 接下來 getMore,因為結果要按_id排序,getMore 還是得繼續把所有符合條件的讀出來排序,並跳過第一次的101條,返回下一批給客戶端。

     2017-11-01T14:02:31.861+0800 I COMMAND  [conn5737355] command db.mycoll command: find { find: "mycoll", filter: { created_at: { $gte: "2017-11-01 13:00:00", $lte: "2017-11-01 13:59:59" } }, projection: { $sortKey: { $meta: "sortKey" } }, sort: { _id: 1 }, limit: 104000, shardVersion: [ Timestamp 5139000|7, ObjectId('590d9048c628ebe143f76863') ] } planSummary: IXSCAN { created_at: -1.0 } cursorid:215494987197 keysExamined:71017 docsExamined:71017 hasSortStage:1 keyUpdates:0 writeConflicts:0 numYields:557 nreturned:101 reslen:48458 locks:{ Global: { acquireCount: { r: 1116 } }, Database: { acquireCount: { r: 558 } }, Collection: { acquireCount: { r: 558 } } } protocol:op_command 598ms
     2017-11-01T14:02:32.036+0800 I COMMAND  [conn5737355] command db.mycoll command: getMore { getMore: 215494987197, collection: "mycoll" } cursorid:215494987197 keyUpdates:0 writeConflicts:0 numYields:66 nreturned:8510 reslen:4194703 locks:{ Global: { acquireCount: { r: 134 } }, Database: { acquireCount: { r: 67 } }, Collection: { acquireCount: { r: 67 } } } protocol:op_command 120ms
    

方案2:使用 _id 索引

整個執行路徑為

  1. 根據 _id 索引,掃描所有的記錄 (按_id索引的順序掃描,對應的文檔的created_at是隨機的,無規律)
  2. 把滿足 created_at 條件的文檔返回,第一次find,要找到101個符合條件的文檔返回

如下是走這個索引的2條典型日誌,可以看出

  • 第一次掃描了17w,才找到101條符合條件的記錄,耗時46s
  • 第二次要累計近4MB符合條件的文檔(8419條)才返回,需要全表掃描更多的文檔,最終耗時1個小時,由於全表掃描對cache非常不友好,所以一直是要從磁盤讀取,所以導致大量的IO。

    2017-11-01T14:03:25.648+0800 I COMMAND  [conn5735095] command db.mycoll command: find { find: "mycoll", filter: { created_at: { $gte: "2017-11-01 13:00:00", $lte: "2017-11-01 13:59:59" } }, projection: { $sortKey: { $meta: "sortKey" } }, sort: { _id: 1 }, limit: 75000, shardVersion: [ Timestamp 5139000|7, ObjectId('590d9048c628ebe143f76863') ] } planSummary: IXSCAN { _id: 1 } cursorid:215174255789 keysExamined:173483 docsExamined:173483 fromMultiPlanner:1 replanned:1 keyUpdates:0 writeConflicts:0 numYields:2942 nreturned:101 reslen:50467 locks:{ Global: { acquireCount: { r: 5886 } }, Database: { acquireCount: { r: 2943 } }, Collection: { acquireCount: { r: 2943 } } } protocol:op_command 46232ms
    2017-11-01T15:04:17.498+0800 I COMMAND  [conn5735095] command db.mycoll command: getMore { getMore: 215174255789, collection: "mycoll" } cursorid:215174255789 keyUpdates:0 writeConflicts:0 numYields:161127 nreturned:8419 reslen:4194961 locks:{ Global: { acquireCount: { r: 322256 } }, Database: { acquireCount: { r: 161128 } }, Collection: { acquireCount: { r: 161128 } } } protocol:op_command 3651743ms
    

總結

IOPS高是因為選擇的索引不是最優,那為什麼MongoDB沒有選擇最優的索引來執行這個任務呢?

  • 從日誌可以看出,絕大部分情況,MongoDB 都是走的 created_at 索引
  • 上述case,那個索引更優,其實是跟數據的分布情況相關的
    • 如果滿足 created_at 查詢條件的文檔特別多,那麼對大量的文檔排序的開銷也是很大的
    • 如果 created_at 字段分布非常離散(如本案例中的數據),則全表掃描找出符合條件的文檔開銷更大
  • MongoDB 的索引是基於采樣代價模型,一個索引對采樣的數據集更優,並不意味著其對整個數據集也最優
    • MongoDB 一個查詢第一次執行時,如果有多個執行計劃,會根據模型選出最優的,並緩存起來,以提升效率
    • 當 MongoDB 發生集合創建/刪除索引時,會將緩存的執行計劃清空掉,並重新選擇
    • MongoDB 在執行的過程中,也會根據執行計劃的表現,比如一個執行計劃,很多次迭代都沒遇到符合條件的文檔,就會考慮這個執行計劃是否最優了,會觸發重新構建執行計劃的邏輯(具體觸發的策略還沒有詳細研究,後續再分享),比如方案2裏的find查詢,執行計劃裏包含了 {replanned: 1} 說明是重新構建了執行計劃;當它發現這個執行計劃實際執行起來效果更差時,最終還是會會到更優的執行計劃上。
  • 最懂數據的還是業務自身,對於查詢優化器搞不定的case,可以通過在查詢時加 hint,自己指定的索引來構建執行計劃。

最後更新:2017-11-01 21:05:22

  上一篇:go  雙十一駕臨,阿裏雲榮譽榜,四大功能雙管齊下
  下一篇:go  模型融合