MongoDB 监控(十)MongoDB 副本集集群搭建及获取副本集状态

使用 Docker Compose 搭建 MongoDB 三节点副本集,说明 keyFile、Primary、Secondary、Arbiter、rs.initiate、rs.status 和 replSetGetStatus 的监控价值。

作者 快猫实习生

MongoDB 监控

这是 MongoDB 监控系列文章的第十篇,前面几篇文章的链接如下:

之前的章节我们重点研究的是 MongoDB 单节点模式,这一节我们探索一下 MongoDB 的副本集集群以及相关的监控命令。MongoDB 的副本集集群有点像是 MySQL 的一主多从的方式,具备高可用能力,但是不具备水平扩展能力,如果想要水平扩展能力,则需要使用分片集群。多个副本集集群组成一套分片集群。

核心要点

  • MongoDB 副本集用于提升可用性,常见角色包括 Primary、Secondary 和可选的 Arbiter;它不是水平扩展方案,水平扩展需要分片集群。
  • 本文用 Docker Compose 启动 3 个 MongoDB 实例,并通过 keyFile 让副本集成员之间完成内部认证。
  • rs.initiate() 用于初始化副本集,rs.status() 可以查看节点角色、健康状态、复制时间、心跳和选举信息。
  • 监控采集器通常会通过 db.runCommand({replSetGetStatus: 1}) 获取副本集状态,它和 rs.status() 的观察目标一致。

MongoDB 副本集集群搭建

参考:https://www.cnblogs.com/studyjobs/p/17643808.html

我们使用 docker compose 快速启动一个 MongoDB 副本集集群,首先创建三个目录,存放三个 MongoDB 实例的数据:

mkdir -p /Users/ulric/works/mongos/mongo1
mkdir -p /Users/ulric/works/mongos/mongo2
mkdir -p /Users/ulric/works/mongos/mongo3

mongodb 使用 keyFile 进行认证,副本集群中的每个节点的 mongodb 使用 keyFile 的内容作为认证其他成员的共享密码。mongodb 实例只有拥有正确的 keyFile 才可以加入副本集群,集群中所有成员的 keyFile 内容必须相同。

cd /Users/ulric/works/mongos
openssl rand -base64 666 > mongodb.key

mongodb 的集群其实有 3 种角色:主节点、从节点、仲裁节点(可选,可有可无)

  • 主节点(Primary):主要负责数据的写操作,当然也可以读取数据,大部分命令需要在主节点才能运行。
  • 从节点(Secondary):从主节点实时同步数据,只能读取数据,不能进行写操作。
  • 仲裁节点(Arbiter):不保留任何数据的副本,只能用来投票选举使用。

本篇博客的演示中,没有去设置仲裁节点,3 个节点都保留数据副本。在 /Users/ulric/works/mongos 目录下创建 docker-compose.yml 文件:

这套示例适合本机学习和验证监控采集逻辑。生产环境的节点分布、认证、网络、存储和备份策略要按实际架构设计,不能直接照搬本机 Docker Compose 示例。

services:
  # 服务名称
  mongodb1:
    # 使用最新的 mongodb 镜像
    image: mongo:latest
    # docker 服务启动时,自动启动 mongo 容器
    restart: always
    # 容器的名称
    container_name: mongo1
    # 宿主机中的目录和文件,映射容器内部的目录和文件
    volumes:
      - /Users/ulric/works/mongos/mongo1:/data/db
      - /Users/ulric/works/mongos/mongodb.key:/data/mongodb.key
    ports:
      # 宿主机的端口映射容器内的端口
      - 27017:27017
    environment:
      # 初始化一个 root 角色的用户 jobs 密码是 123456
      - MONGO_INITDB_ROOT_USERNAME=jobs
      - MONGO_INITDB_ROOT_PASSWORD=123456
    # 使用创建的桥接网络,把各个 mongodb 容器连接在一起
    networks:
      - mongoNetwork
    # 启动容器时,在容器内部额外执行的命令
    # 其中 --replSet 参数后面的 mongos 是集群名称,这个很重要
    command: mongod --replSet mongos --keyFile /data/mongodb.key
    entrypoint:
      - bash
      - -c
      - |
        chmod 400 /data/mongodb.key
        chown 999:999 /data/mongodb.key
        exec docker-entrypoint.sh $$@        

  mongodb2:
    image: mongo:latest
    restart: always
    container_name: mongo2
    volumes:
      - /Users/ulric/works/mongos/mongo2:/data/db
      - /Users/ulric/works/mongos/mongodb.key:/data/mongodb.key
    ports:
      - 27018:27017
    environment:
      - MONGO_INITDB_ROOT_USERNAME=jobs
      - MONGO_INITDB_ROOT_PASSWORD=123456
    networks:
      - mongoNetwork
    command: mongod --replSet mongos --keyFile /data/mongodb.key
    entrypoint:
      - bash
      - -c
      - |
        chmod 400 /data/mongodb.key
        chown 999:999 /data/mongodb.key
        exec docker-entrypoint.sh $$@        

  mongodb3:
    image: mongo:latest
    restart: always
    container_name: mongo3
    volumes:
      - /Users/ulric/works/mongos/mongo3:/data/db
      - /Users/ulric/works/mongos/mongodb.key:/data/mongodb.key
    ports:
      - 27019:27017
    environment:
      - MONGO_INITDB_ROOT_USERNAME=jobs
      - MONGO_INITDB_ROOT_PASSWORD=123456
    networks:
      - mongoNetwork
    command: mongod --replSet mongos --keyFile /data/mongodb.key
    entrypoint:
      - bash
      - -c
      - |
        chmod 400 /data/mongodb.key
        chown 999:999 /data/mongodb.key
        exec docker-entrypoint.sh $$@        

# 创建一个桥接网络,把各个 mongodb 实例连接在一起,该网络适用于单机
# 如果在不同的宿主机上,使用 docker swarm 需要创建 overlay 网络
networks:
  mongoNetwork:
    driver: bridge

然后执行 docker compose up -d 启动 MongoDB 副本集集群,启动后可以通过 docker ps 查看容器是否启动成功。

随便进入其中一个容器,比如进入 mongo1 docker exec -it mongo1 bash,然后进入 mongo 客户端 mongosh -u jobs -p 123456,执行以下命令配置将 3 个节点初始化为一个副本集群:

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

上面的 host 使用的是示例机器上的地址和端口。你在自己的环境中执行时,需要替换成 MongoDB 节点之间可以互相访问的地址。副本集成员地址配置错误,会直接影响节点发现、心跳和复制。

初始化完成之后,通过 rs.status() 命令可以看到副本集集群的状态:

mongos [direct: primary] test> use admin
switched to db admin
mongos [direct: primary] admin> rs.status()
{
  set: 'mongos',
  date: ISODate('2024-11-18T09:39:48.651Z'),
  myState: 1,
  term: Long('1'),
  syncSourceHost: '',
  syncSourceId: -1,
  heartbeatIntervalMillis: Long('2000'),
  majorityVoteCount: 2,
  writeMajorityCount: 2,
  votingMembersCount: 3,
  writableVotingMembersCount: 3,
  optimes: {
    lastCommittedOpTime: { ts: Timestamp({ t: 1731922780, i: 1 }), t: Long('1') },
    lastCommittedWallTime: ISODate('2024-11-18T09:39:40.165Z'),
    readConcernMajorityOpTime: { ts: Timestamp({ t: 1731922780, i: 1 }), t: Long('1') },
    appliedOpTime: { ts: Timestamp({ t: 1731922780, i: 1 }), t: Long('1') },
    durableOpTime: { ts: Timestamp({ t: 1731922780, i: 1 }), t: Long('1') },
    writtenOpTime: { ts: Timestamp({ t: 1731922780, i: 1 }), t: Long('1') },
    lastAppliedWallTime: ISODate('2024-11-18T09:39:40.165Z'),
    lastDurableWallTime: ISODate('2024-11-18T09:39:40.165Z'),
    lastWrittenWallTime: ISODate('2024-11-18T09:39:40.165Z')
  },
  lastStableRecoveryTimestamp: Timestamp({ t: 1731922730, i: 1 }),
  electionCandidateMetrics: {
    lastElectionReason: 'electionTimeout',
    lastElectionDate: ISODate('2024-11-18T08:21:08.285Z'),
    electionTerm: Long('1'),
    lastCommittedOpTimeAtElection: { ts: Timestamp({ t: 1731918057, i: 1 }), t: Long('-1') },
    lastSeenWrittenOpTimeAtElection: { ts: Timestamp({ t: 1731918057, i: 1 }), t: Long('-1') },
    lastSeenOpTimeAtElection: { ts: Timestamp({ t: 1731918057, i: 1 }), t: Long('-1') },
    numVotesNeeded: 2,
    priorityAtElection: 1,
    electionTimeoutMillis: Long('10000'),
    numCatchUpOps: Long('0'),
    newTermStartDate: ISODate('2024-11-18T08:21:08.329Z'),
    wMajorityWriteAvailabilityDate: ISODate('2024-11-18T08:21:08.820Z')
  },
  members: [
    {
      _id: 0,
      name: '10.211.55.2:27017',
      health: 1,
      state: 1,
      stateStr: 'PRIMARY',
      uptime: 5067,
      optime: { ts: Timestamp({ t: 1731922780, i: 1 }), t: Long('1') },
      optimeDate: ISODate('2024-11-18T09:39:40.000Z'),
      optimeWritten: { ts: Timestamp({ t: 1731922780, i: 1 }), t: Long('1') },
      optimeWrittenDate: ISODate('2024-11-18T09:39:40.000Z'),
      lastAppliedWallTime: ISODate('2024-11-18T09:39:40.165Z'),
      lastDurableWallTime: ISODate('2024-11-18T09:39:40.165Z'),
      lastWrittenWallTime: ISODate('2024-11-18T09:39:40.165Z'),
      syncSourceHost: '',
      syncSourceId: -1,
      infoMessage: '',
      electionTime: Timestamp({ t: 1731918068, i: 1 }),
      electionDate: ISODate('2024-11-18T08:21:08.000Z'),
      configVersion: 1,
      configTerm: 1,
      self: true,
      lastHeartbeatMessage: ''
    },
    {
      _id: 1,
      name: '10.211.55.2:27018',
      health: 1,
      state: 2,
      stateStr: 'SECONDARY',
      uptime: 4731,
      optime: { ts: Timestamp({ t: 1731922780, i: 1 }), t: Long('1') },
      optimeDurable: { ts: Timestamp({ t: 1731922780, i: 1 }), t: Long('1') },
      optimeWritten: { ts: Timestamp({ t: 1731922780, i: 1 }), t: Long('1') },
      optimeDate: ISODate('2024-11-18T09:39:40.000Z'),
      optimeDurableDate: ISODate('2024-11-18T09:39:40.000Z'),
      optimeWrittenDate: ISODate('2024-11-18T09:39:40.000Z'),
      lastAppliedWallTime: ISODate('2024-11-18T09:39:40.165Z'),
      lastDurableWallTime: ISODate('2024-11-18T09:39:40.165Z'),
      lastWrittenWallTime: ISODate('2024-11-18T09:39:40.165Z'),
      lastHeartbeat: ISODate('2024-11-18T09:39:47.018Z'),
      lastHeartbeatRecv: ISODate('2024-11-18T09:39:47.014Z'),
      pingMs: Long('0'),
      lastHeartbeatMessage: '',
      syncSourceHost: '10.211.55.2:27017',
      syncSourceId: 0,
      infoMessage: '',
      configVersion: 1,
      configTerm: 1
    },
    {
      _id: 2,
      name: '10.211.55.2:27019',
      health: 1,
      state: 2,
      stateStr: 'SECONDARY',
      uptime: 4731,
      optime: { ts: Timestamp({ t: 1731922780, i: 1 }), t: Long('1') },
      optimeDurable: { ts: Timestamp({ t: 1731922780, i: 1 }), t: Long('1') },
      optimeWritten: { ts: Timestamp({ t: 1731922780, i: 1 }), t: Long('1') },
      optimeDate: ISODate('2024-11-18T09:39:40.000Z'),
      optimeDurableDate: ISODate('2024-11-18T09:39:40.000Z'),
      optimeWrittenDate: ISODate('2024-11-18T09:39:40.000Z'),
      lastAppliedWallTime: ISODate('2024-11-18T09:39:40.165Z'),
      lastDurableWallTime: ISODate('2024-11-18T09:39:40.165Z'),
      lastWrittenWallTime: ISODate('2024-11-18T09:39:40.165Z'),
      lastHeartbeat: ISODate('2024-11-18T09:39:47.018Z'),
      lastHeartbeatRecv: ISODate('2024-11-18T09:39:47.013Z'),
      pingMs: Long('0'),
      lastHeartbeatMessage: '',
      syncSourceHost: '10.211.55.2:27017',
      syncSourceId: 0,
      infoMessage: '',
      configVersion: 1,
      configTerm: 1
    }
  ],
  ok: 1,
  '$clusterTime': {
    clusterTime: Timestamp({ t: 1731922780, i: 1 }),
    signature: {
      hash: Binary.createFromBase64('pGZHCVLcTS6roDiQL8DsOUTkKD0=', 0),
      keyId: Long('7438531461411504133')
    }
  },
  operationTime: Timestamp({ t: 1731922780, i: 1 })
}

通过上面的输出可以看到,三个实例,其中一个是主节点,两个是从节点,这样就搭建了一个 MongoDB 副本集集群。每个实例都有很多字段,比如 health、state、uptime、optime、lastHeartbeat 等等,正常的集群,三个节点的 optimeDate 应该是类似的。一般监控数据采集器 agent 会采集 MongoDB 的副本集信息,通常执行的是 db.runCommand({replSetGetStatus: 1}) 命令,和 rs.status() 命令效果一样。

rs.status() 里哪些字段值得监控

字段 监控含义 使用方式
set 副本集名称 确认实例归属,避免采集对象混淆
myState 当前节点状态编号 可和状态枚举一起转换成 PRIMARY、SECONDARY 等可读状态
members[].health 成员健康状态 判断节点是否可达和正常参与副本集
members[].stateStr 成员角色 关注 PRIMARY 是否存在、SECONDARY 是否正常
members[].uptime 成员运行时间 辅助判断节点是否发生重启
members[].optimeDate 已应用 oplog 的时间 多个节点差异过大时,可能存在复制延迟
members[].lastHeartbeat 最近心跳时间 用于判断节点通信是否正常
members[].syncSourceHost 同步源 排查复制链路和同步来源

从搭建示例到监控规则

副本集监控的目标不是证明“集群能启动”,而是持续回答三个问题:

  1. 是否有可写的 Primary。
  2. Secondary 是否健康,并且复制进度没有明显落后。
  3. 成员状态变化、心跳异常、重启和选举是否能及时发现。

因此,仪表盘可以展示角色分布、节点健康、复制时间差、运行时间和心跳信息;告警则可以围绕 Primary 缺失、节点不健康、复制延迟、状态长时间异常和节点频繁重启来设计。具体阈值需要结合业务写入量、跨机房延迟和维护窗口确定。

总结

本文用 Docker Compose 搭建了一个三节点 MongoDB 副本集,并通过 rs.status() 观察副本集状态。对监控来说,真正有价值的是理解这些字段如何映射到可用性问题:角色是否正确、节点是否健康、复制是否跟得上、心跳是否正常。后续研究采集器逻辑时,可以重点看它如何执行 replSetGetStatus、如何把成员状态转成指标、以及如何为每个节点附加标签。

FAQ

Q1:MongoDB 副本集能解决水平扩展问题吗? A:不能。原文里已经说明,副本集主要提供高可用能力;如果要水平扩展,需要使用分片集群。

Q2:rs.status()replSetGetStatus 是什么关系? A:rs.status() 用于在 shell 中查看副本集状态;采集器通常执行 db.runCommand({replSetGetStatus: 1}) 获取类似信息,用于转成监控指标。

Q3:副本集监控最应该关注什么? A:优先关注 Primary 是否存在、成员健康状态、Secondary 复制进度、节点运行时间和心跳情况。这些字段更直接影响可用性和故障定位。

联系我们交流

延伸路径

继续看解决方案和产品对比

如果你正在做监控、可观测性或故障定位相关选型,建议从解决方案和产品对比继续往下看。

快猫星云 联系方式 快猫星云 联系方式
快猫星云 联系方式
快猫星云 联系方式
快猫星云 联系方式
快猫星云