本文介绍腾讯云 MongoDB 的性能优化方法和常见问题排查指南,包括性能分析工具使用、常见性能问题诊断、开发阶段检查清单等内容,帮助开发者在开发和运维过程中快速定位并解决性能问题。
性能问题场景
在 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": ...}}}
常见性能问题诊断
全表扫描(COLLSCAN)
问题表现
执行 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" 阶段。
排查步骤
// 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 阶段
解决方案
方案 | 说明 |
创建索引(推荐) | 为排序字段创建索引,索引排序无内存限制。 |
减少排序数据量 | 增加筛选条件,减少需要排序的文档数。 |
分页优化 | |
临时方案 | 使用 allowDiskUse(性能较差)。 |
热点数据与热点分片
问题表现
分片集群中,某个特定分片的 CPU、I/O 负载或存储空间占用远高于其他分片。
写入流量(QPS)集中在单个分片,其他分片处于闲置状态。
集群中 Chunk 迁移频繁,但各分片间的数据不均衡状态未能得到有效改善。
排查步骤
// 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" })
常见原因与解决方案
原因 | 解决方案 |
分片键是单调递增字段 | 使用 Hash 分片或复合分片键 |
分片键基数太低 | 选择高基数字段或复合键 |
写入集中在最新数据 | 使用时间戳 Hash 分片 |
某些热点值访问频繁 | 业务层缓存热点数据 |
连接数耗尽
问题表现:
客户端或应用日志抛出错误:
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 |
检查连接泄漏 | 确保连接正确释放 |
增加实例规格 | 提升实例支持的最大连接数 |
减少应用实例 | 降低总连接需求 |
锁等待与死锁
问题表现
数据库整体或部分特定操作的响应时间出现大幅波动,表现不稳定。
执行 currentOp() 时,结果中存在大量操作处于 waitingForLock: true 的等待状态。
某些读写操作长时间处于挂起状态,无法及时返回执行结果。
排查步骤
// 1. 查看等待锁的操作db.currentOp({ "waitingForLock": true })// 2. 查看持有锁时间长的操作db.currentOp({"active": true,"secs_running": { "$gt": 10 }})// 3. 查看锁状态db.serverStatus().locks
解决方案
原因 | 解决方案 |
长事务 | 缩短事务执行时间,拆分大事务 |
长时间运行的查询 | 优化查询,添加索引 |
前台建索引 | 使用 background:true |
DDL 操作 | 安排在低峰期执行 |
开发阶段检查清单
上线前 SQL 审核检查项
查询语句检查
检查项 | 验证方法 | 通过标准 |
查询使用索引 | explain("executionStats") | stage 为 IXSCAN |
无内存排序 | explain("executionStats") | 无 SORT 阶段 |
扫描效率 | totalDocsExamined/nReturned | 比值接近1 |
返回字段最小化 | 检查是否使用投影 | 仅返回必要字段 |
分页方式合理 | 检查分页实现 | 使用基于排序字段的游标分页 |
写入语句检查
检查项 | 验证方法 | 通过标准 |
Update 带条件 | 代码审查 | 禁止空条件更新 |
使用$set 更新 | 代码审查 | 禁止全文档替换 |
批量操作有控制 | 代码审查 | 单批次≤1000条 |
Write Concern 合理 | 代码审查 | 核心业务使用 majority |
索引覆盖检查
检查现有索引是否满足查询需求
// 列出所有索引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("---");});