3. 从零搭建三节点副本集

本节把前面学到的副本集概念落到一台机器(或三台机器)上,用配置文件方式启动三个 mongod 实例,组成一个名为 rs0 的三节点副本集,并通过 rs.initiate()、rs.add() 和 rs.status() 完成初始化、扩容与状态验证。相比命令行参数启动,配置文件方式更适合生产环境:参数集中、可版本管理、便于批量部署。

一、规划节点与目录

三节点副本集的最小可用拓扑是「一主两从」,其中两个从节点都持有完整数据副本,任意一个节点宕机都不影响读写。为了在一台机器上演示,我们用不同端口区分三个实例:

角色主机:端口数据目录日志文件
主候选节点 A127.0.0.1:27017/data/rs0/node1/data/rs0/node1/mongod.log
从节点 B127.0.0.1:27018/data/rs0/node2/data/rs0/node2/mongod.log
从节点 C127.0.0.1:27019/data/rs0/node3/data/rs0/node3/mongod.log

真实生产环境应把三个节点分散到三台物理机或三个可用区,避免单机故障导致多数派丢失。这里用单机多端口只是为了教学演示,副本集的行为与多机部署完全一致。

二、编写 mongod 配置文件

每个实例使用一份独立的 YAML 配置文件。以节点 A 为例,/etc/mongod-rs0-1.conf 内容如下:

storage:
  dbPath: /data/rs0/node1
  journal:
    enabled: true
systemLog:
  destination: file
  path: /data/rs0/node1/mongod.log
  logAppend: true
net:
  bindIp: 127.0.0.1
  port: 27017
replication:
  replSetName: rs0
processManagement:
  fork: true

关键字段说明:replication.replSetName 必须三个节点完全一致,否则无法加入同一副本集;storage.dbPath 与 systemLog.path 各节点必须不同;processManagement.fork: true 让 mongod 以守护进程方式运行。节点 B、C 只需把端口改为 27018/27019、目录改为 node2/node3 即可。

创建目录并启动三个实例:

mkdir -p /data/rs0/node{1,2,3}
mongod -f /etc/mongod-rs0-1.conf
mongod -f /etc/mongod-rs0-2.conf
mongod -f /etc/mongod-rs0-3.conf

此时三个实例各自独立运行,尚未组成副本集。用 mongosh --port 27017 连接节点 A,执行 rs.status() 会返回 NotYetInitialized,这是正常的。

三、初始化副本集并添加节点

在节点 A 上执行初始化。这里显式传入 _id 和 members,一次性把三个节点都写进配置,比先 initiate 再逐个 add 更简洁:

rs.initiate({
  _id: "rs0",
  members: [
    { _id: 0, host: "127.0.0.1:27017" },
    { _id: 1, host: "127.0.0.1:27018" },
    { _id: 2, host: "127.0.0.1:27019" }
  ]
})

返回 { ok: 1 } 表示配置已写入。如果只想先建单节点再扩容,可以执行 rs.initiate()(使用默认配置),随后用 rs.add("127.0.0.1:27018") 和 rs.add("127.0.0.1:27019") 逐个加入。两种方式最终配置等价,区别在于前者一次成型、后者便于观察节点加入过程。

初始化后需要等待几秒到几十秒,让节点完成选举和数据同步。此时在节点 A 上执行 rs.status(),可以看到 myState 为 1(PRIMARY),另外两个节点 stateStr 为 SECONDARY。

四、验证主从同步

验证同步最直接的方式是写入后到从节点读取。在 PRIMARY 上插入一条数据:

use testdb
db.users.insertOne({ name: "alice", age: 30 })

然后在从节点 B 上连接并查询。注意从节点默认不允许读,需要先开启从节点读权限:

mongosh --port 27018
rs.slaveOk()          // 旧版本写法
// 新版本使用:
db.getMongo().setReadPref("secondary")
db.users.find()

如果能看到 alice 这条记录,说明主从复制链路正常。也可以在主节点执行 db.users.getDB().stats() 或查看 rs.printReplicationInfo() 观察 oplog 时间窗口,用 rs.printSecondaryReplicationInfo() 查看各从节点落后主节点多少秒。

五、rs.status() 输出解读

rs.status() 是排查副本集问题最重要的命令,返回结构包含 set、myState、members 等字段。下面这张图把关键字段与含义对应起来,便于快速定位问题:

rs.status() 关键字段解读 rs.status() 在 PRIMARY 上执行 顶层字段 set / myState / members ok / date members[] 数组 每个节点一个对象 _id / name / health state / stateStr / uptime myState 1 = PRIMARY 2 = SECONDARY 0 = STARTUP 8 = ARBITER stateStr "PRIMARY" 主节点 "SECONDARY" 从节点 "ARBITER" 仲裁节点 "(not reachable)" 不可达 health / uptime health: 1 正常 health: 0 异常 uptime: 运行秒数 lastHeartbeat 心跳时间 快速判断路径 myState = 1 ? 是 当前节点是主 检查 members 中 health 若某节点 stateStr 为 "(not reachable)",优先检查网络连通性、端口监听和防火墙规则。

解读时按以下顺序看:先看 ok 是否为 1,再看 myState 确认当前节点角色,最后逐个检查 members 中每个节点的 health 和 stateStr。如果某个节点 health 为 0,说明心跳失败,通常是网络不通或进程已退出;如果 stateStr 长期停留在 STARTUP2,说明正在做初始同步,数据量大时需要耐心等待。

六、常见问题与排查

  • 初始化报 "already initialized":说明该实例已经属于某个副本集,需要先删除 dbPath 下的 local 数据库或使用新目录重新启动。
  • 节点一直处于 STARTUP2:检查主节点 oplog 是否足够大,从节点能否拉取到全量数据;网络带宽不足也会导致同步缓慢。
  • rs.add() 后节点不可达:确认 bindIp 是否包含目标地址,副本集成员之间必须能互相访问配置中的 host 地址。
  • 选举频繁切换:检查节点间网络延迟和心跳超时设置,默认 heartbeatTimeoutSecs 为 10 秒。
生产环境建议为每个节点配置 keyFile 开启内部认证,否则任何能连上端口的客户端都可以加入副本集,存在安全风险。