1. 项目背景

业务场景:本地生活电商的运维半夜被手机告警叫醒——"MongoDB 连接数超过 500"。运维 SSH 上去看,机器一切正常,但用户投诉订单创建失败。排查了 20 分钟才发现是 3 小时前上线的一个新功能带来了一个全表扫描的查询,慢查询堆积导致连接池耗尽。如果有完善的监控体系——连接数的趋势图能提前 2 小时看到线性增长;慢查询日志的实时仪表盘能立即定位到罪魁祸首的查询;Prometheus 告警规则能在连接数超过阈值之前提前发通知——这个故障本可以避免或被更早发现。

痛点:没有可观测性体系的 MongoDB 是黑盒运行。CPU 飙满不知道是谁在跑;磁盘空间悄悄逼近上限没人盯;复制延迟超过 30 秒才发现从库不可用;慢查询列表只在事后看,无法实时预警。更关键的是——Prometheus + Grafana + mongodb_exporter 这套标准的监控栈如果不会搭建,团队只能靠运维不定期登录看两眼。

2. 项目设计

小胖(打着哈欠):大师,昨晚又被故障叫起来了。MongoDB 连接池满,查了半天才发现是一个慢查询。有没有办法让系统在"出事之前"就告诉我?

大师:可观测性三道防线——指标(Metrics)看趋势、日志(Logs)看现场、告警(Alerting)及时响应。MongoDB 提供的观测信息非常丰富,但你不把它们串起来就等于没装监控。

小胖:先说指标——哪些是关键的?我在 serverStatus 里看到几百个指标,完全看不过来。

大师:精简到 6 类黄金指标就够用了——

类别 核心指标 代表故障
连接 connections.current / connections.available 连接池满 → 服务不可用
操作 opcounters.insert/query/update/delete QPS 异常飙升或暴跌
复制 replSetGetStatus 各节点 optimeDate 差异 Primary 延迟大 → 从库不可用
内存 wiredTiger.cache 使用率 + eviction 页数 缓存不够 → 查询走磁盘变慢
locks.Global.acquireWaitCount 锁等待高 → 事务/long op 阻塞
磁盘 dbStats.dataSize + indexSize 磁盘满 → 写入失败

技术映射:这六类指标覆盖了 RED(Rate-Error-Duration)和 USE(Utilization-Saturation-Errors)两种监控方法论在数据库层面的映射。

小胖:那慢查询呢?能不能自动收集到一个仪表盘?

大师:三种方式组合使用——① MongoDB 内置 profiler(第 15 章讲过的 system.profile)记录慢查询日志;② mongodb_exporter 把这些指标的时序数据暴露给 Prometheus;③ Grafana 把 Prometheus 的数据变成可视化仪表盘。

小白:FTDC 呢?我看 MongoDB 的 diagnostic.data 目录一直在写东西。

大师:FTDC(Full-Time Diagnostic Data Capture)是 MongoDB 自己的轻量级诊断数据采集器,每秒自动采一次 serverStatus 的快照,写入 diagnostic.data 目录。这个是 MongoDB 技术支持排查问题时首先要的——它开销极低(<1% CPU),默认开启,不要关闭。但它和 Prometheus 不冲突——FTDC 是事后诊断用的"法医数据",Prometheus 是实时告警用的"心电监控"。

技术映射:Prometheus + mongodb_exporter = 实时指标采集 + 告警。FTDC = 历史诊断快照(MongoDB 技术支持依赖这个数据)。

大师(总结):搭建可观测性的最低配置——mongodb_exporter 暴露指标 → Prometheus 采集 + 存储 → Grafana 可视化 → Alertmanager 发告警。6 条核心告警规则:连接数 > 80%、复制延迟 > 3s、慢查询 QPS > 10/min、磁盘使用率 > 85%、缓存淘汰频繁、主从切换事件。

3. 项目实战

3.1 环境准备

需要以下组件(通过 Docker Compose 一键启动):

# mongodb-lab/observability/docker-compose-obs.yml
services:
  prometheus:
    image: prom/prometheus:v2.50.0
    ports: ["9090:9090"]
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
      - prometheus_data:/prometheus
  grafana:
    image: grafana/grafana:10.x
    ports: ["3000:3000"]
    environment:
      GF_SECURITY_ADMIN_PASSWORD: admin
    volumes:
      - grafana_data:/var/lib/grafana
  mongodb_exporter:
    image: percona/mongodb_exporter:0.40
    ports: ["9216:9216"]
    command:
      - '--mongodb.uri=mongodb://admin:admin123@host.docker.internal:27017/?authSource=admin'
      - '--collect-all'
      - '--compatible-mode'
volumes:
  prometheus_data:
  grafana_data:
# prometheus.yml
global:
  scrape_interval: 15s
scrape_configs:
  - job_name: 'mongodb'
    static_configs:
      - targets: ['host.docker.internal:9216']  # mongodb_exporter

3.2 分步实现

步骤一:核心指标采集——serverStatus 速查

目标:学会从 serverStatus 中提取 6 类黄金指标。

// mongodb-health-metrics.js —— MongoDB 核心指标采集脚本
use admin
const s = db.serverStatus()

// 1. 连接数
print("=== CONNECTIONS ===")
print(`  current: ${s.connections.current}`)
print(`  available: ${s.connections.available}`)
print(`  active: ${s.connections.active}`)
print(`  utilization: ${(s.connections.current / s.connections.available * 100).toFixed(1)}%`)

// 2. 操作计数器(需配合差值算 QPS)
print("=== OPCOUNTERS (累计) ===")
print(`  insert: ${s.opcounters.insert}`)
print(`  query: ${s.opcounters.query}`)
print(`  update: ${s.opcounters.update}`)
print(`  delete: ${s.opcounters.delete}`)
print(`  command: ${s.opcounters.command}`)

// 3. 内存
print("=== WIREDTIGER CACHE ===")
if (s.wiredTiger?.cache) {
  const cache = s.wiredTiger.cache
  const usedMB = cache["bytes currently in the cache"] / 1024 / 1024
  const maxMB = cache["maximum bytes configured"] / 1024 / 1024
  const dirtyMB = (cache["tracked dirty bytes in the cache"] || 0) / 1024 / 1024
  print(`  used: ${usedMB.toFixed(0)}MB / ${maxMB.toFixed(0)}MB (${(usedMB/maxMB*100).toFixed(1)}%)`)
  print(`  dirty: ${dirtyMB.toFixed(0)}MB`)
  print(`  eviction (app): ${cache["pages evicted by application threads"] || 0}`)
  // eviction > 0 且频繁增长 = 缓存不够用
}

// 4. 锁
print("=== LOCKS ===")
if (s.locks?.Global) {
  print(`  global acquire: ${s.locks.Global.acquireCount?.r || '?'}R / ${s.locks.Global.acquireCount?.w || '?'}W`)
  print(`  global wait: ${s.locks.Global.acquireWaitCount?.r || '?'}R / ${s.locks.Global.acquireWaitCount?.w || '?'}W`)
}

// 5. 网络
print("=== NETWORK ===")
print(`  bytesIn: ${(s.network?.bytesIn || 0) / 1024 / 1024}MB`)
print(`  bytesOut: ${(s.network?.bytesOut || 0) / 1024 / 1024}MB`)
print(`  requests: ${s.network?.numRequests || 0}`)

步骤二:复制延迟监控

目标:计算 Secondary 与 Primary 之间的复制延迟。

// replicaset-lag-monitor.js
try {
  const replStatus = db.adminCommand({ replSetGetStatus: 1 })
  const primary = replStatus.members.find(m => m.stateStr === "PRIMARY")
  const primaryOptime = primary?.optime?.ts

  print("=== REPLICATION ===")
  print(`  Primary: ${primary?.name}`)

  replStatus.members.forEach(m => {
    if (m.stateStr === "PRIMARY") return
    const lagSeconds = primaryOptime && m.optime?.ts
      ? (primaryOptime.getHighBits() - m.optime.ts.getHighBits()) // 近似秒
      : "N/A"
    const health = m.health === 1 ? "✓" : "✗"
    print(`  ${health} ${m.name} (${m.stateStr}): 延迟 ${lagSeconds}s`)
  })

  // Oplog 窗口
  const oplog = db.getSiblingDB("local").oplog.rs
  const first = oplog.find().sort({ $natural: 1 }).limit(1).next()
  const last = oplog.find().sort({ $natural: -1 }).limit(1).next()
  if (first && last) {
    const windowHours = (last.ts.getHighBits() - first.ts.getHighBits()) / 3600
    print(`  Oplog窗口: ${windowHours.toFixed(1)}h`)
  }
} catch (e) {
  print("非复制集环境,跳过复制监控")
}

步骤三:配置 Prometheus 告警规则

目标:定义 6 条核心告警。

# prometheus-alerts.yml
groups:
  - name: mongodb_alerts
    rules:
      # 1. 连接数告警
      - alert: MongoDBHighConnections
        expr: mongodb_connections_current / mongodb_connections_available > 0.8
        for: 5m
        labels: { severity: warning }
        annotations:
          summary: "MongoDB 连接数超过 80%"
          description: "当前连接数 {{ $value | humanizePercentage }}"

      # 2. 复制延迟
      - alert: MongoDBReplicationLag
        expr: mongodb_mongod_replset_member_replication_lag_seconds > 3
        for: 1m
        labels: { severity: critical }
        annotations:
          summary: "MongoDB 复制延迟 > 3s"

      # 3. 慢查询速率
      - alert: MongoDBSlowQueries
        expr: rate(mongodb_mongod_metrics_cursor_open_total[5m]) > 10
        for: 2m
        labels: { severity: warning }

      # 4. 磁盘使用率
      - alert: MongoDBDiskUsage
        expr: mongodb_mongod_dbstats_dataSize / mongodb_mongod_dbstats_storageSize > 0.85
        for: 5m
        labels: { severity: critical }

      # 5. 缓存淘汰频繁
      - alert: MongoDBCacheEviction
        expr: rate(mongodb_wiredtiger_cache_evicted_entries[5m]) > 0
        for: 10m
        labels: { severity: warning }
        annotations:
          summary: "WiredTiger 缓存发生淘汰,建议增大 cacheSizeGB"

      # 6. 主从切换事件
      - alert: MongoDBPrimaryChanged
        expr: changes(mongodb_mongod_replset_my_state[10m]) > 0
        labels: { severity: info }
        annotations:
          summary: "MongoDB Primary 已切换"

步骤四:慢查询治理自动化

目标:编写脚本自动收集和报告慢查询。

// slow-query-audit.js —— 每周一次自动分析慢查询
use local_life

// 查找过去 24 小时的慢查询
const oneDayAgo = new Date(Date.now() - 24 * 3600 * 1000)

const slowQueries = db.system.profile.aggregate([
  { $match: {
      ts: { $gte: oneDayAgo },
      millis: { $gt: 200 }   // > 200ms
  }},
  { $group: {
      _id: {
        op: "$op",
        ns: "$ns",
        // 提取查询形状的前 100 个字符作为分类
        shape: { $substr: [
          { $ifNull: ["$command.filter", "$command.pipeline", "$command"] },
          0, 80
        ]}
      },
      count: { $sum: 1 },
      avgMillis: { $avg: "$millis" },
      maxMillis: { $max: "$millis" },
      totalDocsExamined: { $sum: "$docsExamined" }
  }},
  { $match: { count: { $gt: 5 } } },   // 出现 > 5 次才报告
  { $sort: { avgMillis: -1 } },
  { $limit: 20 }
]).toArray()

print("=== 过去 24h 慢查询 Top 20 ===")
if (slowQueries.length === 0) {
  print("  ✓ 无高频慢查询")
} else {
  slowQueries.forEach((q, i) => {
    print(`  ${i+1}. [${q._id.op}] ${q._id.ns}`)
    print(`     出现: ${q.count}次, 平均: ${q.avgMillis.toFixed(1)}ms, 最大: ${q.maxMillis}ms`)
    print(`     形状: ${(q._id.shape || '').slice(0, 100)}`)
    print(`     扫描文档: ${q.totalDocsExamined}`)
  })
}

// 检测 COLLSCAN
const collScans = db.system.profile.countDocuments({
  ts: { $gte: oneDayAgo },
  planSummary: "COLLSCAN",
  millis: { $gt: 100 }
})
print(`\n⚠ 全表扫描 (COLLSCAN): ${collScans} 次`)
if (collScans > 0) {
  print("  建议:为以下集合增加索引——")
  const scanColls = db.system.profile.distinct("ns", {
    ts: { $gte: oneDayAgo }, planSummary: "COLLSCAN"
  })
  scanColls.forEach(coll => print(`    - ${coll}`))
}

步骤五:构建 Grafana 仪表盘——最小监控面板

目标:了解 Grafana 面板的组织方式(概念级别)。

// === Grafana 仪表盘布局建议 ===
// 该部分是对 Grafana 配置的概念描述,非直接可执行代码

/*
Grafana Dashboard: "MongoDB 生产监控"
┌───────────────────────────────────────┐
│ Row 1: 连接 & QPS                     │
│ ┌────────────┐ ┌────────────┐          │
│ │ 活跃连接数  │ │ QPS (R/W)  │          │
│ │  gauge     │ │  graph     │          │
│ └────────────┘ └────────────┘          │
│                                        │
│ Row 2: 复制延迟 & Oplog 窗口           │
│ ┌──────────────────────┐               │
│ │ 复制延迟 (timeseries)│               │
│ └──────────────────────┘               │
│                                        │
│ Row 3: 缓存 & 磁盘                     │
│ ┌────────────┐ ┌────────────┐          │
│ │ WT 缓存使用 │ │ 磁盘使用率  │          │
│ └────────────┘ └────────────┘          │
│                                        │
│ Row 4: 慢查询日志 (table)              │
│ ┌──────────────────────────────────┐   │
│ │ Timestamp | Collection | Op | ms │   │
│ └──────────────────────────────────┘   │
│                                        │
│ Row 5: 锁等待 & Page Fault             │
│ ┌────────────┐ ┌────────────┐          │
│ │ 锁等待次数  │ │ 页错误数   │          │
│ └────────────┘ └────────────┘          │
└───────────────────────────────────────┘

PromQL 查询示例:
- QPS: rate(mongodb_op_counters_total{type="query"}[1m])
- 连接: mongodb_connections{state="current"}
- 复制延迟: mongodb_mongod_replset_member_replication_lag_seconds
- 缓存: mongodb_wiredtiger_cache_bytes{type="currently_in_cache"} / 
          mongodb_wiredtiger_cache_max_bytes
*/

步骤六:FTDC 数据提取

目标:了解如何读取和利用 MongoDB 自带的 FTDC 诊断数据。

# FTDC 数据位置
ls -la /data/db/diagnostic.data/
# metrics.2026-03-21T08-00-00Z-00000 (二进制格式)

# 用 mongod 的 --diagnosticMetrics 提取 FTDC 数据为 JSON
# ngod --diagnosticMetrics diagnostic.data/metrics.xxx > ftdc.json

# 关键 FTDC 指标:
# - serverStatus.connections.current
# - serverStatus.opcounters
# - serverStatus.wiredTiger.cache
# - replSetGetStatus.members[*].optimeDate
# 这些指标每次采样都包含,可以回溯任意时间点的 MongoDB 状态

3.3 完整代码清单

文件 用途
mongodb-lab/observability/docker-compose-obs.yml Prometheus + Grafana + exporter
mongodb-lab/observability/prometheus.yml Prometheus 配置
mongodb-lab/observability/alerts.yml 6 条核心告警规则
mongodb-lab/observability/health-metrics.js 6 类黄金指标采集
mongodb-lab/observability/repl-lag.js 复制延迟监控脚本
mongodb-lab/observability/slow-query-audit.js 慢查询自动审计

3.4 测试验证

// 1. 验证 Prometheus exporter 可用
// curl localhost:9216/metrics | grep mongodb_connections_current

// 2. 在 mongosh 中验证指标采集
print("=== 自检 ===")
const s = db.serverStatus()
print("连接: " + (s.connections ? "✓" : "✗"))
print("操作计数器: " + (s.opcounters ? "✓" : "✗"))
print("WiredTiger: " + (s.wiredTiger ? "✓" : "✗"))
print("锁: " + (s.locks ? "✓" : "✗"))
print("网络: " + (s.network ? "✓" : "✗"))

// 3. 验证慢查询审计
const recentSlow = db.system.profile.countDocuments({
  ts: { $gte: new Date(Date.now() - 3600 * 1000) }
})
print("近 1 小时慢查询:", recentSlow, recentSlow >= 0 ? "PASS" : "profiler未开启")

// 4. 验证告警规则语法(promtool)
// promtool check rules alerts.yml

print("\n=== 可观测性体系验证完成 ===")

4. 项目总结

4.1 监控工具对比

工具 数据源 用途 周期 开销
mongodb_exporter serverStatus 实时 API 指标采集 → Prometheus 15s
Profiler 每次慢操作写 system.profile 慢查询追溯 实时 中(仅记录慢操作)
FTDC 后台自动采样 历史诊断回溯 1s 极低
db.currentOp() 实时进程快照 排障时用 按需
mongostat / mongotop serverStatus + 系统指标 终端实时监控 1s

4.2 适用场景

可观测性体系适用

  1. 生产环境 7×24 运行——Prometheus + Grafana 持续采集。
  2. 故障后复盘——FTDC 回溯任意时间点的 MongoDB 指标。
  3. 慢查询治理——Profiler 每周自动化审计。
  4. 容量规划——Prometheus 时序数据库的长期趋势视图。

不适用场景

  1. 单机开发环境——全套监控栈的资源占用可能比 MongoDB 本身还大。
  2. 纯批处理任务——非 7×24 运行的任务不需要实时告警。

4.3 注意事项

注意事项 说明
FTDC 不能替代 Prometheus FTDC 是事后诊断工具,没有实时告警能力
Profiler 级别 2 不要长期开 全量操作记录会产生大量磁盘写入和性能损耗
mongodb_exporter 不要直连分片 通过 mongos 采集分片集群指标——同 mongod 直连会丢失分片级别的聚合视图
Grafana 面板不要套用通用模板 每个项目的 Query Pattern 不同,需要自定义适合自己业务的面板
告警阈值需要随业务变化调整 大促期间 QPS 峰值是平日的 10 倍,固定的 QPS 阈值会误报

4.4 常见踩坑经验

故障案例一:mongodb_exporter 拉取全部 serverStatus 导致 CPU 飙升

某生产环境 mongodb_exporter 配置了 --collect-all,每 15 秒拉取一次 serverStatusdbStatscollStatstop。数据库有 3000 个集合,collStats 对所有集合遍历统计——一次拉取耗时 8 秒,几乎占满一个 CPU 核。解决:关闭 collStats 收集(--no-collector.collstats),保留 serverStatusreplSetGetStatus;将 dbStats 的收集间隔调整为 5 分钟而非 15 秒。

故障案例二:Prometheus 在 MongoDB 高负载时拉不到指标导致误报

MongoDB CPU 100% 时,serverStatus 命令内部也需要排队——Prometheus 的 15 秒的 scrape 超时,连续 3 次失败触发"Node Down"告警。实际上 MongoDB 还在运行(只是很慢)。解决:设置 scrape_timeout: 30s 并加 scrape_interval: 30s 以容忍高负载期间的慢响应;"Node Down"告警改为连续 10 次失败才触发。

故障案例三:FTDC 数据不完整导致无法定位故障根因

某故障后团队想通过 FTDC 回溯问题时间点的指标,发现 FTDC 文件在那个时间点缺失。根因是运维在上次磁盘空间不足时手动删掉了 diagnostic.data 目录——FTDC 日志轮转逻辑依赖完整的文件序列。解决:FTDC 目录不应被手动清理;配置 MongoDB 日志轮转(logRotate)仅对 mongod.log,而非 FTDC 文件。

4.5 思考题

  1. 为什么 mongodb_exporter 推荐通过 mongos 采集分片集群指标,而不直接连每个分片的 mongod?有哪些指标在 mongos 层面是聚合的、在 mongod 层面是分开的?
  2. 如果一个慢查询的 explain 显示 totalDocsExamined 很小但执行时间仍然很长,除了索引问题,还有什么可观测性指标能帮助定位根因?

(答案将在第 29 章末尾揭晓)

4.6 推广计划提示

部门 阅读重点 协作事项
运维/DBA Prometheus + Grafana 搭建、6 条核心告警 将本章的 dashboard 导入 Grafana 并调整阈值为业务基线
开发 慢查询审计脚本、explain 接入 CI 管道 新增查询在 Code Review 时提供 explain 输出并检查索引命中
测试 性能基线回归——变更前后监控对比 每次大版本变更后导出 Prometheus 数据做性能基线的差异分析

上一章思考题答案

  1. 分片集群中 _id 的查询能否路由到单个分片——取决于分片键是什么。如果分片键是 userId 而非 _idupdateOne({_id: productId,...}) 无法精确路由——mongos 不知道 productId 属于哪个分片,只能广播到所有分片逐个查找,性能急剧下降。解决:① 分片键设为 _id(如果查询主要是按 _id);② 或者添加一个 shardKeyId 冗余字段辅助路由。

  2. maxWaitTime=0(永远等待)在高并发下的后果——连接池满了之后,新请求永远不超时,无限排队。线程池/协程耗尽,请求堆积越来越多,内存耗尽——最终整个服务因 OOM 被 kill,远比快速失败+重试的后果严重。这就是为什么 maxWaitTime 必须设定一个合理的短超时值(如 2 秒)实现 Fail Fast。

延伸阅读与资源

Redis 8 实战精讲:从 CRUD 到源码,构建高可用缓存系统
Redis 实战修炼与原理进阶
Python 3实战精进:从脚本到高并发订单引擎
MongoDB 实战进阶与内核修炼
python入门:Rquests从菜鸟脚本到企业级SDK的网络实战圣经
Milvus向量数据库实战修炼:从 0 到 1精通向量检索与生产落地
后端工程师的 AI 转型第一课:Ollama 与私有化大模型实战
10倍开发者的 Dify 魔法书:从零构建全栈 AI 应用
后端工程师转型AI第一课-Ollama 与私有化大模型实战
大型语言模型(LLM) vLLM 高性能推理落地实战
Agent开发之LlamaIndex 实战修炼与源码进阶
大语言模型Transformers 实战修炼与源码剖析

posted on 2026-08-12 14:35  一天不进步,就是退步  阅读(4)  评论(0)    收藏  举报