2024 年我们把 MongoDB 4.4 升到 7.0,从 3 节点副本集变成 6 节点分片集群。期间业务零停机,但坑了 6 次。最大的两个:索引升级触发 collscan、Causal Consistency 默认开启导致跨节点延迟暴涨。本文复盘整个升级流程,讲透 MongoDB 5/6/7 各版本的关键变化和真实坑点。
升级背景
现状(2024.01):
- MongoDB 4.4.18,3 节点副本集
- 数据量:800GB(单实例)
- 集合数:120 个,最大集合 4 亿文档
- QPS:写 3w,读 6w
- 痛点:
* 容量瓶颈(单机 1TB SSD 快满)
* 复杂聚合慢($lookup + $group 经常 5s+)
* 时序数据没有专门优化
* Change Stream 慢(全表扫 oplog)
升级目标:
- MongoDB 7.0(LTS)
- 6 分片(每分片 1 主 1 从 1 仲裁)
- 容量 → 4.8TB,可水平扩展
- Time Series Collection(7.0 新)
- Queryable Encryption(7.0 新)
版本路线图
MongoDB 不能跨 major 版本直升,必须逐级升:
4.4 → 5.0 → 6.0 → 7.0
每个版本必须先 setFeatureCompatibilityVersion 才能继续
5.0 关键变化:
- Time Series Collection
- Resharding(在线重新分片)
- Versioned API(/api/v=1/...)
- $lookup 优化
6.0 关键变化:
- Queryable Encryption 公测
- Change Stream 全文档(pre/post image)
- 索引并行构建
- $documents 阶段(没 collection 也能跑 pipeline)
- ESR 索引推荐(Equality/Sort/Range)
7.0 关键变化:
- Time Series 增强(secondary index 任意字段)
- Queryable Encryption GA
- 复合通配符索引
- Approximate Atlas Search
升级前准备
# 1. 检查 Driver 版本(很重要!)
# Node.js mongodb driver 4.x → 必须升到 6.x 支持 7.0
# Java mongo-java-driver 4.x → 必须 4.10+
# Python pymongo 3.x → 必须 4.5+
# 2. 检查 deprecated 用法
$ mongosh --eval 'db.runCommand({serverStatus:1}).deprecatedUsage'
{
"find.fields": 0,
"getLastError": 12345, // 4.4 还能用,5.0 移除
"geoSearch": 23, // 5.0 移除
}
# 3. 备份(必须!)
$ mongodump --uri="mongodb://primary:27017" \
--out /backup/2024-01-15/ \
--gzip --numParallelCollections=4
# 4. 检查 collection 兼容性
$ mongosh --eval 'db.adminCommand({listCollections: 1})' | grep -E 'view|capped'
# 5. 索引数量
$ mongosh --eval 'db.runCommand({usersInfo: 1})'
# 用户/角色权限要重新审计
4.4 → 5.0 升级
# 1. 滚动升级 secondary
$ sudo systemctl stop mongod
$ apt install -y mongodb-org=5.0.21 mongodb-org-server=5.0.21
$ sudo systemctl start mongod
# 2. 等副本集追平
$ mongosh
> rs.status()
# 看到 stateStr = SECONDARY 后继续下一个
# 3. 升级 primary 前手动 stepDown
> rs.stepDown(120)
# 触发选主,新 primary 接管
# 4. 升级旧 primary,启动后变 secondary
# 5. 全部升完后 setFCV
> db.adminCommand({setFeatureCompatibilityVersion: "5.0"})
# 验证
> db.adminCommand({getParameter: 1, featureCompatibilityVersion: 1})
{ featureCompatibilityVersion: { version: "5.0" } }
坑 1:5.0 移除 geoSearch
升完 5.0,业务报错:
{ "ok" : 0, "errmsg" : "no such command: 'geoSearch'", "code" : 59 }
原因:5.0 移除了 geoNear 老命令和 geoSearch
我们一个老接口用 db.runCommand({geoNear: ...})
修法:改成 $geoNear aggregation
// 旧代码
db.places.runCommand({
geoNear: 'places',
near: [-73.9, 40.7],
spherical: true,
maxDistance: 5000,
limit: 10
});
// 新代码($geoNear)
db.places.aggregate([
{
$geoNear: {
near: { type: "Point", coordinates: [-73.9, 40.7] },
distanceField: "distance",
maxDistance: 5000,
spherical: true
}
},
{ $limit: 10 }
]);
// 索引必须是 2dsphere
db.places.createIndex({ location: "2dsphere" });
5.0 → 6.0 升级
# 同样滚动升级
$ apt install -y mongodb-org=6.0.13
# 6.0 默认开启 Causal Consistency
# 客户端会话内 read-your-own-write 强一致
# 我们某接口写完立刻读,p99 从 5ms 飙到 80ms
# 原因:Causal Consistency 强制读 primary 或等 secondary 追上
# 修法 1:不需要强一致的接口用 unset session
const session = client.startSession();
// 默认 causalConsistency: true,改成 false
const session2 = client.startSession({ causalConsistency: false });
await collection.find(...).session(session2).toArray();
session2.endSession();
# 修法 2:全局 driver 配置
const client = new MongoClient(uri, {
readConcern: { level: 'majority' },
writeConcern: { w: 'majority' },
readPreference: 'secondaryPreferred'
});
坑 2:索引升级触发全表扫描
升 6.0 后某接口突然慢
EXPLAIN 看到 winningPlan 是 COLLSCAN(全表扫)
但代码没改,索引也存在,为什么?
原因:6.0 改了索引选择算法,旧的复合索引顺序不再 optimal
具体:6.0 推荐 ESR 索引(Equality / Sort / Range)
我们的索引是 { status: 1, created_at: 1, user_id: 1 }
查询: db.orders.find({user_id: '...', status: 'paid'}).sort({created_at: -1})
- Equality: user_id, status
- Sort: created_at
- 索引顺序应该是: user_id, status, created_at(ESR)
修复:重建索引
// 创建符合 ESR 的索引
db.orders.createIndex(
{ user_id: 1, status: 1, created_at: -1 },
{ background: true, name: 'esr_user_status_created' }
);
// 删旧索引
db.orders.dropIndex('status_1_created_at_1_user_id_1');
// 验证使用新索引
db.orders.find({ user_id: 'u123', status: 'paid' })
.sort({ created_at: -1 })
.explain('executionStats');
// 关注:
// executionStats.executionStages.stage = 'IXSCAN'(而非 COLLSCAN)
// totalKeysExamined / nReturned 比例越接近 1 越好
6.0 → 7.0 升级
# 升级 7.0
$ apt install -y mongodb-org=7.0.5
# Time Series Collection(7.0 增强)
> db.createCollection('metrics', {
timeseries: {
timeField: 'ts',
metaField: 'sensor',
granularity: 'seconds'
},
expireAfterSeconds: 7 * 24 * 3600 // 7 天 TTL
});
# 7.0 之前 secondary index 只能在 timeField/metaField
# 7.0 可以在任意 measurement 字段建索引
> db.metrics.createIndex({ temperature: 1, 'sensor.zone': 1 });
# 插入数据
> db.metrics.insertMany([
{ ts: ISODate(), sensor: { id: 's1', zone: 'A' }, temperature: 23.5 },
{ ts: ISODate(), sensor: { id: 's2', zone: 'B' }, temperature: 25.1 }
]);
# 查询(自动用 bucketed 优化)
> db.metrics.aggregate([
{ $match: { 'sensor.zone': 'A', ts: { $gte: ISODate('2024-01-01') } } },
{ $group: { _id: '$sensor.id', avg: { $avg: '$temperature' } } }
]);
坑 3:升级后 oplog 空间不够
7.0 升完几天,业务报错 OplogTooFar
原因:7.0 默认 Change Stream pre/post image 占 oplog 空间
我们有几个高频写的 collection 开了 changeStreamPreAndPostImages
oplog 从 50GB 涨到 200GB,磁盘告警
修法:
1. 不需要的 collection 关掉 pre/post image
2. 必需的扩 oplog 到 500GB
// 关掉某个 collection 的 pre/post image
db.runCommand({
collMod: 'orders',
changeStreamPreAndPostImages: { enabled: false }
});
// 扩 oplog
db.adminCommand({
replSetResizeOplog: 1,
size: 500000 // MB = 500GB
});
// 查看当前 oplog 状态
db.getSiblingDB('local').oplog.rs.stats({scale: 1024*1024});
// size, count, maxSize 单位 MB
分片化:副本集 → Sharded Cluster
# 1. 部署 config server 副本集(3 节点)
$ cat /etc/mongod-config.conf
sharding:
clusterRole: configsvr
replication:
replSetName: cfgrs
net:
port: 27019
$ mongod -f /etc/mongod-config.conf
$ mongosh --port 27019
> rs.initiate({_id:'cfgrs', members:[
{_id:0, host:'cfg1:27019'},
{_id:1, host:'cfg2:27019'},
{_id:2, host:'cfg3:27019'}
]});
# 2. 部署 shard 副本集(每 shard 3 节点)
$ cat /etc/mongod-shard.conf
sharding:
clusterRole: shardsvr
replication:
replSetName: shard1
net:
port: 27018
# 6 个 shard 重复
# 3. 部署 mongos
$ cat /etc/mongos.conf
sharding:
configDB: cfgrs/cfg1:27019,cfg2:27019,cfg3:27019
net:
port: 27017
$ mongos -f /etc/mongos.conf
# 4. 连 mongos,加 shard
$ mongosh --port 27017
> sh.addShard('shard1/sh1a:27018,sh1b:27018,sh1c:27018');
> sh.addShard('shard2/sh2a:27018,sh2b:27018,sh2c:27018');
# ... 6 个 shard 加完
把数据从副本集导入分片集群
# 用 mongodump + mongorestore
$ mongodump --uri="mongodb://oldrs/" --out=/tmp/dump --gzip
# 在 sharded cluster 上 restore 之前,先 enableSharding
$ mongosh mongos:27017
> sh.enableSharding('mydb');
# 给主要 collection 选 shard key(关键!)
> sh.shardCollection(
'mydb.orders',
{ user_id: 'hashed' } // hashed 适合均匀分布
);
> sh.shardCollection(
'mydb.events',
{ ts: 1, event_type: 1 } // 复合 shard key,范围查询友好
);
# Restore
$ mongorestore --uri="mongodb://mongos:27017/" \
--gzip --dir=/tmp/dump \
--numParallelCollections=4
# 看 balancer 状态
> sh.status()
# chunks 分布要均匀,不均自动触发 balancer 迁移
坑 4:shard key 选错导致热点
第一次选 shard key 用了 { created_at: 1 }(单调递增时间)
后果:所有新数据都写入最新 chunk → 同一 shard,完全没分散
监控:
> sh.status()
{ "_id" : "shard6", "host" : "shard6/sh6a..." }
data: 95% on shard6 ← 数据全堆 shard6
修法:resharding(7.0 支持)
// 7.0 新功能:在线重新分片
db.adminCommand({
reshardCollection: 'mydb.events',
key: { user_id: 'hashed' }, // 改成 hashed user_id
numInitialChunks: 60
});
// 监控 reshard 进度
db.adminCommand({ currentOp: 1, type: 'op', desc: /Resharding/ });
// 完成后验证
sh.status();
// data: 16% on each shard ← 均匀分布
坑 5:跨 shard 事务慢
业务有跨集合事务:扣库存 + 创建订单 + 写积分
分片后:三个集合可能在 3 个 shard,事务延迟从 5ms → 50ms
原因:分布式事务两阶段提交开销
修法:
1. 把强相关的集合用同 shard key 聚合
- orders + order_items 都按 user_id sharded → 同 shard,本地事务
2. 弱相关用最终一致性(消息队列异步)
- 积分异步发 Kafka,worker 消费写积分
3. 必须跨 shard 的事务,readConcern: 'snapshot' + writeConcern: 'majority'
坑 6:Change Stream 重连丢事件
Change Stream 监听 orders collection,某天断网重连后丢了 200 条事件
原因:resumeToken 过期,默认只能往回追 oplog 范围
oplog 50GB,高峰期只能存 1 小时
修法:
1. 客户端持久化最新 resumeToken(每条都存)
2. 用 startAfter 而非 resumeAfter(更宽容)
3. oplog 扩大到 7 天容量
// 持久化 resumeToken
const changeStream = db.collection('orders').watch([], {
fullDocument: 'updateLookup',
startAfter: lastResumeToken // 从存的 token 续
});
changeStream.on('change', async (change) => {
await processChange(change);
// 关键:每条都持久化
await redis.set('orders:resumeToken', JSON.stringify(change._id));
});
changeStream.on('error', async (err) => {
console.error('Change stream error, restart from saved token');
// 重启会从 lastResumeToken 续
});
监控告警
# 关键指标
- mongodb_op_counters_total # 操作计数
- mongodb_connections # 连接数
- mongodb_replset_member_health # 副本集健康
- mongodb_oplog_size_bytes # oplog 大小
- mongodb_oplog_window_seconds # oplog 时间窗口
- mongodb_locks_time_acquiring # 锁等待
- mongodb_metrics_query_executor # 查询执行
- mongodb_wiredtiger_cache_* # WiredTiger 缓存
# 告警
- alert: MongoDBReplicaLagHigh
expr: mongodb_replset_oplog_lag_seconds > 30
for: 2m
labels: { severity: warning }
- alert: MongoDBOplogWindowLow
expr: mongodb_oplog_window_seconds < 3600
for: 5m
labels: { severity: critical }
annotations:
summary: "oplog 时间窗 < 1 小时,扩 oplog 或减慢写入"
- alert: MongoDBShardChunkImbalance
expr: stddev(mongodb_shard_chunks_count) by (database) > 100
for: 30m
labels: { severity: warning }
升级后效果
指标 4.4 副本集 7.0 分片集群 变化
=========================================================
总容量 800GB 4.8TB +500%
QPS 9w 50w +5x
聚合查询 p99 5s 800ms -84%
Change Stream 延迟 2s 200ms -90%
扩容方式 纵向 横向(加 shard)
故障恢复 30s 15s
时序数据存储 普通 collection Time Series -60% 空间
业务影响:
- 大促容量不愁
- 实时分析查询能进 1s 内
- 时序监控数据存储成本降一半
避坑清单
- 不能跨 major 升级,4.4→7.0 必须经过 5.0/6.0
- 每升一版必须 setFCV 才算完整
- 升级前升 driver,老 driver 连新 MongoDB 不兼容
- shard key 选 hashed 还是 range,看查询模式
- shard key 不能改,选错只能 resharding(7.0+)或全表重建
- 跨 shard 事务慢,设计上避免
- oplog 大小按 7 天容量算,留足缓冲
- Change Stream 业务必须持久化 resumeToken
- 索引按 ESR 规则,Equality / Sort / Range 顺序
- monitoring 必须有 oplog window / replication lag / shard chunk 平衡度
总结
MongoDB 大版本升级 + 副本集 → 分片是两个大动作,不要混在一起做。我们先用 3 个月完成版本升级(4.4→5.0→6.0→7.0),稳定一个月后才做分片化。最大的认知改变:MongoDB 7.0 的 Time Series + Resharding + Queryable Encryption 是真正生产可用的杀手级特性,5.0/6.0 的版本可以跳过。如果你现在还在 4.x,直接规划升 7.0 是最划算的。Sharding 是双刃剑,业务设计不好用,反而比副本集慢 — shard key 选型一定要看真实查询模式。
—— 别看了 · 2026