2. 诊断数据采集体系
MongoDB 性能故障诊断的第一道门槛不是“看懂某个指标”,而是“在正确的时间拿到正确的数据”。mongostat、mongotop、serverStatus、db.currentOp、日志与 profiler 这六类工具,分别覆盖了全局吞吐、集合级热点、内部计数器、实时操作、事件轨迹、慢操作明细六个不同粒度。把它们当成六个孤立命令去背,往往在故障现场手忙脚乱;把它们理解成一条从“宏观异常”到“微观根因”的采集链,才能形成稳定的诊断节奏。
六类工具的分工与粒度
mongostat 是秒级快照,回答“现在整体忙不忙、有没有排队”。它输出 insert/query/update/delete/getmore/command 的每秒次数、dirty used 百分比、used 内存、vsize、res、qr|qw(读写队列)、ar|aw(活跃读写客户端数)、netIn/netOut、conn、time。其中 qr|qw 持续非零说明请求在排队,dirty 接近 20%说明 WiredTiger 缓存脏页比例偏高,conn 突增常与连接泄漏或客户端重试风暴相关。
mongotop 是集合级读写耗时排行,回答“时间花在哪些集合上”。它按 ns 维度输出 total/read/write 的毫秒数,默认每秒刷新。当 mongostat 显示整体繁忙但看不出热点时,mongotop 能快速把范围缩小到具体命名空间。
serverStatus 是内部计数器的全量快照,回答“系统内部发生了什么”。关键字段包括:opcounters(各类操作累计数)、opcountersRepl、connections(current/available/totalCreated)、wiredTiger.cache(bytes currently in the cache、tracked dirty bytes、pages read into cache、pages written from cache)、wiredTiger.concurrentTransactions(read/write 的 out 与 available)、globalLock(activeClients、currentQueue)、metrics.cursor(open、timedOut)、metrics.document(deleted、inserted、returned、updated)、asserts、extra_info.page_faults。它适合做前后差值对比,而不是单点读数。
db.currentOp() 是实时操作视图,回答“此刻谁在跑、跑了多久、卡在哪”。重点字段:opid、secs_running、microsecs_running、op、ns、command、planSummary、numYields、waitingForLock、locks、client、desc。配合 db.killOp(opid) 可以终止失控操作,但必须先确认它不是关键写入。
日志是事件时间线,回答“什么时候发生了什么”。关注 slow query 行(含 planSummary、docsExamined、keysExamined、nreturned、execStats)、连接建立/断开、选举与复制状态变化、WiredTiger 的 eviction 与 checkpoint 记录。
profiler 是慢操作明细库,回答“慢操作长什么样”。通过 db.setProfilingLevel(1, {slowms: 100}) 开启,数据落在 system.profile 集合,可按 ts、millis、ns、op、planSummary、execStats 聚合分析。
协同采集的实操节奏
假设某业务反馈“晚高峰查询变慢”。第一步,在故障节点执行 mongostat --host rs0/127.0.0.1:27017 --rowcount 30 1,观察 30 秒。若看到 qr 从 0 升到 30 以上、dirty 从 5% 升到 18%、conn 从 200 升到 900,可以判断存在排队与缓存压力,但还不知道热点在哪。
第二步,执行 mongotop 5,发现 app.orders 的 read 时间稳定在 800ms/s 以上,而其他集合都在 50ms/s 以下,热点锁定。
第三步,取两次 serverStatus 做差值。示例(教学假设数据):间隔 10 秒,opcounters.query 从 120000 增至 138000,即 1800 次/秒;wiredTiger.cache 的 “tracked dirty bytes” 从 1.2GB 增至 2.1GB,“pages read into cache” 增加 45000;metrics.cursor.open 从 300 增至 1200。这说明查询量本身不算极端,但缓存读放大严重、游标打开数激增,怀疑存在未关闭游标或全表扫描。
第四步,执行 db.currentOp({ "secs_running": { $gt: 3 }, "ns": "app.orders" }),看到若干 op 的 planSummary 为 COLLSCAN,numYields 高达数千,waitingForLock 为 false,说明不是锁等待,而是扫描量过大导致 CPU 与缓存被拖垮。
第五步,开启 profiler 采集窗口:db.setProfilingLevel(1, { slowms: 200 }),运行 2 分钟后关闭。查询 db.system.profile.find({ ns: "app.orders", millis: { $gt: 200 } }).sort({ ts: -1 }).limit(10),典型结果如下(示例):
{
op: "query",
ns: "app.orders",
millis: 1842,
planSummary: "COLLSCAN",
docsExamined: 1284000,
keysExamined: 0,
nreturned: 12,
execStats: { executionTimeMillis: 1830, totalDocsExamined: 1284000 }
}
docsExamined 与 nreturned 的比值超过 10 万倍,是典型的索引缺失信号。此时再回到日志,搜索同一时间窗口的 slow query 行,确认 planSummary 与 profiler 一致,证据链闭合。
数据解读中的常见误判
第一,把 serverStatus 的累计值当瞬时值。opcounters 是自启动以来的累计计数,必须做差值或除以时间间隔,否则会得出“查询量巨大”的错误结论。
第二,把 dirty 高直接等同于故障。dirty 反映的是脏页占缓存比例,短时接近 20% 属于正常波动;只有当它持续高位且伴随 eviction 频繁、qr 排队时,才构成缓存压力。
第三,忽略 numYields 的含义。numYields 高说明操作多次让出锁,通常与扫描量大或并发高相关,但它本身不是错误,需要结合 docsExamined 与 secs_running 一起判断。
第四,profiler 长期开启。profiler 会写入 system.profile,带来额外写放大与存储占用,只应在排查窗口内开启,并设置合理的 slowms 阈值。
一份可直接使用的采集清单
| 顺序 | 工具 | 关键参数/字段 | 判断依据 |
|---|---|---|---|
| 1 | mongostat | qr|qw、dirty、conn | qr>0 持续、dirty≥20%、conn 突增 |
| 2 | mongotop | ns、read、write | 某 ns 耗时显著高于其他 |
| 3 | serverStatus | opcounters、WT cache、cursor | 两次差值判断增长速率 |
| 4 | db.currentOp() | secs_running、planSummary、numYields | COLLSCAN、长耗时、高 yields |
| 5 | 日志 | slow query、planSummary | 与 profiler 时间戳对齐 |
| 6 | profiler | millis、docsExamined、nreturned | 扫描/返回比异常 |
这套体系的价值不在于工具本身,而在于采集顺序与证据对齐。mongostat 负责发现异常,mongotop 负责缩小范围,serverStatus 负责量化内部状态,currentOp 负责锁定实时操作,日志与 profiler 负责提供可复现的明细证据。六者按“宏观→集合→内部→实时→明细”的顺序协同,才能把一次性能故障从现象还原到根因。