3. 慢查询日志与Profiler深度分析

慢查询日志和数据库分析器(Database Profiler)是 MongoDB 性能诊断的两把“手术刀”。慢查询日志负责“事后取证”,它按阈值记录执行时间超标的操作;Profiler 则负责“全程录像”,可以按需采集更细粒度的执行统计。两者结合,才能从“哪条语句慢”深入到“为什么慢、慢在哪个阶段、缺什么索引、查询模式是否异常”。

一、先让证据可采集:慢日志与 Profiler 的开启方式

慢查询日志由 operationProfiling.slowOpThresholdMs 控制,默认 100ms。生产环境通常不建议直接调到 0,而是先设一个合理阈值,例如 200ms,避免日志量过大。动态调整可用:

db.setProfilingLevel(1, { slowms: 200 })
db.getProfilingStatus()
// { was: 1, slowms: 200, sampleRate: 1.0 }

Profiler 有三个级别:0 关闭、1 只记录慢操作、2 记录所有操作。级别 2 仅适合短时间排障,长期开启会显著增加 I/O 与存储压力。Profiler 数据写入 system.profile 集合,可通过查询它来定位问题:

db.system.profile.find(
  { millis: { $gt: 200 }, ns: { $ne: "test.system.profile" } }
).sort({ ts: -1 }).limit(10)

关键字段包括:op(操作类型)、ns(命名空间)、millis(耗时)、planSummary(执行计划摘要)、keysExamined(索引扫描键数)、docsExamined(文档扫描数)、nreturned(返回数)、command(原始命令)。这四个数字的比值,是判断索引缺失与查询模式异常的核心依据。

二、从一条慢日志还原瓶颈:四个数字的判读

假设某电商系统在高峰期出现如下慢日志片段(示例数据,用于说明判读方法):

{
  op: "query",
  ns: "shop.orders",
  millis: 842,
  planSummary: "COLLSCAN",
  keysExamined: 0,
  docsExamined: 1284300,
  nreturned: 12,
  command: {
    find: "orders",
    filter: { status: "PAID", userId: 88231 },
    sort: { createdAt: -1 }
  }
}

这条记录几乎把问题写在了脸上:planSummary 是 COLLSCAN,说明没有走索引;docsExamined 高达 128 万,而 nreturned 只有 12,扫描/返回比超过 10 万倍。这意味着数据库为了找 12 条订单,把整张订单表翻了一遍。修复方向是建立复合索引,且字段顺序要匹配“等值 + 排序”模式:

db.orders.createIndex({ userId: 1, status: 1, createdAt: -1 })

更通用的判读规则是:keysExamined 与 docsExamined 应尽量接近 nreturned。若 docsExamined / nreturned 大于 100,通常说明索引选择性差或缺失;若 keysExamined 远大于 nreturned,则可能是索引前缀被低选择性字段占用,或排序无法被索引覆盖。

三、聚合管道瓶颈:慢在“入口”还是“出口”

聚合管道的慢,往往不是单一阶段的问题,而是阶段顺序不合理。Profiler 中聚合操作的 command.pipeline 会保留完整管道,配合 planSummary 可以判断瓶颈阶段。常见反模式是:先 $match 少量条件,再 $lookup 关联大表,最后才 $match 过滤——这会让关联阶段处理海量中间结果。

正确做法是把过滤尽量前移,让 $match 和 $limit 尽早缩小数据集,再执行 $lookup、$group 等重操作。若 $lookup 的外键字段没有索引,每次关联都会触发一次集合扫描,性能会随文档数线性恶化。可用 explain("executionStats") 查看每个阶段的 totalDocsExamined 与 totalKeysExamined,定位到具体阶段。

慢查询诊断信号链:从采集到根因定位 采集层 slowOpThresholdMs Profiler level 1 / 2 证据层 system.profile millis / planSummary 指标层 keysExamined docsExamined / nreturned 根因层 索引缺失 / 管道顺序 查询模式异常 三类典型异常模式与判读依据 ① 索引缺失 planSummary = COLLSCAN docsExamined ≫ nreturned keysExamined = 0 对策:建复合索引,匹配等值+排序 ② 聚合管道瓶颈 $lookup 前未缩小数据集 $match 位置靠后 外键字段无索引 对策:过滤前移,外键建索引 ③ 查询模式异常 keysExamined ≫ nreturned 索引前缀选择性低 排序无法被索引覆盖 对策:调整索引字段顺序 验证闭环:explain("executionStats") 复核 目标:docsExamined ≈ nreturned,keysExamined 接近 nreturned 若仍不达标,回到指标层重新判读,而非盲目加索引

四、查询模式异常:比单条慢语句更危险

单条慢查询可以靠索引解决,但“查询模式异常”往往意味着应用层设计问题。典型表现有三类:

  • 高频小查询风暴:Profiler 中同一 ns 在短时间内出现大量 millis 不高但 nreturned=1 的查询,累计消耗大量 CPU。这通常是应用层循环调用数据库,应改为批量查询或 $in。
  • 无界查询:nreturned 极大且没有 limit,例如全表导出、无分页列表。应强制分页并加 limit。
  • 排序与索引方向不一致:planSummary 出现 SORT 或 SORT_KEY_GENERATOR,说明排序在内存中完成。当排序数据超过 32MB 会直接报错,应让索引顺序与 sort 方向一致。

识别查询模式异常,不能只看单条记录,而要做聚合统计。例如按命名空间和命令指纹分组,找出“总耗时最高”的操作:

db.system.profile.aggregate([
  { $match: { millis: { $gt: 100 } } },
  { $group: {
      _id: { ns: "$ns", op: "$op" },
      count: { $sum: 1 },
      totalMs: { $sum: "$millis" },
      avgMs: { $avg: "$millis" },
      maxMs: { $max: "$millis" }
  }},
  { $sort: { totalMs: -1 } },
  { $limit: 10 }
])

这条聚合能快速暴露“单条不慢但总量惊人”的操作,是发现查询模式异常的有效手段。

五、一份可直接使用的慢查询分析检查表

下面这份检查表按“采集—判读—定位—验证”组织,可在实际排障时逐项核对。字段和阈值均为经验参考,需结合业务数据量调整。

检查项 观察字段/命令 异常信号 处理方向
是否走索引 planSummary COLLSCAN / IXSCAN 但 keys 极高 建索引或调整索引字段顺序
扫描效率 docsExamined / nreturned 比值 > 100 提高索引选择性,过滤前移
排序是否落盘 planSummary 含 SORT 出现 SORT_KEY_GENERATOR 索引方向匹配 sort 方向
聚合阶段顺序 command.pipeline $lookup 在 $match 之前 过滤、limit 前移
查询模式 按 ns+op 聚合 totalMs 高频小查询总量高 批量查询、缓存、限流
验证效果 explain("executionStats") docsExamined 仍远大于 nreturned 回到指标层重新判读
Profiler 的 sampleRate 参数(MongoDB 5.0+)可以按比例采样,在高负载实例上既能保留统计特征,又能降低采集开销。排障初期可设为 0.1~0.3,定位到具体命名空间后再临时调高。
不要长期开启 Profiler level 2。它会把所有操作写入 system.profile,该集合本身也是普通集合,写入压力会反过来影响被诊断的实例,形成“观测者效应”。

慢查询日志与 Profiler 的价值,不在于记录了多少条慢语句,而在于能否从 planSummary、keysExamined、docsExamined、nreturned 这组数字中读出执行计划的真实行为。索引缺失看扫描比,聚合瓶颈看阶段顺序,查询模式异常看聚合统计。三者结合,才能把“慢”从一个现象,还原成可修复的具体原因。