1. 项目背景
业务场景:本地生活电商准备迎接年度最大促销——"618 年中大促"。运维团队接到死命令:系统必须扛住 10 万 QPS 的读写混合负载,P99 延迟 < 100ms。目前的架构——3 分片 × 3 节点复制集,8 核 32GB × 9 台服务器。压测结果显示——当前配置在 3 万 QPS 时 P99 延迟已经到 300ms,距离目标还有 3 倍差距。
容量规划上也有盲区——运维不知道现有的磁盘能支撑多久(日均增量 50GB,磁盘剩余 1TB——大约 20 天就会满),但还没有扩容预算;内存的工作集到底多大也不清楚(现在是 32GB,但 WiredTiger 缓存配置了 16GB——够不够?);CPU 瓶颈到底是在 MongoDB Server 层还是 WiredTiger 层还是网络层。
痛点:性能调优没有银弹。NUMA 架构可能导致 MongoDB 只用到一半内存;透明大页(Transparent Huge Pages)可能让 WiredTiger 的 4KB 页碎成渣;文件系统选择(ext4 vs xfs)影响 fsync 性能;磁盘 IOPS 不够导致 Checkpoint 期间的写入毛刺;连接池、网络、内核参数……每一个环节都可能成为瓶颈。
2. 项目设计
小胖(盯着压测报告上 P99 300ms 的红线):大师!我们 9 台服务器配了最强的 SSD、64 核 CPU、256GB 内存,结果 3 万 QPS 就 P99 300ms?这不科学!
大师:性能调优第一步——不猜,用数据说话。把压测期间的 MongoDB 指标拉出来,我帮你定位瓶颈在哪一层。
小胖:我看过了,CPU 85%,IOPS 15000,内存用了 90%,连接池也没满……感觉全满了?
大师:全满等于没有重点。我们按 USE 方法论——逐一检查 Utilization(利用率)、Saturation(饱和度)、Errors(错误数),定位瓶颈的精确位置。
- CPU:85% 是整体利用率,但 MongoDB 是单进程的——如果是在 64 核机上只用了 8 核(其他核空闲),那整体 85% 其实意味着那 8 核已经满载了。用
mpstat看每核利用率。 - IOPS:15000 IOPS 看起来高——但对比你的 SSD 的标称能力(假设 50000 IOPS)还有余地。问题可能是 Checkpoint 期间的 IO 尖峰导致的抖动,而不是平均 IOPS。
- 内存:90% 用在哪?如果 WiredTiger 缓存只有 16GB,而工作集是 50GB,缓存淘汰频繁导致额外的磁盘 IO——这才是真正的瓶颈。
技术映射:USE 方法论——CPU 看每核利用率,内存看缓存命中率和 eviction,磁盘看 IOPS 的分布(平均 vs P99),网络看带宽和重传率。
小胖:那怎么找到工作集大小?
大师:工作集大小 = 活跃查询中频繁访问的数据和索引的总和。估算方法——在高峰期运行 db.serverStatus().wiredTiger.cache 查看缓存使用率。如果 16GB 缓存中有 14GB 在被频繁访问且 eviction > 0——说明工作集 > 16GB,缓存不够。实际工作集 = 缓存当前使用量 + 因淘汰而额外产生的磁盘读对应的数据量。
技术映射:工作集 ≈ 缓存大小 × (1 + eviction_io读 / 缓存命中)。如果缓存命中率 < 95%,说明工作集显著大于缓存。
小白:容量规划怎么做?磁盘、内存、CPU 各留多少余量?
大师:一张表说清楚三者的规划:
| 资源 | 规划公式 | 警戒线 |
|---|---|---|
| 磁盘 | (当前数据大小 + 日均增量 × 保留天数) × 1.5(索引+碎片) | 使用率 > 70% 开始扩容 |
| 内存 | 工作集大小 × 1.2 + 2GB(系统开销) | WiredTiger 缓存命中 < 95% 或 eviction > 0 |
| CPU | (目标 QPS × 查询平均 CPU 时间) × 1.3 | 单核利用率 > 80% 或整体 CPU > 70% |
小胖:那系统调优呢?我听人说 NUMA、透明大页、文件系统这些都要调?
大师:对。快速清单:
- NUMA:
numactl --interleave=all mongod——防止 MongoDB 被限制在单 NUMA 节点上只能用一半内存。 - 透明大页:
echo never > /sys/kernel/mm/transparent_hugepage/enabled——WiredTiger 的 4KB 页和 2MB 大页冲突。 - 文件系统:XFS 比 ext4 更适合高并发小 IO——WiredTiger 大量 4KB 页读写更匹配 XFS 的 allocator。
- readahead:WiredTiger 有自己的预读逻辑——操作系统层的 readahead 反而干扰了——设为 8-16KB(而非默认的 128KB)。
- ulimit:文件描述符
ulimit -n 64000,进程数ulimit -u 64000。 - swappiness:
vm.swappiness = 1——尽量不使用 swap,避免 WiredTiger 缓存被换出。
大师(总结):今天记住——性能调优三步走,USE 方法找瓶颈,系统参数做基线(NUMA/THP/XFS),容量规划留余量(30% 磁盘 + 20% CPU + 缓存命中 95%)。最后别忘了——压测后和压测中都要监控所有指标,单靠压测报告的数字是片面的。
3. 项目实战
3.1 环境准备
需要 Linux 服务器环境(用于系统级调优)。Docker 容器内部分参数(NUMA/THP)受宿主机控制。
3.2 分步实现
步骤一:系统级性能基线检查
# === 内核参数检查清单 ===
# 1. 透明大页状态
cat /sys/kernel/mm/transparent_hugepage/enabled
# 期望: [never] 或 always madvise [never] —— never 最前面表示当前禁用
# 禁用命令: echo never > /sys/kernel/mm/transparent_hugepage/enabled
# 2. 内存交换倾向
sysctl vm.swappiness
# 期望: 1(尽量不用 swap)
# 3. 文件描述符限制
ulimit -n
# 期望: >= 64000
# 4. 磁盘 I/O 调度器(SSD 推荐 noop 或 none)
cat /sys/block/sda/queue/scheduler
# 对 NVMe SSD: [none] 多队列无调度
# 5. 文件系统预读
blockdev --getra /dev/sda
# 期望: 8-16(KB)——WiredTiger 有自己的预读逻辑
# 6. NUMA 状态
numactl --hardware
# 如果有多 NUMA 节点——mongod 启动时用 numactl --interleave=all 绑定
步骤二:工作集与缓存命中率诊断
// working-set-diagnose.js —— 工作集诊断
use admin
var s = db.serverStatus()
var cache = s.wiredTiger.cache
var cacheUsedMB = cache["bytes currently in the cache"] / 1024 / 1024
var cacheMaxMB = cache["maximum bytes configured"] / 1024 / 1024
var dirtyMB = (cache["tracked dirty bytes in the cache"] || 0) / 1024 / 1024
var pagesRead = cache["pages read into cache"] || 0
var pagesEvicted = (cache["pages evicted by eviction server"] || 0)
+ (cache["pages evicted by application threads"] || 0)
print("=== 工作集诊断 ===")
print("缓存使用:", cacheUsedMB.toFixed(0), "MB /", cacheMaxMB.toFixed(0), "MB",
"(", (cacheUsedMB / cacheMaxMB * 100).toFixed(1), "%)")
print("脏页:", dirtyMB.toFixed(0), "MB")
print("页读入:", pagesRead)
print("页淘汰:", pagesEvicted)
// 粗略的缓存命中率估计
var totalPageAccess = pagesRead + cache["pages requested from the cache"]
var hitRate = totalPageAccess > 0
? (1 - pagesRead / totalPageAccess) * 100
: null
if (hitRate !== null) {
print("缓存命中率:", hitRate.toFixed(1), "%",
hitRate > 95 ? "PASS" : "⚠ 建议增大缓存")
}
// 工作集估算
// 如果 eviction > 0 且频繁——说明工作集 > 当前缓存
if (cache["pages evicted by application threads"] > 0) {
print("⚠ 应用线程被迫淘汰——缓存严重不足!")
print(" 建议 cacheSizeGB 至少增加到当前使用量的 1.5 倍")
}
步骤三:容量规划计算器
// capacity-planning.js —— 容量规划计算
var s = db.serverStatus()
var dbStats = db.getSiblingDB("local_life").stats()
// 输入参数(运维填写)
var growthRatePerDay = 50 // GB/天
var retentionDays = 90 // 保留天数
var targetQP = 100000 // 目标 QPS
var avgQueryTimeMs = 2 // 平均查询耗时
// 1. 磁盘规划
var currentSizeGB = dbStats.dataSize / 1024 / 1024 / 1024
var indexSizeGB = dbStats.indexSize / 1024 / 1024 / 1024
var totalCurrentGB = currentSizeGB + indexSizeGB
var projectedSizeGB = totalCurrentGB + growthRatePerDay * retentionDays
var safetyMarginGB = projectedSizeGB * 0.3 // 30% 安全余量
print("=== 容量规划 ===")
print("当前数据:", currentSizeGB.toFixed(1), "GB + 索引:", indexSizeGB.toFixed(1), "GB")
print("", retentionDays, "天后预计:", projectedSizeGB.toFixed(0), "GB")
print("建议磁盘:", (projectedSizeGB + safetyMarginGB).toFixed(0), "GB")
// 2. 内存规划
var cacheSizeGB = cache["maximum bytes configured"] / 1024 / 1024 / 1024
var recommendedCache = (cacheUsedMB / 1024) * 1.5 // 当前使用的 1.5 倍
print("\n当前缓存:", cacheSizeGB.toFixed(1), "GB, 建议:", recommendedCache.toFixed(1), "GB")
// 3. CPU 规划
var estimatedCpuPerQuery = avgQueryTimeMs / 1000 // 秒
var requiredCpuSec = targetQP * estimatedCpuPerQuery
var requiredCores = Math.ceil(requiredCpuSec * 1.3) // 30% 余量
print("\n目标 QPS:", targetQP, "→ 需要约", requiredCores, "个 CPU 核心")
步骤四:磁盘 IOPS 与 Checkpoint 毛刺诊断
// iops-check.js —— 磁盘 IO 与 Checkpoint 监控
// 在 mongosh 中每 2 秒采样一次 IO 相关指标
function sampleIO() {
var s = db.serverStatus()
var wt = s.wiredTiger
return {
time: new Date(),
reads: wt["block-manager"]["blocks read"] || 0,
writes: wt["block-manager"]["blocks written"] || 0,
checkpointMs: wt.checkpoint?.["most recent time msecs"] || 0,
evictionApp: wt.cache["pages evicted by application threads"] || 0
}
}
var before = sampleIO()
sleep(5000)
var after = sampleIO()
var blocksRead = after.reads - before.reads
var blocksWritten = after.writes - before.writes
var iops = (blocksRead + blocksWritten) / 5 // 5 秒间隔 → 每秒 IOPS
print("=== IOPS 采样 (5秒) ===")
print("读块:", blocksRead, "写块:", blocksWritten)
print("估算 IOPS:", iops.toFixed(0))
print("最近 Checkpoint 耗时:", after.checkpointMs, "ms")
print("应用线程淘汰:", after.evictionApp - before.evictionApp)
// 如果 Checkpoint > 1000ms(1秒)——说明脏页过多,checkpoint 期间 IO 风暴
// 如果 evictionApp 在 5 秒内新增 > 0——缓存不够,应用被迫参与淘汰
步骤五:mongostat + mongotop 实时监控
# === mongostat 实时指标 ===
# 每秒刷新一次,输出 QPS、连接数、内存、锁等关键指标
mongostat --uri="mongodb://admin:pass@host:27017/?authSource=admin" -n 30 1
# 关键列解读:
# insert/query/update/delete: 每秒操作数
# vsize/res: 虚拟内存/物理内存
# qr/qw: 读/写队列长度(> 0 说明请求在等待)
# ar/aw: 活跃读/写连接数
# netIn/netOut: 网络流量
# conn: 连接数
# dirty: 脏页百分比
# used: 缓存使用率
# === mongotop 集合级读写负载 ===
mongotop --uri="mongodb://admin:pass@host:27017/?authSource=admin" 5
# 每 5 秒刷新一次,显示每个集合的读写时间占比
# 找出"最忙的集合"——读写时间最高的那个 → 优化它的索引或分片
步骤六:火焰图定位 CPU 热点
# perf 采集 30 秒调用栈并生成火焰图(核心步骤)
# 1. 采集(在生产环境低峰期进行)
sudo perf record -F 99 -p $(pgrep mongod) -g -- sleep 30
# 2. 生成火焰图
sudo perf script | ./FlameGraph/stackcollapse-perf.pl > mongod.folded
./FlameGraph/flamegraph.pl mongod.folded > mongod_flamegraph.svg
# 3. 分析火焰图中的主要 CPU 消耗
# 常见热点:
# - __wt_btree_insert → 索引写入热点 → 考虑批量写入或分片
# - __wt_row_search → B-Tree 查找 → 索引选择性差或缺少索引
# - mongo::BSONObj::toString → BSON 序列化 → 文档过大或返回字段过多
# - malloc/free → 频繁内存分配 → 文档碎片化或工作集不稳定
3.3 完整代码清单
| 文件 | 用途 |
|---|---|
mongodb-lab/tuning/kernel-check.sh |
内核参数基线检查 |
mongodb-lab/tuning/working-set-diagnose.js |
工作集与缓存命中率诊断 |
mongodb-lab/tuning/capacity-planning.js |
容量规划计算器 |
mongodb-lab/tuning/iops-checkpoint.js |
IOPS 与 Checkpoint 毛刺诊断 |
mongodb-lab/tuning/mongostat-capture.sh |
mongostat 捕获脚本 |
3.4 测试验证
use admin
// 1. 验证系统指标可采集
var s = db.serverStatus()
print("WiredTiger:", s.wiredTiger ? "PASS" : "FAIL")
print("连接:", s.connections ? "PASS" : "FAIL")
// 2. 验证磁盘统计
var dbStats = db.getSiblingDB("local_life").stats()
print("磁盘统计:", dbStats.dataSize > 0 ? "PASS" : "FAIL")
// 3. 验证 Checkpoint 信息
var cp = db.serverStatus().wiredTiger.checkpoint
print("Checkpoint:", cp ? "PASS" : "FAIL")
// 4. 验证 mongostat 可用
// 在宿主机命令行运行: mongostat --version
print("\n=== 性能调优工具验证完成 ===")
4. 项目总结
4.1 性能调优速查清单
| 层级 | 调优项 | 推荐值/操作 |
|---|---|---|
| 内核 | 透明大页 | 禁用 (never) |
| 内核 | swappiness | 1 |
| 内核 | readahead | 8-16KB |
| 文件系统 | XFS | 优于 ext4 |
| 硬件 | NUMA | numactl --interleave=all |
| MongoDB | cacheSizeGB | 物理内存 × 50%-70% |
| MongoDB | Oplog 大小 | 24-48 小时写入量 |
| MongoDB | 连接池 maxPoolSize | 50-100(按 QPS 调) |
| 应用 | 批量写入 | bulkWrite 替代逐条 insert |
| 应用 | 原子更新 | $inc + filter 替代 read-then-write |
4.2 适用场景
极端性能调优适用:
- 大促前的容量评估和压测调优。
- 数据库迁移到新硬件后的参数复查。
- 周期性性能衰退的根因分析(碎片、缓存命中率下降)。
- 新业务上线前的工作集评估和资源申请。
4.3 注意事项
| 注意事项 | 说明 |
|---|---|
| 内核参数修改后需重启 mongod | THP、swappiness 等修改需要 mongod 重启才生效 |
| perf 采集有性能开销 | 生产环境低峰期采集 30s,不要长时间运行 |
| XFS 优于 ext4 但并非银弹 | ext4 在纯读场景下可能更快 |
| 容量规划的数字是经验值 | 根据实际压测数据调整余量——模型需要校准 |
4.4 常见踩坑经验
故障案例一:NUMA 导致只用一半内存
某 128GB 服务器上 MongoDB 设置了 cacheSizeGB: 80,但实际只用了 40GB(一半)。根因:服务器是双路 NUMA 架构,mongod 进程只在一个 NUMA 节点上分配了内存——另一个节点的 64GB 空闲。解决:numactl --interleave=all mongod 或在 systemd service 中配置 CPUAffinity 和 MemoryPolicy=interleave。
故障案例二:Checkpoint 期间的磁盘 IO 风暴
某 SSD 服务器的 MongoDB 每 60 秒出现一次 2-3 秒的写入毛刺——Checkpoint 期间脏页刷盘产生大量 IO。根因:脏页比例过高(30%+),一次性刷盘量大。解决:缩短 Checkpoint 间隔到 30 秒(checkpoint=(wait=30,log_size=1GB)),使脏页更频繁但更平缓地刷盘;同时增大 WiredTiger 缓存以减少总脏页量。
故障案例三:压缩算法选 snappy 导致磁盘膨胀
某团队用 snappy 做默认压缩,半年后磁盘使用率超预期——数据量 800GB,snappy 压缩后还有 650GB(压缩率不到 20%)。改用 zstd 后降到 420GB——节省了 35% 的空间。代价是 CPU 升高 15%,但磁盘成本节省远大于 CPU 成本。解决:压缩算法要根据数据特征选——高重复度数据(日志/JSON)用 zstd 收益大,二进制数据(图片/文件)压缩率极低时用 none。
4.5 思考题
- 如果 WiredTiger 缓存命中率是 99%——说明缓存基本够用。但 P99 延迟仍然很高——可能的原因是什么?(提示:不只看缓存命中,还要看锁、网络、文档大小)
- 一台 64GB 内存的服务器,MongoDB 的 cacheSizeGB 可以设为 60GB 吗?为什么?
(答案将在第 40 章末尾揭晓)
上一章思考题答案:
WiredTiger 的 MVCC 使用追加新版本的方式——每次更新在原文档的物理位置附近创建一个新版本,旧版本被标记为过时但不立即删除。旧版本由后台的 Checkpoint Cleanup 线程在确认没有活跃事务引用它之后清理。这就是为什么 WiredTiger 需要定期 Checkpoint 来回收旧版本占用的空间。
两个事务——A 读 X 写 Y,B 读 Y 写 X——不会形成传统意义上的死锁(因为 WiredTiger 使用乐观 MVCC 而非悲观锁)。但会触发 WriteConflict——两个事务在各自 commit 时检测到自己读过的数据被对方修改了(版本变化),后提交的那个会被 abort 重试。MongoDB 不会死锁等待,而是主动让后提交者失败并重试——这也是为什么事务需要自动重试。
延伸阅读与资源
NumPy 从入门到生产落地:全链路实战指南(科学计算/向量化)
Redis 8 实战精讲:从 CRUD 到源码,构建高可用缓存系统
Redis 实战修炼与原理进阶
Python 3实战精进:从脚本到高并发订单引擎
python入门:Rquests从菜鸟脚本到企业级SDK的网络实战圣经
Milvus向量数据库实战修炼:从 0 到 1精通向量检索与生产落地
MongoDB 实战进阶与内核修炼
后端工程师的 AI 转型第一课:Ollama 与私有化大模型实战
10倍开发者的 Dify 魔法书:从零构建全栈 AI 应用
后端工程师转型AI第一课-Ollama 与私有化大模型实战
大型语言模型(LLM) vLLM 高性能推理落地实战
Agent开发之LlamaIndex 实战修炼与源码进阶
大语言模型Transformers 实战修炼与源码剖析

微信公众号: 架构师日常笔记 欢迎关注!
浙公网安备 33010602011771号