一、性能问题场景
在 MongoDB 的生产实践中,性能优化是保障系统稳定性的核心环节。随着业务体量增长,开发者必须具备快速定位与排查性能瓶颈的能力。常见的性能异常指标包括:
·查询延迟激增:慢查询高发,响应时间从毫秒级恶化至秒级甚至分钟级。
·CPU 持续高负载:数据库实例 CPU 占用率高居不下,引发系统响应断崖式下跌。
·内存承压:WiredTiger 引擎缓存命中率下降,淘汰(Eviction)机制频繁触发,导致查询抖动。
·磁盘 I/O 遭遇瓶颈:读写吞吐达到物理上限,操作在队列中严重积压。
·连接数过载/耗尽:连接池满载,导致新请求无法建立连接而被拒绝。
这篇档将系统性地介绍如何借助性能分析工具,识别并高效解决上述核心性能痛点。
三、explain()执行计划分析
explain()是 MongoDB 最重要的性能分析工具,用于分析查询的执行计划和性能指标。explain() 的三种模式:
| 模式 | 说明 | 使用场景 |
| queryPlanner(默认) | 仅返回查询计划,不执行查询 | 快速查看优化器选择的计划 |
| executionStats | 执行查询并返回统计信息 | 分析查询实际性能 |
| allPlansExecution | 执行所有候选计划并返回信息 | 深入分析优化器决策 |
四、基本使用
// 查询计划db.t_orders.find({ status: "paid" }).explain()// 执行统计db.t_orders.find({ status: "paid" }).explain("executionStats")// 所有计划执行信息db.t_orders.find({ status: "paid" }).explain("allPlansExecution")
五、关键指标解读
{ "queryPlanner": { "winningPlan": { "stage": "FETCH", // 执行阶段 "inputStage": { "stage": "IXSCAN", // 使用索引扫描 "keyPattern": { "status": 1 }, "indexName": "idx_status", "direction": "forward" } }, "rejectedPlans": […] // 被拒绝的计划 }, "executionStats": { "executionSuccess": true, "nReturned": 1000, // 返回文档数 "executionTimeMillis": 15, // 执行时间(毫秒) "totalKeysExamined": 1000, // 扫描的索引条目数 "totalDocsExamined": 1000, // 扫描的文档数 "executionStages": { "stage": "FETCH", "nReturned": 1000, "docsExamined": 1000, "inputStage": { "stage": "IXSCAN", "nReturned": 1000, "keysExamined": 1000 } } }}
六、执行阶段(Stage)说明
| Stage | 说明 | 是否理想 |
| COLLSCAN | 全表扫描 | 需要创建索引 |
| IXSCAN | 索引扫描 | 理想 |
| FETCH | 根据索引回表获取文档 | 正常 |
| SORT | 内存排序 | 需要为排序字段创建索引 |
| SORT_KEY_GENERATOR | 生成排序键 | 配合 SORT 出现 |
| PROJECTION_COVERED | 覆盖索引投影 | 理想(无需回表) |
| PROJECTION_SIMPLE | 简单投影 | 正常 |
| LIMIT | 限制返回数量 | 正常 |
| SKIP | 跳过文档 | 正常(深度分页时低效) |
| SHARD_MERGE | 分片结果合并 | 正常(分片集群) |
| SINGLE_SHARD | 单分片查询 | 定向查询 |
七、性能指标评估标准
| 指标 | 理想值 | 需优化 |
| totalDocsExamined / nReturned | ≈ 1 | > 10(扫描过多文档) |
| totalKeysExamined / nReturned | ≈ 1 | > 10(索引效率低) |
| executionTimeMillis | < 100ms | > 1000ms(需优化) |
| stage | IXSCAN | COLLSCAN(缺少索引) |
| 是否有 SORT 阶段 | 无 | 有(内存排序) |
八、慢查询日志分析
MongoDB 会自动记录执行时间超过阈值的慢查询,是性能问题排查的重要依据。
九、慢查询配置
// 查看当前慢查询阈值db.getProfilingStatus()// { "was": 1, "slowms": 100 }// 设置慢查询阈值为200msdb.setProfilingLevel(1, { slowms: 200 })// level 0: 关闭// level 1: 仅记录慢查询// level 2: 记录所有操作(慎用,性能影响大)
十、查询慢查询日志
你可以登录控制台,在 慢日志查询 页面直接检索并分析慢日志。具体操作,请参见 慢日志查询。
十一、currentOp 监控当前操作
currentOp 能够实时捕获数据库当前正在执行的所有活动操作快照,适合排查当前性能问题。
// 查看所有当前操作db.currentOp()// 查看执行时间超过5秒的操作db.currentOp({ "active": true, "secs_running": { "$gt": 5 }})// 查看等待锁的操作db.currentOp({ "waitingForLock": true})// 查看特定集合的操作db.currentOp({ "ns": "mydb.t_orders"})// 终止执行时间超过10秒的操作db.currentOp().inprog.forEach(function(item){if(item.secs_running > 10 )db.killOp(item.opid)})
十二、serverStatus 监控指标
serverStatus 用于获取 MongoDB 实例全局状态快照与计数器信息。
db.serverStatus()// 关注的关键指标{ "connections": { "current": 50, // 当前连接数 "available": 950, // 可用连接数 "totalCreated": 1000 }, "opcounters": { "insert": 10000, "query": 50000, "update": 5000, "delete": 1000 }, "mem": { "resident": 4096, // 常驻内存(MB) "virtual": 8192 // 虚拟内存(MB) }, "wiredTiger": { "cache": { "bytes currently in the cache": …, "maximum bytes configured": … } }}
十五、问题表现
·执行 explain() 分析查询计划时,stage 显示为 "COLLSCAN"。
·查询耗时与集合中的数据量呈线性增长关系。
·CPU 利用率和磁盘 I/O 负载显著上升。
十六、排查步骤
// 1. 确认是否全表扫描db.t_orders.find({ status: "paid" }).explain("executionStats")// 检查 winningPlan.stage 是否为 COLLSCAN// 2. 检查现有索引db.t_orders.getIndexes()// 3. 创建合适的索引db.t_orders.createIndex({ status: 1 }, { background: true })// 4. 验证索引生效db.t_orders.find({ status: "paid" }).explain("executionStats")// 确认 stage 变为 IXSCAN
十七、常见原因
| 原因 | 解决方案 |
| 缺少索引 | 为查询条件字段创建索引 |
| 查询条件不匹配索引 | 调整查询或创建合适的复合索引 |
| 使用了无法利用索引的操作符 | 避免使用$ne、$nin、无前缀正则等 |
| 索引选择性低 | 创建复合索引提高选择性 |
十九、问题表现
·数据库返回错误:Sort operation used more than the maximum 33554432 bytes of RAM。
·执行 explain() 分析查询计划时,显示存在 "SORT" 阶段。
20、排查步骤
// 1. 确认是否内存排序db.t_orders.find({ status: "paid" }).sort({ createTime: -1 }).explain("executionStats")// 检查是否有 SORT 阶段// 2. 创建支持排序的索引db.t_orders.createIndex({ status: 1, createTime: -1 }, { background: true })// 3. 验证索引排序db.t_orders.find({ status: "paid" }).sort({ createTime: -1 }).explain("executionStats")// 确认无 SORT 阶段
21、解决方案
| 方案 | 说明 |
| 创建索引(推荐) | 为排序字段创建索引,索引排序无内存限制。 |
| 减少排序数据量 | 增加筛选条件,减少需要排序的文档数。 |
| 分页优化 | 使用基于排序字段的游标分页替代深度分页 skip+sort。具体信息,请参见 避免深度分页。 |
| 临时方案 | 使用 allowDiskUse(性能较差)。 |
23、问题表现
·分片集群中,某个特定分片的 CPU、I/O 负载或存储空间占用远高于其他分片。
·写入流量(QPS)集中在单个分片,其他分片处于闲置状态。
·集群中 Chunk 迁移频繁,但各分片间的数据不均衡状态未能得到有效改善。
24、排查步骤
// 1. 查看分片数据分布db.t_orders.getShardDistribution()// 2. 查看chunk分布db.getSiblingDB("config").chunks.aggregate([ { $match: { ns: "mydb.t_orders" } }, { $group: { _id: "$shard", count: { $sum: 1 } } }])// 3. 检查分片键选择db.getSiblingDB("config").collections.findOne({ _id: "mydb.t_orders" })
25、常见原因与解决方案
| 原因 | 解决方案 |
| 分片键是单调递增字段 | 使用 Hash 分片或复合分片键 |
| 分片键基数太低 | 选择高基数字段或复合键 |
| 写入集中在最新数据 | 使用时间戳 Hash 分片 |
| 某些热点值访问频繁 | 业务层缓存热点数据 |
26、连接数耗尽
问题表现:
·客户端或应用日志抛出错误:connection pool exhausted。
·数据库实例拒绝新的客户端连接请求,导致新连接建立失败。
·应用端的大量数据库请求因处于等待连接状态而触发超时。
排查步骤:
// 1. 查看当前连接数db.serverStatus().connections// { "current": 950, "available": 50, "totalCreated": 10000 }// 2. 查看连接来源db.currentOp(true).inprog.forEach(function(op) { if (op.client) print(op.client);})// 3. 检查应用连接池配置// 确认maxPoolSize设置是否合理
解决方案:
| 方案 | 说明 |
| 优化连接池 | 减少单应用的 maxPoolSize |
| 检查连接泄漏 | 确保连接正确释放 |
| 增加实例规格 | 提升实例支持的最大连接数 |
| 减少应用实例 | 降低总连接需求 |
28、问题表现
·数据库整体或部分特定操作的响应时间出现大幅波动,表现不稳定。
·执行 currentOp() 时,结果中存在大量操作处于 waitingForLock: true 的等待状态。
·某些读写操作长时间处于挂起状态,无法及时返回执行结果。
29、排查步骤
// 1. 查看等待锁的操作db.currentOp({ "waitingForLock": true })// 2. 查看持有锁时间长的操作db.currentOp({ "active": true, "secs_running": { "$gt": 10 }})// 3. 查看锁状态db.serverStatus().locks
30、解决方案
| 原因 | 解决方案 |
| 长事务 | 缩短事务执行时间,拆分大事务 |
| 长时间运行的查询 | 优化查询,添加索引 |
| 前台建索引 | 使用 background:true |
| DDL 操作 | 安排在低峰期执行 |
33、查询语句检查
| 检查项 | 验证方法 | 通过标准 |
| 查询使用索引 | explain("executionStats") | stage 为 IXSCAN |
| 无内存排序 | explain("executionStats") | 无 SORT 阶段 |
| 扫描效率 | totalDocsExamined/nReturned | 比值接近1 |
| 返回字段最小化 | 检查是否使用投影 | 仅返回必要字段 |
| 分页方式合理 | 检查分页实现 | 使用基于排序字段的游标分页 |
34、写入语句检查
| 检查项 | 验证方法 | 通过标准 |
| Update 带条件 | 代码审查 | 禁止空条件更新 |
| 使用$set 更新 | 代码审查 | 禁止全文档替换 |
| 批量操作有控制 | 代码审查 | 单批次≤1000条 |
| Write Concern 合理 | 代码审查 | 核心业务使用 majority |
36、检查现有索引是否满足查询需求
// 列出所有索引db.t_orders.getIndexes()// 检查索引使用统计db.t_orders.aggregate([{ $indexStats: {} }])// 针对主要查询验证索引覆盖const mainQueries = [ { status: "paid" }, { customerId: "C001", createTime: { $gte: ISODate("2024-01-01") } }, { orderId: "ORD001" }];mainQueries.forEach(query => { const plan = db.t_orders.find(query).explain("executionStats"); print("Query:", JSON.stringify(query)); print("Stage:", plan.queryPlanner.winningPlan.stage); print("DocsExamined:", plan.executionStats.totalDocsExamined); print("—");});