广告位 · 页头(后台-模块管理-新建模块后替换此区域)

MongoDB聚合管道里如何获取$latencyStats延迟统计?

MongoDB 的慢操作排查经常需要回答两个问题:某个集合的读写操作平均延迟是多少,以及是否存在少量极高延迟的长尾请求。聚合框架里没有提供单独的 $latencyStats 阶段,但通过 $collStats 阶段的 latencyStats 参数可以拿到几乎相同的数据。本文会把这条路径的用法、指标含义和实战限制讲清楚。

MongoDB聚合管道里如何获取$latencyStats延迟统计?

$collStats 阶段并不像 $match、$project 那样随处可见,它必须放在聚合管道的第一阶段执行,因为它的作用是读取集合或视图的元数据统计,而不是在文档流上做过滤。要给集合 orders 生成延迟统计,最基本写法如下。

db.orders.aggregate([
  {
    $collStats: {
      latencyStats: { histograms: true }
    }
  }
])

这段脚本会在 orders 集合上执行一次聚合,返回一个只包含统计信息的文档。字段 histograms: true 表示除了累计延迟和操作次数之外,还要输出延迟直方图。它是可选参数,默认只返回汇总数据。

一、先厘清入口:$collStats 阶段里的 latencyStats 参数

很多人一开始会去文档里搜索 $latencyStats,结果自然找不到独立阶段。MongoDB 在 shell 里提供了 db.collection.latencyStats() 方法,但聚合框架中对应的能力被放进了 $collStats 阶段。这个阶段专门用来输出集合级统计数据,常见参数还包括 storageStats、count 和 queryExecStats。如果既想拿延迟又想拿存储统计,可以在同一个 $collStats 对象里并列指定,避免多次执行统计操作。

使用时有一个硬性约束:$collStats 必须是管道的第一个阶段。如果你尝试在它前面加 $match 或 $project,MongoDB 会直接报错。这个设计也很好理解,因为统计信息来自集合本身,而不是来自某几条被筛选后的文档。下面的写法就是错误的。

// 错误:$collStats 必须位于第一阶段
db.orders.aggregate([
  { $match: { status: "paid" } },
  { $collStats: { latencyStats: { histograms: true } } }
])

正确做法是先放 $collStats,再把统计结果交给后续的 $project、$match、$merge 等阶段加工。这样延迟数据就变成了普通聚合文档,可以像处理业务数据一样继续处理。

二、返回值拆解:读、写、命令和事务四类指标

执行上面的脚本后,返回文档大致如下。为了展示结构,这里对数值做了简化。

{
  "ns": "test.orders",
  "latencyStats": {
    "reads": {
      "latency": 24512,
      "ops": 18,
      "histogram": [
        { "micros": 16, "count": 2 },
        { "micros": 32, "count": 3 },
        { "micros": 64, "count": 5 },
        { "micros": 128, "count": 6 },
        { "micros": 256, "count": 2 }
      ]
    },
    "writes": { "latency": 1042, "ops": 7, "histogram": [] },
    "commands": { "latency": 986, "ops": 5, "histogram": [] },
    "transactions": { "latency": 0, "ops": 0, "histogram": [] }
  }
}

其中 latencyStats 下面固定包含四类操作:reads、writes、commands 和 transactions。每类又包含三个字段:latency 表示该类操作累计消耗的时间,单位是微秒;ops 表示操作次数;histogram 是可选的直方图数据。把 latency 除以 ops,就能得到平均延迟。

不过平均延迟很容易掩盖问题。比如一个集合有 1000 次读取,其中 999 次都在 100 微秒以内,只有 1 次耗时 5 秒,平均值仍然可能很低。此时更该看直方图,或者把最大值、分位数拆出来。用聚合框架可以很轻松地完成平均延迟计算,示例脚本如下。

db.orders.aggregate([
  { $collStats: { latencyStats: { histograms: false } } },
  {
    $project: {
      collection: "$ns",
      readAvgMicros: {
        $cond: [
          { $gt: ["$latencyStats.reads.ops", 0] },
          { $divide: ["$latencyStats.reads.latency", "$latencyStats.reads.ops"] },
          0
        ]
      },
      writeAvgMicros: {
        $cond: [
          { $gt: ["$latencyStats.writes.ops", 0] },
          { $divide: ["$latencyStats.writes.latency", "$latencyStats.writes.ops"] },
          0
        ]
      }
    }
  }
])

这里用 $cond 做了除零保护,避免某些集合从未发生写入时导致结果为 null 或抛错。得到的结果只是普通数字,方便后续继续计算或导出。

三、直方图在定位长尾延迟上的价值

延迟直方图把同一类操作的耗时按区间划分,每个桶包含一个 micros 阈值和落在该区间的 count。观察直方图时不需要纠结每一个桶的精确含义,重点看分布趋势:大量操作集中在低延迟桶里,少量操作落在高延迟桶里,就说明存在明显的长尾请求。

直方图默认嵌在 reads.histogram 这样的嵌套文档中,不太方便直接观察。可以在聚合管道里先 $unwind 拆开,再按桶排序,把最高的几个桶单独拿出来分析。

db.orders.aggregate([
  { $collStats: { latencyStats: { histograms: true } } },
  { $project: { "reads.histogram": 1, _id: 0 } },
  { $unwind: "$reads.histogram" },
  { $sort: { "reads.histogram.micros": 1 } },
  {
    $project: {
      bucketMicros: "$reads.histogram.micros",
      count: "$reads.histogram.count"
    }
  }
])

这个脚本会输出一组按延迟阈值从小到大排列的桶。实际排查时,如果看到前十几个桶覆盖了 95% 以上的操作,而最后一个桶虽然 count 很小但 micros 非常大,就应该重点去看 currentOp、慢查询日志或应用端超时记录,锁定那几条拖慢整体体验的请求。

需要注意的是,直方图的桶边界并不是固定线性的。随着延迟增加,桶的宽度通常会变大。这样做是为了用较少的桶覆盖很大的延迟范围,但代价是高位桶无法提供毫秒级精确分布。因此它更适合快速发现异常数量级,而不是替代 APM 工具做精细性能分析。

四、把延迟统计纳入日常监控:输出处理与调用频率

如果只是在 shell 里手动跑一次,延迟统计的价值很有限。更实用的方式是把 $collStats 的结果定期写入一个专门的监控集合。下面的脚本会在执行统计后补充一个采样时间,然后用 $merge 写入 latency_snapshots 集合。

db.orders.aggregate([
  { $collStats: { latencyStats: { histograms: true } } },
  {
    $project: {
      collection: "$ns",
      readOps: "$latencyStats.reads.ops",
      readLatency: "$latencyStats.reads.latency",
      writeOps: "$latencyStats.writes.ops",
      writeLatency: "$latencyStats.writes.latency",
      commandOps: "$latencyStats.commands.ops",
      commandLatency: "$latencyStats.commands.latency",
      sampledAt: "$$NOW"
    }
  },
  {
    $merge: {
      into: "latency_snapshots",
      whenMatched: "merge",
      whenNotMatched: "insert"
    }
  }
])

这里使用 $$NOW 记录采样时间。需要注意 $merge 的目标集合不要与业务集合混淆,最好放在独立的监控数据库下,避免影响线上读写。写入后的快照可以用普通查询按时间排序,观察延迟是否在发布、流量高峰或索引变更后出现突增。

调度频率也需要控制。虽然 latencyStats 本身的开销通常不高,但开启 histograms: true 会额外维护更多统计信息。对于高吞吐集合,不建议秒级执行;按分钟或按小时采集已经能覆盖大多数故障场景。同时,分片集群上执行时要留意结果粒度,延迟统计可能按分片返回多份数据,需要在 $merge 之前做汇总或保留分片标识。

五、与 db.collection.latencyStats() 命令的差异

MongoDB shell 提供了一个更直接的方法:db.collection.latencyStats()。它的返回结构和聚合管道中的 latencyStats 基本一致,只是在展示层更友好,适合运维人员临时查看。

db.orders.latencyStats({ histograms: true })

两条路径的选择主要看使用场景。命令方式的优势是简短、直观,排障时随手一跑就能看到结果。聚合管道方式的优势是结果可以被后续阶段继续加工,也能写入外部集合长期保留。比如你想把 10 个核心集合的延迟快照每天汇总一次,命令行脚本要写循环并手动解析输出,而聚合方式可以把每个集合的统计结果固定成同样的字段结构,再交给 BI 或监控系统消费。

对比项聚合管道 $collStatslatencyStats() 方法
是否可继续加工可以,后续接 $project、$merge 等只能直接返回结果
是否便于自动化强,可写入监控集合弱,需要手动或脚本解析
使用复杂度较高,要了解聚合语法低,一条命令即可
适用环境周期性采集、报表输入临时排障、快速检查

无论用哪种方式,都要理解延迟统计反映的是 MongoDB 实例内部记录到的操作耗时,不包括网络往返和应用序列化时间。如果客户端看到的延迟明显高于统计结果,问题可能出在连接池等待、网络抖动或业务代码处理上,这时单看 MongoDB 延迟指标是不够的。

总结起来,$latencyStats 虽然不是独立的聚合阶段,但通过 $collStats 里的 latencyStats 参数完全可以实现聚合管道场景下的延迟统计。配合直方图、投影和 $merge,可以把它变成一套轻量的延迟监控方案,帮助你在慢查询发生前发现趋势,或在故障复盘时快速还原当时的操作延迟分布。

广告位 · 文章底部(后台-模块管理替换此区域)