我是去年接手的一个项目,业务在跑 MongoDB,数据量不大但绝对不能挂。客户预算有限,不上分片集群,也买不起商业版,要求就是高可用。我最后选了“三台云主机 + Docker + MongoDB 副本集”这个方案,一直稳定跑到今天。如果你也打算在生产环境用 Docker 部署 MongoDB 副本集,这篇可以完整给你参考:架构怎么规划、Compose 文件怎么编写、副本集初始化顺序为什么不能乱、认证开启后踩过哪些坑,我都整理在下面。
这个方案比较适合中小规模业务、微服务内网环境、自己私有的测试/预发生产集群,也适合想用一套可复现的方式快速搭建高可用 MongoDB 的团队。读之前先明确一点:这里说的“生产级”不是指大规模高并发,而是指具备持久化、认证、故障自愈、可备份恢复这几个基本能力。如果你是要做日均亿级请求的写入,那架构就完全不是这篇文章的范畴了。
1. 为什么生产环境我坚持用 Docker 跑 MongoDB 副本集
很多人听到“生产”就下意识觉得应该裸机安装,Docker 只是开发环境玩具。这个观念得改改了。我在多套环境里对比过:Docker 跑 MongoDB 的额外损耗在个位数百分比以内,但换来的是部署速度、可迁移性、版本一致性和故障恢复效率的指数级提升。
1.1 直接装 MongoDB 和 Docker 部署的最大差异
直接在三台机器上 apt install mongodb-org 然后改配置,看起来也不复杂,但现实问题在于:
- 三台机器的操作系统版本、依赖库、mongod 版本可能出现差异,一旦某台的 libssl 升级出问题,整个副本集的行为就开始诡异;
- 手动安装时 MongoDB 的数据目录、日志目录、配置文件分散在 /var/lib、/var/log、/etc 下,迁移一台节点时要把它们完整捞出来,非常容易漏;
- 如果要升级 MongoDB 版本,逐台停服务、替换二进制、检查兼容性,一个晚上就没了。
用 Docker 之后,镜像锁定了 mongo 版本,所有配置通过 bind mount 统一管理,数据卷可以在任意同配置机器上拉起来。迁移节点就是一个 docker compose up -d。实测下来,一套三节点副本集从零到全部加入并初始化完成,熟练的话 20 分钟内能搞定。
1.2 Docker 跑数据库的“性能损耗”到底有多少
我在交付前专门用 mongostat 和 mongoperf 对比过物理机直装和 Docker 容器化的吞吐与延迟。结论是:在默认桥接网络、数据目录挂载宿主机普通 SSD 的前提下,吞吐量差异在 3%-5% 左右,延迟增加不到 1ms。这对绝大多数业务来说完全无感。
当然有个前提:你启动容器时不能带着一堆不必要的重定向、磁盘限制和嵌套存储驱动。生产部署建议用 --storage-driver=overlay2、数据卷直接挂到宿主机磁盘,不要用 docker volume 的默认系统盘。云服务器环境里,直接把宿主机的一块独立数据盘挂到 /data,再 bind mount 到容器里是最稳的做法。
1.3 什么时候不推荐用 Docker 部署副本集
说实话,这套方案也不是万能的。如果你遇到下面几种情况,我更建议用物理机、云厂商的托管数据库或者 Kubernetes 方案:
- 单节点写入负载非常高,要求极低延迟、极稳定的磁盘 IOPS,这时宿主机直接跑 mongod 和容器化之间一点性能差距都会被放大;
- 需要精细控制 NUMA、透明大页、文件系统预读这些底层参数,Docker 容器对部分系统级调优存在隔离,虽然可以通过 privileged 绕开,但没必要;
- 团队缺乏 Docker 运维基础,出了问题没人能第一时间处理容器日志。
但如果你的业务属于常规 CRUD、中等 QPS、希望部署快且好维护,Docker 完全能扛住生产要求。下面我就以这套思路来拆解完整方案。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 三机三节点架构规划:从IP、数据盘到系统参数一个都不能少
生产部署最忌讳的是拿到三台机器就开始跑 docker run,最后发现端口冲突、数据目录没分开、机器时钟不同步、重启后容器没拉起。所以第一步一定是纸上谈兵——把架构图画清楚,把我们这次的角色划分和目录规划敲定了再动手。
2.1 副本集角色划分与IP/端口规划
这个方案里三台机器都是数据节点,不单独放仲裁节点。原因很简单:我们在企业中实际遇到的情况是,仲裁节点不存数据,多一台机器就多一份成本,三台机器全部做数据节点能让副本集在任一节点宕机后仍有完整数据副本。如果你的机器资源非常紧张,才考虑“2 数据 + 1 仲裁”,否则一律建议 3 数据节点。
我用到的规划如下:
| 节点 | IP 示例 | 角色 | MongoDB 端口 | 宿主机数据目录 |
|---|---|---|---|---|
| mongo-1 | 192.168.1.11 | 主节点/备份节点 | 27017 | /data/mongo/mongo-1 |
| mongo-2 | 192.168.1.12 | 副本节点 | 27017 | /data/mongo/mongo-2 |
| mongo-3 | 192.168.1.13 | 副本节点 | 27017 | /data/mongo/mongo-3 |
副本集名称我用的是 rs_prod,这个名字会写进 MongoDB 配置文件和初始化命令里,后续所有节点加入时都必须一致。端口统一用 27017,因为副本集内部通信(心跳、数据同步)也走这个端口。这里务必记住一个原则:生产环境的 MongoDB 只监听内网 IP,不要用 0.0.0.0 对公网开放,否则扫描端口的人一分钟内就能找上你。
2.2 数据盘挂载与目录划分
这步是很多人会忽略的坑。MongoDB 的数据文件对 IO 非常敏感,建议至少区分三块区域:
- 数据目录:存放 dbpath,也就是 MongoDB 实际数据文件,需要大容量且高性能;
- 日志目录:mongod 运行日志和慢查询日志,单独放可以避免和数据读写抢占 IO;
- 备份目录:定时全量备份和 oplog 备份的输出位置,最好别和数据库文件在同一个物理盘上。
我实际挂载方案是这样:三台机器都额外购买了一块数据盘,格式化为 ext4,挂载到 /data,然后在 /data 下按节点建目录。启动容器时把 /data/mongo/mongo-1:/data/db 挂进去,配置文件挂到 /etc/mongod.conf。日志我用 Docker 的 json-file 日志驱动,然后通过 logrotate 做宿主机级别的轮转,后续会展开说。
2.3 系统层准备:时钟同步、文件句柄、转发设置
部署副本集之前,三台机器需要统一做一遍基础配置,缺一个都可能让副本集出“灵异问题”。
第一,时钟同步。MongoDB 副本集的 oplog 和心跳都依赖时间戳,节点间时钟偏差过大会导致主节点选举异常或同步报错。生产环境需要配置 NTP 或 chrony,让三台机器的时间偏差保持在 1 秒以内。我之前遇到过一次主节点自动宕机,查了半天发现是某台机器时钟拨快了 10 分钟,副本集判定心跳超时触发了重新选举。
第二,文件句柄限制。MongoDB 官方建议 ulimit -n 不低于 64000。在宿主机修改 /etc/security/limits.conf,然后重启 Docker 服务。
code复制* soft nofile 64000
* hard nofile 64000
* soft nproc 64000
* hard nproc 64000
第三,禁用透明大页(THP)。THP 会让 MongoDB 出现不可预期的延迟尖刺,官方文档也建议关闭。在 /etc/rc.local 里加:
code复制if test -f /sys/kernel/mm/transparent_hugepage/enabled; then
echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag
fi
然后在宿主机执行 cat /sys/kernel/mm/transparent_hugepage/enabled 确认输出 always madvise [never]。
第四,防火墙放行。如果三台机器之间有安全组或 iptables,需要放行 27017 端口的内网互访,也就是 192.168.1.0/24 网段之间的 TCP 27017。
为啥这些参数这么重要?因为 MongoDB 本质上是个对系统环境相当敏感的应用,时钟和内存页行为一旦飘了,副本集的“自动故障转移”反而会变成“随机抖动触发器”。咱们要的是生产可用,不是给自己埋雷。
3. Docker Compose 编排核心:这份配置我是这样一行行敲出来的
现在到重头戏了。三台机器上各放一份几乎一致的 docker-compose.yml,只有节点标识和 IP 不同。为什么不直接 docker run?因为 Compose 能同时管理容器定义、重启策略、网络、数据卷和日志配置,后续维护时改一处就能统一更新。下面这份配置我实际用了很久,没有冗余项。
3.1 选择 mongo 镜像版本与拉取策略
镜像我用的是 mongo:7.0(你也可以根据生产环境选择 mongo:6.0,版本号务必写明确,别用 latest。原因不用多说,latest 哪天自动更新成不兼容版本,你的副本集协议可能就静默出问题)。在项目初期我在 Dockerfile 之外直接把镜像版本锁定到了具体小版本 mongo:7.0.14,但后来发现官方 7.0.x 补丁不少,最终固定为 mongo:7.0.14 并把这个版本号记入运维文档。
如果机器在国内、拉取镜像很慢,提前配置好 Docker Registry Mirror,比如加速器地址,不然 docker compose pull 卡在 11%、看着进度条像死了一样,太难受。
3.2 完整的 docker-compose.yml 实例
下面是 mongo-1 节点的配置,mongo-2 和 mongo-3 只需改 container_name 与 IP 相关的环境变量。
yaml复制version: "3.8"
services:
mongodb:
image: mongo:7.0.14
container_name: mongo-1
restart: always
network_mode: host
environment:
MONGO_INITDB_ROOT_USERNAME: clusadmin
MONGO_INITDB_ROOT_PASSWORD: "Your_Strong_Pass_123"
volumes:
- /data/mongo/mongo-1:/data/db
- /data/mongo/mongo-1/log:/var/log/mongodb
- /etc/mongo/mongod.conf:/etc/mongod.conf
- /etc/mongo/mongo-keyfile:/etc/mongo-keyfile
command: ["mongod", "--config", "/etc/mongod.conf"]
logging:
driver: json-file
options:
max-size: "100m"
max-file: "5"
这里我故意用了 network_mode: host 而不是默认的 bridge 端口映射。原因有两个:
- MongoDB 副本集节点之间需要通过 IP 直接通信,host 网络下 mongod 监听的地址就是宿主机 IP,副本集初始化时不用处理 Docker 内部的 NAT 转换;
- host 网络延迟更接近原生,性能更好,不会出现容器内 IP 和宿主机 IP 不一致导致的副本集成员地址错乱。
副作用是 27017 端口直接暴露在宿主机上,所以这就更要求防火墙一定要收口,只允许内网访问。
3.3 mongod.conf 的编写逻辑与关键参数解读
我的 mongod.conf 放三台机器各自的 /etc/mongo/mongod.conf,内容如下:
yaml复制storage:
dbPath: /data/db
wiredTiger:
engineConfig:
cacheSizeGB: 2
collectionConfig:
blockCompressor: snappy
systemLog:
destination: file
path: /var/log/mongodb/mongod.log
logAppend: true
verbosity: 0
net:
port: 27017
bindIp: 192.168.1.11
replication:
replSetName: rs_prod
oplogSizeMB: 2048
security:
authorization: enabled
keyFile: /etc/mongo-keyfile
processManagement:
fork: false
逐个说下关键参数。
dbPath 要和 Compose 里的数据卷挂载目标一致,我统一用容器内的 /data/db。cacheSizeGB 我给了 2GB,你的机器内存如果更大可以适当调高,WiredTiger 缓存越大,热数据命中率越高,但别超过机器内存的 50%,否则可能和操作系统页缓存抢内存,反而拖慢整体性能。
blockCompressor 我选用 snappy,它在压缩率和 CPU 开销之间比较平衡。如果你对磁盘空间极度抠门,可以换成 zlib,写放大和 CPU 开销会高一些。
oplogSizeMB 是生产环境中常被忽略的一环。它决定了副本集节点能保留多久的操作日志,直接影响备份和从节点追赶主节点的能力。我给的是 2GB,在全量备份每天执行一次、业务写入量不是天文数字的情况下,足够覆盖故障窗口。如果你的写入很频繁,建议按“保留 12-24 小时操作日志”的标准来估算这个值。
关键的是 security 这一段,authorization: enabled 和 keyFile 从第一份配置就开始放进去。这就带出一个要注意的顺序问题,我在后面第 4 节完整展开。
3.4 keyFile 生成与权限陷阱
副本集节点之间需要用 keyFile 做内部身份认证,否则节点加入时会被拒绝。生成方式:
bash复制openssl rand -base64 768 > /etc/mongo/mongo-keyfile
chown 999:999 /etc/mongo/mongo-keyfile
chmod 400 /etc/mongo/mongo-keyfile
注意几个细节,都是踩坑换来的:
- 内容必须复制到三台机器上,保持完全一致,否则节点之间验证失败;
- 属主要有用户 ID 999,这是 mongo 官方镜像内置 mongodb 用户的 UID;如果你是直接宿主机挂载文件,不改成这个属主,容器内进程可能读不到;
- 权限必须是 400 或 600,
chmod 777会被 mongod 拒绝启动,提示 keyFile 权限太宽松。
我第一次部署时所有配置都正确,但容器一直重启,docker logs 一看 Permission settings are not allowed on keyFile,就是这个权限问题。记住,任何时候看到 keyFile 报错,第一反应先检查权限。
4. 副本集初始化与认证激活:顺序错了全得重来
很多教程会把初始化和认证揉在一起,导致新手一上来就配好 keyFile 和 authorization,结果容器启动后完全无法建用户、无法执行 rs.initiate。这个环节我把标准操作顺序总结成四个阶段,并解释为什么必须按这个顺序。
4.1 阶段一:先以无认证方式启动节点
先不急着启用 authorization。三台机器上的 mongod.conf 中暂时注释掉 security.authorization 和 security.keyFile,只保留 replication 和 storage 配置。启动容器:
bash复制docker compose up -d
docker compose logs -f mongodb
看到类似 Waiting for connections 的日志说明 mongod 起来了。这个时候我用 mongosh 连到 mongo-1:
bash复制mongosh "mongodb://192.168.1.11:27017"
为什么建议注释掉 authorization 而不是不写?因为即便你写了 authorization 但没配 keyFile,单节点启动时也能通过 localhost 异常访问机制连入,但一旦配了 keyFile 且副本集还没初始化,所有节点都会认为自己是独立实例,后续初始化会异常频繁。所以最干净的做法就是第一阶段不含 security。
4.2 阶段二:在 mongo-1 上执行副本集初始化
在 mongosh 里执行:
javascript复制rs.initiate({
_id: "rs_prod",
members: [
{ _id: 0, host: "192.168.1.11:27017" },
{ _id: 1, host: "192.168.1.12:27017" },
{ _id: 2, host: "192.168.1.13:27017" }
]
})
然后从 mongo-1 上连到 mongo-2、mongo-3,分别在各自的 mongosh 里加节点:
javascript复制rs.add("192.168.1.12:27017")
rs.add("192.168.1.13:27017")
这一步常见报错是 host cannot be found in replica set 或 quorum check failed。多半是 host 网络下 bindIp 没有监听实际 IP,或者三台机器之间 27017 端口不通。用 telnet 192.168.1.12 27017 先测一下,通了再继续。
最好在 mongo-1 上执行 rs.status(),确认三个节点的 stateStr 分别是 PRIMARY、SECONDARY、SECONDARY,并且 health 字段都是 1。到这步,副本集本身已经可用,但此时还没有任何认证,谁连上 27017 都能做任意操作,所以我们必须快速进入阶段三。
4.3 阶段三:创建管理员账号和应用账号
没有认证的副本集等于把数据库裸奔在内网。现在创建管理员账号,我习惯使用:
javascript复制use admin
db.createUser({
user: "clusadmin",
pwd: "Your_Strong_Pass_123",
roles: [
{ role: "root", db: "admin" }
]
})
然后给业务程序单独建一个最小权限的账号,别让应用拿着 root 连接。示例:
javascript复制use testdb
db.createUser({
user: "appuser",
pwd: "App_User_Pass_456",
roles: [
{ role: "readWrite", db: "testdb" }
]
})
4.4 阶段四:启用 authorization 与 keyFile 并重启
把 mongod.conf 里的 security.authorization: enabled 和 security.keyFile: /etc/mongo-keyfile 恢复,然后依次在三台机器上执行:
bash复制docker compose restart mongodb
重启后再次通过 mongosh -u clusadmin -p ... --authenticationDatabase admin 验证能够访问。到这一步,认证正式生效。
为什么顺序不能乱?我解释一下内在逻辑:MongoDB 副本集在初始化过程中需要节点间互信通信,如果一开始就启用 keyFile 认证,初始化命令会因为节点之间无法完成内部认证而失败;而如果你在启用 authorization 之前没提前建好用户,启用后你又无法通过本地异常访问机制创建用户(因为副本集已经形成,不再允许本地单机直连)。所以整个过程应该理解为“复制集先行、账号随后、认证兜底”。这个顺序一旦乱掉,最轻是报错,最重是数据节点反复重启后自己把自己踢出副本集。
5. 部署完成后的验证、容灾演练与真实故障复盘
部署完不等于万事大吉。我每次交付都要做一轮完整的验证和故障演练,把副本集的“自愈能力”真实跑一遍,确认它不是纸面高可用。
5.1 验证命令与状态解读
进入 mongosh 后执行 rs.status(),重点看这几个字段:
members[].stateStr:应该有一个 PRIMARY、两个 SECONDARY;members[].health:全部为 1;members[].lastHeartbeat:心跳时间应该非常接近当前时间;members[].optimeDate:三个节点的 optime 应该基本一致,差值不能太大。
再看同步情况:
javascript复制rs.printSecondaryReplicationInfo()
这个命令会输出每个 SECONDARY 的同步源、复制延迟和 oplog 时间窗口。如果 syncedTo 里的时间落后 PRIMARY 太久,说明写入量超过了从节点追赶能力,这时候要么加大 oplogSize,要么检查从节点的性能。
5.2 故障演练:杀掉 PRIMARY 会发生什么
我直接在 mongo-1(当前 PRIMARY)上执行:
bash复制docker stop mongo-1
然后观察 mongo-2、mongo-3 的状态。正常情况下 10-30 秒内会自动选举出新的 PRIMARY,你的业务连接如果配了 directConnection=false 且设置了 replicaSet 参数,驱动会自动重连到新主节点。
javascript复制rs.status()
此时应看到新的 PRIMARY 出现在 mongo-2 或 mongo-3 上,mongo-1 的 health 变成 0。然后把 mongo-1 重新启动:
bash复制docker start mongo-1
它会自动从当前 PRIMARY 同步新数据,在 rs.status() 里恢复为 SECONDARY。这才是完整的高可用链路。
这个演练我强烈建议你在正式上线之前至少做一次,并且把结果截图都留给同事和客户。没有演练过的高可用不能叫高可用,只能叫“看起来高可用”。
5.3 我遇到过的一个“自动故障转移”翻车复盘
有一次客户反馈某一个从节点数据不一致,我没有直接重启,先查日志,发现这个从节点长期处于 RECOVERING 状态。rs.status() 显示它的 errmsg 为 syncFrom 失败,再往下查,发现这台机器当时磁盘写满了,oplog 被截断,导致它无法从主节点同步进入稳定状态。
解决办法:把该节点的数据目录清空后重新以全新的 SECONDARY 加入副本集,让它做一次全量 resync。如果你的节点是重要数据节点,别在磁盘满时反复重启,先把磁盘空间释放出来,预留足够空间让 mongod 正常同步。
生产环境为了避免同类事故,我在每台宿主机上加了磁盘空间告警,当 /data 使用量超过 80% 时立刻告警,低于这个阈值才允许写入继续。
5.4 备份策略与恢复验证
副本集不是备份。很多人以为有了 PRIMARY/SECONDARY 就不需要额外备份了,这是很大的误解——如果你的业务或人为误操作执行了 db.dropDatabase(),副本集会把这条命令也同步到所有节点,数据一样没得救。所以必须做备份。
我推荐“周期全量备份 + 持续 oplog 备份”的组合方案:
- 每天凌晨 2 点从 SECONDARY 节点执行
mongodump,输出到独立备份目录; - 每 5 分钟备份一次 admin 库的
local.oplog.rs,记录当前时间点,用于恢复到任意时间窗口。
bash复制mongodump --host 192.168.1.12:27017 \
--username clusadmin --password 'Your_Strong_Pass_123' \
--authenticationDatabase admin \
--oplog \
--out /backup/mongo_$(date +%Y%m%d)
恢复时先用 mongorestore 恢复全量备份,再回放 oplog 到指定时间点。这套流程建议每季度做一次真实的恢复演练,验证备份文件没有损坏。只备份不恢复的备份文件,本质上只是“心理安慰”。
6. 上线运行期间一定要盯的几件事
三机三节点副本集真正上线后,日常运维并不是什么都不用管了。以我的经验来看,至少要盯住下面几项,它们才是判断这套集群“生产级”够不够格的核心指标。
6.1 日志轮转与持久化收敛
如果容器连续跑上一个月,默认 json-file 日志驱动可能把宿主机磁盘撑爆。我写的 Compose 里已经加了 max-size: "100m" 和 max-file: "5",也就是说每个节点的容器日志最多保留 500MB。同时,mongod 自己的 /var/log/mongodb/mongod.log 也要靠宿主机 logrotate 轮转:
code复制/data/mongo/mongo-1/log/mongod.log {
daily
rotate 7
compress
delaycompress
missingok
notifempty
copytruncate
}
日志这类看似边缘的事,最容易在半夜变成事故。我不止一次见过因为日志文件暴涨把数据盘占满,最后 MongoDB 直接拒绝写入的情况。
6.2 监控指标与告警阈值
生产级部署,状态可视化和异常告警不能缺。我的方案是在每台宿主机部署 node_exporter,然后通过 Prometheus + Alertmanager 收集并告警。重点监控这么几类指标:
- 复制延迟(replication lag):如果 SECONDARY 的 optime 落后 PRIMARY 超过 10 秒,需要立刻处理;
- 心跳状态:任一节点 health 字段为 0,立即告警;
- 磁盘使用率:超过 70% 就要扩容或者清理;
- 连接数:MongoDB 达到最大连接数后,新请求会排队甚至被拒,需要提前关注;
- Cache 命中率:WiredTiger 缓存命中率持续走低时,应用该考虑优化查询或提升 cacheSize。
你可以在 mongosh 里执行 db.serverStatus().wiredTiger.cache 查看缓存数据,异常时结合慢查询日志基本能定位问题。
6.3 升级与变更的灰度节奏
MongoDB 版本升级、参数调整这类变更,最忌讳的是三台机器同时改。我的建议是运维变更采用“先备后主”的节奏:先从某个 SECONDARY 开始,备份数据,修改配置,验证无误,再逐步操作第二个、第三个节点。主节点放到最后,等所有从节点都稳定后,再通过 rs.stepDown() 手动切换主节点,完成最后这台变更。
rs.stepDown() 是个好命令,它会把 PRIMARY 主动降级为 SECONDARY,触发一次优雅切换,而不是暴力重启让集群自己选主。生产环境所有可以预见的切换,我都建议用主动降级,而不是直接 docker restart。
6.4 安全基线自查清单
最后给一份上线前的安全自查清单,这是我在每次交付前逐条打勾的:
- MongoDB 端口只监听内网 IP,不向公网暴露;
- 副本集 keyFile 权限为 400,属主正确;
- 所有账号使用强密码,应用账号无 root 权限;
- 开启 authorization,本地无认证实例仅存在于初始化阶段;
- 关闭 MongoDB 的 HTTP 状态接口和 REST 接口(默认关闭,但确认无异常开启);
- 数据传输如果跨机房,建议启用 TLS,否则至少保证内网隔离。
这些事项看上去琐碎,但每一项背后都对应着真实发生过的安全事件或故障。生产环境不是“能跑就行”,而是“能长期稳定、安全、可恢复地跑”。把这个基本盘守住,才算把“生产级部署”这四个字真正落地了。
