1. 项目背景

业务场景:周五下午 5:55,本地生活电商的运维群里炸了锅。开发 A 的 Spring Boot 服务集体报错"Timed out after 30000 ms while waiting to connect",运营后台白屏。运维登录服务器一看——MongoDB 容器磁盘使用率 97%,慢查询日志里有一条全表扫描跑了 45 秒,db.currentOp() 里堆积了 300 多个活跃操作。整个团队手足无措——开发说"我不会看 MongoDB 日志",运维说"我不会查慢查询",最后靠着重启大法勉强恢复。事后复盘发现,如果有基本的排障能力,这个问题 5 分钟就能定位:磁盘满是因为日志没轮转,慢查询是因为一个正则搜索没索引,连接失败是因为连接池被那 300 多个堆积请求耗光了。

痛点:MongoDB 突然变慢或不可用,最常见的故障场景恰恰是日常操作中最容易忽略的。团队缺的不是排查工具,而是排查的思维框架和操作顺序。连接失败先看网络还是先看认证?慢查询是从慢查询日志入手还是 currentOp?磁盘爆满是先删数据还是先建索引?没有 SOP(标准操作流程),每次故障都是"凭感觉摸"。

2. 项目设计

小胖(急匆匆跑来,笔记本屏幕还在转圈):大师大师!线上 MongoDB 挂了!Spring Boot 报 Timeout,全站白屏,老板在群里催……

大师:别慌。先回答三个问题:报错信息具体是什么?你最近改了什么?MongoDB 还活着吗?

小胖:报"Timed out after 30000 ms while waiting to connect"。我最近就加了个商品搜索功能。MongoDB 容器是 Up 状态,CPU 100%。

大师:CPU 100% + 连接超时,大概率是慢查询把资源耗光了,导致连接池里的请求排队,新的连接建立不进来。排查三步走:

  1. 看看谁在忙db.currentOp() 找到正在执行的操作。
  2. 看看谁在慢:慢查询日志(profiler)找到罪魁祸首。
  3. 看看资源够不够db.serverStatus() 看看连接数、内存、锁状态。

小胖currentOp 是啥?是不是跟 Linux 的 top 命令一样——看哪个进程在吃 CPU?

大师:对,而且 db.currentOp() 能看到 MongoDB 内部正在执行的所有操作——每个操作的命令类型、耗时、所在的数据库和集合、锁等待状态。你可以加过滤条件,只看耗时超过 5 秒的慢操作:

db.currentOp({
  "active": true,
  "secs_running": { $gt: 5 }
})

技术映射currentOp 返回的是当前正在执行的操作的快照——类似 MySQL 的 SHOW PROCESSLISTsecs_running 表示该操作已经运行了多少秒。

小白:那和 profiler(慢查询日志)有什么区别?

大师:一个是实时的(currentOp),一个是事后的(profiler)。打个比方:

  • currentOp 是急诊室的心电监护——告诉你"现在"谁在出问题。
  • profiler 是病历——告诉你"过去一段时间"谁出过问题。

技术映射:profiler 默认关闭,需要手动开启。db.setProfilingLevel(1, { slowms: 100 }) 把所有超过 100ms 的操作记录到 system.profile 集合中。

小胖:那磁盘爆满是咋回事?我半夜被告警短信叫醒,上去一看 MongoDB 磁盘用了 97%。

大师:磁盘爆满常见的三个原因:

  1. Oplog 不轮转:复制集中 Oplog 是固定大小(capped collection),但如果主节点写入太快从节点跟不上,Oplog 不会膨胀——它是定长的。真正可能膨胀的是 Oplog 之外的集合。
  2. 日志不轮转:MongoDB 自己的日志文件(mongod.log)如果不加轮转,时间长了能到几十 GB。
  3. 索引膨胀 + 数据增长:业务数据自然增长,索引也跟着涨。有时一个索引比数据本身还大——比如多键索引(Multikey)在数组很大的情况下。

小白:怎么快速定位磁盘都是谁吃掉的?

大师:三步:

// 1. 看数据库级别的占用
db.stats()  // 返回 dataSize、indexSize、storageSize

// 2. 看集合级别
db.products.stats()  // 单个集合的详细大小

// 3. 看索引大小
db.products.stats().indexSizes  // 哪个索引最胖

然后用 db.runCommand({ compact: "products" }) 压缩碎片(但耗时且不推荐在业务高峰期执行)。

技术映射db.stats() = df -h(看磁盘),collection.stats() = du -sh(看具体目录)。wiredTiger.block-manager.file bytes available for reuse 指标说明有可回收的碎片空间。

大师(总结):今天讲的三种故障场景:连接失败 → 检查连接池是否满了;慢查询 → 从 currentOp + profiler 定位 SQL;磁盘爆满 → 从 stats() 自顶向下排查。要有自己的排障 SOP,凭感觉摸解决不了根本问题。

3. 项目实战

3.1 环境准备

docker compose -f mongodb-lab/docker-compose.yml ps

3.2 分步实现

步骤一:currentOp —— 实时查看正在执行的操作

目标:学会用 currentOp 快速定位活跃慢操作和阻塞操作。

use admin

// ---- 查看所有活跃操作 ----
const allOps = db.currentOp({ active: true, "$ownOps": false })
print("当前活跃操作数:", allOps.inprog.length)

// ---- 只看耗时超过 1 秒的 ----
const slowOps = db.currentOp({
  active: true,
  secs_running: { $gt: 1 }
})
print("慢操作 (>1s):", slowOps.inprog.length)
slowOps.inprog.forEach(op => {
  print(`  ${op.opid} | ${op.ns || '无'} | ${op.secs_running}s | ${JSON.stringify(op.command).slice(0, 80)}`)
})

// ---- Kill 一个卡死的操作 ----
// 如果发现某个操作已经跑了 300 秒,可以 kill 它
// db.killOp(opId)

// ---- 查看等待锁的操作 ----
const waitingOps = db.currentOp({
  waitingForLock: true
})
print("等待锁的操作:", waitingOps.inprog.length)
// 这些操作被其他操作持有的锁阻塞了——通常是长事务或大量写入

步骤二:System Profile —— 开启慢查询分析

目标:开启 profiler 并分析历史慢查询。

use local_life

// ---- 开启 Slow Query Profiler ----
// 级别 0:关闭
// 级别 1:只记录慢查询(超过 slowms 阈值的)
// 级别 2:记录所有操作(调试用,生产慎用!)
db.setProfilingLevel(1, { slowms: 100 })
// 记录所有超过 100ms 的操作

// 查看当前配置
const profileStatus = db.getProfilingStatus()
print("Profiler 状态:")
print("  级别:", profileStatus.was, "(0=关 1=慢查询 2=全部)")
print("  慢查询阈值:", profileStatus.slowms, "ms")
print("  采样率:", profileStatus.sampleRate || "不适用")

// ---- 生成一些慢查询用于演示 ----
// 先建一个无索引的大集合
db.profile_test.drop()
for (let i = 0; i < 50000; i++) {
  db.profile_test.insertOne({
    name: "慢查询测试_" + i,
    value: Math.random(),
    category: "CAT_" + (i % 10),
    createdAt: new Date()
  })
}

// 执行一条慢查询(全表扫描,无索引)
db.profile_test.find({
  $or: [
    { name: /测试_49999/ },
    { value: { $gt: 0.99 } }
  ]
}).explain("executionStats")
// 上面会用 explain 而非实际查询,
// explain 的操作也会被记录在 profiler 中!

// ---- 查看慢查询记录 ----
// profiler 数据存在 system.profile 集合中
const recentSlow = db.system.profile.find({
  millis: { $gt: 100 }
}).sort({ ts: -1 }).limit(5).toArray()

print("\n=== 最近 5 条慢查询 ===")
recentSlow.forEach(q => {
  print(`  时间: ${q.ts}`)
  print(`  耗时: ${q.millis}ms`)
  print(`  命令: ${q.op} | ${q.ns}`)
  print(`  查询条件: ${JSON.stringify(q.command?.filter || q.command?.pipeline || {}).slice(0, 100)}`)
  print(`  扫描文档: ${q.docsExamined || 'N/A'}`)
  print(`  ---`)
})

// ---- 分析慢查询模式 ----
// 按集合分组统计慢查询
const slowStats = db.system.profile.aggregate([
  { $match: { millis: { $gt: 100 } } },
  { $group: {
      _id: "$ns",
      count: { $sum: 1 },
      avgMillis: { $avg: "$millis" },
      maxMillis: { $max: "$millis" }
  }},
  { $sort: { avgMillis: -1 } }
]).toArray()
print("\n=== 慢查询按集合分布 ===")
slowStats.forEach(s => {
  print(`  ${s._id}: ${s.count}次, 平均${s.avgMillis.toFixed(0)}ms, 最大${s.maxMillis}ms`)
})

// ---- 关闭 Profiler(生产环境建议定期开关,避免长期采样的性能开销) ----
// db.setProfilingLevel(0)

步骤三:serverStatus —— 核心监控指标解读

目标:学会读取 db.serverStatus() 中的关键指标。

use admin

const status = db.serverStatus()

// 1. 连接数
const connections = status.connections
print("=== 连接 ===")
print(`  当前连接: ${connections.current} / 可用: ${connections.available}`)
print(`  活跃连接: ${connections.active}`)
print(`  总创建数: ${connections.totalCreated}`)
// 如果 current 接近 available,连接池即将耗尽

// 2. Oplog(复制集才有)
if (status.oplog) {
  print("\n=== Oplog ===")
  print(`  窗口大小(秒): ${status.oplog?.timeDiff || 'N/A'}`)
}
// Oplog 窗口太小会导致从库追不上

// 3. 锁统计
const locks = status.locks
print("\n=== 锁统计 ===")
if (locks?.Global) {
  print(`  全局锁获取: ${locks.Global.acquireCount?.r || 0}R / ${locks.Global.acquireCount?.w || 0}W`)
  print(`  全局锁等待: ${locks.Global.acquireWaitCount?.r || 0}R / ${locks.Global.acquireWaitCount?.w || 0}W`)
}

// 4. WiredTiger 缓存
const wt = status.wiredTiger
if (wt) {
  print("\n=== WiredTiger 缓存 ===")
  print(`  缓存最大: ${(wt.cache["maximum bytes configured"] / 1024 / 1024 / 1024).toFixed(2)} GB`)
  print(`  当前使用: ${(wt.cache["bytes currently in the cache"] / 1024 / 1024 / 1024).toFixed(2)} GB`)
  print(`  脏页: ${(wt.cache["tracked dirty bytes in the cache"] / 1024 / 1024).toFixed(2)} MB`)
  print(`  淘汰页(eviction): ${wt.cache["pages evicted by application threads"] || 0}`)
  // 如果 eviction > 0 且频繁增长,说明缓存不够用
}

// 5. 操作计数器
const ops = status.opcounters
print("\n=== 操作计数(自启动以来) ===")
print(`  insert: ${ops.insert}, query: ${ops.query}, update: ${ops.update}`)
print(`  delete: ${ops.delete}, getmore: ${ops.getmore}, command: ${ops.command}`)

步骤四:磁盘空间诊断

目标:从数据库 → 集合 → 索引逐级排查磁盘占用。

use local_life

// ---- 1. 数据库级别 ----
const dbStats = db.stats()
print("=== 数据库: local_life ===")
print(`  数据大小: ${(dbStats.dataSize / 1024 / 1024).toFixed(2)} MB`)
print(`  索引大小: ${(dbStats.indexSize / 1024 / 1024).toFixed(2)} MB`)
print(`  存储大小: ${(dbStats.storageSize / 1024 / 1024).toFixed(2)} MB`)
print(`  集合数: ${dbStats.collections}`)
print(`  碎片可复用: ${(dbStats.fsUsedSize || 0 / 1024 / 1024).toFixed(2)} MB`)

// ---- 2. 集合级别——找出占用最大的集合 ----
const collections = db.getCollectionNames()
  .filter(name => !name.startsWith("system."))
  .map(name => {
    const stats = db.getCollection(name).stats()
    return {
      name: name,
      dataSize: stats.size || 0,
      indexSize: stats.totalIndexSize || 0,
      count: stats.count || 0,
      avgObjSize: stats.avgObjSize || 0
    }
  })
  .sort((a, b) => (b.dataSize + b.indexSize) - (a.dataSize + a.indexSize))
  .slice(0, 10)

print("\n=== 磁盘占用 Top 10 集合 ===")
collections.forEach(c => {
  const totalMB = ((c.dataSize + c.indexSize) / 1024 / 1024).toFixed(2)
  print(`  ${c.name}: ${totalMB} MB (数据${(c.dataSize/1024/1024).toFixed(1)} + 索引${(c.indexSize/1024/1024).toFixed(1)}) | ${c.count}文档`)
})

// ---- 3. 索引级别——找出最占磁盘的索引 ----
if (collections.length > 0) {
  const topColl = collections[0].name
  const indexStats = db.getCollection(topColl).stats().indexSizes
  print(`\n=== ${topColl} 的索引大小 ===`)
  Object.entries(indexStats)
    .sort((a, b) => b[1] - a[1])
    .slice(0, 5)
    .forEach(([name, size]) => {
      print(`  ${name}: ${(size / 1024 / 1024).toFixed(2)} MB`)
    })
}

步骤五:连接错误排查

目标:模拟常见的连接错误场景并给出排查流程。

// 错误场景速查表

// 场景 1:"Authentication failed"
// 检查项:
//   - 用户名密码是否正确
//   - authSource 是否指向正确的库(通常是 admin)
//   - 账号被锁定或已过期

// 场景 2:"Timed out while waiting to connect"
// 检查项:
//   - MongoDB 进程是否存活(docker ps)
//   - 端口是否可通(telnet host 27017)
//   - 连接池是否已满(db.serverStatus().connections)
//   - 是否有慢查询占用了大量连接资源
//   - 应用方是否配置了合适的 connectTimeoutMS

// 场景 3:"not authorized on xxx to execute command"
// 检查项:
//   - 账号的角色是否覆盖当前数据库
//   - 是否在正确的数据库上操作
//   - 查看用户角色:db.getUser("username")

步骤六:排障 SOP 脚本

目标:输出一份标准化排障脚本,日常巡检和故障修复均可使用。

// mongodb-troubleshoot.js —— MongoDB 基础排障 SOP
// 用法:mongosh "mongodb://admin:password@host:27017/admin" --file mongodb-troubleshoot.js

print("╔════════════════ MongoDB 排障诊断 ════════════════╗")

// 1. 连接状况
const conn = db.serverStatus().connections
print("║ 1. 连接: 活跃", conn.active, "/ 总", conn.current, "/ 限制", conn.available)
if (conn.current >= conn.available * 0.8) {
  print("║    ⚠ 连接数超过 80%,请检查连接池配置")
}

// 2. 慢查询
const slow = db.currentOp({ active: true, secs_running: { $gt: 5 } })
if (slow.inprog.length > 0) {
  print("║ 2. 活跃慢操作 (>5s):", slow.inprog.length, "个")
  slow.inprog.forEach(op => {
    print(`║      [${op.secs_running}s] ${op.op} on ${op.ns || 'unknown'}`)
  })
} else {
  print("║ 2. 无活跃慢操作")
}

// 3. 磁盘
const dbS = db.stats()
const totalMB = (dbS.dataSize + dbS.indexSize) / 1024 / 1024
print(`║ 3. 磁盘: 数据+索引 ${totalMB.toFixed(0)} MB`)

// 4. 内存
const wtCache = db.serverStatus().wiredTiger?.cache
if (wtCache) {
  const usedGB = wtCache["bytes currently in the cache"] / 1024 / 1024 / 1024
  const maxGB = wtCache["maximum bytes configured"] / 1024 / 1024 / 1024
  print(`║ 4. 缓存: ${usedGB.toFixed(1)} GB / ${maxGB.toFixed(1)} GB (${(usedGB/maxGB*100).toFixed(0)}%)`)
}

// 5. 复制延迟(仅复制集有效)
try {
  const replStatus = db.adminCommand({ replSetGetStatus: 1 })
  replStatus.members?.forEach(m => {
    if (m.stateStr !== "PRIMARY") {
      const lag = m.optimeDate ? (Date.now() - m.optimeDate.getTime()) / 1000 : "N/A"
      print(`║ 5. 复制: ${m.name} (${m.stateStr}) 延迟 ${lag}s`)
    }
  })
} catch (e) {
  print("║ 5. 复制: 非复制集环境")
}

print("╚══════════════════════════════════════════════════╝")

3.3 完整代码清单

文件 用途
mongodb-lab/scripts/ch15-current-op-check.js currentOp 排查脚本
mongodb-lab/scripts/ch15-profiler-setup.js profiler 开启与分析
mongodb-lab/scripts/ch15-server-status.js serverStatus 核心指标
mongodb-lab/scripts/ch15-disk-analysis.js 磁盘空间诊断脚本
mongodb-lab/scripts/ch15-troubleshoot-sop.js 排障 SOP 脚本

3.4 测试验证

use admin

// 1. 验证 currentOp 返回结果
const ops = db.currentOp({ active: true })
print("currentOp 可用:", Array.isArray(ops.inprog) ? "PASS" : "FAIL")

// 2. 验证 profiler 是否正确配置
const profStatus = db.getProfilingStatus()
print("Profiler 级别:", profStatus.was >= 1 ? "PASS (已开启)" : "FAIL (未开启)")

// 3. 验证慢查询被记录
const profileCount = db.system.profile.countDocuments()
print("慢查询日志记录数:", profileCount, profileCount > 0 ? "PASS" : "需手动执行慢查询后查看")

// 4. 验证 serverStatus 核心指标不为空
const ss = db.serverStatus()
print("连接数:", ss.connections?.current ?? "无")
print("WiredTiger 缓存:", ss.wiredTiger ? "PASS" : "N/A")

// 5. 验证数据库统计
const dbs = db.stats()
print("数据库大小:", dbs.dataSize + dbs.indexSize, "bytes",
      dbs.dataSize > 0 ? "PASS" : "FAIL")

// 6. 清理 profiler(可选)
// db.setProfilingLevel(0)
// db.system.profile.drop()

print("\n=== 排查工具验证完成 ===")

4. 项目总结

4.1 排障工具速查表

故障现象 首选工具 关键命令/指标
接口突然变慢 currentOp secs_running > 1 查看活跃慢操作
间歇性慢查询 system.profile millis > 500 过滤历史慢查询
连接超时 serverStatus connections.current vs available
磁盘爆满 db.stats() / collStats dataSize + indexSize 自顶向下排查
内存不足 serverStatus WiredTiger cache eviction 频繁 → 不够用
锁等待 currentOp waitingForLock: true
复制延迟大 replSetGetStatus Secondary optime 与 Primary 的时间差
CPU 100% /currentOp 再看慢查询 通常是慢查询 + 无索引导致

4.2 适用场景

排障工具使用场景

  1. 日常巡检——serverStatus 核心指标,发现资源趋势异常。
  2. 线上告警响应——currentOp 快照定位当前问题操作。
  3. 性能优化回溯——profiler 分析一周的慢查询模式,批量优化索引。
  4. 容量规划——stats() 趋势分析,预测磁盘和内存增长。
  5. 故障复盘——profiler 历史日志还原故障时间点的操作序列。

4.3 注意事项

注意事项 说明
Profiler 级别 2 慎用 记录所有操作会严重影响性能,生产环境仅用级别 1
db.currentOp() 本身也有开销 不要在循环中频繁调用,采样间隔 ≥ 5 秒
killOp 能救命但不能乱用 kill 一个正在建索引的操作会导致索引处于不一致状态
serverStatus 每次调用都遍历所有指标 如需高频采集,用 FTDC(MongoDB 默认每秒采样的诊断数据)
日志级别 severity 生产环境设为 0(INFO),排查时可临时调高但记得调回

4.4 常见踩坑经验

故障案例一:连接池满但实际没那么多业务请求

某服务 health check 接口每分钟调用一次 MongoDB,每次只查一条数据,release 后连接放回池中。但监控显示连接数 5 分钟内从 0 涨到 98(maxPool=100)。根因:连接建立时 Driver 默认也不设置 maxIdleTimeMS,连接永不过期——服务启动后连接池里的连接只增不减。解决:设置 maxConnectionIdleTimeMS=600000(10 分钟),空闲连接复用而非永远不释放。

故障案例二:profiler 日志写满磁盘

某团队开启了 setProfilingLevel(2) 用于调试,调试完成后忘了关。一周后 MongoDB 磁盘爆满——system.profile 集合膨胀到 200GB(记录了几亿条操作日志)。根因:级别 2 记录所有操作(含每秒数十万次的心跳和系统命令)。解决:立即 db.setProfilingLevel(0),删除 system.profile 集合,生产环境永远只用级别 1。

故障案例三:慢查询阈值设太高漏掉了问题

某项目 slowms 设为 5000(5 秒),认为低于 5 秒的查询都够快。结果大量 2-3 秒的查询在高峰期堆积,QPS 不高但每个请求都慢,用户体验极差但 profiler 没有记录。根因:慢查询阈值与 QPS 相关——低 QPS 时 3 秒不算慢,但高 QPS 时 200ms 就可能导致资源堆积。解决:根据 P95 延迟设定 slowms(通常设为 P95 的 3-5 倍),而非拍一个固定值。

4.5 思考题

  1. 如果 db.currentOp() 显示一个 find 操作已经运行了 600 秒还是 "active" 状态,可能的原因有哪些?为什么不直接 killOp?
  2. MongoDB 的 FTDC(Full-Time Diagnostic Data Capture)和 serverStatus 有什么区别?为什么 MongoDB 默认启用 FTDC?

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


上一章思考题答案

  1. 每天 1TB 增量的备份策略:① 增量备份——使用 Oplog 做连续增量备份,全量备份只在周末低峰期执行一次。② 文件系统快照 + 块级增量——利用 LVM/ZFS 的块级别增量快照,只备份变化的块,速度远快于 mongodump 全量扫描。③ 分片并行备份——在分片集群中对每个分片并行执行 mongodump,总吞吐量接近分片数 × 单分片吞吐。④ 关闭 Journal 压缩(临时)提升备份读取速度,但风险高不推荐。

  2. 分片集群备份的关键区别:mongodump 在分片集群中必须连接到 mongos(而非单个 shard),否则拿到的数据不完整。但通过 mongos 做全量备份时,各分片的数据在 mongos 层面合并——跨分片一致性仍需依赖 Oplog。最佳实践:对每个 Config Server 和每个 Shard 的 Primary 分别做文件系统快照,保证所有分片在同一物理时刻的快照一致。


下一章预告:第 16 章是基础篇的综合实战——我们将把前 15 章的知识融会贯通,为"本地生活电商"搭建一个完整的 MongoDB 数据层,从 Docker Compose 到 Spring Boot API 到 Testcontainers 测试,交付一个可运行的生产级项目模板。

延伸阅读与资源

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

posted on 2026-07-27 19:00  一天不进步,就是退步  阅读(9)  评论(0)    收藏  举报