MongoDB 4.4 升 7.0 + 副本集变分片实录:6 个真实坑

MongoDB 4.4→5.0→6.0→7.0 滚动升级 + 副本集变 6 分片全过程:6 大坑(geoSearch 移除/Causal Consistency 默认开/ESR 索引/oplog 暴涨/shard key 选错/Change Stream 丢事件)+ Time Series + Resharding 在线重分片。容量 +500%,QPS +5x。

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 内
- 时序监控数据存储成本降一半

避坑清单

  1. 不能跨 major 升级,4.4→7.0 必须经过 5.0/6.0
  2. 每升一版必须 setFCV 才算完整
  3. 升级前升 driver,老 driver 连新 MongoDB 不兼容
  4. shard key 选 hashed 还是 range,看查询模式
  5. shard key 不能改,选错只能 resharding(7.0+)或全表重建
  6. 跨 shard 事务慢,设计上避免
  7. oplog 大小按 7 天容量算,留足缓冲
  8. Change Stream 业务必须持久化 resumeToken
  9. 索引按 ESR 规则,Equality / Sort / Range 顺序
  10. 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
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

Redis 主从迁 Cluster 4 周复盘:5 个大坑和真实数据

2026-5-19 12:21:33

技术教程

Vue 2 升 Vue 3 + Vite + TS 五个月实录:22 万行代码 9 个坑

2026-5-19 12:27:02

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索