mongodb走了索引,依旧是慢查询,请看执行计划

根据索引in查询, 扫描索引3条 keysExamined 3, 返回文档数1条 nReturned 1, 耗时 millis 452毫秒, execStats中executionTimeMillisEstimate都是0.
问题1: executionTimeMillisEstimate都是0. 为啥整个查询却消耗452毫秒.
问题2: 走了索引,只扫描索引3条,返回文档数1条,消耗452毫秒,怎么优化或者怎么调整它.
问题3: 在其它的执行计划中我看到inputStage.stage.FETCH操作耗时也很高,在200~1000毫秒.FETCH根据索引取下文档为啥还要这么多时间呢.
求助 @Mongoing中文社区 @bguo

{
  "op": "query",
  "ns": "webDevice",
  "query": {
    "find": "webDevice",
    "filter": {
      "lid": {
        "$in": [
          "40CnwyHkVmnA9kbScMLNLneaxuS4Tcj",
          "140CnwyHkVmnA9kbScMLNLneaxuS4Tcj"
        ]
      }
    },
    "projection": {
      "$sortKey": {
        "$meta": "sortKey"
      }
    },
    "sort": {
      "createTime": -1
    },
    "limit": 1,
    "shardVersion": [
      {
        "$timestamp": {
          "t": 106,
          "i": 0
        }
      },
      {
        "$oid": "59b0039e9b5e66530435be05"
      }
    ]
  },
  "keysExamined": 3,
  "docsExamined": 1,
  "hasSortStage": true,
  "cursorExhausted": true,
  "keyUpdates": 0,
  "writeConflicts": 0,
  "numYield": 0,
  "locks": {
    "Global": {
      "acquireCount": {
        "r": 2
      }
    },
    "Database": {
      "acquireCount": {
        "r": 1
      }
    },
    "Collection": {
      "acquireCount": {
        "r": 1
      }
    }
  },
  "nreturned": 1,
  "responseLength": 1267,
  "protocol": "op_command",
  "millis": 452,
  "execStats": {
    "stage": "CACHED_PLAN",
    "nReturned": 1,
    "executionTimeMillisEstimate": 0,
    "works": 2,
    "advanced": 1,
    "needTime": 0,
    "needYield": 0,
    "saveState": 0,
    "restoreState": 0,
    "isEOF": 1,
    "invalidates": 0,
    "inputStage": {
      "stage": "PROJECTION",
      "nReturned": 1,
      "executionTimeMillisEstimate": 0,
      "works": 5,
      "advanced": 1,
      "needTime": 4,
      "needYield": 0,
      "saveState": 0,
      "restoreState": 0,
      "isEOF": 1,
      "invalidates": 0,
      "transformBy": {
        "$sortKey": {
          "$meta": "sortKey"
        }
      },
      "inputStage": {
        "stage": "SORT",
        "nReturned": 1,
        "executionTimeMillisEstimate": 0,
        "works": 5,
        "advanced": 1,
        "needTime": 4,
        "needYield": 0,
        "saveState": 0,
        "restoreState": 0,
        "isEOF": 1,
        "invalidates": 0,
        "sortPattern": {
          "createTime": -1
        },
        "memUsage": 1031,
        "memLimit": 33554432,
        "limitAmount": 1,
        "inputStage": {
          "stage": "SORT_KEY_GENERATOR",
          "nReturned": 0,
          "executionTimeMillisEstimate": 0,
          "works": 4,
          "advanced": 0,
          "needTime": 2,
          "needYield": 0,
          "saveState": 0,
          "restoreState": 0,
          "isEOF": 1,
          "invalidates": 0,
          "inputStage": {
            "stage": "SHARDING_FILTER",
            "nReturned": 1,
            "executionTimeMillisEstimate": 0,
            "works": 3,
            "advanced": 1,
            "needTime": 1,
            "needYield": 0,
            "saveState": 0,
            "restoreState": 0,
            "isEOF": 1,
            "invalidates": 0,
            "chunkSkips": 0,
            "inputStage": {
              "stage": "FETCH",
              "nReturned": 1,
              "executionTimeMillisEstimate": 0,
              "works": 3,
              "advanced": 1,
              "needTime": 1,
              "needYield": 0,
              "saveState": 0,
              "restoreState": 0,
              "isEOF": 1,
              "invalidates": 0,
              "docsExamined": 1,
              "alreadyHasObj": 0,
              "inputStage": {
                "stage": "IXSCAN",
                "nReturned": 1,
                "executionTimeMillisEstimate": 0,
                "works": 3,
                "advanced": 1,
                "needTime": 1,
                "needYield": 0,
                "saveState": 0,
                "restoreState": 0,
                "isEOF": 1,
                "invalidates": 0,
                "keyPattern": {
                  "lid": -1
                },
                "indexName": "lid_-1",
                "isMultiKey": false,
                "isUnique": false,
                "isSparse": false,
                "isPartial": false,
                "indexVersion": 1,
                "direction": "forward",
                "indexBounds": {
                  "lid": [
                    "[\"40CnwyHkVmnA9kbScMLNLneaxuS4Tcj\", \"40CnwyHkVmnA9kbScMLNLneaxuS4Tcj\"]",
                    "[\"140CnwyHkVmnA9kbScMLNLneaxuS4Tcj\", \"140CnwyHkVmnA9kbScMLNLneaxuS4Tcj\"]"
                  ]
                },
                "keysExamined": 3,
                "dupsTested": 0,
                "dupsDropped": 0,
                "seenInvalidated": 0
              }
            }
          }
        }
      }
    }
  },
  "ts": {
    "$date": 1514285478923
  },
  "client": "10.105.122.126",
  "allUsers": [
    {
      "user": "__system",
      "db": "local"
    }
  ],
  "user": "__system@local",
  "_id": "c044e94198e245f3e61b39d230feb393-20171226105118923-200109374"
}

补充说明

我补充一下我的环境: 机器是8核16内存. 机器上部署有5个mongodb实例(在docker容器里面),1个mongos,1个config,3个shard(1个主,1个从,1个arbiter).
以下是docker的内存使用情况.

cpu使用情况 内存使用情况
arbiter实例 1.83% 80.18MiB / 15.51GiB
shard2从 3.09% 5.306GiB / 15.51GiB
config 1.81% 1.449GiB / 15.51GiB
shard3主 2.56% 5.025GiB / 15.51GiB
mongos 0.37% 188.3MiB / 15.51GiB

其中config,shard主,shard从.3个实例都设置了CacheSizeGB为3.
目前从资源使用情况来看,CPU使用率都很低,内存config虽然限制了3GB,但整个docker容器只用了1.5GB的内存.

  1. 疑问1: 按照你说的方式调小cacheSizeGB,应该怎么调整比较合理
  2. 疑问2: config只用了1.5GB内存(限制了CacheSizeGB3GB).可以说明索引都加载到了内存中吗?
  3. 疑问3: mongodb remove数据后,查询却越来越慢是什么情况?
阅读 10.1k
1 个回答

下次记得把原始查询也发出来,我们看着更方便些。从执行计划来反推,查询大约是

db.webDevice.find({
    lid: {
        $in: [
            "40CnwyHkVmnA9kbScMLNLneaxuS4Tcj",
            "140CnwyHkVmnA9kbScMLNLneaxuS4Tcj"
        ]
    }
}).sort({createTime: -1}).limit(1);

因为命中索引,这个查询获取数据的速度实际上比较快,你也提到在一段时间内第二次查询就快了,这是一个很明显的特点,代表内存不足。MongoDB和其他数据库一样,都会使用空闲内存缓冲索引和数据,当内存不足时就会使用LRU算法清理旧数据换入新数据再继续查询。因为这个过程涉及到磁盘数据交换,速度会大大降低。发生这种情况有一个特点,就是一段时间内第二次执行时速度就快了(因为数据已经在内存中)。但是如果过一段时间再执行,速度又会变慢(因为又被换出内存了)。所以你的情况实际上就是受限于硬件。
既然如此最直接的解决办法就是:

  1. 使用更快的硬盘;
  2. 使用更大的内存;

如果不能从硬件方面解决,有一点可以尝试就是用CPU换内存。做法是尽可能调小cacheSizeGB(是的没错,是调小)。空闲出来的内存操作系统会用来缓冲磁盘数据,而磁盘数据是经过压缩的,体积更小,因此可以缓冲更多数据到内存中。但作为交换,在使用这些数据时需要经过CPU再进行一次解压从而额外消耗CPU。即使这样,效果也比从磁盘读取要好很多。整个过程是自动进行的,你需要做的只是调小cacheSizeGB
这种做法可以在一定程度上缓解内存不足的问题,但不是万能的:

  • 首先它会增加CPU消耗,如果你的系统本身已经没有剩余的CPU资源,这种做法就不合适;
  • 其次受制于压缩率,这样做之后内存能容纳的数据并不会比以前多很多,所以并不是万能的;

补充回答

基于你的新的疑问,以下几点补充:

疑问1: 按照你说的方式调小cacheSizeGB,应该怎么调整比较合理

我在上面有提到,这样做的效果有限,是受制于压缩率的限制。所以多容纳的数据实际就是压缩的数据。比如1G的内存能放多少数据?

  • 如果不压缩(压缩率100%),1G内存能放1GB数据;
  • 如果压缩率90%,1G内存可以放10/9GB~=1.11数据;
  • 如果压缩率80%,1G内存可以放10/8 = 1.25GB数据;
  • 以此类推……

所以调到多少,其实要看你想往内存里放多少数据,而这往往是一个不确定的数值。这里会有另外一个概念叫做工作集(working set),就是你经常用到的那些数据。比如你的数据库共有100GB数据,但是你经常用到的部分只有10GB,那么你的内存只要能装下10GB数据,应用在大部分时候就可以非常快,剩下的情况忽略就好了。
基于上面这些分析,你应该可以算一下,你的工作集有多大,数据压缩率有多大,那么需要把cacheSizeGB调到多小才能容纳下工作集(又或者调到多小都不可能容纳下工作集)。你的情况是内存不足CPU空闲,所以如果懒得算,直接把cacheSizeGB调到最小值就好了。

疑问2: config只用了1.5GB内存(限制了CacheSizeGB3GB).可以说明索引都加载到了内存中吗?

这说明config数据库(元数据)的索引和数据都加载到内存中了。注意数据的索引肯定是在shard中,与config无关。而且大头是在shard上面。
顺便提一下,如果不是实验目的,根本没必要分这么多片。因为在一台机器上分再多片,硬件资源也只有这么多,对于性能没有什么意义,反而还会有额外的传输开销。

疑问3: mongodb remove数据后,查询却越来越慢是什么情况?

remove之后跟查询没有本质上的联系,可能只是凑巧发生在一起。如果你有足够的证据觉得这两者确实有联系,请另开问题描述清楚问题的上下文以及你发现的情况,必要的时候也可以求助于MongoDB JIRA。如果一定要做一个无根据的猜测,我觉得可能是remove时导致热数据被换出内存(注意remove也需要先找到满足条件的数据然后才能删除),引起后面的查询需要重新从磁盘上加载数据造成的。

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题
宣传栏