数据库“现在还能查到数据”只是最低要求。纸舟书店进入持续运行阶段后,我们还要知道请求是否变慢、连接是否逼近上限、复制是否落后、磁盘和缓存是否吃紧,以及哪一种查询形状造成了问题。本节建立一条从症状到证据的诊断路径。
一条慢请求可能来自应用等待连接、网络往返、查询扫描过多文档、锁等待、磁盘压力或 Secondary 延迟。只看数据库 CPU 很容易误判。
最少要把四类信息放在同一时间线上:
监控的价值不在图表数量,而在于能从“下单接口 P95 变慢”追到具体查询形状和资源证据。
serverStatus 是服务器快照db.serverStatus() 返回大量运行指标。直接保存整份输出不利于阅读,初学时先提取 uptime、连接、操作计数和 WiredTiger 缓存。
计数器大多从进程启动后累计,不能把两个不同时长的总数直接比较。监控系统通常对计数器求单位时间增量,再和请求量、延迟放在一起。
下面只读服务器状态,不修改配置:
docker exec paperboat-mongo mongosh --quiet --eval '
const s = db.serverStatus();
printjson({
host: s.host,
version: s.version,
uptimeSeconds: s.uptime,
connections: {
current: s.connections.current,
available: s.connections.available,
totalCreated: s.connections.totalCreated
},
opcounters: s.opcounters,
cache: {
bytesCurrentlyInCache:
s.wiredTiger.cache["bytes currently in the cache"],
maximumBytesConfigured:
s.wiredTiger.cache["maximum bytes configured"]
}
})
'会得到类似摘要:
{
host: '...',
version: '8.0.26',
uptimeSeconds: Long('2145'),
connections: {
current: 2,
available: 838858,
totalCreated: 37
},
opcounters: {
insert: Long('18'),
query: Long(
这些数值会随执行时刻和容器资源变化。判断时更关心关系:
current 持续接近连接上限,说明要检查连接泄漏、池大小和并发峰值。totalCreated 快速增长但业务量不高,常见原因是每个请求都创建新 MongoClient。command 包括许多管理与协议命令,不能直接当作业务请求数。Node.js 服务应在进程生命周期内复用一个 MongoClient。为每个 HTTP 请求新建并关闭客户端,会反复建连、认证和发现拓扑,也会让连接指标产生无意义的抖动。
explain 判断扫描范围性能问题最常见的第一步不是调大内存,而是确认查询做了多少工作。第 8 节已经看到:
没有合适索引:COLLSCAN,检查 8004 篇文档,返回 23 篇
建立复合索引:IXSCAN,检查 23 个索引键和 23 篇文档,返回 23 篇executionStats 中最常用的字段是:
nReturned:返回多少文档。totalDocsExamined:读取并检查多少文档。totalKeysExamined:检查多少索引键。executionTimeMillis:本次执行时间,只是一次观测,不能代替稳定压测。winningPlan:最终采用的计划。扫描数远大于返回数时,应检查过滤条件、排序和投影是否与索引匹配。小数据上 executionTimeMillis: 0 也不能证明查询能随数据量增长。
纸舟书店搜索接口不应允许请求无限占用资源。下面为查询设置 2 秒上限,并带上可搜索的注释:
docker exec paperboat-mongo mongosh --quiet --eval '
const rows = db.getSiblingDB("bookstore").books
.find({
category: "数据库",
price: { $lte: 100 }
})
.sort({ price: 1 })
.comment("book-search-v1")
.maxTimeMS(2000)
.toArray();
printjson(rows.map(b => ({
title: b.title,
price: b.price
})))
'[
{ title: '数据建模的艺术', price: 79 },
{ title: 'MongoDB 从入门到实践', price: 89 }
]maxTimeMS 是服务端执行预算,不包括应用拿到连接前的全部等待。HTTP 请求超时、驱动的 server selection timeout、socket timeout 和服务端 maxTimeMS 应形成有意的层次,而不是互相矛盾。
数据库 Profiler 可以把符合条件的操作写入该数据库的 system.profile。它带来额外开销,还可能记录查询形状和字段,因此不应在繁忙环境中无边界地长期开到最高级别。
常见级别:
0:关闭 Profiler。1:记录慢于阈值或满足过滤条件的操作。2:记录所有操作,开销和敏感信息风险更高。本节短暂使用级别 1,把 slowms 设为 0,只为稳定捕获一条带注释的演示查询;读取结果后立即恢复。
先开启采样:
docker exec paperboat-mongo mongosh --quiet --eval '
const d = db.getSiblingDB("bookstore");
printjson(d.setProfilingLevel(1, {
slowms: 0,
sampleRate: 1
}))
'输出会显示修改前的配置:
{
was: 0,
slowms: 100,
sampleRate: 1,
ok: 1
}执行一条没有合适前缀索引的正则查询,并加上唯一注释:
docker exec paperboat-mongo mongosh --quiet --eval '
db.getSiblingDB("bookstore").books
.find({ title: /实践|开发|艺术/ })
.comment("profile-course-001")
.toArray();
print("query finished")
'query finished最后从 system.profile 读取这条记录:
docker exec paperboat-mongo mongosh --quiet --eval '
const p = db.getSiblingDB("bookstore").system.profile.findOne(
{ "command.comment": "profile-course-001" },
{
op: 1,
ns: 1,
planSummary: 1,
keysExamined: 1,
docsExamined: 1,
nreturned: 1,
millis: 1
}
);
printjson(p)
'小数据集上会看到类似结果:
{
op: 'query',
ns: 'bookstore.books',
planSummary: 'COLLSCAN',
keysExamined: 0,
docsExamined: 4,
nreturned: 3,
millis: 0
}millis: 0 说明这四篇文档很快扫完,不代表这种查询形状适合四百万篇文档。planSummary: 'COLLSCAN' 和扫描比例才是容量增长前要处理的证据。
诊断结束后关闭 Profiler,再删除本节产生的 profile 集合。删除前必须先关闭:
docker exec paperboat-mongo mongosh --quiet --eval '
const d = db.getSiblingDB("bookstore");
printjson(d.setProfilingLevel(0));
printjson(d.system.profile.drop())
'{
was: 1,
slowms: 0,
sampleRate: 1,
ok: 1
}
truewas: 1 表示调用前处于慢操作采样级别,true 表示演示记录已删除。生产系统中不要随意删除仍用于调查的 profile 数据;应先按数据治理要求导出或保留证据。
currentOp 看正在发生的操作Profiler 适合回看已经记录的操作,$currentOp 适合查看当前活动。管理员可用下面的聚合读取活动客户端操作:
db.getSiblingDB("admin").aggregate([
{
$currentOp: {
allUsers: true,
idleConnections: false
}
},
{
$match: {
active: true,
op: { $in: ["query", "command", "update", "insert", "remove"] }
}
},
{
$project: {
opid: 1
不要看到长操作就立即 killOp。先确认它属于哪个服务、是否正在完成必要迁移或索引构建、终止后会不会触发重试风暴,再执行变更。诊断权限本身也应只给受控管理员。
容器日志能帮助识别启动警告、连接、认证、慢操作和选举事件:
docker logs --tail 30 paperboat-mongo阅读时优先关注时间、严重级别、组件、消息 ID、上下文和属性,不要只搜索单个英文词。生产环境应把日志送到有保留、访问控制和检索能力的集中系统,并避免把连接串密码或业务敏感字段写进去。
查询注释是很实用的关联点。应用可以用固定的查询名称或经过清洗的 trace id 填入 comment,让 Profiler、日志和 $currentOp 更容易对应到 API 路由。不要把用户提交的整段文本直接放进注释。
Node.js 驱动中的 maxPoolSize 不是越大越好。假设有 20 个应用实例,每个实例允许 100 条连接,理论上就可能产生 2000 条连接,还没有计算管理任务和突发扩容。
池设计需要同时回答:
先复用一个 MongoClient,再根据真实指标调整 maxPoolSize、minPoolSize 和 waitQueueTimeoutMS。用超大的池掩盖慢查询,通常只会把压力更快送进数据库。
当纸舟书店的图书搜索变慢,可以按以下顺序工作:
先定义症状:哪个 API、从什么时候开始、P50/P95/P99 变化多少、错误率是否同时上升。没有时间范围就无法对齐证据。
把应用 trace、驱动命令耗时、MongoDB 慢操作和系统指标放在同一时间窗口,判断时间花在连接池、服务器选择、执行还是返回结果。
对实际查询形状使用 explain("executionStats"),比较 nReturned、totalDocsExamined、totalKeysExamined 和排序阶段。
这个顺序比“看到 CPU 高就扩容”更慢一点开始,却能减少无效修改和新的索引负担。
一个最小可用的告警集合可以包括:
阈值应来自基线和容量计划。把所有瞬时波动都报警,会让真正故障淹没在噪声中。
完成本节后,你已经能从服务器摘要、查询计划、Profiler、活动操作和日志五个入口收集证据,并且知道诊断结束后要恢复临时配置。
只改变一个主要因素,例如新增候选索引或缩小返回字段,在接近真实数据量的环境中重跑相同负载。
检查写入成本、缓存占用、磁盘和其他查询是否退化。确认收益后再逐步发布,并保留回退方案。