部署跨多数据中心分布的 MongoDB 副本集

MongoDB

最近把自己的一个小项目迁移了服务器, 结果发现接口响应速度慢了很多, 排查了不少时间后突然想起来是因为新服务器和 MongoDB Atlas 不在同一个地区, 光是二者之间的网络延迟就很高.

而 MongoDB Atlas 提供的免费实例要备份数据和迁移比较麻烦, 又因为 MongoDB 单实例不支持事务, 所以决定干脆自建一个 MongoDB 副本集 😎.

环境

本文环境本文环境

  • OS: Debian Bookworm
  • MongoDB: 7.0.0
  • 使用 Docker 安装 MongoDB

我手上有几台不同服务商不同地区的服务器, 下文都以 Server A, Server B, Server C 代表.

为了申请证书, 还需要至少一个域名.

安装 MongoDB

首先要在每台服务器上都安装 MongoDB, 这里使用 Docker Compose.

yaml
# docker-compose.yml
services:
  mongo:
    image: mongo
  #  restart: always
    environment:
      MONGO_INITDB_ROOT_USERNAME: root
      MONGO_INITDB_ROOT_PASSWORD: password
    ports:
      - 27017:27017
    volumes:
      - ./data:/data/db
      - ./config:/etc/mongo
    command: ["--config", "/etc/mongo/mongod.conf"]

然后 ./config 目录下新建我们的配置文件 mongod.conf:

yaml
# mongod.conf

# for documentation of all options, see:
#   http://docs.mongodb.org/manual/reference/configuration-options/

# Where and how to store data.
storage:
  dbPath: /data/db
#  engine:
#  wiredTiger:

# network interfaces
net:
  port: 27017
  bindIp: 127.0.0.1

# how the process runs
processManagement:
  timeZoneInfo: /usr/share/zoneinfo

# where to write logging data.
# systemLog:
#   destination: file
#   logAppend: true
#   path: /var/log/mongodb/mongod.log

暂时只需要上面这一点配置就可以啦, 此时可以先启动容器, 接下来需要配置副本集之间认证用的 keyfile.

配置节点 keyfile

生成 keyfile

keyFile 用于 MongoDB 副本集节点之间的认证, 每一个节点需要有相同的 keyFile 才能加入副本集.

keyFile 的内容就相当于密码啦, 用什么软件生成一个都行, 这里就使用 openssl:

bash
openssl rand -base64 756 > keyfile

然后需要把 keyfile 分发到每一个节点上, 并且仅为文件所有者提供读取权限.

修改 keyfile 权限

如果你是在本机直接安装的 MongoDB, 只需要使用 chmod 修改权限:

bash
chmod 400 keyfile

但是如果使用 Docker 安装, 不仅要把 keyfile 挂载到容器内, 还要进入容器来修改权限, 因为容器内部的文件权限和外面是不一样的!

这里我把 keyfile 挂载到了 /etc/mongo/keyfile

然后进入容器修改权限:

bash
docker exec -it <container_id> /bin/bash
chmod 400 /etc/mongo/keyfile
chown 999:999 /etc/mongo/keyfile

修改完权限后把容器停掉, 因为我们还要修改配置文件.

TIP提示

每一个节点都要使用相同的 keyfile 进行上面相同的操作.

配置副本集认证

配置完了所有节点的 keyfile, 现在来修改配置文件启用副本集, 依然是所有节点的配置文件都要修改.

yaml
# mongod.conf

# for documentation of all options, see:
#   http://docs.mongodb.org/manual/reference/configuration-options/

# Where and how to store data.
storage:
  dbPath: /data/db
#  engine:
#  wiredTiger:

# network interfaces
net:
  port: 27017
  bindIp: 127.0.0.1

# how the process runs
processManagement:
  timeZoneInfo: /usr/share/zoneinfo

# where to write logging data.
# systemLog:
#   destination: file
#   logAppend: true
#   path: /var/log/mongodb/mongod.log

security:
  clusterAuthMode: keyFile
  authorization: enabled
  keyFile: /etc/mongo/keyfile

replication:
  replSetName: "rs0" # 副本集名称

如果是同机房所有节点使用内部网络的话, 这样就可以直接启动副本集了, 但是我的节点是分布在不同地区的, 所以还需要开放外部连接.

yaml
# mongod.conf
net:
  port: 27017
  bindIp: 0.0.0.0

既然开放了外部连接, 那么就需要对传输使用 TLS 加密.

配置 TLS

准备证书

使用 TLS 就需要证书, 这里使用 Let's Encrypt 的证书.

申请证书的方式和为网站申请证书一样, 这里不再赘述, 可以参考:

TIP提示

也可以使用自签名证书, 但其实自己维护一个 CA 比申请第三方 CA 的证书麻烦多了, 可以参考:

而且自签名证书也不够安全.

一般来说, 申请成功证书会得到两个文件: fullchain.pemprivkey.pem, 分别是证书链和私钥.

但是 MongoDB 还需要 CA 证书, 可以在 Let’s Encrypt ISRG Root X1 下载.

接下来需要把 fullchain.pem, privkey.pem '合并' 为一个文件 (MongoDB 要求的!)

bash
cat fullchain.pem privkey.pem > mongod.pem

其实就是把 fullchain.pemprivkey.pem 的内容拼接到一起, 得到的 mongod.pem 就是 MongoDB 需要的证书文件.

可以使用泛域名证书可以使用泛域名证书

如果你打算把所有服务器解析到一个域名上, 那么可以使用泛域名证书, 这样就不用为每一个节点都单独申请证书了.

配置证书

准备好每个节点自己的 mongod.pem, 把 CA 根证书 isrgrootx1.pem 拷贝给每一个节点. 我这里仍是直接挂载在了容器的 /etc/mongo/ 目录.

然后修改它们的配置文件:

yaml
net:
  port: 27017
  bindIp: 0.0.0.0

  tls:
    mode: requireTLS
    certificateKeyFile: /etc/mongo/mongod.pem
    CAFile: /etc/mongo/isrgrootx1.pem
    allowConnectionsWithoutCertificates: true

关于 net.tls 的配置详情参考:

配置完证书后可以准备启动副本集了.

完整 mongod.conf

yaml
# mongod.conf

# for documentation of all options, see:
#   http://docs.mongodb.org/manual/reference/configuration-options/

# Where and how to store data.
storage:
  dbPath: /data/db

net:
  port: 27017
  bindIp: 0.0.0.0

  tls:
    mode: requireTLS
    certificateKeyFile: /etc/mongo/mongodb.pem
    CAFile: /etc/mongo/isrgrootx1.pem
    allowConnectionsWithoutCertificates: true

# how the process runs
processManagement:
  timeZoneInfo: /usr/share/zoneinfo

security:
  clusterAuthMode: keyFile
  authorization: enabled
  keyFile: /etc/mongo/keyfile

replication:
  replSetName: "rs0"

启动副本集

示例节点名域名
Server Aservera.mongodb.example.com
Server Bserverb.mongodb.example.com
Server Cserverc.mongodb.example.com

启动所有节点, 然后使用支持的客户端连接你打算作为主节点的节点, 使用 rs.initiate() 初始化副本集, 以 Server A 为例:

WARNING注意

只在一个节点上执行 rs.initiate()!

js
rs.initiate( {
   _id : "rs0",
   members: [
      { _id: 0, host: "servera.mongodb.example.com:27017" },
      { _id: 1, host: "serverb.mongodb.example.com:27017" },
      { _id: 2, host: "serverc.mongodb.example.com:27017" }
   ]
})

或者, 也可以不向 rs.initiate() 传入配置, 而是初始化后再添加节点:

js
rs.initiate()
rs.add("serverb.mongodb.example.com:27017")
rs.add("serverc.mongodb.example.com:27017")

参考:

如果你的操作都正确, 那这里应该已经启动成功了 😇. 使用 rs.status() 查看副本集状态:

0

PRIMARY 就是主节点, SECONDARY 是从节点.

如果你发现有其他状态的节点, 可以查看 副本集成员状态 排查问题.

连接副本集

在上文, 我们连接到了主节点来初始化副本集, 它的连接字符串通常是像这样的:

text
mongodb://user:password@servera.mongodb.example.com:27017

但这样并不是连接到了 "整个" 副本集. 显然如果主节点挂了, 连接就会断开.

其实 MongoDB 可以使用多服务器连接字符串, 例如:

text
mongodb://user:pssword@servera.mongodb.example.com:27017,serverb.mongodb.example.com:27017,serverc.mongodb.example.com:27017/?replicaSet=rs0

服务器之间用逗号分隔, 这样就可以连接到整个副本集了, 如果主节点挂了, 客户端会自动切换到其他节点.

但是这样的连接字符串太长了, 十分不方便, 这时可以使用 SRV 记录来连接.

SRV 记录

SRV 记录(Service Record)是一种 DNS 记录, 用于指定特定服务在域中的主机名和端口号. 可以用于负载均衡和服务发现, 使客户端可以动态地查找提供特定服务的服务器.

SRV 记录的设置需要在 DNS 服务商处设置, 以 Cloudflare 为例:

1
选项说明
Name_mongodb._tcp.mongodbsrvSRV 记录的 Name 由服务名和域名组成, _mongodb._tcp 为服务名, 这里 mongodbsrv 是二级域名
Priority0优先级, 对于 MongoDB 副本集不需要特别设置
Weight1权重, 对于 MongoDB 副本集不需要特别设置
Port27017端口号
Targetservera.mongodb.example.com主机名, 填写解析到节点的域名

要为每一个节点都设置 Name 相同的 SRV 记录哦.

TIP提示

设置 SRV 记录的域名和解析到节点使用的域名不一定要相同

假设我们设置 SRV 记录的域名是 example.com, 接下来就可以使用 mongodb+srv 的字符串格式来连接副本集了:

text
mongodb+srv://user:password@mongodbsrv.example.com/?replicaSet=rs0

🎉

FAQ

一些其他的自问自答:

  1. 为什么集群内认证不使用 X.509 证书?

    太麻烦了, 使用 X.509 在节点间认证实际上是使用了基于角色的访问控制, 要在 $external 数据库中为节点配置角色, 还要自己维护 CA.

  2. 配置中的 tls.mode: requireTLSallowConnectionsWithoutCertificates: true 是不是冲突的?

    并不, requireTLS 是要求客户端使用 TLS 连接, allowConnectionsWithoutCertificates 是允许客户端不使用证书进行身份验证.

  3. 为什么不配置 net.tls.clusterFile ?

    因为我们对集群内使用的是密钥文件 (keyfile) 验证.

    clusterFile 用于集群内使用 X.509 证书进行身份验证.

    即使集群内使用 X.509 证书验证, 不配置 clusterFile, MongoDB 也会自动使用 certificateKeyFile 的证书进行验证.


Q.E.D.
在 WSL2 上安装 CUDA