1. 先搞清楚"集群"到底指什么,否则第一步就走偏
很多人搜"docker 部署 mongodb 集群",脑子里想的是一套东西,实际动手时才发现 MongoDB 的"集群"至少分三种形态,部署方式完全不同。我见过太多人兴冲冲拉起三个容器,结果连副本集都跑不起来,问题就出在没先分清目标。
第一种是副本集(Replica Set):一份数据存在多个节点上,主节点写、从节点读,主节点挂了自动选举。这是生产环境最常见的高可用方案,也是绝大多数人真正需要的东西。第二种是分片集群(Sharded Cluster):数据按分片键切分到不同分片上,前面有 mongos 做路由,后面有 config server 存元数据。这个适合单机装不下、写入量极大的场景,组件多、部署复杂。第三种是很多人误以为的"集群"——其实就是同一台机器上跑多个独立实例,互不相干,根本不是集群。
这篇内容主要围绕副本集展开,因为它是性价比最高、使用面最广的方案,同时也把分片集群的组件关系和部署要点讲清楚,你按自己的业务量选。如果你只是想在本地做开发测试,单节点副本集(一个容器,但配置成副本集模式)就够用了,因为很多驱动和事务特性要求必须连到副本集,单机模式会报错。
先把结论说在前面:用 Docker 部署 MongoDB 副本集,核心难点不在容器编排,而在"节点之间怎么互相发现"和"初始化只能做一次"这两个坑上。容器内的 MongoDB 默认只监听本地回环,配置文件要改;副本集初始化时的 host 名必须用容器网络里的可解析名称,不能用 127.0.0.1,否则选举和同步全乱。这两点后面会详细拆。
提示:本文所有操作基于 MongoDB 6.x/7.x 与 Docker 24+ 验证,低版本命令基本兼容,但
rs.initiate的返回格式和部分参数有差异,实操时留意。
适合读这篇的人:有基本 Docker 使用经验(会写 compose 文件、会看容器日志),了解 MongoDB 基本增删改查,但没在生产环境搭过副本集,或者搭过但踩了坑没搞明白为什么。完全没碰过 Docker 的,建议先把镜像、容器、网络、卷这四个概念过一遍再回来。
2. 容器化 MongoDB 的三条硬约束:为什么不能照搬物理机经验
在物理机上装 MongoDB 副本集,大家习惯把配置文件写在/etc/mongod.conf,用 systemd 管进程,节点间用主机名或 IP 通信。搬到容器里,这套经验会碎一地。原因不是 Docker 不好用,而是容器的运行模型和物理机有本质差异。理解这三条约束,后面的配置你就不用死记硬背。
2.1 约束一:容器内进程必须前台运行,且不能"自杀"
Docker 容器的生命周期绑定在主进程上,主进程一退,容器就停。MongoDB 官方镜像默认就是前台运行mongod,这点没问题。但有个细节容易翻车:如果你在启动命令里加了--fork(后台守护),容器会立刻退出,日志里什么都看不到,docker ps -a显示Exited (0),看着像成功其实是失败。物理机上用--fork是常规操作,容器里绝对不行。
另一个相关问题是信号处理。docker stop默认发 SIGTERM,等 10 秒后发 SIGKILL 强杀。MongoDB 收到 SIGTERM 会走正常关闭流程,刷盘、释放锁。如果你在 compose 里设置了很短的健康检查间隔或者用docker kill,可能打断写入导致数据文件损坏。我一般会给stop_grace_period显式设成 30 秒,给足刷盘时间:
services: mongo1: image: mongo:7.0 stop_grace_period: 30s这个参数不起眼,但在频繁重启的调试阶段救过我好几次,尤其是数据量大了以后,刷盘时间会明显变长。
2.2 约束二:监听地址必须从 127.0.0.1 改成 0.0.0.0
官方镜像的默认配置里,bindIp是127.0.0.1。容器内部这个地址只指向容器自己,别的容器根本连不进来。你必须通过配置文件或命令行参数把它改成0.0.0.0,让 MongoDB 监听所有网卡。
这里有个安全层面的顾虑需要说清楚:改成0.0.0.0意味着容器网络内所有能访问到该端口的进程都能连。所以必须配合认证机制,也就是启用--auth或者配置文件里的security.authorization: enabled,并且创建管理员账号。只在开发环境图省事不开认证,上线前一定要补上。我的做法是:首次启动不开认证,用 localhost 例外机制创建好管理员用户(MongoDB 有个本地连接不受认证限制的特性),然后重启开启认证,这个流程后面会给完整脚本。
2.3 约束三:数据必须落在命名卷或绑定挂载上
容器删了数据就没了,这是常识。但这里有个更隐蔽的问题:副本集的 local 数据库。MongoDB 在local库里存了 oplog 和节点身份信息,如果用匿名卷,容器重建后 local 库丢失,节点会认为自己是个全新节点,需要重新加入副本集,甚至可能触发全量同步。数据量大的时候,这个全量同步能跑几个小时。
所以数据目录必须用命名卷(named volume)持久化,并且同一个节点的卷不能和其他节点共用。我见过有人为了省事,三个节点挂同一个宿主机目录,结果三个 mongod 抢同一批文件,数据直接损坏。这种错误在容器里特别容易犯,因为挂载路径写错一个字符就撞上了。
| 约束点 | 物理机做法 | 容器里的正确做法 | 踩坑后果 |
|---|---|---|---|
| 进程管理 | systemd + --fork | 前台运行,不加 fork | 容器秒退,误判成功 |
| 监听地址 | bindIp 127.0.0.1 | bindIp 0.0.0.0 + 认证 | 节点间无法通信 |
| 数据持久化 | 本地磁盘路径 | 每节点独立命名卷 | 数据丢失/文件损坏 |
| 关闭方式 | systemctl stop | 预留足够 stop_grace_period | 写中断、文件损坏 |
这张表建议存下来,排查问题时对照看,能省不少时间。
3. 动手前的地基工程:网络、密钥与目录规划
真正开始写 compose 之前,有三件事必须先定好,否则后面改起来很痛苦。这三件事是:容器网络怎么划、副本集密钥怎么生成、宿主机目录(或卷)怎么分配。很多人上来就抄一份 compose 文件,跑起来发现节点互相连不上,再回头改网络,前面配的东西全要重来。
3.1 自定义 bridge 网络是必须的,默认网络不够用
Docker 的默认 bridge 网络有个坑:容器之间只能用 IP 互相访问,不支持通过容器名做 DNS 解析。而 MongoDB 副本集的配置里,成员地址最好写可解析的主机名,否则主节点换了 IP 之后从节点就找不到它了。虽然 MongoDB 支持用 IP 配成员,但 IP 变化(容器重启会变)会导致副本集配置过时,需要手动rs.reconfig,运维成本很高。
所以第一步是建一个自定义网络:
docker network create mongo-cluster --subnet 172.20.0.0/16指定 subnet 不是必须的,但建议指定。理由有两个:一是避免和宿主机上已有的网段冲突(默认分配的网段有时会撞);二是固定网段后,排查网络问题时心里有数,docker network inspect一看就知道地址段。子网大小/16够用了,副本集加监控组件也就十来个容器。
建好网络后,所有节点都加进去,这样节点之间可以用服务名互相解析。在 compose 里体现就是每个服务加networks: - mongo-cluster,并且给每个服务起一个固定名字,比如mongo1、mongo2、mongo3。
3.2 副本集密钥文件:权限和一致性都不能马虎
副本集内部节点之间要做认证,靠的是 keyfile。这个文件有两个硬性要求:所有节点的密钥内容必须完全一致,文件权限必须是 400。少一个条件,mongod启动时直接报错退出,日志里写permissions on /data/keyfile are too open,一看就懂。
生成方式很简单:
openssl rand -base64 756 > mongo-keyfile chmod 400 mongo-keyfile756 字节的 base64 是个经验值,MongoDB 要求密钥长度在 6 到 1024 字符之间,756 够长且不会因为太长影响性能。生成一次,三个节点挂同一个宿主机文件(用绑定挂载只读挂进去)。注意这里是同一个文件挂三次,不是每个节点复制一份——虽然复制一份内容一致也行,但同步维护容易忘,改了一次没改另外两次就是灾难。
注意:keyfile 里绝对不能有多余的换行或者空格。用
openssl rand生成一般没问题,但如果你手动编辑过,最好用wc -c确认一下长度,cat -A看一眼有没有^M这种 Windows 换行符混进去。
挂载到容器里之后,权限问题还有个补充:容器内 mongod 默认以mongodb用户(uid 999)运行,宿主机文件属主如果不是这个 uid,可能读不了。保险做法是把 keyfile 的属主改成 999:
sudo chown 999:999 mongo-keyfile不同镜像版本 uid 可能不一样,mongo:7.0是 999,更早的版本可能是 999 或者 1000,用docker run --rm mongo:7.0 id mongodb可以确认。
3.3 目录规划:一个节点一套配置,但可以复用模板
三个节点的配置文件内容高度相似,只有少数几项不同。我不建议为每个节点手写一份完整配置,容易抄漏。推荐做法是:写一份模板,用环境变量或者 compose 里的 command 覆盖关键项。
MongoDB 的配置文件是 YAML 格式,关键配置项有这些:
storage: dbPath: /data/db wiredTiger: engineConfig: cacheSizeGB: 1 systemLog: destination: file path: /data/db/mongod.log logAppend: true net: port: 27017 bindIp: 0.0.0.0 replication: replSetName: rs0 security: keyFile: /data/keyfile几个值的取法值得说说。cacheSizeGB是 WiredTiger 缓存大小,默认取(物理内存 - 1GB) / 2,但容器里 MongoDB 读到的是宿主机内存,容易把缓存开得过大导致被 OOM Killer 干掉。所以容器化部署建议显式设置,一般给容器内存上限的 50% 到 60%。如果你给容器分配 2GB 内存,cacheSizeGB设 1 就合理。replSetName三个节点必须一致,我习惯用rs0,你也可以用业务相关的名字,但一旦初始化后就不能改,改名意味着重建副本集。
systemLog.destination: file是可有可无的选择。设成 file 之后,日志写在容器内的文件里,docker logs就看不到内容了,排查时要去卷里翻文件。我个人的偏好是设成stdout(或者干脆不配这一段用默认),让日志走标准输出,docker logs -f mongo1就能实时看,容器场景下这个更顺手。日志文件写多了还会占满卷空间,需要额外做轮转配置,反而增加复杂度。
4. 一份能直接跑的 compose 文件,逐段拆给你看
前面铺垫完了,这里是完整的 compose 文件。我把它拆成几个段落讲,每段说明为什么这么写,你可以整体复制后按需修改。
services: mongo1: image: mongo:7.0 container_name: mongo1 hostname: mongo1 restart: unless-stopped stop_grace_period: 30s ports: - "27017:27017" volumes: - mongo1-data:/data/db - ./mongo-keyfile:/data/keyfile:ro - ./mongod.conf:/etc/mongod.conf:ro command: ["mongod", "--config", "/etc/mongod.conf"] networks: - mongo-cluster ulimits: nofile: soft: 64000 hard: 64000 deploy: resources: limits: memory: 2g mongo2: image: mongo:7.0 container_name: mongo2 hostname: mongo2 restart: unless-stopped stop_grace_period: 30s ports: - "27018:27017" volumes: - mongo2-data:/data/db - ./mongo-keyfile:/data/keyfile:ro - ./mongod.conf:/etc/mongod.conf:ro command: ["mongod", "--config", "/etc/mongod.conf"] networks: - mongo-cluster deploy: resources: limits: memory: 2g mongo3: image: mongo:7.0 container_name: mongo3 hostname: mongo3 restart: unless-stopped stop_grace_period: 30s ports: - "27019:27017" volumes: - mongo3-data:/data/db - ./mongo-keyfile:/data/keyfile:ro - ./mongod.conf:/etc/mongod.conf:ro command: ["mongod", "--config", "/etc/mongod.conf"] networks: - mongo-cluster deploy: resources: limits: memory: 2g volumes: mongo1-data: mongo2-data: mongo3-data: networks: mongo-cluster: external: true4.1 为什么要写 hostname,它和 container_name 有什么区别
container_name决定容器在宿主机上的名字,docker ps看到的是它。hostname决定容器内部hostname命令返回什么,以及在自定义网络里 DNS 解析用的名字。这两个我都设成一样的值,是为了避免自己看混。
关键在于:MongoDB 副本集初始化时,成员地址用的是mongo1:27017这种形式,这个mongo1必须能在容器网络里解析到。自定义 bridge 网络下,Docker 内置 DNS 会把容器的 hostname 和 container_name 都注册进去(不同版本行为略有差异),所以两个都设成相同值最保险。如果你只设 container_name 不设 hostname,绝大多数情况也能解析,但个别 Docker 版本或者用了 swarm 模式时会有差异,索性都写上。
端口映射这块,三个节点都映射到宿主机不同端口(27017/27018/27019)。这么做的目的是方便你从宿主机上的 MongoDB Compass 或者 mongosh 直连调试。生产环境如果不打算从宿主机直接访问,可以不映射,只让容器网络内部通信,安全性更好。这里保留映射是为了演示方便。
4.2 ulimits 为什么必须调,默认值会出什么事
MongoDB 对文件描述符数量有要求,官方建议 64000 以上。Docker 默认的nofile限制是 1024(继承自宿主机 systemd 的默认值),在连接数一多或者并发写入大的时候,会出现Too many open files错误,日志里能看到,然后连接被拒。
这个错误有个特点:低负载测试时完全不出现,压测或者上线后才爆发。我一开始搭测试环境没设 ulimits,本地跑得好好的,接入真实流量后半夜告警,排查了半天才定位到。所以别省这一行配置。deploy.resources.limits也是同理,容器不限制内存的话,MongoDB 会按宿主机总内存来计算 WiredTiger 缓存,很可能把整台机器拖垮。
这里要提醒一点:deploy段在docker compose(非 swarm 模式)下的支持情况随版本变化,较新的 Compose V2 是支持的。如果你用的版本不生效,可以改用mem_limit: 2g这种顶层写法,效果一样。
4.3 首次启动后必须做的初始化,以及为什么只能做一次
三个容器起来之后,副本集还没形成,需要手动初始化。这一步是新手最容易出问题的地方。
先进入任意一个节点:
docker exec -it mongo1 mongosh然后执行初始化:
rs.initiate({ _id: "rs0", members: [ { _id: 0, host: "mongo1:27017", priority: 2 }, { _id: 1, host: "mongo2:27017", priority: 1 }, { _id: 2, host: "mongo3:27017", priority: 1 } ] })返回{ ok: 1 }表示成功。等几秒,提示符会从>变成rs0 [primary] >,说明这个节点当选为主节点。
为什么说初始化只能做一次:rs.initiate在一个已经有配置的节点上再次执行会报错already initialized。更麻烦的情况是:你在一个节点上初始化了,又在另一个节点上执行,可能导致配置冲突,需要rs.reconfig甚至清空数据重来。所以流程是:只在一个节点上初始化,其他节点什么都不用做,等它们自动同步配置。
还有个隐蔽的坑:初始化时 members 里的 host 写的是mongo1:27017,这个地址会被写进副本集配置,所有节点都按这个地址去连。如果你写成了127.0.0.1:27017,主节点自己是能连上自己,但从节点连127.0.0.1连的是它自己,发现"这个地址上的节点 ID 不对",同步就失败了。这是最经典的错误,日志里表现为replica set member not reachable或者心跳超时。
priority字段的作用是控制选举优先级,值大的优先当选主节点。我给 mongo1 设了 2,另外两个是 1,这样在主节点选举时 mongo1 更可能胜出,行为可预测。生产环境里通常不这么设,让它们平等(都是 1),避免某个节点长期承担写压力。这里这么配主要是演示这个字段的用法。
5. 认证、初始化数据与连接串:上线前必须补齐的三件事
副本集跑起来、rs.status()显示三个节点都健康,很多人到这就以为完事了。实际上还差三步才能算可用:创建用户并开启认证、初始化业务数据、把连接串配好。这三件事顺序有讲究,搞反了要回滚重来。
5.1 先建用户再开认证,顺序不能反
MongoDB 有个特性叫localhost exception(本地连接例外):在没有创建任何用户的情况下,从本机(127.0.0.1)连接可以执行创建用户的命令,不需要认证。这个机制就是给初始化用的。
所以正确流程是:
- 副本集初始化完成,主节点就绪,此时还没开认证(配置文件里
security.authorization没写或者注释掉) - 从主节点容器内部连进去,创建管理员用户
- 修改配置文件,加上
security.authorization: enabled - 重启三个节点
- 用管理员账号登录,创建业务库的专用用户
创建管理员的命令:
use admin db.createUser({ user: "admin", pwd: "换成强密码", roles: [{ role: "root", db: "admin" }] })密码别用弱口令,也别硬编码在脚本里提交到代码仓库。我的习惯是放到环境变量文件里,compose 用env_file引入,.env加进.gitignore。
这里有个顺序陷阱:如果你先开了认证再想创建用户,会发现连不上——因为没有用户能通过认证。这时候只能把配置改回去重启,或者用--noauth临时启动。我踩过一次,配置文件改完直接docker compose restart,然后所有操作都被拒,折腾了十几分钟才反应过来。
注意:副本集模式下创建用户,命令必须在主节点执行。如果你连的是从节点,会报
not master错误。rs.status()里看myState为 1 的就是主节点。
5.2 业务用户与权限最小化,别图省事全用 root
管理员建好后,业务侧要单独建用户,只给必要的权限:
use appdb db.createUser({ user: "appuser", pwd: "另一个强密码", roles: [ { role: "readWrite", db: "appdb" } ] })权限最小化不是形式主义。用 root 账号跑业务代码,一旦应用被入侵或者代码有注入漏洞,攻击者拿到的是整个数据库实例的控制权,能删库、能改其他库的数据。给readWrite只限单库,破坏范围可控。如果业务需要读其他库的集合,单独加对应的 role,别嫌麻烦。
还有个细节:副本集里用户信息是存在admin库的system.users集合里,会通过 oplog 同步到所有节点。所以你在主节点建的用户,从节点也能用来认证,不需要在每个节点重复建。
5.3 连接串的写法,以及 readPreference 怎么选
应用侧连接副本集,连接串要写全所有成员,或者至少写多个,让驱动能做故障转移:
mongodb://appuser:password@mongo1:27017,mongo2:27017,mongo3:27017/appdb?replicaSet=rs0&authSource=admin几个参数值得展开说。replicaSet=rs0告诉驱动这是个副本集,驱动会做拓扑发现,自动识别主从。authSource=admin指定认证用的库,用户建在 admin 库就必须带上这个参数,否则驱动会去appdb里找用户,认证失败。这个参数漏掉是高频错误,报错信息也不直白,只说认证失败,不告诉你去哪找用户。
readPreference决定了读操作打到哪。默认是primary,所有读都走主节点,数据最新但主节点压力大。改成secondaryPreferred可以让读请求优先走从节点,分担主节点压力,代价是可能读到稍微旧的数据(复制延迟通常在毫秒级,但网络抖动时可能到秒级)。业务能接受轻微延迟就用这个,对读多写少的场景提升明显。如果要强一致,就保持primary。
从外部(宿主机)访问的话,注意连接串里的主机名。容器网络里的mongo1在宿主机上解析不了,要用localhost:27017,localhost:27018,localhost:27019。但这里有个坑:副本集配置里存的是容器内的地址,从宿主机连的时候驱动做拓扑发现会拿到mongo1:27017这些地址,宿主机解析不了,导致连接失败。解决办法有两个:一是在宿主机/etc/hosts里加上容器 IP 的映射,二是通过 SSH 隧道或者进容器内部操作。开发阶段我一般直接docker exec -it mongo1 mongosh进去操作,省去这些麻烦。
| 场景 | 连接串主机部分 | 额外参数 |
|---|---|---|
| 应用在容器网络内 | mongo1:27017,mongo2:27017,mongo3:27017 | replicaSet=rs0&authSource=admin |
| 应用在宿主机 | localhost:27017,localhost:27018,localhost:27019 | 需配 hosts 映射才能拓扑发现 |
| 生产环境 | 各节点内网域名 | 加 tls=true 与超时参数 |
6. 验证副本集真的健康:比 rs.status 更细的几个检查点
rs.status()返回ok: 1不代表一切正常。我见过状态显示正常但实际同步卡住的情况,也见过节点显示SECONDARY但其实延迟了几个小时。这里给几个更细的检查维度,都是实战里攒下来的。
6.1 逐个看成员状态和同步延迟
rs.status().members.forEach(m => { print(m.name, m.stateStr, m.optimeDate, m.health) })关注三个字段:stateStr应该是PRIMARY一个、SECONDARY两个(或相反数量,取决于节点数);health为 1 表示健康,0 表示不可达;optimeDate各节点之间差距应该在秒级以内。如果某个从节点的optimeDate落后主节点很多,说明同步有问题,可能是网络慢、磁盘 IO 瓶颈,或者从节点在做全量同步。
全量同步有个识别特征:从节点状态是STARTUP2而不是SECONDARY,rs.status()里能看到syncingTo字段。这个阶段数据量大的话可能持续很久,期间从节点不对外服务。如果你不知道这个状态,会误以为节点挂了。
6.2 写一条数据验证端到端链路
光看状态不够,实际写一条验证:
use testdb db.testcoll.insertOne({ msg: "hello", ts: new Date() }) db.testcoll.countDocuments()然后在从节点上读(需要先在从节点执行rs.secondaryOk()允许从节点读):
rs.secondaryOk() use testdb db.testcoll.find()能看到刚才写的数据,说明复制链路通了。这个验证比看状态更实在,因为状态可能因为配置问题显示正常但实际不复制——虽然少见,但确实遇到过:keyfile 权限对了、网络通了、配置也一致,但某个节点的replSetName拼写差了一个字符,状态显示在集合里但一直不同步。这种问题只能靠实际读写发现。
6.3 故障转移演练:主动停掉主节点看会不会自动选主
这一步很多人跳过,但它是副本集存在的意义,必须验证。操作很简单:docker stop mongo1,然后等 10 到 30 秒(默认 electionTimeoutMillis 是 10 秒),再连 mongo2 看状态:
rs.status().members.forEach(m => print(m.name, m.stateStr))正常情况下,mongo2 或 mongo3 会变成PRIMARY。这时候往新主节点写入应该成功。然后把 mongo1 启回来docker start mongo1,它会以SECONDARY身份重新加入,并同步停掉期间缺失的数据。
这个演练有几个观察点:选主耗时多长(正常 10 到 30 秒)、期间应用有没有正确重试、旧主节点回来后能不能追上数据。如果旧主节点回来一直停在STARTUP2,可能是它的 oplog 里缺失的数据太多,需要全量同步,数据量大的话恢复时间很长——这也是为什么生产环境要把 oplog 调大,让从节点能通过增量同步追上,而不是每次重启都全量同步。
oplog 大小在配置文件里设:
replication: replSetName: rs0 oplogSizeMB: 2048默认 oplog 大小跟磁盘和引擎有关,通常偏小。按经验,oplog 至少要能覆盖一次全量备份的时间窗口加上业务高峰的写入量。给 2GB 起步,写入量大的给到 10GB 以上。调整 oplog 大小需要重启节点并用replSetResizeOplog命令,不是改配置就立刻生效,这点要注意。
7. 分片集群:什么时候真的需要,以及多出来的组件怎么摆
副本集解决了高可用,但没解决容量和写入吞吐的横向扩展。当单机磁盘装不下、或者单节点写入成为瓶颈时,才需要上分片集群。我先把话说清楚:分片集群的运维复杂度是副本集的数倍,能不分片就不分片。很多业务量用副本集加读写分离、索引优化就能撑住,盲目上分片反而增加故障点。
真到了要分片的时候,组件关系是这样的:mongos(路由)接收客户端请求,根据分片键把请求转发到对应的分片;config server(配置服务器)存集群元数据,必须是副本集形式(3 个节点);shard(分片)存实际数据,每个分片本身也是一个副本集。所以一个最小可用的分片集群,容器数量是mongos + 3 个 config + 3 个 shard 节点起,至少 7 个容器。
7.1 config server 的部署要点
config server 必须以副本集模式启动,配置里加--configsvr参数,副本集名字通常用cfg:
services: cfg1: image: mongo:7.0 hostname: cfg1 command: ["mongod", "--configsvr", "--replSet", "cfg", "--bind_ip_all", "--port", "27019"] volumes: - cfg1-data:/data/configdb networks: - mongo-cluster注意数据目录是/data/configdb而不是/data/db,这是官方约定。三个 config server 初始化成一个叫cfg的副本集,方法跟前面副本集初始化一样,进去执行rs.initiate。
config server 数量必须是奇数,3 个是标配。少于 3 个没有冗余,多于 3 个增加通信开销收益不大。数据量很小(只存元数据),内存配置不用太高。
7.2 shard 与 mongos 的启动参数差异
shard 的启动参数里没有--configsvr,但通常加上--shardsvr:
mongod --shardsvr --replSet shard1 --bind_ip_all每个 shard 自己是个副本集,先把它初始化好(用rs.initiate),然后在 mongos 上执行sh.addShard("shard1/shard1a:27018,shard1b:27018")把它加入集群。
mongos 的启动方式不同,它不存数据,不用--dbpath:
mongos --configdb cfg/cfg1:27019,cfg2:27019,cfg3:27019 --bind_ip_all --port 27017--configdb指向 config server 副本集的名称和成员地址。这个参数写错了 mongos 起不来,日志里会明确说连不上 config server,还算好排查。
7.3 分片键的选择,比部署更容易出错的地方
集群搭起来只是开始,真正决定性能的是分片键。这里给几条实战经验,都是从慢查询和热点问题里总结的。
分片键必须有高基数和写分散两个特性。比如用户 ID 做分片键,基数够高,但如果是自增 ID(比如时间戳拼接),写入会全部落到最后一个分片上,形成热点,分片失去意义。哈希分片键({_id: "hashed"})能解决写分散问题,但范围查询会打到所有分片,性能不如范围分片。所以选择要看查询模式:读多且以范围查询为主,用范围分片键;写多且查询以等值为主,用哈希分片键。
分片键一旦选定就不能改(MongoDB 5.0 之后支持reshardCollection在线重分片,但代价很大),所以要在建集合时就规划好。我见过项目上线后才想分片,发现分片键选错,只能新建集合迁数据,业务停机好几个小时。
集合还需要显式开启分片:
sh.enableSharding("appdb") sh.shardCollection("appdb.orders", { userId: "hashed" })enableSharding是按库开启,shardCollection是按集合指定分片键。没执行这两条,数据还是全落在一个分片上,白搭。
提示:分片集群下连接串指向 mongos,不要直连 shard。直连 shard 写入的数据可能不在正确的分片上,导致查询查不到。mongos 地址写多个做冗余,格式和副本集类似,但不需要写
replicaSet参数。
8. 运维阶段的高频故障与我的排查顺序
部署完只是开始,线上跑起来会遇到各种问题。这部分列几个高频故障,以及我自己的排查顺序,都是实际处理过的。
8.1 节点一直处于 STARTUP2 或 RECOVERING
先看日志:docker logs --tail 200 mongo2。重点找关键词:sync、oplog、network。
如果是网络问题,日志里会有connection refused或者host unreachable,检查docker network inspect mongo-cluster看节点是否都在网络里,ping mongo1在容器内能否通。
如果是 oplog 缺口问题,日志里有we need to resync或者oplog source not found,说明从节点的 oplog 太旧,追不上主节点了,只能全量同步。加速的办法是临时把syncSource指定为某个同步较快的节点,或者直接让节点重新同步(删掉数据目录重启,代价大)。
8.2 主节点选举频繁发生,业务频繁抖动
rs.status()里看electionDate,如果短时间内多次选举,说明主节点不稳定。常见原因有三个:主节点资源不足(CPU 打满或内存吃紧被 OOM)、网络抖动导致心跳超时、priority配置导致某个弱节点反复被选中。
排查顺序:先看docker stats看资源占用,再看docker events看有没有容器重启记录,最后检查网络延迟。如果是资源问题,加内存或者调低cacheSizeGB;如果是网络问题,检查宿主机网卡和 Docker 网络配置。
有个经验值可以参考:选举超时默认 10 秒,心跳间隔 2 秒。如果宿主机负载很高(比如跑了很多其他容器),心跳可能延迟,导致误判主节点失联。这种环境下可以把electionTimeoutMillis调大到 20 秒,代价是故障转移变慢。
8.3 磁盘空间被日志和 oplog 吃掉
容器里最容易忽视磁盘增长。三个地方会涨:数据文件、oplog、日志文件。数据文件是正常的,oplog 有上限(设了oplogSizeMB就不会无限涨),日志文件如果设成写文件且没做轮转,会一直涨。
如果发现磁盘突然满了,先docker system df -v看各卷的占用,再进容器du -sh /data/db/*看具体是哪个文件大。mongod.log 涨到几个 G 的情况我遇到过,因为开了 verbose 日志级别忘了改回来。
预防措施:日志走 stdout(不落文件),oplog 显式设大小,给卷所在磁盘留 20% 以上余量,加一个简单的磁盘监控。这些不是高深技巧,但能避免半夜被磁盘告警叫醒。
8.4 备份与恢复:副本集不等于备份
这一点必须强调:副本集提供的是高可用,不是备份。三个节点同时被误删数据(比如误执行dropDatabase),这个删除操作会同步到所有节点,高可用救不了你。
备份用mongodump,在从节点上执行减少主节点压力:
docker exec mongo2 mongodump --host localhost --port 27017 \ --username admin --password xxx --authenticationDatabase admin \ --out /tmp/backup然后把备份文件从容器里拷出来:docker cp mongo2:/tmp/backup ./backup-$(date +%F)。生产环境建议用卷挂载备份目录,或者直接备份到对象存储。
恢复用mongorestore,注意要恢复到主节点(或者通过 mongos)。恢复前最好先在一个独立实例上验证备份文件完整性,别直接往生产库灌。
| 故障现象 | 优先排查 | 常见根因 |
|---|---|---|
| 节点 STARTUP2 不转 SECONDARY | 日志 sync 关键词 | oplog 缺口需全量同步 |
| 频繁选主 | docker stats、events | 资源不足或网络抖动 |
| 写入报 not master | rs.status 看主节点 | 连到了从节点 |
| 认证失败 | 连接串 authSource | 用户建在 admin 库未指定 |
| 磁盘满 | docker system df -v | 日志未轮转、oplog 过大 |
最后分享一个我在调试期常用的小技巧:给三个节点都加一个只读的mongosh别名脚本,封装了rs.status()的格式化输出,排查时一条命令看全所有节点状态,比一个个进容器敲命令快得多。这种小工具不值钱,但在故障处理的紧要关头能省下不少来回切换的时间。
另外一个体会是,容器化部署 MongoDB 集群,配置文件版本管理特别重要。mongod.conf用 git 管起来,每次改动都记一笔,出问题时能快速回滚到上一个正常版本。我吃过一次亏:调试认证问题时改了三处配置,没记清楚改了什么,结果新问题比原问题还难查,最后靠对比 git diff 才理清。这种流程上的习惯,比任何技巧都值钱。