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 适用场景
可观测性体系适用:
- 生产环境 7×24 运行——Prometheus + Grafana 持续采集。
- 故障后复盘——FTDC 回溯任意时间点的 MongoDB 指标。
- 慢查询治理——Profiler 每周自动化审计。
- 容量规划——Prometheus 时序数据库的长期趋势视图。
不适用场景:
- 单机开发环境——全套监控栈的资源占用可能比 MongoDB 本身还大。
- 纯批处理任务——非 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 秒拉取一次 serverStatus、dbStats、collStats、top。数据库有 3000 个集合,collStats 对所有集合遍历统计——一次拉取耗时 8 秒,几乎占满一个 CPU 核。解决:关闭 collStats 收集(--no-collector.collstats),保留 serverStatus 和 replSetGetStatus;将 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 思考题
- 为什么
mongodb_exporter推荐通过 mongos 采集分片集群指标,而不直接连每个分片的 mongod?有哪些指标在 mongos 层面是聚合的、在 mongod 层面是分开的? - 如果一个慢查询的 explain 显示
totalDocsExamined很小但执行时间仍然很长,除了索引问题,还有什么可观测性指标能帮助定位根因?
(答案将在第 29 章末尾揭晓)
4.6 推广计划提示
| 部门 | 阅读重点 | 协作事项 |
|---|---|---|
| 运维/DBA | Prometheus + Grafana 搭建、6 条核心告警 | 将本章的 dashboard 导入 Grafana 并调整阈值为业务基线 |
| 开发 | 慢查询审计脚本、explain 接入 CI 管道 | 新增查询在 Code Review 时提供 explain 输出并检查索引命中 |
| 测试 | 性能基线回归——变更前后监控对比 | 每次大版本变更后导出 Prometheus 数据做性能基线的差异分析 |
上一章思考题答案:
分片集群中
_id的查询能否路由到单个分片——取决于分片键是什么。如果分片键是userId而非_id,updateOne({_id: productId,...})无法精确路由——mongos 不知道productId属于哪个分片,只能广播到所有分片逐个查找,性能急剧下降。解决:① 分片键设为_id(如果查询主要是按_id);② 或者添加一个shardKeyId冗余字段辅助路由。
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 实战修炼与源码剖析

微信公众号: 架构师日常笔记 欢迎关注!
浙公网安备 33010602011771号