使用docker-compose 搭建mongo多副本

MongoDB 副本集（Replica Set）是有自动故障恢复功能的主从集群，有一个 Primary 节点和一个或者多个 Secondary 节点组成。副本集没有固定的主节点，当主节点发生故障时，整个集群会选举一个主节点 为系统提供服务以保证系统的高可用。

副本集拥有三种节点:主节点、从节点、仲裁节点
1.主节点 负责处理客户端请求，读、写数据。
2.从节点 从主节点中复制数据，并可以接收读请求。在主节点故障时可通过投票选举出新的主节点。
3.仲裁节点 不持有数据副本，只参与投票过程，决定哪个节点成为主节点。

本文使用 mongo5.0.8 作为样例

1.docker-compose.yaml

这里先直接给出单个 mongo 的 docker-compose.yaml

Copy
version: '3.0'
services:
mongo1:
hostname: mongo1
image: mongo:5.0.8
restart: unless-stopped
container_name: mongodb1
command: --replSet app --bind_ip_all --keyFile /data/mongodb/keyFile
environment:
TZ: 'Asia/Shanghai'
MONGO_INITDB_ROOT_USERNAME: 'admin'
MONGO_INITDB_ROOT_PASSWORD: 'password'
ports:
- 27020:27017
volumes:
- ./data1:/data/db
- ./keyFile:/data/mongodb/keyFile
networks:
- bridge_network
mongo2:
hostname: mongo2
image: mongo:5.0.8
restart: unless-stopped
container_name: mongodb2
command: --replSet app --bind_ip_all --keyFile /data/mongodb/keyFile
environment:
TZ: 'Asia/Shanghai'
MONGO_INITDB_ROOT_USERNAME: 'admin'
MONGO_INITDB_ROOT_PASSWORD: 'password'
ports:
- 27018:27017
volumes:
- ./data2:/data/db
- ./keyFile:/data/mongodb/keyFile
networks:
- bridge_network
mongo3:
hostname: mongo3
image: mongo:5.0.8
restart: unless-stopped
container_name: mongodb3
command: --replSet app --bind_ip_all --keyFile /data/mongodb/keyFile
environment:
TZ: 'Asia/Shanghai'
MONGO_INITDB_ROOT_USERNAME: 'admin'
MONGO_INITDB_ROOT_PASSWORD: 'password'
ports:
- 27019:27017
volumes:
- ./data3:/data/db
- ./keyFile:/data/mongodb/keyFile
networks:
- bridge_network
networks:
bridge_network:
driver: bridge

这里可以选择在一台机器上启动 3 个容器，也可以在不同的机器上启动。本例用上述文件分别在 IP 为 10.0.1.11 (主)、10.0.1.31 (从)、10.0.1.32 (从) 三台机器上启动。

在 10.0.1.11 执行docker compose up -d mongo1
在 10.0.1.31 执行docker compose up -d mongo2
在 10.0.1.32 执行docker compose up -d mongo3

这里 docker compose up -d 后的 mongo1、mongo2、mongo3 是指只启动 yaml 文件中的一个服务。如果将 yaml 文件拆分成不同的文件分别在对应的机器里启动就不需要在命令后面加服务名称了。

需要注意：
组成副本的每一台机器的 keyFile 文件必须一样！！！mongodb 实例只有拥有正确的 keyfile 才可以加入副本集。keyFile 文件的具体操作方法可以参考【mongo 事务】使用 docker-compose 启动 mongo,‘单副本模式‘实现支持事务

2. 容器启动后

在容器启动后，我们使用 docker exec 命令进入作为主节点的容器，进入 mongo shell

Copy
mongo -u admin --authenticationDatabase admin

初始化 mongo 副本集

Copy
rs.initiate({
_id: "app",
members: [
{ _id: 0, host: "10.0.1.11:27020" },
{ _id: 1, host: "10.0.1.31:27018" },
{ _id: 2, host: "10.0.1.32:27019" }
]
})

注意，当初我在初始化时，误以为可以直接使用 rs.initiate () 无参的初始化方法，然后再依次使用 rs.add (‘ip’) 将剩余的节点加入副本集。但是这是错误的，因为在初始化时，如果使用无参的初始化方法，mongo 会直接获取宿主机 (这时对于 mongo 来说宿主机是 docker 容器) 的 hostname + 端口组成 mongo 的 host 并写入配置，但是由于 mongo 的宿主机其实是容器，所以会获取到容器的 hostname 以及 mongo 在容器里的端口。

显然这个当多个不同机器的 mongo 副本相互通信时，这个 host 是无效的。

所以在初始化时最好直接指定好具体的机器。

当初始化完成以后可以使用rs.status()查看副本集状态
截取部分内容

Copy
"members" : [
{
"_id" : 0,
"name" : "10.0.1.11:27020",
"health" : 1,
"state" : 1,
"stateStr" : "PRIMARY",
"uptime" : 2299,
"optime" : {
"ts" : Timestamp(1717560117, 1),
"t" : NumberLong(1)
},
"optimeDate" : ISODate("2024-06-05T04:01:57Z"),
"lastAppliedWallTime" : ISODate("2024-06-05T04:01:57.808Z"),
"lastDurableWallTime" : ISODate("2024-06-05T04:01:57.808Z"),
"syncSourceHost" : "",
"syncSourceId" : -1,
"infoMessage" : "",
"electionTime" : Timestamp(1717559887, 1),
"electionDate" : ISODate("2024-06-05T03:58:07Z"),
"configVersion" : 1,
"configTerm" : 1,
"self" : true,
"lastHeartbeatMessage" : ""
},
{
"_id" : 1,
"name" : "10.0.1.31:27018",
"health" : 1,
"state" : 2,
"stateStr" : "SECONDARY",
"uptime" : 249,
"optime" : {
"ts" : Timestamp(1717560117, 1),
"t" : NumberLong(1)
},
"optimeDurable" : {
"ts" : Timestamp(1717560117, 1),
"t" : NumberLong(1)
},
"optimeDate" : ISODate("2024-06-05T04:01:57Z"),
"optimeDurableDate" : ISODate("2024-06-05T04:01:57Z"),
"lastAppliedWallTime" : ISODate("2024-06-05T04:01:57.808Z"),
"lastDurableWallTime" : ISODate("2024-06-05T04:01:57.808Z"),
"lastHeartbeat" : ISODate("2024-06-05T04:02:05.988Z"),
"lastHeartbeatRecv" : ISODate("2024-06-05T04:02:05.581Z"),
"pingMs" : NumberLong(1),
"lastHeartbeatMessage" : "",
"syncSourceHost" : "10.0.1.11:27020",
"syncSourceId" : 0,
"infoMessage" : "",
"configVersion" : 1,
"configTerm" : 1
},
{
"_id" : 2,
"name" : "10.0.1.32:27019",
"health" : 1,
"state" : 2,
"stateStr" : "SECONDARY",
"uptime" : 249,
"optime" : {
"ts" : Timestamp(1717560117, 1),
"t" : NumberLong(1)
},
"optimeDurable" : {
"ts" : Timestamp(1717560117, 1),
"t" : NumberLong(1)
},
"optimeDate" : ISODate("2024-06-05T04:01:57Z"),
"optimeDurableDate" : ISODate("2024-06-05T04:01:57Z"),
"lastAppliedWallTime" : ISODate("2024-06-05T04:01:57.808Z"),
"lastDurableWallTime" : ISODate("2024-06-05T04:01:57.808Z"),
"lastHeartbeat" : ISODate("2024-06-05T04:02:05.988Z"),
"lastHeartbeatRecv" : ISODate("2024-06-05T04:02:05.629Z"),
"pingMs" : NumberLong(1),
"lastHeartbeatMessage" : "",
"syncSourceHost" : "10.0.1.11:27020",
"syncSourceId" : 0,
"infoMessage" : "",
"configVersion" : 1,
"configTerm" : 1
}
],

这里可以看到执行初始化的机器为主节点，剩下的为从节点

关于 stateStr 字段的各个含义：

Copy
STARTUP：成员正在启动。
PRIMARY：成员是主节点。
SECONDARY：成员是次节点。
RECOVERING：成员正在恢复。
ARBITER：成员是仲裁节点。
DOWN：成员无法与大多数节点通信。
UNKNOWN：成员的状态未知。
ROLLBACK：成员正在进行回滚操作。
REMOVED：成员已经被副本集移除。

最后如果有后续需要加入的新节点可以使用 rs.add (‘ip’) 加入。如果需要加入仲裁节点 (不推荐)
可以使用 rs.add (‘ip’，true) 加入

关于节点的数量
mongodb 官方说当节点超过 2 个时，推荐副本集成员为奇数个成员，而不使用仲裁节点。所以在实际运用中尽可能的保证奇数个节点而不使用仲裁节点。如果迫不得已正常主从节点总数为偶数，这时仲裁节点就可以加入副本集（仲裁节点不参与存储等业务，只参与投票，所以仲裁节点对于性能的要求很小，加一个仲裁节点的难度不会很大），这样拥有投票权的节点总数又为奇数了。
具体为什么需要奇数个节点，这需要投票选举的算法以及实际部署的情况来解释（脑裂问题）。