☰
MongoDB监控实战:Prometheus+Grafana+exporter搭建可视化性能监控体系
2026/10/13 4:22:36 网站建设 项目流程

半夜被一条“数据库响应变慢”的短信叫醒,打开服务器看到 Mongo shell 里mongostat的结果一条一条刷,却说不清楚到底哪里先出问题——这是很多后端和运维都经历过的场景。自那之后我给自己定了个规矩:MongoDB 一定要有可视化仪表板,而且要能保存历史曲线。最终落地的方案就是标题里这套经典组合:用Prometheus周期抓取指标,用Grafana做性能监控仪表板,中间再用mongodb-exporter把mongod的状态翻译成标准指标格式。这套链路搭完,数据库的健康状态终于不再是“感觉”,而是清清楚楚的曲线和告警。这篇实战记录写给正在自己运维 MongoDB、想从零搭一套可复现监控体系的读者,里面的步骤我已经跑过很多遍。

1. 为什么非要在Grafana里单独看MongoDB——先想清楚要解决什么问题

1.1 只有一份mongostat的截图,等于没有监控

很多团队对 MongoDB 的日常巡检,停留在“出现问题后跑几条命令”的阶段。db.serverStatus()能看实时状态,mongostat能动态刷连接和操作数,mongotop能看每个集合的读写耗时,但它们都解决不了同一个问题:没有时间轴。

实时命令只告诉你“此刻”的数字,而你真正需要回答的是“半小时前是不是就开始异常了”。没有历史曲线,就没法做对比,也没法判断告警是偶发抖动还是持续恶化。更现实的一点是,这些命令的产出散落在多个终端里,谁也没法每天固定时间去执行,监控本质上等于依赖人的自觉。

Prometheus 被大量项目选作监控底座,核心原因就是它天然带时间序列存储:每个指标都带时间戳和标签,可以保留很长一段历史。Grafana 则负责把这堆点连成线,变成可交互的仪表板。这套组合好就好在它“通用”——今天监控 MongoDB,明天加一个 Redis 或者业务容器,只是加配置和面板的事,不需要再引一套新系统。

1.2 Grafana、Prometheus、exporter三个组件在链路里各站什么岗

先把角色分清楚,后面踩坑的时候才能快速定位。整个链路是这样流动的:

  • mongod 进程本身不会主动吐出 Prometheus 格式的指标,mongodb-exporter每隔一段时间去连接 MongoDB,获取serverStatus、replSetGetStatus、dbStats等数据,再暴露成一个 HTTP 端点。你可以把 exporter 理解成一个“翻译官”。
  • Prometheus 按照prometheus.yml里的抓取配置,定时去访问 exporter 的/metrics端点,把指标拉回来存进自己的时序库。Prometheus 只负责采集和存储,不负责画图。
  • Grafana 连到 Prometheus,通过 PromQL 查询里面存好的指标,画成仪表板。它还负责发告警,我们最终盯着的界面就是它。

需要注意的是,Prometheus 不是直接去连 MongoDB 的 27017 端口,这一点新手最容易绕弯。它只跟 exporter 打交道,exporter 再去连数据库。如果监控面板里没有数据,第一件事就是沿着这条链路逐段排查,而不是去改 MongoDB 的连接参数。

2. 监控链路选型与部署:从 mongod 到 Prometheus 的关键一步

2.1 exporter选型:社区版本池子有点深,先看这里

mongodb-exporter 的版本情况在中文社区里经常让人犯迷糊,主要是历史上有两条线:早期比较流行的prometheuscommunity/mongodb-exporter,以及 Percona 维护的mongodb_exporter。现在新版本基本已经收敛到同一个维护体系,可网上老教程里两种命令、两套指标名称还混着在传,直接照着抄很容易翻车。

我的建议很简单:用新版本,且以你实际部署版本的 README 为准。老版本往往需要显式指定一堆 collector 参数,新版本通常只需要给一个--mongodb.uri连接串,exporter 会自动完成大多数常规指标的采集。换个说法就是:先别管网上那堆繁琐的 flag,把基础跑通,再按需看文档加参数即可。

对比点老版本常见形态新版本常见形态
连接方式一个连接串配合多个收集开关单 URI,大部分指标默认采集
指标命名不同维护方各自一套逐渐统一,但仍可能有前缀差异
镜像名称mongodb-exporter / mongodb_exporter 混杂一般以mongodb-exporter命名
注意事项老教程中的 flag 在新版本可能已失效以仓库 README 为唯一标准

还有一个实际经验:指标名会随版本变化。我在升级 exporter 之后遇到过整块面板全部显示 “No data” 的情况,后来查出来是某几个指标名从mongodb_xxx改成了mongodb_yyy。因此在做仪表板面板时,我一定先打开 exporter 的/metrics端点,亲自确认环境里真实的指标名,再写 PromQL。

2.2 最小可落地的Docker Compose方案

如果只是验证和搭建最小环境,用 Docker Compose 是最省事的。下面这份配置我自己反复用过,结构上拆成了 MongoDB、exporter、Prometheus、Grafana 四个服务。注意这里面的服务名,后面配置里的地址都靠它们互相引用。

services: mongodb: image: mongo:6.0 container_name: mongodb restart: unless-stopped ports: - "27017:27017" volumes: - ./mongo-data:/data/db healthcheck: test: ["CMD", "mongosh", "--quiet", "--eval", "db.runCommand({ ping: 1 }).ok"] interval: 10s timeout: 5s retries: 5 mongodb-exporter: image: quay.io/prometheuscommunity/mongodb-exporter container_name: mongodb-exporter restart: unless-stopped ports: - "9216:9216" environment: MONGODB_URI: "mongodb://monitor:ExamplePass123@mongodb:27017/admin?authSource=admin" depends_on: mongodb: condition: service_healthy prometheus: image: prom/prometheus container_name: prometheus restart: unless-stopped ports: - "9090:9090" volumes: - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro - ./prometheus-data:/prometheus grafana: image: grafana/grafana container_name: grafana restart: unless-stopped ports: - "3000:3000" environment: - GF_SECURITY_ADMIN_PASSWORD=admin volumes: - ./grafana-data:/var/lib/grafana

这里最值得注意的细节是监控账号。MongoDB 默认的root账号能连,但生产环境我不会用它。专门建一个只读监控账号更稳妥,在 Mongo shell 里执行:

db.getSiblingDB("admin").createUser({ user: "monitor", pwd: "ExamplePass123", roles: [ { role: "clusterMonitor", db: "admin" }, { role: "readAnyDatabase", db: "admin" } ] });

clusterMonitor角色专门给监控工具用,能读serverStatus、复制集和 oplog 相关的信息,但不具备写数据能力。少了这个角色,后面很多指标会采集不上来,典型报错就是not authorized on admin to execute command,这个问题后面还会再见到。

depends_on加condition: service_healthy是为了避免 exporter 在 MongoDB 还没就绪时就去连接,反复报连接失败。这个配置在本地验证环境下已经足够稳。

2.3 单机、副本集和分片集群该把采集目标放在哪

不同部署形态,采集目标的设计很不一样。单机最简单,一个 exporter 指向本地 27017 就可以。副本集就要多留个心眼:副本集的每个节点都应该被采集,主库的写入指标、从库的复制延迟,都是独立存在的。你可以给每个 mongod 各起一个 exporter 进程,也可以在一个 exporter 上通过--mongodb.uri连接多个地址,但实际维护时我更倾向每个节点单独一个 target,这样 Grafana 面板上能直接用 instance 标签区分节点。

分片集群会更加复杂。除了每个分片副本集的 mongod 以外,mongos路由层和 config server 也承担着重要职责。mongos 上是客户端真实进入集群的入口,连接数、command ops 如果只在分片节点上看,会出现“分片负载不高但 mongos 快被打爆”的盲区。所以凡是参与请求链路的进程,都值得单独采集,再来谈仪表板。

3. Prometheus抓取配置与核心排查:别让你的数据管道断掉

3.1 prometheus.yml该写什么

Prometheus 的配置比很多人想象中简单,核心就是一个scrape_configs数组。下面是我常驻配置的骨架:

global: scrape_interval: 15s evaluation_interval: 15s scrape_configs: - job_name: "mongodb-node1" static_configs: - targets: ["10.0.0.11:9216"] labels: cluster: replset-orders node: "primary" - job_name: "mongodb-node2" static_configs: - targets: ["10.0.0.12:9216"] labels: cluster: replset-orders node: "secondary" - job_name: "mongodb-shard-mongos" static_configs: - targets: ["10.0.0.20:9216"] labels: cluster: shard1-mongos node: "mongos"

scrape_interval默认 15 秒,对大多数 MongoDB 监控够用。副本集里如果集合特别多、exporter 自动发现的采集对象很重,默认的 10 秒抓取超时有可能不够,我一般会在特定的 job 下显式调大scrape_timeout: 30s。这个值必须小于scrape_interval,否则 Prometheus 会直接报错。

labels里的cluster和node是自定义的,值可以随意定。它们的用途是在 Grafana 面板上做筛选:当你有两套集群时,用cluster=~"$cluster"就能在一个仪表板里切换,不需要复制一堆面板。

3.2 用labels区分环境

如果你手里只有一套测试 MongoDB,labels 可有可无。但只要有“正式环境”和“测试环境”两套实例,不加 labels 的后果很快就会显现:面板上所有曲线都混在一起,连告警都没法区分是谁家的数据库。

我的习惯是统一用两个标签:env标注prod或test,cluster标注集群名称。Prometheus 的标签(labels)在查询筛选上非常强大,比如告警规则里写env="prod"只对正式环境生效,测试环境的抖动不会半夜吵醒人。写面板变量时,用label_values(mongodb_up, cluster)自动列出当前所有集群名,下拉框选择后全盘面板跟着过滤,这一个操作就能让仪表板的复用价值翻倍。

3.3 验证链路与故障排查

配置完第一件事不是打开 Grafana,而是先验证 Prometheus 有没有成功抓取。我排查的顺序是:

  1. 直接访问 exporter:curl http://10.0.0.11:9216/metrics,能返回一堆mongodb_开头的指标,说明 exporter 已经连上 MongoDB。
  2. 进 Prometheus 的 Targets 页面,看目标是否up。UP 不等于数据都对,但至少链路通了。
  3. 打开 Prometheus 的 Graph 页,输入mongodb_up查一下,值为 1 代表 exporter 那边连接正常。

常见问题我在下面这个表里列一下,基本覆盖了九成新手状况:

现象可能原因排查思路
访问 exporter 端口被拒绝exporter 没启动或容器端口没映射检查容器状态和docker compose ps
/metrics能打开但没有mongodb_指标exporter 连 MongoDB 失败看 exporter 日志里的连接错误
指标里出现not authorized监控账号缺clusterMonitor角色用管理员给 monitor 账号补角色
Prometheus 的 Target 显示 DOWN网络不通或 job 配置地址错在 Prometheus 容器里 curl 目标地址
有少量指标但mongodb_up是 0exporter 内部认证或 URI 错误检查MONGODB_URI的密码和 authSource

我再补充一个容易被忽略的细节:exporter 在同一个容器里时,它连接 MongoDB 的地址要写 compose 里的服务名mongodb,而不是localhost。如果用localhost,exporter 会尝试连自己这个容器里的 27017 端口,结果自然是失败。

4. Grafana仪表板搭建:从导入到按业务语义调优面板

4.1 数据源配置与初始化面板

Grafana 安装完成后,浏览器打开 http://localhost:3000,默认账号密码我在 compose 里设成了admin/admin。第一步是添加 Prometheus 数据源,在左侧菜单里点击 Add data source,类型选 Prometheus,URL 一栏填http://prometheus:9090,这里必须用 compose 内部网络的服务名。保存之后立刻点一次 “Save & Test”,绿色提示就是通了。

面板的来源有两种:直接用别人做好的仪表板 JSON,或者自己从零开始建。我的建议是从现有模板起步,在 Grafana 的 Import 页面里输入 ID 或者上传 JSON,搜索 “MongoDB” 会看到不少方案。不过一定要选指标名匹配你当前 exporter 版本的模板,否则导入后一堆 "No data"。

自己造面板其实也不难,关键是想清楚要看什么。Grafana 左侧 Dashboards → New Dashboard → Add visualization,选 Prometheus 数据源后,用 PromQL 表达式查询,右侧设置面板的统计方式、单位、图例。下面我列出四个我保留至今的核心方向,用它们拼出的仪表板比很多花哨模板更实用。

4.2 我建议保留的四类核心面板

这四类面板几乎能覆盖 MongoDB 日常健康检查的大部分场景:

第一类:连接数。连接数太多不一定代表有问题,但它是应用压力的直接信号。常见指标名是mongodb_connections,用sum by (conn_type) (mongodb_connections)把活跃、可用分开看。当你看到连接数曲线陡增,而应用 QPS 没有同步上涨,多半是某次的慢查询把连接占住不放。

第二类:操作计数。QPS 拆开看类型非常有用。指标名通常是mongodb_op_counters_total,PromQL 写法:

sum by (type) (rate(mongodb_op_counters_total[$__rate_interval]))

type会区分为 insert、query、update、delete、command。如果把 command 单独拆出来,看到它突然猛涨,往往不是业务流量涨,而是后台任务、索引构建或者聚合管道在作祟。

第三类:内存与页错误。MongoDB 的 WiredTiger 引擎默认会尽量使用内存做缓存,单纯看内存占用率意义不大,更关键的是页错误数。页错误指标可能叫mongodb_metrics_extra_info_page_faults之类,名字因版本而异,用irate()计算单位时间内的增长速率。页错误持续走高,说明工作集已经超出缓存能力,磁盘读的压力开始上来。

第四类:复制集状态。这是数据库高可用维度的重中之重,看每个成员的当前状态(PRIMARY、SECONDARY、STARTUP、RECOVERING 等)。当某个节点状态从 SECONDARY 变成 RECOVERING,通常意味着它开始落后主库太多,或者磁盘出了问题。这类面板在官方模板里通常都有,直接复用即可。

4.3 用变量实现多套集群共用一个仪表板

一套仪表板如果只给一个 MongoDB 节点用,参数写死没毛病。但生产上我至少有两个环境,每套环境还有主从节点,写死指标固然能看,却要维护一堆重复面板。

Grafana 的 Dashboard Variables 功能就是为此设计的。在仪表板设置里的 Variables 中新增一个变量,比如叫cluster,查询类型选 Label values,Label 填cluster,Query 填label_values(mongodb_up, cluster)。这样下拉框会自动收集 Prometheus 中所有cluster标签的值。

接下来在每一个面板的 PromQL 表达式里,给涉及到集群的指标加上过滤条件,比如:

sum by (type) (rate(mongodb_op_counters_total{cluster=~"$cluster"}[$__rate_interval]))

切换下拉框,整套仪表板就跟着切换到对应的集群。这个操作看起来很简单,但能把维护成本从“每个环境一套面板”压缩成“一套面板走天下”。

4.4 少堆图表,多盯语义

仪表板很容易搭建,也特别容易变成“数据废物陈列馆”。我见过一些新同事把几十个指标全摆上去,一眼望去五颜六色,但真正出问题时还是不知道看哪块。我的原则是:一个面板画出来之前,先回答一个问题——如果有人被这个面板叫醒,他马上该做哪个动作?

按这个标准,那些纯装饰性的面板会被砍掉不少。比如磁盘 IO 在 MongoDB 层面很难直接解释,不如交给 node-exporter 的节点监控面板;比如缓存命中率,在 WiredTiger 引擎下不能简单解读为“缓存不够”。仪表板保持精简的意义在于:可观测性是要让我们更快做决策,而不是让我们看更多数字。

5. 关键指标解读:哪些数字出现异常时才真正值得你紧张

5.1 连接数:活跃和可用要分开看

MongoDB 的connections信息里有current、available和active几个维度。current是当前所有连接数,available是还有多少可用的空闲连接槽位,active则是在执行操作的连接数。如果available一直降到 0,意味着连接数满了,新的应用连接会被拒绝,这是很明确的告警信号。

但我要提醒一句:连接数的绝对值本身没有通用阈值。一个应用可能天生就是用大量连接来获取并发,另一个可能只是几十个连接就已经跑得很吃力。判断连接数是否异常,正确的姿势是看它的“突变率”和“与基线对比”——过去 14 天同时段的平均水平是多少,今天有没有显著偏离。监控面板上画出这些曲线,比设置一个固定数字告警要靠谱得多。

5.2 opcounters与扫描/返回比:读放大是慢查询的前奏

查出慢查询的原因时,我经常会去看metrics.queryExecutor.scanned和metrics.document.returned这两个计数器。它们表达的意思是:为了返回一批文档,MongoDB 到底扫描了多少条文档。扫描数和返回数的比值一旦很高,基本可以确认这条查询没有走到合适的索引,走了全表扫描。

比值就是一个“读放大”指标。比如返回 100 条文档却扫描了 10 万条,这个查询即使单次执行只有几百毫秒,在流量上来之后会把 CPU 和 IO 都拖垮。监控面板里我会放一个面板单独看这个比值,并且在它连续 15 分钟大于某个倍数时触发告警。排查这类问题,通常是到执行计划里看是否命中了错误索引,或者查询条件里用了$regex、$where这类无法高效走索引的操作符。

5.3 内存、页错误与工作集

WiredTiger 默认使用系统内存的一定比例作为缓存,exporter 采集到的resident内存代表 MongoDB 实际占用的物理内存。这里有一个常见误区:看到 MongoDB 内存占用很高,不代表内存泄漏。WiredTiger 就是尽量用内存换性能,缓存占不满才是白搭。

真正值得关注的是页错误。当客户端需要的数据不在缓存里,不得不去磁盘读取时,就会发生页错误。vmstat里的 page fault 和 MongoDB 采集到的页错误指标都能看。页错误率突然翻倍上涨,往往对应着查询的工作集超过缓存容量,或者出现了扫全表的大量请求,把脏页和热数据同时压在磁盘 IO 上。

处理方式分两步:第一步确认是否有慢查询在做大范围扫描,砍掉不合理的查询;第二步才考虑调大storage.wiredTiger.engineConfig.cacheSizeGB,但这是一个需要谨慎评估的参数,改小了会降低缓存命中,改大了可能挤压操作系统内存,甚至触发 OOM。

5.4 复制延迟与分片路由健康

副本集的复制延迟,是 MongoDB 集群里最容易被误判的指标之一。简单说,从库的 oplog 应用进度如果一直跟不上主库的写入进度,延迟会不断累积。监控面板上有延时段的时间序列后,先别急着下结论是网络慢。oplog 窗口太小,从库长时间停机后重新追,也可能会出现同样现象。用db.getReplicationInfo()能看到 oplog 的时间窗口,如果窗口只够撑几个小时,那就要考虑加大 oplogSizeMB。

分片集群里的 mongos 也一样值得盯。它是所有应用请求的第一站,连接数、操作量都会先打在它身上。如果 mongos 的连接数已经非常高,但各分片节点看起来还“很闲”,说明可能是路由层本身成了瓶颈。对这种进程,监控指标选择上不需要额外增加太多,把连接和 opcounters 两个维度建好就行。

5.5 慢查询落地:从profile到TopN分析

很多 MongoDB 实例默认是不开慢查询日志的,因为 profiling 本身也有一点性能开销。但当你要做性能优化时,没有慢查询依据基本等于盲人摸象。

我习惯在 mongod.conf 里加上这个配置:

operationProfiling: mode: slowOp slowOpThresholdMs: 100

也可以在运行时动态开,但重启 mongod 后配置会丢失。slowOp表示只记录超过阈值的操作,相比记录所有操作开销小很多。我把阈值设成 100ms,低于这个级别的操作对大多数业务无感,先不追。

开始采集之后,用下面的方式查看 Top 慢查询:

db.system.profile .find({ op: { $in: ["query", "update", "remove", "command"] } }) .sort({ ts: -1 }) .limit(20) .forEach(function (doc) { printjson({ ns: doc.ns, op: doc.op, millis: doc.millis, ts: doc.ts }); });

看结果的时候,我第一眼关注ns和millis,然后拿ns里的集合名和查询 shape 去对比索引。system.profile是一个有容量上限的 capped collection,数据滚动覆盖,所以发现慢查询后应该尽快把这些事保存下来,而不是等过几个小时再去查。

6. 告警规则设计与实战踩坑记录

6.1 从“深夜告警”到“可执行告警”的设计原则

告警最忌讳的是“整天在响”,因为过一段时间大家就会无条件忽略它。我的设计原则很简单:每条告警都必须能催生一个具体的决策,否则就别配。

下面这张表是我实际在用、且觉得有效的一组规则:

告警名触发条件示例级别备选处理动作
MongoDB 实例不可用mongodb_up == 0持续 2 分钟critical检查网络、进程、认证
复制集成员异常成员状态非 PRIMARY/SECONDARYcritical检查该节点日志
页错误速率激增irate(page_faults)高于基线两倍warning排查大扫描查询
连接数接近上限current连接数超过阈值 80%warning横向看应用连接池
慢查询持续出现system.profile中毫秒级操作持续存在warning分析执行计划、补索引

阈值这个坑特别需要说明:所有数字型阈值都应当基于你自己的历史数据来定。比如连接数,可以先用两周的数据做基线,把峰值乘 1.5 当作告警线。设置完成后跑几天,如果一周都不响,再把线收紧;如果每天半夜都响,就说明阈值太敏感,需要调整而不是硬扛。

Prometheus 的告警规则文件里一般长这样:

groups: - name: mongodb-alerts rules: - alert: MongoDBNotUp expr: mongodb_up == 0 for: 2m annotations: summary: "MongoDB 实例采集失败"

for: 2m很重要,它过滤掉瞬时抖动,避免 expporter 一重启就疯狂报警。

6.2 我踩过的四个坑:指标名、权限、超时、容器依赖

下面这些坑我几乎全部都亲自踩过,列出来排雷。

第一个是指标名漂移。某一次我从旧版 exporter 升级后,所有面板一夜之间全空。原因是新版把一批指标名改成了带_total后缀的新式命名。后来我养成了个习惯:升级 exporter 后,先curl /metrics和旧配置做一次 diff,再决定要不要开兼容模式。很多 exporter 都提供了兼容旧指标名的参数,宁可在这一步多花十分钟,也比半夜发现仪表板全空要好。

第二个是监控授权问题。只给了read角色,然后看到serverStatus里一堆指标是空值,日志里全是not authorized。MongoDB 监控不是简单给一个库里读数据就完事,必须加clusterMonitor角色。这个角色在 admin 库里授权,前面建账号那段我已经给过完整操作。以后如果发现某个mongodb_指标一直没值,先怀疑权限。

第三个是抓取超时。当 MongoDB 上数据库和集合数量很多时,exporter 默认自动发现所有集合并采集个级统计,一次抓取可能要远超 10 秒。Prometheus 默认scrape_timeout就是 10 秒,一旦超时,这个周期的数据就是空的。解决方法是调大对应 job 的scrape_timeout到 30 秒,或者在 exporter 层面关闭不必要的高成本采集项,不必所有指标都全量收。

第四个是容器启动顺序。没有健康检查的容器编排里,exporter 经常在 MongoDB 尚未就绪时就启动,然后一直重试失败。用 compose 时,把depends_on配成service_healthy,给 MongoDB 加 healthcheck,就能避免这一层混乱。稳妥的监控系统应当从启动顺序开始就是确定的,而不是碰运气。

6.3 监控也要持续维护

把监控真正用起来之后,我发现它不是一个一次性的“装修工程”,而是需要持续维护的基础设施。MongoDB 版本一升级、exporter 一升级、业务模式一变,指标的含义和告警阈值就得跟着复查一遍。我现在每隔几周就会专门翻一次慢查询 TopN,看一下面板基线有没有漂移,再决定是收紧还是放宽告警线。

这套系统给我带来的最大改变不是面板本身,而是把“数据库到底还行不行”这个问题,从靠猜变成了靠数据。如果你也只经历过对着 mongostat 不停刷屏幕的日子,按照这条链路从 exporter 到 Prometheus 再到 Grafana 走一遍,会发现凌晨被叫醒的次数会明显变少。最后分享一个小习惯:每当我改完 MongoDB 的某个参数,都会顺手截一张关键面板的曲线图存档,等下次变更后对比。这些历史截图往往比告警规则更能说明问题。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询