写点什么

记某核心 MongoDB 集群索引优化实践

  • 2022 年 3 月 04 日
  • 本文字数:11950 字

    阅读完需:约 39 分钟

记某核心MongoDB集群索引优化实践

腾讯云数据库 MongoDB 天然支持高可用、分布式、高性能、高压缩、schema free、完善的客户端访问均衡策略等功能。云上某重点用户基于 MongoDB 这些优势,选用 MongoDB 作为主存储服务,该用户业务场景如下:

· 存储电商业务核心数据

· 查询条件多变、查询不固定,查询较复杂,查询组合众多

· 对性能要求较高

· 对存储成本有要求

· 流量占比:insert 较少、update 较多、find 较多、峰值流量较高

· 高峰期读写流量数千/秒


通过和业务沟通,了解业务使用场景和业务述求后,通过一系列的索引优化,最终完美解决读写性能瓶颈问题。本文重点分析该核心业务索引优化过程,通过本文可以学习到以下知识点:

· 如何确定无用索引?

· 如何确定重复索引?

· 如何创建最优索引?

· 对索引的一些错误认识?

· 索引优化收益(节省 90%以上 CPU 资源、85%磁盘 IO 资源、20%存储成本)


问题分析过程

收到用户集群性能瓶颈反馈后,通过集群监控信息及服务器监控信息可以看出集群存在如下现象:

· Mongod 节点 CPU 消耗过高,CPU 时不时消耗接近 90%,甚至 100%

· 磁盘 IO 消耗过高,单节点 IO 资源消耗占整服务器 60%

· 大量慢日志(主要集中在 find 和 update),高峰期每秒数千条慢日志

· 慢日志类型各不相同,查询条件众多

· 所有慢查询都有匹配到索引


登录服务器对应节点后台,获取慢日志信息,发现 mongod.log 中包含大量不同类型 find 和 update 的慢日志,慢日志都有走索引,任意提取一条慢日志其内容如下:

Mon Aug  2 10:34:24.928 I COMMAND  [conn10480929] command xxx.xxx command: find { find: "xxx", filter: { $and: [ { alxxxId: "xxx" }, { state: 0 }, { itemTagList: { $in: [ xx ] } }, { persxxal: 0 } ] }, limit: 3, maxTimeMS: 10000 } planSummary: IXSCAN { alxxxId: 1.0, itemTagList: 1.0 } keysExamined:1650 docsExamined:1650 hasSortStage:0 cursorExhausted:1 keyUpdates:0 writeConflicts:0 numYields:15 nreturned:3 reslen:8129 locks:{ Global: { acquireCount: { r: 32 } }, Database: { acquireCount: { r: 16 } }, Collection: { acquireCount: { r: 16 } } } protocol:op_command 227ms   Mon Aug  2 10:34:22.965 I COMMAND  [conn10301893] command xx.txxx command: find { find: "txxitem", filter: { $and: [ { itxxxId: "xxxx" }, { state: 0 }, { itemTagList: { $in: [ xxx ] } }, { persxxal: 0 } ] }, limit: 3, maxTimeMS: 10000 } planSummary: IXSCAN { alxxxId: 1.0, itemTagList: 1.0 } keysExamined:1498 docsExamined:1498 hasSortStage:0 cursorExhausted:1 keyUpdates:0 writeConflicts:0 numYields:12 nreturned:3 reslen:8039 locks:{ Global: { acquireCount: { r: 26 } }, Database: { acquireCount: { r: 13 } }, Collection: { acquireCount: { r: 13 } } } protocol:op_command 158ms  
复制代码

从上面的日志打印可以看出,查询都有走 { alxxxId: 1.0, itemTagList: 1.0 } 索引,走该索引扫描的 keysExamined 为 1498 行,扫描的 docsExamined 为 1498 行,但是返回的 doc 文档数却只有 nreturned=3 行。


通过上面的日志核心信息可以看出,满足条件的数据只有 3 条,但是却扫描了 1498 行数据和索引,说明查询有走索引,但是不是最优所有。


获取用户 SQL 查询模型及已有索引信息

上面的分析可以确定问题出现在索引不是最优,大量查询找了很多无用数据。


3.1. 和用户接触,了解用户 SQL 模型

通过和用户沟通,收集到用户查询、更新主要涉及以下 SQL 类型:

· 常用查询、更新类 SQL

基于AlxxxId(用户ID)+itxxxId(单个或多个)  基于AlxxxId查询count  基于AlxxxId通过时间范围(createTime)进行分页查询,部分查询会拼接state及其他字段基于AlxxxId,ParentAlxxxId,parentItxxxId,state组合查询  基于ItxxxId(单个或多个)查询数据  基于AlxxxId, state, updateTime组合查询  基于AlxxxId, state,createTime, totalStock(库存数量)组合查询  基于AlxxxId(用户ID)+itxxxId(单个或多个)+任意其他字段组合  基于AlxxxId, digitalxxxrmarkId(水印ID), state进行查询基于AlxxxId, itemTagList(标签ID),state等进行查询基于AlxxxId+itxxxId(单个或多个) +其他任意字段进行查询其他查询
复制代码


· 统计类 count 查询 SQL


AlxxxId,state, persxxal 组合  AlxxxId, state,itemType 组合  AlxxxId(用户ID)+itxxxId(单个或多个)+任意其他字段组合
复制代码


3.2. 获取集群已有索引

通过 db.xxx.getindex({})获取到该表的索引信息如下,总计 30 个索引:

{ "alxxxId" : 1, "state" : -1, "updateTime" : -1, "itxxxId" : -1, "persxxal" : 1, "srcItxxxId" : -1 }          { "alxxxId" : 1, "image" : 1 }                                                             { "itexxxList.vidxxCheck" : 1, "itemType" : 1, "state" : 1 }                                              { "alxxxId" : 1, "state" : -1, "newsendTime" : -1, "itxxxId" : 1, "persxxal" : 1 }                           { "_id" : 1 }                                                                              { "alxxxId" : 1, "createTime" : -1, "checkStatus" : 1 }                                                      { "alxxxId" : 1, "parentItxxxId" : -1, "state" : -1, "updateTime" : -1, "persxxal" : 1, "srcItxxxId" : -1 }     { "alxxxId" : 1, "state" : -1,  "parentItxxxId" : 1, "updateTime" : -1, "persxxal" : -1 }  { "srcItxxxId" : 1 }                                                                        { "createTime" : 1 }                                                                       { "itexxxList.boyunState" : -1, "itexxxList.wozhituUploadServerId": -1, "itexxxList.photoQiniuUrl" : 1, "itexxxList.sourceType" : 1 }      { "alxxxId" : 1, "state" : 1, "digitalxxxrmarkId" : 1, "updateTime" : -1 } { "itxxxId" : -1 }                                                                   { "alxxxId" : 1, "parentItxxxId" : 1, "parentAlxxxId" : 1, "state" : 1 }                    { "alxxxId" : 1, "videoCover" : 1 }                                                        { "alxxxId" : 1, "itemType" : 1 }                                                          { "alxxxId" : 1, "state" : -1, "itemType" : 1, "persxxal" : 1, "updateTime" : 1 }  { "alxxxId" : 1, "itxxxId" : 1 }                                                            { "itxxxId" : 1, "alxxxId" : 1 }                                                            { "alxxxId" : 1, "parentAlxxxId" : 1, "state" : 1 }                                        { "alxxxId" : 1, "itemTagList" : 1 }                                                       { "itexxxList.photoQiniuUrl" : 1, "itexxxList.boyunState" : -1, "itexxxList.sourceType" : 1, "itexxxList.wozhituUploadServerId" : -1 }           { "alxxxId" : 1, "parentItxxxId" : 1, "state" : 1 }                                         { "alxxxId" : 1, "parentItxxxId" : 1, "updateTime" : 1 }                                    { "updateTime" : 1 }                                                                       { "itemPhoxxIdList" : -1 }     { "alxxxId" : 1, "state" : -1, "isTop" : 1 }    { "alxxxId" : 1, "state" : 1, "itemResxxxIdList" : 1, "updateTime" : -1 }   { "alxxxId" : 1, "state" : -1, "itexxxList.photoQiniuUrl" : 1 }  { "itexxxList.qiniuStatus" : 1, "itexxxList.photoNetUrl" : 1, "itexxxList.photoQiniuUrl" : 1 }       { "itemResxxxIdList" : 1  }
复制代码


索引优化过程

从上一节可以看出,该集群查询复杂,同时索引众多,通过分析已有索引和用户数据模型,对已有索引做如下优化,最终有效索引减少到 8 个。


4.1. 第一轮优化:删除无用索引

MongoDB 默认提供有索引统计命令来获取各个索引命中的次数,该命令如下:

> db.xxxxx.aggregate({"$indexStats":{}})  { "name" : "alxxxId_1_parentItxxxId_1_parentAlxxxId_1", "key" : { "alxxxId" : 1, "parentItxxxId" : 1, "parentAlxxxId" : 1 },"host" : "TENCENT64.site:7014", "accesses" : { "ops" : NumberLong(11236765),"since" : ISODate("2020-08-17T06:39:43.840Z") } }
复制代码


该聚合输出中的几个核心指标信息如下表:


上表中的 ops 代表命中次数,如果命中次数为 0 或者很小,说明该索引很少被选为最优索引使用,因此可以任务是无用索引,可以直接删除。


获取用户核心表索引统计信息,如下:

db.xxx.aggregate({"$indexStats":{}})  { "alxxxId" : 1, "state" : -1, "updateTime" : -1, "itxxxId" : -1, "persxxal" : 1, "srcItxxxId" : -1 }                      "ops" : NumberLong(88518502)  { "alxxxId" : 1, "image" : 1 }                            "ops" : NumberLong(293104)  { "itexxxList.vidxxCheck" : 1, "itemType" : 1, "state" : 1 }    "ops" : NumberLong(0)  { "alxxxId" : 1, "state" : -1, "newsendTime" : -1, "itxxxId" : -1, "persxxal" : 1 }                                              "ops" : NumberLong(33361216)  { "_id" : 1 }                                              "ops" : NumberLong(3987)   { "alxxxId" : 1, "createTime" : 1, "checkStatus" : 1 }      "ops" : NumberLong(20042796) { "alxxxId" : 1, "parentItxxxId" : -1, "state" : -1, "updateTime" : -1, "persxxal" : 1, "srcItxxxId" : -1 }                 "ops" : NumberLong(43042796){ "alxxxId" : 1, "state" : -1,  "parentItxxxId" : 1, "updateTime" : -1, "persxxal" : -1 }                                  "ops" : NumberLong(3042796){ "itxxxId" : -1 }      "ops" : NumberLong(38854593){ "srcItxxxId" : -1 }                                "ops" : NumberLong(0)  { "createTime" : 1 }                               "ops" : NumberLong(62)  { "itexxxList.boyunState" : -1, "itexxxList.wozhituUploadServerId" : -1, "itexxxList.photoQiniuUrl" : 1, "itexxxList.sourceType" : 1 }    "ops" : NumberLong(0)   { "alxxxId" : 1, "state" : 1, "digitalxxxrmarkId" : 1, "updateTime" : -1 }                  "ops" : NumberLong(140238342)  { "itxxxId" : -1 }                 "ops" : NumberLong(38854593)  { "alxxxId" : 1, "parentItxxxId" : 1, "parentAlxxxId" : 1, "state" : 1 }    "ops" : NumberLong(132237254)  { "alxxxId" : 1, "videoCover" : 1 }        { "ops" : NumberLong(2921857)  { "alxxxId" : 1, "itemType" : 1 }          { "ops" : NumberLong(457)  { "alxxxId" : 1, "state" : -1, "itemType" : 1, "persxxal" : 1, " itxxxId " : 1 }        "ops" : NumberLong(68730734)  { "alxxxId" : 1, "itxxxId" : 1 }       "ops" : NumberLong(232360252)  { "itxxxId" : 1, "alxxxId" : 1 }       "ops" : NumberLong(145640252)  { "alxxxId" : 1, "parentAlxxxId" : 1, "state" : 1 }          "ops" : NumberLong(689891)  { "alxxxId" : 1, "itemTagList" : 1 }                    "ops" : NumberLong(2898693682)  { "itexxxList.photoQiniuUrl" : 1, "itexxxList.boyunState" : 1, "itexxxList.sourceType" : 1, "itexxxList.wozhituUploadServerId" : 1 }        "ops" : NumberLong(511303207) { "alxxxId" : 1, "parentItxxxId" : 1, "state" : 1 }                "ops" : NumberLong(0)  { "alxxxId" : 1, "parentItxxxId" : 1, "updateTime" : 1 }          "ops" : NumberLong(0)  { "updateTime" : 1 }                                         "ops" : NumberLong(1397)  { "itemPhoxxIdList" : -1 }        "ops" : NumberLong(0)  { "alxxxId" : 1, "state" : -1, "isTop" : 1 }       "ops" : NumberLong(213305)  { "alxxxId" : 1, "state" : 1, "itemResxxxIdList" : 1, "updateTime" : 1 }       "ops" : NumberLong(2591780)  { "alxxxId" : 1, "state" : 1, "itexxxList.photoQiniuUrl" : 1}  "ops" : NumberLong(23505){ "itexxxList.qiniuStatus" : 1, "itexxxList.photoNetUrl" : 1, "itexxxList.photoQiniuUrl" : 1 }                  "ops" : NumberLong(0)  { "itemResxxxIdList" : 1  }               "ops" : NumberLong(7)
复制代码


该业务已经运行一段时间,首先把 ops 小于 10000 的索引删除,满足该条件的索引如上面的红色部分索引。


通过该轮删除 11 个无用索引后,剩余有用索引 30-11=19 个索引。


4.2. 第二轮优化:删除重复索引

重复索引主要包括以下几类:

· 查询顺序引起的索引重复

例如该业务不同开发写了如下两种查询:

db.xxxx.find({{ "alxxxId" : xxx, "itxxxId" : xxx }})  db.xxxx.find({{ " itxxxId " : xxx, " alxxxId " : xxx }}) 
复制代码


为了应对这两种查询 SQL,DBA 创建了两个索引:

{ alxxxId :1, itxxxId:1 }和{ itxxxId :1, alxxxId:1}

这两个 SQL 实际上创建任何一个索引即可,无需两个索引,因此这就是无用索引。


· 最左原则匹配引起的索引重复

例如下面的三个索引存在重复索引:

{ itxxxId:1, alxxxId:1 }和{ itxxxId :1}这两个索引,{ itxxxId :1}即为重复索引。


· 包含关系引起的索引重复

例如上面的以下两个索引为重复索引:

{ "alxxxId" : 1, "parentItxxxId" : 1, "parentAlxxxId" : 1, "state" : 1 }   { "alxxxId" : 1, "parentAlxxxId" : 1, "state" : 1 }  { "alxxxId" : 1, " state " : 1 }
复制代码


和用户确认,用户创建这三个索引,是因为有如下三个查询:

Db.xxx.find({ "alxxxId" : xxx, "parentItxxxId" : xx, "parentAlxxxId" : xxx, "state" : xxx })  Db.xxx.find({ "alxxxId" : xxx, " parentAlxxxId " : xx, " state " : xxx }) Db.xxx.find({ "alxxxId" : xxx,  " state " : xxx })
复制代码


这几个查询都包含公共字段,因此可以合并为一个索引来满足这两类 SQL 的查询,合并后的索引如下:

{ "alxxxId" : 1, " state " : 1, " parentAlxxxId " : 1, parentItxxxId :1}
复制代码


通过以上几个索引原则优化后,以下几个索引可以优化,优化前索引:

{ itxxxId:1, alxxxId:1 }  { alxxxId:1, itxxxId:1 }  {itxxxId:1 }  { "alxxxId" : 1, "parentItxxxId" : 1, "parentAlxxxId" : 1, "state" : 1 }    { "alxxxId" : 1, "parentAlxxxId" : 1, "state" : 1 }    { "alxxxId" : 1, " state " : 1 }
复制代码


这 6 个索引可以合并优化为如下 2 个索引:

{ itxxxId:1, alxxxId:1 }  { "alxxxId" : 1, "parentItxxxId" : 1, "parentAlxxxId" : 1, "state" : 1 }
复制代码


4.3. 第三轮优化:获取数据模型,剔除唯一索引引起的无用索引

通过分析表中数据各个字段模块组合,发现 alxxxId 和 itxxxId 字段为高频字段,通过分析字段 schema 信息,随机抽取一部分数据,发现这两个字段组合是唯一的。于是和用户确认,用户反馈这两个字段的任意组合都代表一条唯一的数据。


如果{alxxxId:1, itxxxId:1}索引可以确定唯一性,则这两个字段和任何字段的组合都是唯一的。因此下面的几个索引可以合并为一个索引:

{ "alxxxId" : 1, "state" : -1, "updateTime" : -1, "itxxxId" : 1, "persxxal" : 1, "srcItxxxId" : -1 }       { "alxxxId" : 1, "state" : -1, "itemType" : 1, "persxxal" : 1, " itxxxId " : 1 }   { "alxxxId" : 1, "state" : -1, "newsendTime" : -1, "itxxxId" : 1, "persxxal" : 1 }          { "alxxxId" : 1, "state" : 1, "itxxxId" : 1, "updateTime" : -1 }       { itxxxId:1, alxxxId:1 }
复制代码


上面的 5 个索引可以去重合并为以下一个索引:{ itxxxId:1, alxxxId:1 }  


4.4. 第四轮优化:非等值查询引起的无用重复索引优化

从前面的 30 个索引可以看出,索引中有部分为时间类型字段,如 createTime、updateTime,这类字段一般用于范围查询,通过和用户确认,这些字段确实用于各种范围查询。由于范围查询属于非等值查询,如果范围查询字段出现在索引字段前面,则后面字段无法走索引,例如如下查询及索引:

db.collection.find({{ "alxxxId" : xx, "parentItxxxId" : xx, "state" : xx, "updateTime" : {$gt: xxxxx}, "persxxal" : xxx, "srcItxxxId" : xxx }    })     db.collection.find({{ "alxxxId" : xx, "state" : xx, "parentItxxxId" : xx, "updateTime" : {$lt: xxxxx}, "persxxal" : xxx}    })
复制代码


用户为这两个查询增加了以下两个索引:

第一个索引如下:{ "alxxxId" : 1, "parentItxxxId" : -1, "state" : -1, "updateTime" : -1, "persxxal" : 1, "srcItxxxId" : -1 }   第二个索引如下:{ "alxxxId" : 1, "state" : -1,  "parentItxxxId" : 1, "updateTime" : -1, "persxxal" : -1 }
复制代码


由于这两个查询都包含 updateTime 字段,并进行范围查询。由于除了 updateTime 字段以外的字段都是等值查询,因此上面两个查询实际上 updateTime 右边的字段无法走索引。也就是上面的第一个索引 persxxal 和 srcItxxxId 字段无法匹配索引,第二个索引 persxxal 字段无法匹配索引。


同时由于这两个索引字段基本相同,为了更好保证更多字段走索引,因此可以合并优化为如下一个索引,确保更多字段能够走索引:     


 { "alxxxId" : 1, "state" : -1,  "parentItxxxId" : 1,  "persxxal" : -1, "updateTime" : -1 }
复制代码


4.5. 第五轮优化:去除查询频率较低的字段对应索引

第一轮优化删除无用索引的时候,过滤掉了命中率低于 10000 次以下的索引。但是,还有一部分索引相比高频命中次数(数十亿次)命中次数也相对较低(命中次数只有几十万),这部分较低频命中次数的索引如下:

{ "alxxxId" : 1, "image" : 1 }          "ops" : NumberLong(293104)    { "alxxxId" : 1, "videoCover" : 1 }     "ops" : NumberLong(292857)   
复制代码


调低慢日志时延阈值,分析这两个查询对应日志,如下:

Mon Aug  2 10:56:46.533 I COMMAND  [conn5491176] command xxxx.tbxxxxx command: count { count: "xxxxx", query: { alxxxId: "xxxxxx", itxxxId: "xxxxx", image: "http:/xxxxxxxxxxx/xxxxx.jpg" },   limit: 1 } planSummary: IXSCAN { itxxxId: 1.0,alxxxId:1.0 } keyUpdates:0 writeConflicts:0 numYields:1 reslen:62 locks:{ Global: { acquireCount: { r: 4 } }, Database:   { acquireCount: { r: 2 } }, Collection: { acquireCount: { r: 2 } } } protocol:op_query 4ms  Mon Aug  2 10:47:53.262 I COMMAND  [conn10428265] command xxxx.tbxxxxx command: find { find: "xxxxx", filter: { $and: [ { alxxxId: "xxxxxxx" }, { state: 0 }, { itemTagList: { $size: 0 } } ] }, limit: 1, singleBatch: true } planSummary: IXSCAN { alxxxId: 1, videoCover: 1 } keysExamined:128 docsExamined:128 cursorExhausted:1 keyUpdates:0 writeConflicts:0 numYields:22 nreturned:0 reslen:108 locks:{ Global:{ acquireCount: { r: 46 } }, Database: { acquireCount: { r: 23 } }, Collection: { acquireCount: { r: 23 } } } protocol:op_command 148ms  
复制代码


分析日志发现用户请求中的 image 都是和 alxxxId,itxxxId 进行组合查询,前面提到 alxxxId,itxxxId 是唯一的,从查询计划也可以看出,image 字段完全没有走索引。因此{ "alxxxId" : 1, "ixxxge" : 1 }索引可以删除。


同理,分析日志发现用户查询条件中没有携带 videoCover,只是部分查询走了{ alxxxId: 1, videoCover: 1 } 索引,并且 keysExamined、docsExamined 与 nreturned 不相同,所以可以确认实际只匹配了 alxxxId 索引字段。因此,该索引{ alxxxId: 1, videoCover: 1 } 可以删除。


4.6. 第六轮优化:分析日志高频查询,添加高频查询最优索引

调低日志阈值,通过 mtools 工具分析一段时间的查询,获取到如下热点查询信息:


这部分高频热点查询几乎占用了 99%以上的查询,因此务必确保这部分查询需要所有字段能走索引。分析该类查询对应日志,得到如下信息:

Mon Aug  2 10:47:58.015 I COMMAND  [conn4352017] command xxxx.xxx command: find { find: "xxxxx", filter: { $and: [ { alxxxId:"xxxxx" }, { state: 0 }, { itemTagList: { $in: [ xxxxx ] } }, { persxxal: 0 } ] }, projection: { $sortKey: { $meta: "sortKey" } },  sort: { updateTime: 1 }, limit: 3, maxTimeMS: 10000 } planSummary: IXSCAN { alxxxId: 1.0, itexxagList: 1.0 } keysExamined:1327 docsExamined:1327 hasSortStage:1 cursorExhausted:1 keyUpdates:0 writeConflicts:0 numYields:23 nreturned:3 reslen:12036 locks:{ Global: { acquireCount: { r: 48 } }, Database: { acquireCount: { r: 24 } }, Collection: { acquireCount: { r: 24 } } } protocol:op_command 151ms  
复制代码


上面日志可以看出,该高频查询扫描数据行数和最终返回的数据行数差距很大,扫描了 1327 行,最终只获取到了 3 条数据,走的是  { alxxxId: 1.0, itexxagList: 1.0 }

索引,该索引不是最优索引。该高频查询是四字段的等值查询,只有两个字段走了索引,可以把该索引优化为如下索引:{ alxxxId: 1.0, itexxagList: 1.0 , persxxal:1.0, stat:1.0}


此外,从日志可以看出,该高频查询实际上还有个 sort 排序和 limit 限制,整个查询原始 SQL 如下:

db.xxx.find({ $and: [ { alxxxId:"xxxx" }, { state: 0 }, { itexxagList: { $in: [ xxxx ] } },{ persxxal: 0 } ] }).sort({updateTime:1}).limit(3)  
复制代码


该查询模型为普通多字段等值查询 + sort 排序类查询 + limit 限制。该类查询最优索引可能是下面两个索引中的一个:


· 索引 1:普通多字段等值查询对应索引

对应查询中的如下 SQL 查询条件:

{ $and: [ { alxxxId:"xxx" }, { state: 0 }, { itexxagList: { $in: [ xxxx ] } }, { persxxal: 0 } ] }
复制代码


该 SQL 四个字段都为等值查询,按照散列度创建最优索引,取值越散列的字段放最左边,可以得到如下最优索引:

  { alxxxId: 1.0, itexxagList: 1.0 , persxxal:1.0, stat:1.0}
复制代码


如果选择该索引作为最优索引,则整个普通多字段等值查询 + sort 排序类查询 + limit 限制查询执行流程如下:

1. 通过{ alxxxId: 1.0, itexxagList: 1.0 , persxxal:1.0, stat:1.0}索引找出满足{ $and: [ { alxxxId:"xxxx" }, { state: 0 }, { itexxagList: { $in: [ xxxx ] } }, { persxxal: 0 } ] }条件的所有数据。

2. 对这些满足条件的数据进行内存排序

3. 取排序好的前三条数据


· 索引 2:Sort 排序对应最优索引

由于查询中带有 limit,因此有可能直接走{updateTime:1}排序索引,通过该索引找出三条满足以下查询条件的数据:

{ $and: [ { alxxxId:"xxxx" }, { state: 0 }, { itexxagList: { $in: [ xxxx ] } }, { persxxal: 0 } ] }
复制代码

 

整个普通多字段等值查询 + sort 排序类查询 + limit 限制查询对应索引选择索引 1 和索引 2 和数据分布有较大的关系,由于该查询为超高频查询,因此建议这类 SQL 添加 2 个索引,由 MongoDB 内核根据实际查询条件和数据分布自己决定选择那个索引作为最优索引,该高频查询对应索引如下:

{ alxxxId: 1.0, itexxagList: 1.0 , persxxal:1.0, stat:1.0}  {updateTime:1}
复制代码


4.7. 小结

通过前面六轮优化后,最终只保留如下 8 个索引:

{ "itxxxId" : 1, "alxxxId" : 1 }       { "alxxxId" : 1, "state" : 1, "digitalxxxrmarkId" : 1, "updateTime" : 1 }  { "alxxxId" : 1, "state" : -1,  "parentItxxxId" : 1, "persxxal" : -1, "updateTime" : 1 }                                                       { "alxxxId" : 1, "itexxxList.photoQiniuUrl" : 1, }                       { "alxxxId" : 1, "parentAlxxxId" : 1, "state" : 1"parentItxxxId" : 1}       { alxxxId: 1.0, itexxagList: 1.0 , persxxal:1.0, stat:1.0}    {updateTime:1}   { "alxxxId" : 1,"createTime" : -1}
复制代码


索引优化收益

通过一系列索引优化后,最终索引减少到 8 个,整体收益非常明细,主要收益如下:

· 节省 90% 以上 CPU 资源

峰值 CPU 消耗从之前的 90%多降到优化后的 10%以内

· 节省 85% 左右磁盘 IO 资源

磁盘 IO 消耗从之前的 60%-70%降低到 10%以内

· 节省 20%磁盘存储成本

由于每个索引都对应一个磁盘索引文件,索引从 30 个减少到 8 个,数据+索引最终真实磁盘消耗减少 20%左右。

· 慢日志减少 99%

索引优化前慢日志每秒数千条,优化后慢日志条数降低到数十条。优化由慢日志主要有求 count 引起,满足条件数据太多,这是正常现象。

 

最后,索引对 MongoDB 数据库查询性能起着至关重要的作用,用最少索引满足用户查询需求会极大提升数据库性能,并减少存储成本。腾讯云 DBbrain for MongoDB 基于 SQL 分类+索引规则+代价计算完美实现索引智能推荐,下期将为大家带来腾讯云索引推荐方案及实现细节的分享。


作者:腾讯云 MongoDB 团队

腾讯云 MongoDB 当前服务于游戏、电商、社交、教育、新闻资讯、金融、物联网、软件服务等多个行业;MongoDB 团队(简称 CMongo)致力于对开源 MongoDb 内核进行深度研究及持续性优化(如百万库表、物理备份、免密、审计等),为用户提供高性能、低成本、高可用性的安全数据库存储服务。后续持续分享 MongoDb 在腾讯内部及外部的典型应用场景、踩坑案例、性能优化、内核模块化分析。

用户头像

还未添加个人签名 2019.07.27 加入

还未添加个人简介

评论

发布
暂无评论
记某核心MongoDB集群索引优化实践_mongodb_MongoDB中文社区_InfoQ写作平台