# 部署

MongoDB 常见的部署架构:

  • 单实例
  • 主从集群
    • 与 MySQL 的主从集群类似。
    • MongoDB v4.0 开始弃用主从集群,要改用副本集群。
  • 副本集群
  • 分片集群

# 版本

  • v1.0 :2009 年发布。
  • v2.0 :2011 年发布。
  • v3.0 :2015 年发布。
  • v4.0 :2018 年发布。
    • 原本 mongo 不支持事务操作,只是保证了 CRUD 操作的原子性。现在开始支持事务。客户端可以调用 startTransaction()、commitTransaction() 等函数创建事务,在一个事务中处理多个文档。
  • v4.2
    • 对于副本集群,函数 rs.slaveOk() 改名为 rs.secondaryOk() 。
    • 对于分片集群,支持分布式事务。
  • v4.4
    • mongod 日志改为 JSON 格式。
    • 执行 compact 命令时,不再阻塞当前数据库的所有 CRUD 操作。
  • v5.0 :2021 年发布。
    • 支持创建时序集合。
    • 原本 mongod 重启时,会自动重新执行尚未完成的 index build 任务。现在会根据重启前的进度,继续执行任务。
    • 对于分片集群,增加 reshardCollection 命令,对在线数据重新分片。
  • v5.2
    • 对于分片集群,默认的 chunkSize 从 64MB 改为 128MB 。
  • v5.3
    • 对于副本集群,默认禁止添加多个仲裁节点。
  • v6.0 :2022 年发布。
    • 支持查询加密数据:客户端使用密钥创建一个加密集合,该集合内的数据会加密之后存储在服务器,允许对加密数据进行查询。

# 单实例

# 部署

  • 用 yum 安装:

    wget https://repo.mongodb.org/yum/redhat/7/mongodb-org/4.0/x86_64/RPMS/mongodb-org-server-4.0.5-1.el7.x86_64.rpm
    wget https://repo.mongodb.org/yum/redhat/7/mongodb-org/4.0/x86_64/RPMS/mongodb-org-shell-4.0.5-1.el7.x86_64.rpm
    wget https://repo.mongodb.org/yum/redhat/7/mongodb-org/4.0/x86_64/RPMS/mongodb-org-tools-4.0.5-1.el7.x86_64.rpm
    wget https://repo.mongodb.org/yum/redhat/7/mongodb-org/4.0/x86_64/RPMS/mongodb-org-mongos-4.0.5-1.el7.x86_64.rpm
    yum install -y mongodb-org-*.rpm
    rm -f mongodb-org-*.rpm
    

    然后启动:

    mongod                      # 启动 mongo 服务器
          -f /etc/mongod.conf   # 使用指定的配置文件
    
    • 服务器启动时,默认不会读取配置文件,在前台运行,监听端口 27017 ,
  • 或者用 docker-compose 部署:

    version: "3"
    
    services:
      mongo:
        container_name: mongo
        image: mongo:5.0.5
        command: [mongod, -f, /etc/mongod.conf]
        restart: unless-stopped
        environment:
          MONGO_INITDB_ROOT_USERNAME: root      # 非副本集群时,自动创建管理员用户
          MONGO_INITDB_ROOT_PASSWORD: ******
        ports:
          - 27017:27017
        volumes:
          - /etc/localtime:/etc/localtime:ro
          - ./mongod.conf:/etc/mongod.conf
          - ./data:/data/db   # 官方镜像将 /data/db 目录声明为了 volume ,不挂载到该目录则会自动挂载一个匿名 volume
    
    • 需要调整挂载目录的权限:
      chown -R 999 data
      

# 管理

  • 执行以下命令,即可终止 mongod 服务器:

    mongod -f /etc/mongod.conf --shutdown
    

    或者在客户端执行:

    use admin
    db.shutdownServer()
    
  • 不要用 kill 命令强制终止 mongod 进程,否则磁盘中的数据文件可能损坏。导致 mongod 启动之后,从磁盘读取某些数据文件时会崩溃退出。

    • 可以尝试修复 mongod 的数据文件:
      mongod --repair --dbpath /data/db     # 直接修复原数据目录
      mongod --repair --dbpath /data/db --repairpath /data/db_new   # 将修复之后的数据文件保存到另一个目录
      
      • repair 会检查每个 collection-xx.wt 文件中的每个数据页 pages 并修复,然后重建 index-xx.wt 文件。每小时大概处理 150G 的磁盘文件。
      • repair 操作不能中断,否则不能启动 mongod 。
  • 升级 mongod 服务器版本时:

    • 如果沿用之前的数据目录,则只能升级到最近一个子版本。比如从 v4.0 升级到 V4.2 ,再升级到 v4.4 ,不能跨版本升级。
    • 通过 mongodump 迁移数据,则可以跨版本升级。

# 客户端

命令:

mongo [options] [server_uri] [script.js]
      -u <username>
      -p <password>
  • mongo 服务器的 URI 采用以下格式:
    mongodb://[username:password@]host1[:port1][,hostN[:portN]]...[/db][?options]]
    
    • 例:
      mongodb://127.0.0.1:27017/test                              # 这是 mongo 客户端的默认配置,会连接到 localhost:6379 地址的服务器,使用 test 数据库
      mongodb://root:******@127.0.0.1:27017/test?authSource=admin # 传入用户名、密码,并通过 URI 参数指定验证数据库
      mongodb://10.0.0.1:27017,10.0.0.2:27017,10.0.0.3:27017/?replicaSet=rs1 # 连接到副本集群
      
  • 启动客户端时,默认是打开一个 JavaScript 终端,因此可以执行 js 命令、定义变量、定义函数。
    • 也可以指定一个要执行的 js 脚本,或者从 stdin 传入要执行的 js 命令。如下:
      mongo $server_uri test.js
      echo 'show dbs' | mongo $server_uri
      

# 备份数据

# 导出

  • mongodump 命令:

    mongodump
              -h <host>[:port]
              --authenticationDatabase=admin
              -u <username>
              -p <password>
    
              -d <db>           # 只处理指定的一个数据库。如果不指定,则默认会导出所有数据库,除了 local 数据库
              -c <collection>   # 只处理指定的一个集合。如果不指定,则默认会处理所有集合
              -j 4              # 导出多个集合时,同时最多处理几个集合
              -q <expression>   # --query ,用一个 JSON 格式的表达式筛选文档
              -o <path>         # 导出之后保存到指定目录,默认为 ./dump
              --gzip            # 导出时进行 gzip 压缩
    
    • 例:
      mongodump -h 10.0.0.1:27017 -d db1 -c col1 -q '{"_id": -1}'   # 不导出文档,只导出 index 。而 collection 会在导入 index 时自动创建
      mongodump -h 10.0.0.1:27017 -d db1 -c col1 -q '{"_id": {"$gte": {"$oid": "62a03eb3ec744e00015b1f26"}}}'  # 只导出 _id 大于指定值的文档
      
      • 导出时,会在 -o 目录下按数据库名分别创建子目录,为每个集合保存 <collection>.bson<collection>.metadata.json 两个数据文件。
      • mongo shell 中的查询表达式,不一定能直接用于 mongodump -q 选项。可能需要转换格式,以符合 JSON 语法。
    • mongod 的 WiredTiger 存储引擎默认会将文档、索引压缩之后才存储到磁盘,压缩率大概为 50% 。假设占用磁盘为 50G ,则所有数据加载到内存中大概为 100G 。
      • 如果 mongodump 时不启用 gzip 压缩,则导出文件的体积大概为 100G 。
      • 如果 mongodump 时启用 gzip 压缩,则导出文件的体积大概为 25G 。不过该压缩文件不支持用 tar 命令解压。
      • mongodump 导出的速度大概为每分钟 4G ,mongorestore 导入的速度大概慢一半。
    • mongodump 导出数据时,mongod 需要从磁盘读取这些数据,载入内存,可能导致磁盘 IO 速度达到瓶颈、内存使用率过高。
    • mongodump 导出大量数据时,建议带上查询条件,分批导出数据。比如根据 oid 范围,每次导出一个月的数据。这样导出第一批数据之后,就可以开始导入,减少迁移数据的耗时。
    • mongodump 不适合在线迁移 mongod 的数据。因为在 mongodump 导出数据的过程中,mongod 可能又修改了数据。可采用以下措施:
      • 先执行一次 mongodump + mongorestore 进行全量迁移,再执行几次 mongodump ,根据 _id 导出最近几天的新增文档,进行增量同步。但这样不完全可靠,可能存在几天以前的旧文档,_id 不变而其它字段被修改了。
      • 先执行一次 mongodump + mongorestore 进行全量迁移,再执行 mongoshake ,基于 oplog 增量同步。
  • mongoexport 命令:

    mongoexport                 # 导出为 JSON 或 CSV 格式。该命令兼容 mongodump 命令的大部分参数
              --out out.json    # 导出之后保存到指定文件。默认输出到 stdout
              --type json       # 导出格式,默认为 json
              --pretty          # 导出格式为 JSON 时,进行缩进排版。默认是每个文档占一行
              --limit <int>     # 限制导出的文档数
    

# 导入

  • mongodump 导出的数据要用 mongorestore 命令导入。而 mongoexport 导出的数据,以及其它来源的 JSON、CSV 数据,要用 mongoimport 命令导入。

  • mongorestore 命令:

    mongorestore <path>         # 指定要导入的 BSON 文件。也可以指定目录,这会自动发现该目录下的 bson 文件,但不会发现子目录的
              -h <host>[:port]
              --authenticationDatabase=admin
              -u <username>
              -p <password>
    
              -d <db>
              -c <collection>
              -j 4              # 导入多个集合时,同时最多处理几个集合。并行数过多可能导致服务器过载,中断导入
              --numInsertionWorkersPerCollection 1  # 导入每个集合时,创建几个线程进行并发 insert
              --gzip            # 导入时进行 gzip 解压
              --drop            # 导入每个集合时,如果存在同名的集合,则删除
              --stopOnError     # 遇到报错时立即终止该命令(默认会忽略报错,且命令的返回码依然为 0 )
    
    • mongorestore 导入每个集合时,会先 insert 所有文档,然后 build index 也需要一定时间。
    • mongorestore 导入每个文档时,如果已存在相同 _id 的文档,则会报错 duplicate key ,跳过导入该文档。而且此时需要从磁盘读取 _id 索引的大量数据,导入速度会慢几倍。
    • mongorestore 不支持导入重复的文档,这种问题需要用 mongoimport 命令。
  • mongoimport 命令:

    mongoimport <file>          # 指定文件,导入 JSON 或 CSV 格式的数据,兼容 mongorestore 的大部分参数
              --mode insert     # 导入模式,默认为 insert
                                # 如果导入的某个新文档,遇到字段重复(即该字段建立了唯一索引,并且与已有文档的同一字段取值重复),则会根据 mode 决定如何处理
                                # 有以下几种 mode :
                                # insert :报错说新文档的字段重复,然后继续导入
                                # upsert :用新文档覆盖已有文档。但不允许修改已有文档的 _id 字段
                                # merge :合并
                                # delete :删除与新文档字段重复的所有已有文档
               --upsertFields=_id,<filed>,... # 指定一组字段,用于在 upsert、merge、delete 模式下查询字段值重复的已有文档。默认指定 _id 字段
    

# mongoshake

:一个命令行工具,用于将一个 Mongo 的数据,同步到其它 Mongo ,也可同步到 Kafka 等目的端,属于异步复制。

  • GitHub (opens new window)
  • 由阿里云开源,采用 Golang 语言开发。
  • 用法:
    1. 下载 mongoshake 。
    2. 修改配置文件 collector.conf :
      mongo_urls = mongodb://username:[email protected]:27017   # 源 Mongo 的地址
      # mongo_cs_url =                                    # 源 Mongo 为分片集群时,mongoshake 需要连接 config server ,并分别连接各个 shard server
      # tunnel = direct                                   # 通道模式。默认为 direct ,表示将数据直接同步到其它 Mongo
      tunnel.address = mongodb://10.0.0.1:27017           # 目标端的地址
      sync_mode = all
        # 同步模式,有几种取值:
          # all  :表示全量同步 + 增量同步
          # full :表示仅全量同步,这需要从源 Mongo 获取各个集合的数据
          # incr :表示仅增量同步,这需要从源 Mongo 获取 oplog
      
      # filter.namespace.black = db1.col1,db2             # 同步数据时,不同步哪些 db、collection ,用逗号分隔。默认不会同步系统数据库 admin、config、local
      # filter.namespace.white =                          # 同步的白名单,不能与黑名单同时配置
      
      # 关于全量同步
      # full_sync.reader.collection_parallel = 6          # 并行同步的集合数
      # full_sync.reader.write_document_parallel = 8      # 同步一个集合时,创建多少个线程来并发写入目标端
      # full_sync.collection_exist_drop = true            # 同步一个集合时,如果目的端已存在同名的集合,是否删除它再同步
      full_sync.create_index = background                 # 是否创建索引。background 表示开始同步时,就创建索引
      # full_sync.executor.insert_on_dup_update = false   # 同步插入一个文档时,如果目的端已存在相同 _id 的文档,是否将 insert 语句改为 update 语句
      
      # 关于增量同步
      # incr_sync.mongo_fetch_method = oplog              # 从源 Mongo 获取 oplog ,从而实现增量同步
      incr_sync.shard_key = id                            # 从源 Mongo 获取到文档之后,按 _id 进行哈希,将文档分配给各个 worker 线程
      incr_sync.worker.worker = 32                        # 将文档写入目的端时,创建多少个 worker 线程,进行并发写入
      # incr_sync.target_delay = 0                        # 源 Mongo 的数据,延迟多久才同步到目标端,单位为秒
      # incr_sync.executor.insert_on_dup_update = false   # 同步插入一个文档时,如果目的端已存在相同 _id 的文档,是否将 insert 语句改为 update 语句
      # incr_sync.executor.upsert = false                 # 同步更新一个文档时,如果目的端不存在相同 _id 的文档,是否将 update 语句改为 insert 语句
      # checkpoint.storage.db = mongoshake                # 默认将已成功同步的 oplog 时间戳记录在源 Mongo 里,因此重启 mongoshake 之后也会继续同步
      checkpoint.start_position = 1970-01-01T00:00:00Z    # 增量同步指定时刻之后的 oplog 。如果源 Mongo 已记录 checkpoint ,则忽略该配置参数
      
    3. 执行以下命令,开始同步:
      ./collector.linux -conf collector.conf -verbose 2
      
      查看进度:
      curl 127.0.0.1:9101/progress  # 查看全量同步的进度
      curl 127.0.0.1:9100/repl      # 查看增量同步的进度。其中 logs_repl 表示重放了多少条 oplog ,tps 表示每秒同步多少条 oplog ,LSN_ACK 表示已成功同步的 oplog 的 ts 时间戳
      

# 副本集群

  • 副本集群(Replication)由多个 mongod server 组成,server 的角色分为三种:

    • Primary
      • :主节点,负责处理客户端的读、写请求。
      • 副本集群内,有且仅存在一个 Primary 节点,还可存在任意个(包括零个) Secondary、Arbiter 节点。
    • Secondary
      • :从节点,负责复制主节点的数据。
      • 从节点不能处理客户端的写请求,但能处理读请求,可实现读写分离。
    • Arbiter
      • :仲裁节点。不同步数据(因此不容易出故障),仅参与投票,不能被选举。
      • 仲裁节点不存储数据,而主节点、从节点都属于数据节点。
  • 与 mongod 单实例相比,副本集群能大幅提高可用性。当主节点故障时,其它节点会自动选出一个新的主节点,实现主从切换。

    • 副本集群内只存在一个节点时,它会担任 Primary 节点,供客户端读写,但不能进行主从切换。
    • 副本集群内有超过半数的投票节点在线时,才能开始选举。
      • 选举过程有几秒长。
      • Mongo 限制了一个副本集群最多存在 7 个投票节点。
      • 与 zk 集群类似,投票节点的数量建议为奇数,为偶数时并不会提高可用性。
    • 副本集群至少要部署 3 个投票节点,才能进行主从切换。
      • 建议至少部署 1 主 + 2 从节点。
      • 建议最多部署一个仲裁节点。因为部署多个仲裁节点时,可能投票选出一个尚未完全同步的从节点,担任新的主节点。

# 原理

  • oplog.rs

    • :副本集模式下,会在 local 数据库中创建一个名为 oplog.rs 的集合,采用 capped 类型,用于记录 mongod 执行过的各个写操作,称为 operation log 。
    • oplog 中所有写操作按时间戳 ts 排序。每个写操作具有幂等性,例如将 $inc 操作记录成 $set 操作。
    • 例:oplog 记录的一个写操作
      {
        "op": "c",              // operation 的类型。i 表示 insert ,u 表示 update ,d 表示 delete ,c 表示 db cmd
        "ns": "config.$cmd",    // 命名空间,格式为 <database>.<ollection>
        "ui": UUID("45f0789d-1de0-96f0-a82a-18c49c625b14"),
        "o": {                  // operation 的内容
          "create": "transactions",
          "idIndex": {
            "v": 2,
            "key": {
              "_id": 1
            },
            "name": "_id_"
          }
        },
        "ts": Timestamp(1677498832, 2),   // 前一个数表示 UNIX 时间戳,单位为秒。后一个数表示在这一秒执行了多个操作时,每个操作的序号
        "t": NumberLong(1),               // term ,表示副本集群经过了几次选举
        "v": NumberLong(2),
        "wall": ISODate("2023-03-01T12:00:00.000Z")
      }
      
  • 从节点的启动流程:

    1. 从节点已启动,但尚未加入副本集群,此时处于 STARTUP 状态。
    2. initial sync 阶段:从节点连接到主节点,进行一次全量同步,复制主节点的所有数据库(除了 local 数据库)的文档到本机,然后构建索引。此时处于 STARTUP2 状态。
      • 从节点触发 initial sync 的几种情况:
        • 从节点的 oplog 为空,说明是一个新节点。
        • 从节点的 local 数据库的 replset.minvalid 集合中记录了 "_initialSyncFlag": true ,说明从节点在 initial sync 的过程中重启了。
    3. replication 阶段:从节点通过 oplog 增量同步主节点的数据。此时处于 SECONDARY 状态。
      • 流程如下:
        1. 从节点记录开始 initial sync 的时间戳 t ,作为初次查询 oplog 的起始位置。
        2. initial sync 完成之后,从节点的后台线程 BackgroundSync 会循环执行:先选取其它一个数据节点作为同步源,然后向它查询在时间戳 t 以后的 oplog ,复制到本机的内存缓冲区 OplogBu​​ffer 。
          • BackgroundSync 会循环执行,不断查询是否存在新的 oplog :db.oplog.rs.find({ts:{$gte: t}})
          • 如果某次成功复制 oplog 并重放,则刷新时间戳 t ,作为下一次查询 oplog 的起始位置。
        3. 从节点的线程 ReplBatcher 从 OplogBu​​ffer 中获取 oplog ,存入 opQueue 。
        4. 从节点的线程 OplogApplier 以 batch 为单位从 opQueue 中获取 oplog 。然后创建默认 16 个 writer 线程,重放这些 oplog 中的写操作。
          • 多个 writer 线程并行工作,不能保证所有 oplog 都是按时间戳 ts 的顺序执行。但能保证每个 batch 中同一个 collection 的所有 oplog ,会被同一个线程有序执行。
        5. 从节点将已重放的 oplog 保存到本机的 local 数据库中。
      • 如果一个从节点在 SECONDARY 状态终止,过了很长时间才重启。则可能发现自己最新的一条 oplog 的时间戳 ts ,比主节点最旧的一条 oplog 的时间戳 ts 更旧,说明从节点落后了太多数据,不能通过 oplog 正常同步。此时从节点会报错 We are too stale ,阻塞在 RECOVERING 状态,用户可采取以下几种措施:
        • 清空从节点的数据目录 dbpath ,让它重新 initial sync 。
        • 将其它节点的数据目录 dbpath 拷贝到从节点,这包括了普通数据库和 local 数据库。
      • 建议不要在 mongod 运行时拷贝 dbpath ,因为数据文件可能正在被修改。
      • 增加 oplog 的体积,可容忍从节点落后更多数据。
  • 客户端连接副本集群时,写请求只能发送到主节点,而读请求有几种偏好(Read Preference):

    primary             # 读请求只能发送到主节点。这是默认值
    primaryPreferred    # 读请求尽量发送到主节点,也可以发送到从节点
    secondary           # 读请求只能发送到从节点
    secondaryPreferred  # 读请求尽量发送到从节点,也可以发送到主节点
    nearest             # 读请求发送给网络延迟较低的节点
    
    • 默认从节点不支持读请求,除非执行 rs.secondaryOk() 。因为从节点采用半同步复制,可能没有最新的数据。
    • 数据库负载不是很大的话,没必要读写分离,客户端可以将读、写请求都发给主节点。
  • writeConcern

    • :Mongo 的一个配置参数,用于控制主从节点之间的数据同步策略。如下:
      db.col1.insertOne(
          {...},
          {writeConcern: {w: "majority", j: true, wtimeout: 100} }
      );
      
    • w :表示每提交一个写操作时,要等待多少个数据节点确认写入,才认为写操作完成,然后才能开始下一个写操作。
      • 如果为 1 ,则只要等待一个数据节点确认写入,即主节点自己。属于异步复制。
      • 如果为 majority ,则要等待大多数(writeMajorityCount)的数据节点确认写入,属于半同步复制。
        • 此时副本集群不能是 1 主 + 1 从 + 1 仲裁节点,否则一个数据节点故障时,剩下一个数据节点不能提交写操作。因为 writeMajorityCount 为 2 ,而仲裁节点不属于数据节点。
      • w 的默认值在不同 mongo 版本、不同场景有差异,建议手动查看。
    • j :表示怎样才算确认写入。
      • 如果为 true ,则等待数据节点将写操作保存到预写日志,才算确认写入。
      • 如果为 false ,则在内存中执行了写操作,就算确认写入。此时可能因为断电而丢失数据。
      • j 的默认值,取决于 writeConcernMajorityJournalDefault 。
    • wtimeout 表示等待写操作完成的超时时间。如果超时,则报错。
      • 如果不指定超时时间,则可能一直阻塞等待。

# 部署

  1. 集群节点之间默认采用密钥文件进行认证。执行以下命令,生成密钥文件:

    openssl rand -base64 512 > mongo.key
    chown mongod mongo.key
    chmod 400 mongo.key
    

    然后将密钥文件拷贝到各个节点。

  2. 在各个节点的 mongod.conf 文件中增加配置:

    replication:                      # 让 mongod 工作在副本集群模式。默认为 Standalone 模式
      # oplogSizeMB: <int>            # oplog 集合的体积。默认为磁盘空间的 5% ,默认值最大为 50GB
      replSetName: rs0                # 副本集群名称。同一副本集群中的所有节点必须采用相同的 replSetName
    security:
      authorization: enabled
      keyFile: /etc/mongo/mongo.key   # 密钥文件的路径
    
  3. 登录一个节点,启动副本集群:

    rs.initiate({
        _id: "rs0",             // 副本集群的名称
        members: [              // 副本集群的成员列表
            { _id: 0, host: "mongo-1:27017" },
            { _id: 1, host: "mongo-2:27017" },
            { _id: 2, host: "mongo-3:27017", priority: 0}
        ]
    })
    
    • 副本集群必须初始化一次之后才能启动,也只需要初始化一次。
    • 副本集群的 members 列表,至少要包含一个 mongo 节点,此时它担任 Primary 角色,作为单节点的副本集群运行。
    • 不允许在 priority 为 0 的节点,或仲裁节点上进行初始化。
  4. 创建账号:

    use admin
    db.createUser(
      {
        user: 'root',
        pwd: '******',
        roles: [{role: 'root', db: 'admin'}]
      }
    )
    db.auth('root', '******')
    
    • 初始化副本集群之前,只允许从本机地址登录 mongod ,比如执行 mongo 127.0.0.1:27017 。初始化之后,才可以创建账号,从其它地址登录。

# 管理

相关命令:

rs.initiate(cfg)                // 启动副本集群,可传入一些配置参数进行初始化,其余的配置参数则采用默认值
rs.reconfig(cfg)                // 重新配置副本集群,可覆盖已有的配置参数
rs.conf()                       // 显示副本集群的配置
rs.status()                     // 显示副本集群的状态

rs.add("mongo-1:27017")         // 添加从节点
rs.remove("mongo-1:27017")      // 删除从节点
rs.addArb("mongo-4:27017")      // 添加仲裁节点
rs.secondaryOk()                // 当客户端连接到从节点时,执行该命令,则允许读取从节点的数据,不过有效期仅限当前会话

rs.printReplicationInfo()           // 显示当前节点的 oplog 的状态
rs.printSecondaryReplicationInfo()  // 显示所有从节点的同步状态
  • 例:查看副本集群的状态

    rs0:SECONDARY> rs.status()
    {
        "set" : "rs0",
        "date" : ISODate("2019-12-19T09:22:27.736Z"),
        "myState" : 1,
        "term" : NumberLong(1),
        "syncingTo" : "",
        "syncSourceHost" : "",
        "syncSourceId" : -1,
        "heartbeatIntervalMillis" : NumberLong(2000),
        "majorityVoteCount" : 2,
        "writeMajorityCount" : 2,
        ...
        "members" : [                       // 所有节点的信息
            {
                "_id" : 0,
                "name" : "mongo-1:27017",
                "ip" : "10.244.25.178",
                "health" : 1,               // 0 代表下线,1 代表在线
                "state" : 1,                // 该节点的状态
                "stateStr" : "PRIMARY",     // 该节点的状态描述
                "uptime" : 1798,            // 该节点的在线时长
                "optime" : {
                    "ts" : Timestamp(1576746303, 1),
                    "t" : NumberLong(1)
                },
                "optimeDate" : ISODate("2019-12-19T09:05:03Z"),  // 该节点最后一次同步 oplog 的时间
                "syncingTo" : "",
                "syncSourceHost" : "",
                "syncSourceId" : -1,
                "infoMessage" : "could not find member to sync from",
                "configVersion" : 1,
                "self" : true,              // 该节点是否为当前登录的节点
                "lastHeartbeatMessage" : ""
            },
            ...
    
    • 节点的状态分为以下几种,分别对应一个数字编号:
      0   STARTUP     # 已启动,但尚未加入副本集群
      1   PRIMARY
      2   SECONDARY
      3   RECOVERING  # 正在恢复数据,不能接受客户端的读请求
      5   STARTUP2    # 已启动,而且加入了某个副本集群,正在进行初始同步。随后会变成 PRIMARY、SECONDARY 等状态
      6   UNKNOWN     # 其它节点不能判断该节点的状态
      7   ARBITER     # 仲裁者
      8   DOWN        # 其它节点不能访问该节点
      9   ROLLBACK    # 正在回滚
      10  REMOVED     # 该节点从副本集群中移除了
      
      • 从节点处于 STARTUP2、RECOVERING 状态时,有权投票,但不能被选举为主节点。
  • 例:修改副本集群的配置

    rs0:PRIMARY> rs.conf()
    {
        "_id" : "rs0",
        "version" : 1,
        "protocolVersion" : NumberLong(1),
        "writeConcernMajorityJournalDefault" : true,
        "members" : [
            {
                "_id" : 0,
                "host" : "mongo-2:27017",
                "arbiterOnly" : false,
                "buildIndexes" : true,
                "hidden" : false,  // 该节点是否隐藏。隐藏节点的 priority 为 0 ,有权投票,但不能被选举为主节点
                "priority" : 1,    // 该节点在竞选主节点时的优先级,取值范围为 0.0~1000.0 。取值越大则越容易当选,取值为 0 则不会当选
                "tags" : {},
                "slaveDelay" : NumberLong(0),
                "votes" : 1        // 该节点是否有权投票
            },
            ...
    
    
    cfg = rs.conf()
    cfg.members[2].priority = 0
    rs.reconfig(cfg)
    

# 分片集群

  • 分片集群(Shard Cluster):在 MongoDB 中划分多个分片(shard),将数据分散存储到各个分片。

    • 优点:方便横向扩容,能大幅提高数据容量、并发读写量。
  • 分片集群需要部署 3 种服务器,每种服务器都可以部署多实例以提高可用性:

    • shard server
      • :运行 mongod 进程,负责存储分片的数据。
      • 每个分片可以部署一个 mongo 单实例,或副本集群。
    • config server
      • :运行 mongod 进程,并设置 configsvr=true ,负责存储集群的元数据、配置信息。
    • mongos
      • :运行 mongos 进程,从 config server 获取集群信息,然后接收客户端的请求、路由转发到各个 shard mongo 。
  • shard mongo 以 chunk 为单位存储数据。

    • 默认限制了 chunkSize 为 64MB 。如果写入新文档之后,一个 chunk 超过最大体积,则自动拆分成两个 chunk 。
    • config server 会运行一个 balancer 进程,监控各个 shard 的 chunk 数量,自动迁移 chunk 以使得它们的 chunk 数量差不多,实现负载均衡。
      • 迁移需要遵循分片规则。
      • 如果 chunk size 设置得较小,则容易经常迁移,导致大量读写磁盘、网络传输,增加服务器负载。
  • 几种分片方式:

    • 哈希分片(Hash Sharding)
      • :选取 Mongo 文档的一个或复合字段作为 shard key ,计算 hash(shard_key) / shard_count,从而决定将文档分配到第几个分片。
      • 优点:分片很均匀,各个分片的文档数量差不多。
      • 缺点:范围查询时的效率低。比如要查询一串 key 取值连续的文档时,需要分别读取多个分片,类似顺序读写磁盘与随机读写磁盘的区别。
    • 范围分片(Range Sharding)
      • :将 shard key 取值分为几个范围,然后分配到各个分片。比如 key 取值为 0~10000 的文档都分配到第 1 个分片。
      • 优点:key 取值连续的一组文档会被保存到同一个分片,因此范围查询时的效率高。
      • 缺点:分片不均匀。
    • 区域分片(Zone Sharding)
      • :将 shard key 取值分为几个区域,每个区域分配一个或多个分片。
      • 优点:与范围分片相比,分片更均匀。

# 部署

  1. 以副本集群模式,部署 config server ,建议部署 3 个节点。

    • docker-compose.yml 的配置示例:
      version: "3"
      
      services:
        mongo:
          container_name: mongo-configsvr
          image: mongo:4.4
          command: mongod -f /etc/mongod.conf
          restart: unless-stopped
          ports:
            - 27018:27018
          volumes:
            - /etc/localtime:/etc/localtime:ro
            - ./mongod.conf:/etc/mongod.conf
            - ./mongo.key:/etc/mongo/mongo.key
            - ./data:/data/db
      
    • mongod.conf 的配置示例:
      net:
        port: 27018
        bindIp: 0.0.0.0
      
      storage:
        dbPath: /data/db
        directoryPerDB: true
        engine: wiredTiger
      
      security:
        authorization: enabled
        keyFile: /etc/mongo/mongo.key
      
      replication:                  # 启用副本集群
        replSetName: rs-configsvr   # 设置副本集群名称。当前分片集群中,所有 config server 必须配置同一个 replSetName
      
      sharding:                     # 启用分片集群
        clusterRole: configsvr
      
    • 启动 config server 之后,执行 mongo 127.0.0.1:27018 从本机地址登录,进行初始化:
      rs.initiate( {
          _id: "rs-configsvr",
          configsvr: true,      // 需要声明该副本集群为 config server 类型
          members: [
              { _id: 0, host: "10.0.0.1:27018"},
              { _id: 1, host: "10.0.0.2:27018"},
              { _id: 2, host: "10.0.0.3:27018"}
          ]
      } )
      rs.status()
      
      然后用 db.createUser() 创建账号。服务器之间采用 mongo.key 文件进行认证,创建的账号专供用户使用。
  2. 以副本集群模式,部署 shard server 。可以有多个分片,每个分片部署一个 mongod 副本集群。

    • docker-compose.yml 的配置与 config server 差不多。
    • mongod.conf 的配置示例:
      ...
      
      replication:
        replSetName: rs-shardsvr-0
      
      sharding:
        clusterRole: shardsvr
      
    • 启动 config server 之后,执行 mongo 127.0.0.1:27017 从本机地址登录,进行初始化:
      rs.initiate( {
          _id: "rs-shardsvr-0",   // 同一个分片中的 shard server 必须配置同一个 replSetName
          members: [
              { _id: 0, host: "10.0.0.1:27017"},
              { _id: 1, host: "10.0.0.2:27017"},
              { _id: 2, host: "10.0.0.3:27017"}
          ]
      } )
      rs.status()
      
      然后用 db.createUser() 创建账号。
    • 上述部署了一个分片 rs-shardsvr-0 ,包含 3 个 mongod 节点。还可部署更多分片,比如 rs-shardsvr-1、rs-shardsvr-2 。
  3. 以单实例模式,部署 mongos 。

    • docker-compose.yml 的配置示例:
      version: "3"
      
      services:
        mongo:
          container_name: mongos
          image: mongo:4.4
          command: mongos -f /etc/mongos.conf   # mongos 的启动命令
          restart: unless-stopped
          ports:
            - 27019:27019
          volumes:
            - /etc/localtime:/etc/localtime:ro
            - ./mongos.conf:/etc/mongos.conf
            - ./mongo.key:/etc/mongo/mongo.key
            # - ./data:/data/db     # mongos 运行时会从 config server 读写数据,不会直接写磁盘
      
    • mongos.conf 的配置示例:
      net:
        port: 27019
        bindIp: 0.0.0.0
      
      security:
        # authorization: enabled    # mongos 没有 authorization 配置参数
        keyFile: /etc/mongo/mongo.key
      
      sharding:
        configDB: rs-configsvr/10.0.0.1:27018,10.0.0.2:27018,10.0.0.3:27018   # config server 的地址
      
    • 启动 mongos 之后,执行 mongo 127.0.0.1:27019 从本机地址登录,进行初始化:
      sh.addShard('rs-shardsvr-0/10.0.0.1:27017,10.0.0.2:27017,10.0.0.3:27017')   // 添加一组 shard server ,mongos 会将这些地址保存到 config server
      sh.addShard('rs-shardsvr-1/10.0.0.4:27017,10.0.0.5:27017,10.0.0.6:27017')
      sh.status()
      
      然后用 db.createUser() 创建账号。
    • 上述部署了一个 mongos 。建议部署 3 个实例,用 nginx 反向代理之后给客户端访问,从而实现负载均衡。各个 mongos 独立工作,只依赖 config server 。
  4. 部署了分片集群之后,客户端连接到 mongos 的地址,即可使用集群。

    • 需要给数据库、集合启用分片模式:
      sh.enableSharding("db1")
      sh.shardCollection("db1.col1", {"_id" : 1}) // 对 db1 数据库中的 col1 集合开启分片模式,以 _id 字段作为分片键
      
      • {"_id" : 1} 表示基于范围的分片,而 {"_id" : "hashed"} 表示基于哈希的分片。