MongoDB 复制集与分片集群是文档数据库做高可用和水平扩展的两根支柱:复制集解决单点故障与读扩展,分片集群解决单机容量与写入吞吐瓶颈。很多团队上线时只跑单实例,等数据涨到几百 GB、QPS 打满磁盘 IO 才想起改架构,此时迁移成本极高。本文按生产路径把两件事讲透:先把三节点复制集搭起来并演练故障转移,再判断什么时候该分片、分片键怎么选,最后给出一份踩过坑的配置清单。
一、先分清复制集与分片集群的职责
这两个概念经常被混着说,但解决的问题完全不同。复制集是把同一份数据在多个节点上保存多份副本;分片是把一份大数据按规则切成多段,分散到不同的复制集上。生产环境的标准形态是「分片集群的每个分片本身就是一个复制集」。
| 维度 | 复制集 Replica Set | 分片集群 Sharded Cluster |
|---|---|---|
| 解决问题 | 高可用、读扩展、数据冗余 | 存储容量、写入吞吐水平扩展 |
| 最小节点 | 3 个(1 主 2 从,或 1 主 1 从 1 仲裁) | config 复制集 + 2 分片 + mongos |
| 数据分布 | 每个节点存全量数据 | 每个分片存一部分数据 |
| 写入能力 | 只有主节点可写,写不扩展 | 多分片并行写,随分片数线性扩展 |
| 运维复杂度 | 低,建议所有生产环境必配 | 高,数据量小于 1TB 通常不必上 |
| 典型触发点 | 任何线上业务 | 单分片数据超 1–2TB 或写入打满磁盘 |
结论很直接:复制集是必选项,分片是按需项。如果你现在还是单实例,第一步永远是补复制集,而不是急着分片。这个判断逻辑和关系型数据库一致,可以对照我们之前写过的 MySQL 主从复制与读写分离:高可用架构实战 一起看,两者在选主、延迟、读写分离上的取舍非常相似。
二、三节点复制集搭建:从配置到初始化
2.1 配置文件与启动
三台机器使用同一个 replSetName,并开启 keyFile 内部认证(生产必须开,否则任意节点可加入集群)。先在任一节点生成密钥并同步到三台:
# 生成集群内部认证密钥(三节点必须完全一致)
openssl rand -base64 756 > /etc/mongo/keyfile
chmod 400 /etc/mongo/keyfile
chown mongod:mongod /etc/mongo/keyfile
# 分发到另外两台
scp /etc/mongo/keyfile root@10.0.0.12:/etc/mongo/keyfile
scp /etc/mongo/keyfile root@10.0.0.13:/etc/mongo/keyfile
三台节点的 /etc/mongod.conf 内容一致,只需保证 bindIp 能被其他节点访问:
storage:
dbPath: /var/lib/mongo
wiredTiger:
engineConfig:
# 建议设为物理内存的 50%,不要贪心占满
cacheSizeGB: 8
net:
port: 27017
bindIp: 0.0.0.0
security:
authorization: enabled
keyFile: /etc/mongo/keyfile
replication:
replSetName: rs0
# oplog 决定从节点能落后多久还能追上,生产建议 20-50GB
oplogSizeMB: 20480
operationProfiling:
mode: slowOp
slowOpThresholdMs: 200
2.2 初始化复制集并设置优先级
三台都启动后,连到计划作为主节点的那台执行初始化。priority 控制选主倾向,hidden + priority:0 可以做只备份不参与选主的隐藏节点:
rs.initiate({
_id: "rs0",
members: [
{ _id: 0, host: "10.0.0.11:27017", priority: 10 },
{ _id: 1, host: "10.0.0.12:27017", priority: 5 },
// 隐藏节点:只做备份和离线分析,不接客户端读,也不会被选为主
{ _id: 2, host: "10.0.0.13:27017", priority: 0, hidden: true, votes: 1 }
],
settings: {
// 心跳超时,网络抖动频繁的机房可适当放大到 15
electionTimeoutMillis: 10000
}
})
// 查看状态:关注 stateStr(PRIMARY/SECONDARY)与 optimeDate 差值
rs.status().members.forEach(m =>
print(m.name, m.stateStr, m.optimeDate)
)
// 复制延迟一屏看清
rs.printSecondaryReplicationInfo()
初始化成功后必须马上建管理员账号,否则开了 authorization 却没有用户,重连就进不去了:
use admin
db.createUser({
user: "root",
pwd: passwordPrompt(),
roles: [ { role: "root", db: "admin" } ]
})
// 业务库单独授权,禁止业务方直接用 root
use appdb
db.createUser({
user: "app_rw",
pwd: passwordPrompt(),
roles: [ { role: "readWrite", db: "appdb" } ]
})
三、故障转移演练:别等真宕机才验证
复制集搭好只是第一步,真正决定可用性的是「主节点挂掉后业务多久恢复写入」。上线前必须演练一次,而且要在有真实流量压测的情况下演练。手动触发切主:
# 在当前主节点执行:主动让位,60 秒内不再参与选主
rs.stepDown(60)
# 观察选举耗时(正常 2-12 秒完成新主选举)
watch -n1 'mongosh --quiet --eval "rs.status().members.map(m=>m.name+\":\"+m.stateStr)"'
# 更真实的演练:直接 kill 主节点进程
systemctl stop mongod
演练时重点观察三件事:选举耗时是否在 SLA 内、应用是否自动重连、有没有写丢失。第三点取决于 writeConcern 配置。连接串必须写全部节点并带 replicaSet 参数,驱动才能感知拓扑变化:
# 正确写法:列出所有可路由节点 + replicaSet + 写关注 + 重试写
mongodb://app_rw:pwd@10.0.0.11:27017,10.0.0.12:27017/appdb?replicaSet=rs0&w=majority&retryWrites=true&readPreference=primaryPreferred&maxPoolSize=100
# 错误写法:只写单节点,主挂了应用直接全线报错,不会自动切换
mongodb://app_rw:pwd@10.0.0.11:27017/appdb
3.1 写关注与读偏好的取舍
这两个参数直接决定「数据安全」和「延迟」的平衡点,是复制集调优里最容易配错的地方:
| 参数 | 取值 | 效果与代价 | 建议场景 |
|---|---|---|---|
| writeConcern | w:1 | 主节点写成功即返回,主挂可能丢最后几条 | 日志、埋点等可容忍丢失的数据 |
| writeConcern | w:”majority” | 多数节点确认才返回,延迟增加但不丢,配合 retryWrites 可安全重试 | 订单、支付、账务(推荐默认) |
| writeConcern | j:true | 额外要求刷盘日志,延迟再增 | 金融强一致场景 |
| readPreference | primary | 强一致,但读压力全在主 | 写后立即读的业务 |
| readPreference | secondaryPreferred | 读打到从节点,可能读到旧数据 | 报表、列表页、搜索 |
| readPreference | primaryPreferred | 主可用走主,主挂降级读从 | 通用默认,可用性优先 |
一个高频误区:以为配了 secondaryPreferred 就等于读扩展翻倍。实际上从节点还要执行 oplog 回放,本身也有 IO 开销;如果查询没走索引,从节点只会更慢。读扩展的前提永远是索引先做对,这部分可以参考 MongoDB 索引优化:复合索引、覆盖查询与慢查询诊断。
四、什么时候该上分片
分片会带来 mongos 路由层、config 元数据集群、chunk 迁移、跨分片聚合等一整套复杂度。满足以下任一条件再考虑,否则先做索引优化和垂直扩容:
- 单个复制集数据量接近或超过 1–2TB,备份恢复窗口已经不可接受
- 工作集(热数据)远大于内存,WiredTiger cache 命中率长期低于 90%,磁盘随机 IO 打满
- 写入 QPS 已经把主节点 CPU 或磁盘写带宽压满,加索引和批量写都救不回来
- 业务有明确的按租户、按时间隔离归档需求,分片能顺带解决数据生命周期管理
五、分片集群落地:分片键决定生死
分片集群由三部分组成:mongos 无状态路由、config server 复制集存元数据、若干 shard(每个都是复制集)。搭建顺序是 config → shard → mongos:
# 1) config server 复制集(配置文件加 clusterRole)
# sharding:
# clusterRole: configsvr
mongosh --port 27019 --eval 'rs.initiate({
_id:"cfg0", configsvr:true,
members:[{_id:0,host:"10.0.1.11:27019"},{_id:1,host:"10.0.1.12:27019"},{_id:2,host:"10.0.1.13:27019"}]
})'
# 2) 每个分片各自初始化为复制集(clusterRole: shardsvr)
# 3) 启动 mongos 指向 config 复制集
mongos --configdb cfg0/10.0.1.11:27019,10.0.1.12:27019,10.0.1.13:27019 \
--bind_ip 0.0.0.0 --port 27017 --keyFile /etc/mongo/keyfile --fork \
--logpath /var/log/mongos.log
# 4) 通过 mongos 把分片加入集群
mongosh --port 27017 --eval '
sh.addShard("shard1/10.0.2.11:27017,10.0.2.12:27017");
sh.addShard("shard2/10.0.3.11:27017,10.0.3.12:27017");
sh.status();'
5.1 分片键选择:三个必须满足的条件
分片键一旦选错,后果是数据倾斜到单个分片(等于没分片)或所有查询都要广播到全部分片。好的分片键要同时满足高基数、写入分布均匀、与高频查询条件匹配:
# 开启数据库分片
sh.enableSharding("appdb")
# 反例:用自增时间做范围分片键 —— 新数据全部涌向最后一个 chunk,热点分片
sh.shardCollection("appdb.events", { createdAt: 1 })
# 反例:纯哈希 _id —— 分布均匀但按租户查询会广播到所有分片
sh.shardCollection("appdb.orders", { _id: "hashed" })
# 推荐:复合分片键,前缀是高频查询维度,后缀提供高基数打散
sh.shardCollection("appdb.orders", { tenantId: 1, orderId: 1 })
# 时序数据推荐:租户 + 哈希打散,避免时间热点
sh.shardCollection("appdb.events", { tenantId: 1, deviceId: "hashed" })
# 检查数据分布是否倾斜(关注各分片 chunk 数与 size 是否接近)
db.orders.getShardDistribution()
分片键的设计其实是文档模型设计的延续——先想清楚查询模式,再决定字段结构和键顺序。如果模型阶段就没规划好访问路径,分片键几乎必然会选错,建议先回看 MongoDB 文档建模:嵌入式 vs 引用式设计与实战。
5.2 balancer 与 chunk 迁移窗口
MongoDB 会自动在分片间迁移 chunk 做均衡,但迁移本身消耗 IO 和网络。生产环境应把 balancer 限制在业务低峰执行:
use config
// 只允许凌晨 1:00-5:00 迁移
db.settings.updateOne(
{ _id: "balancer" },
{ $set: { activeWindow: { start: "01:00", stop: "05:00" } } },
{ upsert: true }
)
// 大促前临时关闭均衡,结束后再打开
sh.stopBalancer()
sh.startBalancer()
// 查看是否正在迁移
sh.isBalancerRunning()
六、生产避坑清单
- 偶数投票节点:两节点复制集无法完成多数选举,主挂即全集群只读。至少三个投票成员,或补一个 arbiter(但 arbiter 不存数据,会削弱 w:majority 的安全性,能用数据节点就别用 arbiter)。
- oplog 太小:从节点停机维护稍久就超出 oplog 窗口,只能全量重同步。用
rs.printReplicationInfo()确认 oplog 覆盖时长至少大于最长维护窗口的 2 倍。 - 忘记 keyFile:只开 authorization 不配 keyFile,节点间认证失败会导致复制集反复选主。
- 连接串只写一个节点:驱动无法感知新主,切换后应用持续报错,这是故障放大的头号原因。
- 跨分片聚合未优化:
$lookup、无分片键前缀的sort会退化为 scatter-gather,延迟随分片数上升。用explain("executionStats")确认是否走 SHARD_MERGE。 - 没有监控延迟:复制延迟和 chunk 分布必须进监控面板,否则从节点落后几小时都没人知道。指标接入可参考 Prometheus + Grafana 监控面板实战,用 mongodb_exporter 采集
mongodb_mongod_replset_member_replication_lag。 - 用分片替代索引:慢查询根因是缺索引时,加分片只会把慢查询复制到每个分片,问题不解决还更贵。
七、总结
MongoDB 高可用的落地顺序是清晰的:先三节点复制集 + w:majority + retryWrites,把可用性和数据安全兜住;再用监控确认瓶颈到底在索引、内存还是磁盘;只有确认是容量或写入吞吐触顶,才引入分片,并且把分片键设计当成和表结构同等重要的决策来做。多数团队真正需要的是把复制集配对、把索引建对,而不是过早引入分片集群的运维负担。上线前务必做一次带流量的故障转移演练——没演练过的高可用,等于没有高可用。




