MongoDB 基础使用与运维

702 阅读52分钟

MongoDB 基本介绍

MongoDB-2021.jpg

特点

MongoDB 作为一个现代的,有特色的非关系型数据库管理系统,有以下几个显著的特点:

  1. 格式灵活: MongoDB 使用类 JSON 的格式 (BSON) 来存储数据,这使得其能够支持包括数组和嵌套文档等多样的数据类型。
  2. 无模式的数据模型: MongoDB 是一个基于文档的数据库,其中每个文档都可以有自己的独特结构。不同的文档之间并无固定的模式,非常适合应对需求变化频繁的开发环境。
  3. 水平可扩展性: MongoDB 支持分片,从而能进行大规模的数据存储。你可以通过添加更多的节点,横向扩展你的数据库,以满足数据量的增长。
  4. 支持丰富的查询和处理操作: 你可以在 MongoDB 中执行复杂的查询,比如过滤,排序以及聚合等。
  5. 索引: 你可以在任何字段上创建索引,以支持快速的查询。索引在 MongoDB 中发挥着至关重要的作用。
  6. 高可用性: MongoDB 的副本集可以提供数据冗余和高可用性。在副本集中,多个节点可以拥有相同的数据集,从而提供数据冗余,保证了系统在单个节点故障时的可用性。
  7. 易于管理和运维: MongoDB 设计了简洁易用的管理工具,使得运维工作简单且高效。
  8. 地理空间数据: MongoDB 支持地理空间数据和查询,这使得其在地理信息系统中非常具有优势。

适用场景

  1. 大规模的数据存储: 由于其内建的水平可扩展性,MongoDB 是大规模的数据存储的理想选择。非结构化的数据存储,如图像和视频,可以直接存储在 MongoDB 中,而不需要预先定义模式。
  2. 实时分析和数据处理: MongoDB 的聚合管道让我们可以在数据库层实现实时的数据分析和处理。
  3. 地理空间数据: 如果你的项目需要处理地理空间数据,那么 MongoDB 是一个非常好的选择,因为它有对地理空间索引以及查询的内建支持。
  4. 移动应用: MongoDB 能够处理大规模的数据并且有快速的查询速度,所以它非常适合作为移动应用的后端服务。
  5. 内容管理系统: 由于 MongoDB 的灵活性,它十分适合储存不同类型和结构的内容。这对于内容管理系统来说非常重要,因为不同的内容可能有不同的属性。
  6. 物联网: 在物联网领域,可以使用 MongoDB 来收集和处理来自各种设备的大量数据。

名词解释

术语解释
数据库(Database)数据库是数据的物理容器,它可以容纳多个集合(collections)。每个数据库都独立拥有自己的磁盘文件。在 MongoDB 中,通常会为不同的应用程序或不同的数据组设置不同的数据库。
集合(Collection)集合等同于关系型数据库中的表格,它用来存储文档(documents),并且所有文档必须存储在某个集合内。MongoDB 的集合不需要预定义的结构。
文档(Document)文档是一个键值(key-value)对,类似于 JSON 对象。在 MongoDB 中,文档是数据的主要组成部分,不同的文档间不需要有相同的结构。
字段(Field)字段可以包含各种类型的数据,包括数组和其他文档。它们是文档的构成部分,类似于关系型数据库中的列。
索引(Index)MongoDB 中的索引用于快速查询数据库。索引是对数据库表中一列或多列的值进行排序的结构
主键(Primary Key)在 MongoDB 中,主键通常是一个特殊的字段,名为 "_id"。MongoDB 自动为每个文档创建这个字段。你也可以自定义 "_id",只要确保其值在集合中是独一无二的即可。

MongoDB 安装部署

安装 MongoDB 7.0

  1. 进入 www.mongodb.com/try/downloa… ,点击 Select package:

  1. 选择所需的 Version、Platform、Package 后,点击 Copy link:

  1. 在你的服务器上下载并安装 MongoDB 数据库:
# 从 MongoDB 的官方网站下载 7.0.4 的 tar 压缩包
root@mdb:~# wget https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-debian11-7.0.4.tgz

# 创建目录来存放 MongoDB 的文件
root@mdb:~# mkdir -p /usr/local/mongodb

# 解压下载好的 MongoDB 压缩包到目标目录
root@mdb:~# tar -xf mongodb-linux-x86_64-debian11-7.0.4.tgz -C /usr/local/mongodb

# 在全局环境变量文件中添加 MongoDB 的路径
root@mdb:~# cat >> /etc/profile <<EOF
export MONGODB_HOME=/usr/local/mongodb/mongodb-linux-x86_64-debian11-7.0.4
export PATH=\${MONGODB_HOME}/bin:\${PATH}
EOF

# 更新当前会话的环境变量,使设置生效
root@mdb:~# source /etc/profile

# 检查 MongoDB 的版本,确保安装成功
root@mdb:~# mongod --version
db version v7.0.4
Build Info: {
    "version": "7.0.4",
    "gitVersion": "38f3e37057a43d2e9f41a39142681a76062d582e",
    "openSSLVersion": "OpenSSL 1.1.1n  15 Mar 2022",
    "modules": [],
    "allocator": "tcmalloc",
    "environment": {
        "distmod": "debian11",
        "distarch": "x86_64",
        "target_arch": "x86_64"
    }
}

# 创建 MongoDB 的数据库目录和日志目录
root@mdb:~# mkdir -p /data/mongodb/{db,log}

# 以守护程序模式启动 MongoDB 服务
root@mdb:~# mongod --dbpath /data/mongodb/db --logpath /data/mongodb/log/mongod.log --fork
about to fork child process, waiting until server is ready for connections.
forked process: 2400
child process started successfully, parent exiting

# 检查 MongoDB 服务是否正在运行
root@mdb:~# ps -ef | grep mongod | grep -v grep
root        2400       1  2 15:36 ?        00:00:00 mongod --dbpath /data/mongodb/db --logpath /data/mongodb/log/mongod.log --fork

安装 Mongosh 工具

MongoDB Shell(mongosh)的语法是构建在 JavaScript 之上的。这意味着你可以在 MongoDB Shell 里使用 JavaScript 的大部分功能,从简单的算术运算和变量操作,到诸如循环、条件语句以及函数等更复杂的运算。此外,你还可以使用一些额外的命令和方法来操作 MongoDB 数据库。这让 MongoDB Shell 既能承担交互式操作 MongoDB 的接口的角色,也拥有处理复杂查询和组织数据的能力。

在 MongoDB 6.0 之前,我们都是通过 mongo 来连接,在后续版本通过 mongosh 来连接,需要单独下载和安装。

  1. 选择 MongoDB 客户端连接工具:

  1. 在你的服务器上下载并安装 MongoDB Shell
# 从 MongoDB 的官方网站下载 mongosh 的 .deb 安装包
root@mdb:~# wget https://downloads.mongodb.com/compass/mongodb-mongosh_2.1.1_amd64.deb

# 使用 dpkg 命令安装下载好的 .deb 包
root@mdb:~# dpkg -i mongodb-mongosh_2.1.1_amd64.deb

# 检查 mongosh 的版本,确认安装成功
root@mdb:~# mongosh --version
2.1.1

# dpkg -i 用于安装 .deb 包,请注意,可能在安装过程中遇到缺少依赖的问题,可以通过运行 apt-get -f install 来解决这类问题。

开启身份验证模式

  1. 默认情况下,MongoDB 在无认证模式下运行:
root@mdb01:~# mongosh
Current Mongosh Log ID:        658693f946e08bc5bb23dd09
Connecting to:                mongodb://127.0.0.1:27017/?directConnection=true&serverSelectionTimeoutMS=2000&appName=mongosh+2.1.1
Using MongoDB:                7.0.4
Using Mongosh:                2.1.1

For mongosh info see: https://docs.mongodb.com/mongodb-shell/


To help improve our products, anonymous usage data is collected and sent to MongoDB periodically (https://www.mongodb.com/legal/privacy-policy).
You can opt-out by running the disableTelemetry() command.

------
   The server generated these startup warnings when booting
   2023-12-23T15:36:10.562+08:00: Access control is not enabled for the database. Read and write access to data and configuration is unrestricted
   2023-12-23T15:36:10.562+08:00: You are running this process as the root user, which is not recommended
   2023-12-23T15:36:10.562+08:00: This server is bound to localhost. Remote systems will be unable to connect to this server. Start the server with --bind_ip <address> to specify which IP addresses it should serve responses from, or with --bind_ip_all to bind to all interfaces. If this behavior is desired, start the server with --bind_ip 127.0.0.1 to disable this warning
   2023-12-23T15:36:10.562+08:00: /sys/kernel/mm/transparent_hugepage/enabled is 'always'. We suggest setting it to 'never'
   2023-12-23T15:36:10.562+08:00: Soft rlimits for open file descriptors too low
------

test> show dbs
admin   40.00 KiB
config  12.00 KiB
local   40.00 KiB
  1. 创建管理员用户:
// 切换到 admin 数据库
test> use admin
switched to db admin

// 创建一个名为 "admin" 的管理员用户
// 赋予两种角色: "userAdminAnyDatabase" 和 "readWriteAnyDatabase"
// 前者允许用户在任何数据库上创建和修改用户和角色,后者允许用户在任何数据库上进行读写操作
admin> db.createUser(
...   {
...     user: "admin",
...     pwd: "admin@12345",
...     roles: [ { role: "userAdminAnyDatabase", db: "admin" }, "readWriteAnyDatabase" ]
...   }
... )
{ ok: 1 }

// 创建一个名为 "root" 的超级管理员用户
// 赋予 "root" 角色,这个角色有最高的权限,可以在所有数据库上进行任何操作
admin> db.createUser(
...   {
...     user: "root",
...     pwd: "1qaz!QAZ",
...     roles: [ "root" ]
...   }
... )
{ ok: 1 }

// 查看当前数据库(这里是 admin 数据库)所有的用户信息
admin> show users
[
  {
    _id: 'admin.admin',
    userId: UUID('df806993-cfd2-416f-a592-a1177f2b70f6'),
    user: 'admin',
    db: 'admin',
    roles: [
      { role: 'readWriteAnyDatabase', db: 'admin' },
      { role: 'userAdminAnyDatabase', db: 'admin' }
    ],
    mechanisms: [ 'SCRAM-SHA-1', 'SCRAM-SHA-256' ]
  },
  {
    _id: 'admin.root',
    userId: UUID('3e4aa8bf-6533-4e85-ab81-618d6fc01a3c'),
    user: 'root',
    db: 'admin',
    roles: [ { role: 'root', db: 'admin' } ],
    mechanisms: [ 'SCRAM-SHA-1', 'SCRAM-SHA-256' ]
  }
]

// 查看所有数据库的用户信息
admin> db.system.users.find()
[
  {
    _id: 'admin.admin',
    userId: UUID('df806993-cfd2-416f-a592-a1177f2b70f6'),
    user: 'admin',
    db: 'admin',
    credentials: {
      'SCRAM-SHA-1': {
        iterationCount: 10000,
        salt: 'bRjOjM/ONVvRuZ7KZJTV1w==',
        storedKey: 'H2c86WtjpftOcAG0KHiG5vfAE1c=',
        serverKey: 'ReW52R/ZY498JpkJlBjUjlhodkw='
      },
      'SCRAM-SHA-256': {
        iterationCount: 15000,
        salt: 'eX2zi8g9hAbkrOBImKR5AfGIuykF1yhzYvKlrw==',
        storedKey: '9Tvu5dmo4JOjSbkvMzB0KIs9JuT5ka0UzZEPA/MCmWo=',
        serverKey: 'Ru0VEbWgTENP6BHIDFb6Hj7LD/9K2GxzoTCQBi3UW+s='
      }
    },
    roles: [
      { role: 'readWriteAnyDatabase', db: 'admin' },
      { role: 'userAdminAnyDatabase', db: 'admin' }
    ]
  },
  {
    _id: 'admin.root',
    userId: UUID('3e4aa8bf-6533-4e85-ab81-618d6fc01a3c'),
    user: 'root',
    db: 'admin',
    credentials: {
      'SCRAM-SHA-1': {
        iterationCount: 10000,
        salt: '0Q/c9p04yNNIFygnJwdf6A==',
        storedKey: 'lMs5dzDokIbP8tCYWtgpt55Tfc0=',
        serverKey: 'AgxG7vX1Y3S0DDMGNbz1PwdeeHY='
      },
      'SCRAM-SHA-256': {
        iterationCount: 15000,
        salt: 'l8AK7D7QsN6vFOOvzf59E3wqUS/jkJy1uZdxKQ==',
        storedKey: '3pFc8EK4p1slUjkj4VL8qXnRuqP2pkRk087h0ES0548=',
        serverKey: 'CUiS/zgS/Pz1otzRO3A2Z0u33sz9LOGTZrGPBsGgJ54='
      }
    },
    roles: [ { role: 'root', db: 'admin' } ]
  },
  {
    _id: 'testdb.u_test',
    userId: UUID('21510bde-5f03-4331-9813-af55f0948e70'),
    user: 'u_test',
    db: 'testdb',
    credentials: {
      'SCRAM-SHA-1': {
        iterationCount: 10000,
        salt: '2aRBAt1R+UyvS20d3788kQ==',
        storedKey: 'L6IdMZ0fAJsQTsslrTJvv++99+Q=',
        serverKey: 'wxSEWR+jzm++u0LTzA59TR3enrs='
      },
      'SCRAM-SHA-256': {
        iterationCount: 15000,
        salt: 'aAPBtKOOORfYXxFmN65u5xr2yVb48ZxK6RbnJw==',
        storedKey: 'uDam7UgNB0sGaPVieLPgKVi8+GRPzp0MryNhIDNcxoI=',
        serverKey: 'OAeWWXZ+UHo+2oM26ALRefz+V79JU+LhYADaKlKcP28='
      }
    },
    roles: [ { role: 'readWrite', db: 'testdb' } ]
  }
]
  1. 关闭 MongoDB 服务:
# 当然可以直接 kill 掉其进程,我们更建议优雅地关闭 MongoDB 服务
root@mdb:~# mongosh --eval 'db.adminCommand({ shutdown: 1 })'
Current Mongosh Log ID:        6586999c1e8c27de6439c404
Connecting to:                mongodb://127.0.0.1:27017/?directConnection=true&serverSelectionTimeoutMS=2000&appName=mongosh+2.1.1
Using MongoDB:                7.0.4
Using Mongosh:                2.1.1

For mongosh info see: https://docs.mongodb.com/mongodb-shell/

------
   The server generated these startup warnings when booting
   2023-12-23T15:36:10.562+08:00: Access control is not enabled for the database. Read and write access to data and configuration is unrestricted
   2023-12-23T15:36:10.562+08:00: You are running this process as the root user, which is not recommended
   2023-12-23T15:36:10.562+08:00: This server is bound to localhost. Remote systems will be unable to connect to this server. Start the server with --bind_ip <address> to specify which IP addresses it should serve responses from, or with --bind_ip_all to bind to all interfaces. If this behavior is desired, start the server with --bind_ip 127.0.0.1 to disable this warning
   2023-12-23T15:36:10.562+08:00: /sys/kernel/mm/transparent_hugepage/enabled is 'always'. We suggest setting it to 'never'
   2023-12-23T15:36:10.562+08:00: Soft rlimits for open file descriptors too low
------

MongoNetworkError: connection 2 to 127.0.0.1:27017 closed
  1. 启用 MongoDB 的访问控制,并认证测试连接:
# 如果想要在连接 MongoDB 时进行用户身份验证,这通常通过启动 MongoDB 时加上 --auth 选项,
# 或者在配置文件中设置 security.authorization: enabled 来开启。
root@mdb:~# mongod --dbpath /data/mongodb/db --logpath /data/mongodb/log/mongod.log --fork --auth
about to fork child process, waiting until server is ready for connections.
forked process: 3044
child process started successfully, parent exiting

# 开启访问控制后,在连接 MongoDB 时就需要提供用户名和密码进行身份验证了
root@mdb:~# mongosh -u root -p '1qaz!QAZ' --authenticationDatabase admin
Current Mongosh Log ID:        65869d5a0a5414606799ca40
Connecting to:                mongodb://<credentials>@127.0.0.1:27017/?directConnection=true&serverSelectionTimeoutMS=2000&authSource=admin&appName=mongosh+2.1.1
Using MongoDB:                7.0.4
Using Mongosh:                2.1.1

For mongosh info see: https://docs.mongodb.com/mongodb-shell/

------
   The server generated these startup warnings when booting
   2023-12-23T16:40:06.419+08:00: You are running this process as the root user, which is not recommended
   2023-12-23T16:40:06.419+08:00: This server is bound to localhost. Remote systems will be unable to connect to this server. Start the server with --bind_ip <address> to specify which IP addresses it should serve responses from, or with --bind_ip_all to bind to all interfaces. If this behavior is desired, start the server with --bind_ip 127.0.0.1 to disable this warning
   2023-12-23T16:40:06.419+08:00: /sys/kernel/mm/transparent_hugepage/enabled is 'always'. We suggest setting it to 'never'
   2023-12-23T16:40:06.419+08:00: Soft rlimits for open file descriptors too low
------

test>

MongoDB 常用命令

image.png

数据库操作

// 创建和使用 database:切换到名为 "testdb" 的数据库,如果这个数据库不存在,则会被新建
use testdb

// 在 "test" 表中插入一个简单的行数据 {"a": 1},注意 "test" 表会在首次插入行数据时自动创建
db.test.insert({"a": 1})

// 用于列出服务器中存在的所有数据库
show dbs

// 用于删除当前所在的数据库,以及其中的所有集合和数据(在执行命令之前,应该使用 use dbname 命令切换到目标数据库)
db.dropDatabase()

用户权限操作

用户增删改查

// 进入到目标数据库
use testdb

// 创建普通用户并赋权
db.createUser(
  {
    user: "u_testdb_rw",
    pwd: "newpassword",
    roles: [ { role: "readWrite", db: "testdb" } ]
  }
)

// 更新用户权限
db.updateUser("u_testdb_rw", {
  roles: [
    { role : "readWrite", db : "testdb" }, // 已存在的角色,赋予用户在 "testdb" 数据库中数据读写权限
    { role : "read", db : "demodb" }       // 新增角色,赋予用户在 "demodb" 数据库中数据只读权限
  ]
})

// 删除用户
db.dropUser('u_testdb_rw')

// 查看当前数据库中的用户
db.getUser('u_testdb_rw')
db.getUsers()

常见内置角色

MongoDB 中包含一些预定义的角色,这些角色被赋予了用于不同目的的一组权限,你可以查阅 MongoDB 文档中的 Roles and Privileges 部分获取更多关于角色和权限的信息。

角色描述
root超级用户角色,拥有在所有数据库上执行所有操作的权限,包括管理用户及角色、管理数据库和集合、读写数据等。
readWrite提供了在特定(当前)数据库上执行读写操作的权限,包括 CRUD(创建/读取/更新/删除)操作、创建索引和执行 mapReduce 等。
read提供了在特定(当前)数据库上执行读操作的权限,包括查询和获取集合信息等,但不包括写操作。
readWriteAnyDatabase提供了在所有数据库上执行读写操作的权限,包括系统数据库。通常赋予给需要在多个数据库中读写数据的用户。
readAnyDatabase提供了在所有数据库上执行读操作的权限,包括系统数据库。通常赋予给只需要进行数据查询但不修改数据的用户。

角色分类

root (root role)
├── readWriteAnyDatabase       # 在所有数据库中都有读写数据的权限
├── dbAdminAnyDatabase         # 在所有数据库中都有执行管理任务的权限
├── userAdminAnyDatabase       # 在所有数据库中都有管理用户的权限
├── clusterAdmin               # 具有一系列用于管理整个MongoDB集群的权限
│   ├── clusterManager         # 提供管理和监视复制和分片特性所需的权限
│   ├── clusterMonitor         # 只提供监视复制状况和分片状况所需的权限,对数据没有访问权
│   ├── hostManager            # 提供对服务器相关操作的管理权限
│   └── dbAdminAnyDatabase     # 授予任意数据集的DBADMIN权限
├── restore                    # 拥有在数据库中进行恢复操作的权限
└── backup                     # 拥有在数据库中进行备份操作的权限

admin (admin-related roles)
├── readWriteAnyDatabase       # 在所有数据库中都有读写数据的权限
├── dbAdminAnyDatabase         # 在所有数据库中都有执行管理任务的权限
└── userAdminAnyDatabase       # 在所有数据库中都有管理用户的权限

my-app-user (user for your database 'my-database')
└── readWrite on 'my-database' # 仅在'my-database'数据库上有读写数据的权限

增删改查操作

创建集合

// 创建名为 "test" 的集合
testdb> db.createCollection("test")
{ ok: 1 }

// 显示当前数据库的所有集合
testdb> show tables
test

// 显示当前数据库的所有集合
testdb> show collections
test

删除集合

// 删除 "test" 集合
testdb> db.test.drop()
true

// 显示当前数据库的所有集合
testdb> show tables

插入文档

// 往 "t1" 集合插入一条文档,使用 insert 方法
testdb> db.t1.insert({a: 1})
{
  acknowledged: true,
  insertedIds: { '0': ObjectId('6586ca5e0a5414606799cb0f') }
}

// 往 "t1" 集合插入文档,使用 insertOne 方法
testdb> db.t1.insertOne({a: 1, b: 2})
{
  acknowledged: true,
  insertedId: ObjectId('6586ca640a5414606799cb10')
}

// 往 "t1" 集合插入多条文档,使用 insertMany 方法
testdb> db.t1.insertMany([{a: 1}, {b: 2}, {c: 3}])
{
  acknowledged: true,
  insertedIds: {
    '0': ObjectId('6586ca6a0a5414606799cb11'),
    '1': ObjectId('6586ca6a0a5414606799cb12'),
    '2': ObjectId('6586ca6a0a5414606799cb13')
  }
}

// 往 "t2" 集合插入 100 条文档
testdb> for (let i = 0; i < 100; i++) {
...   db.t2.insert({num: i});
... }
{
  acknowledged: true,
  insertedIds: { '0': ObjectId('6586b96f0a5414606799caaa') }
}

// 创建一个包含 100 个元素的数组,每个元素是一个包含 "num" 字段的对象,值从 0 到 99
testdb> var lists = Array.from({length: 100}, (_, i) => ({ num: i }))
// 使用 bulkWrite 方法一次性将数组中的所有元素插入 "t3" 集合
testdb> db.t3.bulkWrite(
...   lists.map(item => ({ insertOne: { document: item } }))
... )

查看文档

// 查找 "t1" 集合中的所有文档
testdb> db.t1.find()
[
  { _id: ObjectId('6586ca5e0a5414606799cb0f'), a: 1 },
  { _id: ObjectId('6586ca640a5414606799cb10'), a: 1, b: 2 },
  { _id: ObjectId('6586ca6a0a5414606799cb11'), a: 1 },
  { _id: ObjectId('6586ca6a0a5414606799cb12'), b: 2 },
  { _id: ObjectId('6586ca6a0a5414606799cb13'), c: 3 }
]

// 在 "t1" 集合中查找 "a" 字段值为 1 的所有文档
testdb> db.t1.find({a: 1})
[
  { _id: ObjectId('6586ca5e0a5414606799cb0f'), a: 1 },
  { _id: ObjectId('6586ca640a5414606799cb10'), a: 1, b: 2 },
  { _id: ObjectId('6586ca6a0a5414606799cb11'), a: 1 }
]

// 显示 "t3" 集合中的部分文档(默认一页将返回20条行数据)
testdb> db.t3.find()
[
  { _id: ObjectId('6586b9b70a5414606799caab'), num: 0 },
  { _id: ObjectId('6586b9b70a5414606799caac'), num: 1 },
  { _id: ObjectId('6586b9b70a5414606799caad'), num: 2 },
  { _id: ObjectId('6586b9b70a5414606799caae'), num: 3 },
  { _id: ObjectId('6586b9b70a5414606799caaf'), num: 4 },
  { _id: ObjectId('6586b9b70a5414606799cab0'), num: 5 },
  { _id: ObjectId('6586b9b70a5414606799cab1'), num: 6 },
  { _id: ObjectId('6586b9b70a5414606799cab2'), num: 7 },
  { _id: ObjectId('6586b9b70a5414606799cab3'), num: 8 },
  { _id: ObjectId('6586b9b70a5414606799cab4'), num: 9 },
  { _id: ObjectId('6586b9b70a5414606799cab5'), num: 10 },
  { _id: ObjectId('6586b9b70a5414606799cab6'), num: 11 },
  { _id: ObjectId('6586b9b70a5414606799cab7'), num: 12 },
  { _id: ObjectId('6586b9b70a5414606799cab8'), num: 13 },
  { _id: ObjectId('6586b9b70a5414606799cab9'), num: 14 },
  { _id: ObjectId('6586b9b70a5414606799caba'), num: 15 },
  { _id: ObjectId('6586b9b70a5414606799cabb'), num: 16 },
  { _id: ObjectId('6586b9b70a5414606799cabc'), num: 17 },
  { _id: ObjectId('6586b9b70a5414606799cabd'), num: 18 },
  { _id: ObjectId('6586b9b70a5414606799cabe'), num: 19 }
]
Type "it" for more
testdb> it // 使用 it 查看更多的文档
[
  { _id: ObjectId('6586b9b70a5414606799cabf'), num: 20 },
  { _id: ObjectId('6586b9b70a5414606799cac0'), num: 21 },
  { _id: ObjectId('6586b9b70a5414606799cac1'), num: 22 },
  { _id: ObjectId('6586b9b70a5414606799cac2'), num: 23 },
  { _id: ObjectId('6586b9b70a5414606799cac3'), num: 24 },
  { _id: ObjectId('6586b9b70a5414606799cac4'), num: 25 },
  { _id: ObjectId('6586b9b70a5414606799cac5'), num: 26 },
  { _id: ObjectId('6586b9b70a5414606799cac6'), num: 27 },
  { _id: ObjectId('6586b9b70a5414606799cac7'), num: 28 },
  { _id: ObjectId('6586b9b70a5414606799cac8'), num: 29 },
  { _id: ObjectId('6586b9b70a5414606799cac9'), num: 30 },
  { _id: ObjectId('6586b9b70a5414606799caca'), num: 31 },
  { _id: ObjectId('6586b9b70a5414606799cacb'), num: 32 },
  { _id: ObjectId('6586b9b70a5414606799cacc'), num: 33 },
  { _id: ObjectId('6586b9b70a5414606799cacd'), num: 34 },
  { _id: ObjectId('6586b9b70a5414606799cace'), num: 35 },
  { _id: ObjectId('6586b9b70a5414606799cacf'), num: 36 },
  { _id: ObjectId('6586b9b70a5414606799cad0'), num: 37 },
  { _id: ObjectId('6586b9b70a5414606799cad1'), num: 38 },
  { _id: ObjectId('6586b9b70a5414606799cad2'), num: 39 }
]
Type "it" for more

更新文档

// 在 "t1" 集合中,找到第一条 "b" 字段值为 2 的文档,并将 "b" 字段的值更新为 20
testdb> db.t1.updateOne({b: 2}, {$set: {b: 20}})
{
  acknowledged: true,
  insertedId: null,
  matchedCount: 1,
  modifiedCount: 1,
  upsertedCount: 0
}

testdb> db.t1.find()
[
  { _id: ObjectId('6586ca5e0a5414606799cb0f'), a: 1 },
  { _id: ObjectId('6586ca640a5414606799cb10'), a: 1, b: 20 },
  { _id: ObjectId('6586ca6a0a5414606799cb11'), a: 1 },
  { _id: ObjectId('6586ca6a0a5414606799cb12'), b: 2 },
  { _id: ObjectId('6586ca6a0a5414606799cb13'), c: 3 }
]

// 在 "t1" 集合中,找到所有 "a" 字段值为 1 的文档,并将这些文档的 "a" 字段值更新为 10
testdb> db.t1.updateMany({a: 1}, {$set: {a: 10}})
{
  acknowledged: true,
  insertedId: null,
  matchedCount: 3,
  modifiedCount: 3,
  upsertedCount: 0
}

testdb> db.t1.find()
[
  { _id: ObjectId('6586ca5e0a5414606799cb0f'), a: 10 },
  { _id: ObjectId('6586ca640a5414606799cb10'), a: 10, b: 20 },
  { _id: ObjectId('6586ca6a0a5414606799cb11'), a: 10 },
  { _id: ObjectId('6586ca6a0a5414606799cb12'), b: 2 },
  { _id: ObjectId('6586ca6a0a5414606799cb13'), c: 3 }
]

删除文档

// 在 "t1" 集合中删除第一条 "b" 字段值为 20 的文档
testdb> db.t1.deleteOne({b: 20})
{ acknowledged: true, deletedCount: 1 }

testdb> db.t1.find()
[
  { _id: ObjectId('6586ca5e0a5414606799cb0f'), a: 10 },
  { _id: ObjectId('6586ca6a0a5414606799cb11'), a: 10 },
  { _id: ObjectId('6586ca6a0a5414606799cb12'), b: 2 },
  { _id: ObjectId('6586ca6a0a5414606799cb13'), c: 3 }
]

// 删除所有 "a" 字段值为 10 的文档
testdb> db.t1.deleteMany({a: 10})
{ acknowledged: true, deletedCount: 2 }

testdb> db.t1.find()
[
  { _id: ObjectId('6586ca6a0a5414606799cb12'), b: 2 },
  { _id: ObjectId('6586ca6a0a5414606799cb13'), c: 3 }
]

// 删除 "t1" 集合中的所有文档
testdb> db.t1.deleteMany({})
{ acknowledged: true, deletedCount: 2 }

// 再次查看 "t1" 集合,此时它应该是空的,没有任何文档
testdb> db.t1.find()

// 显示当前数据库的所有集合,尽管 "t1" 集合现在是空的,但它仍然存在于数据库中
testdb> show tables
t1

高级查询操作

条件查询

// 插入四条文档到 "test" 集合
testdb> db.test.insertMany([{ name: "Charlie", age: 35, location: "New York" },{ name: "David", age: 29, location: "London" },{ name: "Eve", age: 33, location: "Paris" },{ name: "Jack", age: 26, location: "London" }])
{
  acknowledged: true,
  insertedIds: {
    '0': ObjectId('6586d1e80a5414606799cb1b'),
    '1': ObjectId('6586d1e80a5414606799cb1c'),
    '2': ObjectId('6586d1e80a5414606799cb1d'),
    '3': ObjectId('6586d1e80a5414606799cb1e')
  }
}

// 查找 "t1" 集合所有 "age" 字段大于 30 的文档
testdb> db.test.find({age: {$gt: 30}})
[
  {
    _id: ObjectId('6586d1e80a5414606799cb1b'),
    name: 'Charlie',
    age: 35,
    location: 'New York'
  },
  {
    _id: ObjectId('6586d1e80a5414606799cb1d'),
    name: 'Eve',
    age: 33,
    location: 'Paris'
  }
]

// 查找 "t1" 集合所有 "age" 字段小于 30 的文档
testdb> db.test.find({age: {$lt: 30}})
[
  {
    _id: ObjectId('6586d1e80a5414606799cb1c'),
    name: 'David',
    age: 29,
    location: 'London'
  },
  {
    _id: ObjectId('6586d1e80a5414606799cb1e'),
    name: 'Jack',
    age: 26,
    location: 'London'
  }
]

// 查找 "t1" 集合所有 "age" 字段大于18且小于 50 的文档
testdb> db.test.find({age: {$gt: 18, $lt: 50}})
[
  {
    _id: ObjectId('6586d1e80a5414606799cb1b'),
    name: 'Charlie',
    age: 35,
    location: 'New York'
  },
  {
    _id: ObjectId('6586d1e80a5414606799cb1c'),
    name: 'David',
    age: 29,
    location: 'London'
  },
  {
    _id: ObjectId('6586d1e80a5414606799cb1d'),
    name: 'Eve',
    age: 33,
    location: 'Paris'
  },
  {
    _id: ObjectId('6586d1e80a5414606799cb1e'),
    name: 'Jack',
    age: 26,
    location: 'London'
  }
]

// 查找 "t1" 集合所有 "age" 字段大于等于18且小于等于 50 的文档
testdb> db.test.find({age: {$gte: 18, $lte: 50}})
[
  {
    _id: ObjectId('6586d1e80a5414606799cb1b'),
    name: 'Charlie',
    age: 35,
    location: 'New York'
  },
  {
    _id: ObjectId('6586d1e80a5414606799cb1c'),
    name: 'David',
    age: 29,
    location: 'London'
  },
  {
    _id: ObjectId('6586d1e80a5414606799cb1d'),
    name: 'Eve',
    age: 33,
    location: 'Paris'
  },
  {
    _id: ObjectId('6586d1e80a5414606799cb1e'),
    name: 'Jack',
    age: 26,
    location: 'London'
  }
]

// 查找 "t1" 集合所有 "age" 字段不等于 18 的文档
testdb> db.test.find({age: {$ne: 18}})
[
  {
    _id: ObjectId('6586d1e80a5414606799cb1b'),
    name: 'Charlie',
    age: 35,
    location: 'New York'
  },
  {
    _id: ObjectId('6586d1e80a5414606799cb1c'),
    name: 'David',
    age: 29,
    location: 'London'
  },
  {
    _id: ObjectId('6586d1e80a5414606799cb1d'),
    name: 'Eve',
    age: 33,
    location: 'Paris'
  },
  {
    _id: ObjectId('6586d1e80a5414606799cb1e'),
    name: 'Jack',
    age: 26,
    location: 'London'
  }
]

// 查找 "t1" 集合所有 "name" 字段等于 "Charlie" 且 "age" 字段等于 35 的文档
testdb> db.test.find({$and: [{name: "Charlie"}, {age: 35}]})
[
  {
    _id: ObjectId('6586d1e80a5414606799cb1b'),
    name: 'Charlie',
    age: 35,
    location: 'New York'
  }
]

// 查找 "t1" 集合所有 "location" 字段等于 "London" 的文档
testdb> db.test.find({$or: [{location: "London"}]})
[
  {
    _id: ObjectId('6586d1e80a5414606799cb1c'),
    name: 'David',
    age: 29,
    location: 'London'
  },
  {
    _id: ObjectId('6586d1e80a5414606799cb1e'),
    name: 'Jack',
    age: 26,
    location: 'London'
  }
]

排序查询

// 查找 "test" 集合中所有的文档,并根据 "age" 字段升序排列
testdb> db.test.find().sort({age: 1})
[
  {
    _id: ObjectId('6586d1e80a5414606799cb1e'),
    name: 'Jack',
    age: 26,
    location: 'London'
  },
  {
    _id: ObjectId('6586d1e80a5414606799cb1c'),
    name: 'David',
    age: 29,
    location: 'London'
  },
  {
    _id: ObjectId('6586d1e80a5414606799cb1d'),
    name: 'Eve',
    age: 33,
    location: 'Paris'
  },
  {
    _id: ObjectId('6586d1e80a5414606799cb1b'),
    name: 'Charlie',
    age: 35,
    location: 'New York'
  }
]

// 查找 "test" 集合中所有的文档,并根据 "age" 字段降序排列
testdb> db.test.find().sort({age: -1})
[
  {
    _id: ObjectId('6586d1e80a5414606799cb1b'),
    name: 'Charlie',
    age: 35,
    location: 'New York'
  },
  {
    _id: ObjectId('6586d1e80a5414606799cb1d'),
    name: 'Eve',
    age: 33,
    location: 'Paris'
  },
  {
    _id: ObjectId('6586d1e80a5414606799cb1c'),
    name: 'David',
    age: 29,
    location: 'London'
  },
  {
    _id: ObjectId('6586d1e80a5414606799cb1e'),
    name: 'Jack',
    age: 26,
    location: 'London'
  }
]

统计计算

// 计算 "test" 集合中的文档总数
testdb> db.test.countDocuments()
4

// 计算 "location" 为 "London" 的文档数量
testdb> db.test.countDocuments({location: "London"})
2

// 使用聚合(aggregate)来计算集合中所有文档的 "age" 字段的总和、平均值、最大值和最小值
testdb> db.test.aggregate([
...   {
...     $group: {
...       _id: null,
...       totalAge: {$sum: "$age"},
...       averageAge: {$avg: "$age"},
...       maxAge: {$max: "$age"},
...       minAge: {$min: "$age"}
...     }
...   }
... ])
[
  {
    _id: null,
    totalAge: 123,
    averageAge: 30.75,
    maxAge: 35,
    minAge: 26
  }
]

// 先筛选出 "location" 为 "London" 的文档,然后使用聚合来计算这些文档的 "age" 字段的总和、平均值、最大值和最小值
testdb> db.test.aggregate([
...   {
...     $match: {
...       location: "London"
...     }
...   },
...   {
...     $group: {
...       _id: null,
...       totalAge: {$sum: "$age"},
...       averageAge: {$avg: "$age"},
...       maxAge: {$max: "$age"},
...       minAge: {$min: "$age"}
...     }
...   }
... ])
[
  { _id: null, totalAge: 55, averageAge: 27.5, maxAge: 29, minAge: 26 }
]

MongoDB 高可用方案

HA 架构图

www.processon.com/view/658b9c…

三种部署方案

在为项目选择 MongoDB 集群时,主要有三种选择:单节点服务器、复制(副本集)和分片集群。适合的选择取决于你的数据量、读写负载、可靠性需求以及数据扩展性需求。

方式说明
单节点(Standalone)对于非生产环境的小型应用、测试环境或开发环境,可以选择单节点的 MongoDB 服务器。这种配置简单且成本较低。
副本集(Replicaset)复本集是 MongoDB 的一种数据复制方式,主要用于保证数据的持久性和高可用性。一个副本集包含一组有着相同的数据副本的节点,其中一个节点作为主节点处理所有写操作,其余的节点为副本节点,可以用于处理读操作和备份。当主节点出现故障时,副本节点之一可以自动提升为新的主节点。
分片集群(Sharded cluster)分片集群是处理大量数据和高吞吐量应用的理想选择。分片是 MongoDB 中的一项技术,用于在多台机器上存储数据。每个分片存储数据集的一个子集,这使得数据查询可以在多个节点上并行进行,因此可以显著提高性能。同时,由于数据分布在多个节点上,所以容量可以超过单个节点的存储限制,实现线性扩展。

选择合适的架构

副本集在 MongoDB 中,表现得类似于 MySQL 中的主从模型,设计目的主要是为了提供高可用性和数据冗余。与此相反,分片则被用于解决水平扩展问题,这在本质上更像是 MPP 分布式数据库的运作方式。

当我们谈起分片集群,虽然它的确能有效地在多个节点之间分散大量的数据和请求压力,但是,我们不能忽视一些潜在的缺点。当某个查询需要在多个片段之间处理大量的数据时(比如跨片聚合和排序等操作),其处理时间可能会超过在单一节点上执行同样操作所需的时间。这样的问题,并不是只在 MongoDB 中才会遇到,在许多分布式系统中,延迟、数据一致性以及网络通信等问题依然是需要我们去考虑和解决的。

然而,我们需要明白,选择使用 MongoDB 的分片集群,主要目的在于解决单个节点的硬件资源限制问题。当数据体量和请求压力超出了单个节点的处理能力时,水平扩展就变得至关重要。在这些情况下,即使跨片操作的处理时间可能会有所增加,但相比起单一节点的过载或者存储空间不足,这种增加相较而言,还是可以被接受的。这便是 "以时间换空间" 老生常谈的道理。

副本集 分片集群
优点
  • 数据冗余:通过复制数据到多个服务器,可以在主服务器出现问题时,从副本服务器上提取数据,保证数据不丢失。
  • 提高数据读取性能:可以将读操作负载分散到多个副本,降低单个服务器的负载。
  • 高可用性:如果主服务器出现故障,系统会自动选举一个副本作为新的主服务器,确保服务不间断。
  • 水平扩展:伴随数据增长和请求负载增大,可以添加更多的服务器来分散负载。
  • 存储能力:由于数据分布在集群中,所以存储能力可以突破单个服务器的磁盘大小限制。
缺点
  • 写入性能受限:所有写操作只能在主服务器上进行,因此,写入性能取决于主服务器的性能。
  • 延迟问题:副本服务器的数据复制可能会有延迟,如果负载很高或网络故障,副本可能无法立即更新。
  • 需要更多的维护工作:设置和管理分片集群比复制集更复杂。
  • 跨片操作开销大:需要在多个分片上进行查询、聚合或更新操作时,性能会受到影响。
适用场景
  • 对读取性能有要求的应用,因为读取可以从多个副本服务器进行。
  • 对服务可用性有高要求的应用,因为复制集提供了自动故障转移。
  • 数据量或写入请求负载超过单个服务器能处理的范围时。
  • 期望能够进行线性扩展的用例。
CAP 理论
  • MongoDB 的副本集在 CAP 理论中被归类为 CP 系统,即在分区容错性(Partition Tolerance)和一致性(Consistency)之间达到平衡。
  • MongoDB 的分片集群在 CAP 理论中被视为 AP 系统,这意味着它倾向于保证可用性(Availability)和分区容错性(Partition Tolerance)。

总的来说,就像许多工程设计一样,选择适当的 MongoDB 架构,需要在诸多因素之间进行权衡,如应用需求、硬件资源和运维成本等。并非所有应用场景都适用分片集群,并且同样地,也并非所有的应用都能在单一节点或副本集框架下达到最优性能。

一般来说,当你发现单个节点的存储空间、内存或 CPU 不足以应对所面临的负载,或者单一节点无法满足持续的写入需求时,那么你应该考虑使用 分片集群。最后,我们都应该时刻关注应用和服务器的性能情况,并根据数据和请求负载的变化趋势,作出及时且明智的选择。

Tips:在大部分情况下,副本集 的表现实际上已经相当优秀,能够满足我们的业务需求。如果你发现它不能满足你的使用需求,事实上,MongoDB 也提供了 Cluster-to-Cluster Sync 解决方案来支持数据迁移。因此,就算需要进行切换,也不必过于担心。

MongoDB 副本集

物理架构

部署模型
副本集辅助成员
副本集仲裁器

MongoDB 中,ReplicaSet 是数据集群的基本形式,"一主一从一选举" 和 "一主两从" 是它常见的两种部署模式,他们各有优劣。

特性/模式一主一从一选举一主两从
冗余性和可用性仲裁器不持有数据,不能提供数据冗余。如果主节点发生故障,只有一个从节点可以提供服务,从节点发生故障时无法提供高可用性。两个从节点可以同时提供数据冗余,即使有一个节点发生故障,剩余的节点依然可以保证服务不间断,提供了更高的可用性。
写能力所有的写操作都是在主节点上执行的,仲裁器不提供写能力。所以这两个模式在写能力上是一样的。所有的写操作都是在主节点上执行的,所以这两个模式在写能力上是一样的。
读能力从节点提供了额外的读能力。仲裁器不持有数据,不能提供读能力。两个从节点可以同时提供读能力,相比一主一从一选举模式提供了两倍的读能力。
数据一致性数据只需要在主节点和从节点之间同步,数据一致性可以得到保证。数据需要在主节点和两个从节点之间同步,如果同步不及时可能会存在一致性问题。当然,MongoDB提供了读关心(read concern)和写关心(write concern)配置来控制数据一致性。
维护成本需要维护三个节点,但仲裁器的维护成本相对较低,因为它不持有数据,资源需求较低。需要维护三个持有数据的节点,相比一主一从一选举模式,需要更多的存储资源和网络带宽。
故障恢复在主节点发生故障后,可以通过选举立即选择新的主节点,但如果从节点也发生故障,集群将无法提供服务。在主节点发生故障后,可以通过选举立即选择新的主节点,即使有一个从节点发生故障,剩下的从节点也可以提供服务。

复制原理

同步流程

创建副本集之后,系统将首先进行一次全量同步。这个过程会在主节点 (Primary) 上复制除了本地数据库(local) 之外的所有数据库数据,并将数据插入到相应的集合的副本中。在此过程中,目标成员节点会拉取新添加的操作日志 (oplog) 记录,这样能够记录下在全量传输过程中主节点所进行的所有增量操作。

一旦全量数据导入完成,副本集将开始应用 oplog 来确保与主节点的数据保持一致。随后,只要主节点有任何修改操作,这些操作都会被记录在 oplog 中。

此时,会有一个持续运行的线程监控 oplog 的变化。一旦 oplog 发生变化,这些变动就会被传递到各个从节点 (Secondary) 上,并在这些从节点上进行回放。通过这种方式,我们可以确保数据在所有节点上保持同步。

数据一致性

MongoDB 中,复制集的核心运作原理主要依据着被称为 oplog(操作日志)的机制。得益于 oplog 的存在,即使在主节点崩溃的情况下,副本节点仍会极尽可能地与主节点保持数据同步。然而,由于网络延迟或其它相关因素的影响,副本节点的数据可能会稍有延后于主节点的数据状态。

节点角色同步说明
主节点(Primary)主节点在处理完客户端的写入操作后,会将这些操作记录在 oplog 中。oplog 是一个 capped collection,这意味着它有固定的大小,当空间用满后,最老的记录会被新的记录替换。
从节点(Secondary)从节点定期从主节点复制 oplog 的记录,并在本地应用这些操作,来更新它们的数据集。这种操作叫做复制操作。

Failover 原理

心跳和选举

复制集中的成员节点会定期 (2s/per) 向其他成员发送心跳。如果主节点发生故障,其他成员节点会在心跳超时(通常是 10s)后触发选举,选举出一个新的主节点。

故障转移

节点角色恢复说明
主节点故障当主节点发生故障时,复制集会进行自我修复。复制集中的节点会自动触发选举,选出一个新的主节点。副本节点在故障转移过程中,会尝试与新的主节点同步数据。
备节点故障当一个副本节点(备份节点)无法正常工作或者丢失连接时,它是不会触发重新选举,也不会被自动踢出副本集。这个节点暂时失效,可能因为临时的网络问题、硬件问题或者其它可修复的问题而无法连接。当这些问题被修复,该节点可以再次自动加入到副本集中,并自动从主节点同步在它离线期间遗失的数据,重新成为副本集的一部分。

副本集部署

安装

  1. 在每个节点上快速安装 MongoDBMongoDB Shell
#!/bin/bash

# Install MongoDB 7.x
wget -P /tmp https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-debian11-7.0.4.tgz
mkdir -p /usr/local/mongodb
tar -xf /tmp/mongodb-linux-x86_64-debian11-7.0.4.tgz -C /usr/local/mongodb
cat >> /etc/profile <<EOF
export MONGODB_HOME=/usr/local/mongodb/mongodb-linux-x86_64-debian11-7.0.4
export PATH=\${MONGODB_HOME}/bin:\${PATH}
EOF

# Install Mongosh Tool
wget -P /tmp https://downloads.mongodb.com/compass/mongodb-mongosh_2.1.1_amd64.deb
dpkg -i /tmp/mongodb-mongosh_2.1.1_amd64.deb

# Remove temporary files
rm -rf /tmp/mongodb*

# Reload system env
source /etc/profile

# Check if deployment is successful
if mongod --version &> /dev/null && mongosh --version &> /dev/null; then
    echo -e "\e[32mStandalone deployment is successful.\e[0m"
    exit 0
else
    echo -e "\e[31mStandalone deployment is failed.\e[0m"
    exit 1
fi

# Self-destruction of the script
shred -u "$0"

#EOF

配置

  1. 在每个节点上更改主机名和 /etc/hosts 文件
root@localhost:~# hostnamectl set-hostname mdb01

root@localhost:~# cat /etc/hosts
127.0.0.1        localhost

# The following lines are desirable for IPv6 capable hosts
::1     localhost ip6-localhost ip6-loopback
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters

10.2.102.238        mdb01
10.2.102.239        mdb02
10.2.102.240        mdb03
  1. 在主节点上编写和设置以下的 MongoDB 配置
# 设置 /data/mongodb 目录
root@mdb01:~# export BASE_DIR="/data/mongodb"

# 创建用于存储数据(data)、配置文件(conf)、运行时文件(run)和日志(logs)的子目录
root@mdb01:~# mkdir -p ${BASE_DIR}/{data,conf,run,logs}

# 用 openssl 命令生成的一个安全的随机的秘钥,将用于 MongoDB 的认证
root@mdb01:~# openssl rand -base64 756 > ${BASE_DIR}/conf/keyfile

# 更改 keyfile 权限
root@mdb01:~# chmod 400 ${BASE_DIR}/conf/keyfile

# 创建 MongoDB 配置文件
root@mdb01:~# cat > ${BASE_DIR}/conf/mongod.conf <<EOF
systemLog:
  destination: file
  logAppend: true
  path: ${BASE_DIR}/logs/mongod.log
  logRotate: rename
storage:
  dbPath: ${BASE_DIR}/data
  wiredTiger:            # 存储引擎:使用这个 db.serverStatus().storageEngine 命令检查 
    engineConfig:
      cacheSizeGB: 2     # 根据实际业务场景进行 RAM 大小的调整,设置 WiredTiger 的缓存大小
      journalCompressor: snappy
processManagement:
  fork: true
  pidFilePath: ${BASE_DIR}/run/mongod.pid
  timeZoneInfo: /usr/share/zoneinfo
net:
  port: 27017
  bindIp: 0.0.0.0        # 根据安全需求进行调整
replication:
  oplogSizeMB: 4096
  replSetName: mongo_repls
security:
  keyFile: ${BASE_DIR}/conf/keyfile
  authorization: enabled
EOF

分发

  1. 将主节点的 MongoDB 配置文件复制到其他节点,并在所有节点上启动 MongoDB 服务
# 将主节点的文件同样拷贝到其它各个节点
root@mdb01:~# scp -r /data root@mdb02:/
root@mdb01:~# scp -r /data root@mdb03:/

# 将在主节点上以指定的配置文件启动 MongoDB 服务
root@mdb01:~# mongod -f /data/mongodb/conf/mongod.conf
about to fork child process, waiting until server is ready for connections.
forked process: 2422
child process started successfully, parent exiting

# 检查监听的 27017 服务端口
root@mdb01:~# lsof -i:27017
COMMAND  PID USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
mongod  2422 root   15u  IPv4 176137      0t0  TCP *:27017 (LISTEN)

# 在其它各个节点也分别运行启动 MongoDB 服务
root@mdb01:~# ssh root@mdb02 "mongod -f /data/mongodb/conf/mongod.conf"
root@mdb01:~# ssh root@mdb03 "mongod -f /data/mongodb/conf/mongod.conf"

初始化

  1. 在主节点上创建和配置一个 MongoDB 副本集
# 将连接到运行在 27017 端口的 MongoDB 实例
root@mdb01:~# mongosh --port 27017
Current Mongosh Log ID:        658875fe987511f7d5ccebbc
Connecting to:                mongodb://127.0.0.1:27017/?directConnection=true&serverSelectionTimeoutMS=2000&appName=mongosh+2.1.1
Using MongoDB:                7.0.4
Using Mongosh:                2.1.1

For mongosh info see: https://docs.mongodb.com/mongodb-shell/

To help improve our products, anonymous usage data is collected and sent to MongoDB periodically (https://www.mongodb.com/legal/privacy-policy).
You can opt-out by running the disableTelemetry() command.

# 初始化了一个新的复制集"mongo_repl",包含有三个成员
test> rs.initiate({
  _id: "mongo_repls",
  members: [
    {_id: 0, host: 'mdb01:27017', priority: 1},
    {_id: 1, host: 'mdb02:27017', priority: 1},
    {_id: 2, host: 'mdb03:27017', arbiterOnly: true} # 仲裁者:不存储数据,只参与选举主节点
  ]
})
{ ok: 1 }

# 返回复制集的状态信息:复制集的配置、各个成员的状态、主节点的选举信息等
mongo_repls [direct: primary] test> rs.status()
{
  set: 'mongo_repls',
  date: ISODate('2023-12-24T18:19:51.683Z'),
  myState: 1,
  term: Long('1'),
  syncSourceHost: '',
  syncSourceId: -1,
  heartbeatIntervalMillis: Long('2000'),
  majorityVoteCount: 2,
  writeMajorityCount: 2,
  votingMembersCount: 3,
  writableVotingMembersCount: 2,
  optimes: {
    lastCommittedOpTime: { ts: Timestamp({ t: 1703441987, i: 1 }), t: Long('1') },
    lastCommittedWallTime: ISODate('2023-12-24T18:19:47.917Z'),
    readConcernMajorityOpTime: { ts: Timestamp({ t: 1703441987, i: 1 }), t: Long('1') },
    appliedOpTime: { ts: Timestamp({ t: 1703441987, i: 1 }), t: Long('1') },
    durableOpTime: { ts: Timestamp({ t: 1703441987, i: 1 }), t: Long('1') },
    lastAppliedWallTime: ISODate('2023-12-24T18:19:47.917Z'),
    lastDurableWallTime: ISODate('2023-12-24T18:19:47.917Z')
  },
  lastStableRecoveryTimestamp: Timestamp({ t: 1703441956, i: 1 }),
  electionCandidateMetrics: {
    lastElectionReason: 'electionTimeout',
    lastElectionDate: ISODate('2023-12-24T18:19:27.875Z'),
    electionTerm: Long('1'),
    lastCommittedOpTimeAtElection: { ts: Timestamp({ t: 1703441956, i: 1 }), t: Long('-1') },
    lastSeenOpTimeAtElection: { ts: Timestamp({ t: 1703441956, i: 1 }), t: Long('-1') },
    numVotesNeeded: 2,
    priorityAtElection: 1,
    electionTimeoutMillis: Long('10000'),
    numCatchUpOps: Long('0'),
    newTermStartDate: ISODate('2023-12-24T18:19:27.905Z'),
    wMajorityWriteAvailabilityDate: ISODate('2023-12-24T18:19:28.412Z')
  },
  members: [
    {
      _id: 0,
      name: 'mdb01:27017',
      health: 1,
      state: 1,
      stateStr: 'PRIMARY',
      uptime: 345,
      optime: { ts: Timestamp({ t: 1703441987, i: 1 }), t: Long('1') },
      optimeDate: ISODate('2023-12-24T18:19:47.000Z'),
      lastAppliedWallTime: ISODate('2023-12-24T18:19:47.917Z'),
      lastDurableWallTime: ISODate('2023-12-24T18:19:47.917Z'),
      syncSourceHost: '',
      syncSourceId: -1,
      infoMessage: 'Could not find member to sync from',
      electionTime: Timestamp({ t: 1703441967, i: 1 }),
      electionDate: ISODate('2023-12-24T18:19:27.000Z'),
      configVersion: 1,
      configTerm: 1,
      self: true,
      lastHeartbeatMessage: ''
    },
    {
      _id: 1,
      name: 'mdb02:27017',
      health: 1,
      state: 2,
      stateStr: 'SECONDARY',
      uptime: 35,
      optime: { ts: Timestamp({ t: 1703441987, i: 1 }), t: Long('1') },
      optimeDurable: { ts: Timestamp({ t: 1703441987, i: 1 }), t: Long('1') },
      optimeDate: ISODate('2023-12-24T18:19:47.000Z'),
      optimeDurableDate: ISODate('2023-12-24T18:19:47.000Z'),
      lastAppliedWallTime: ISODate('2023-12-24T18:19:47.917Z'),
      lastDurableWallTime: ISODate('2023-12-24T18:19:47.917Z'),
      lastHeartbeat: ISODate('2023-12-24T18:19:49.885Z'),
      lastHeartbeatRecv: ISODate('2023-12-24T18:19:50.884Z'),
      pingMs: Long('0'),
      lastHeartbeatMessage: '',
      syncSourceHost: 'mdb01:27017',
      syncSourceId: 0,
      infoMessage: '',
      configVersion: 1,
      configTerm: 1
    },
    {
      _id: 2,
      name: 'mdb03:27017',
      health: 1,
      state: 7,
      stateStr: 'ARBITER',
      uptime: 35,
      lastHeartbeat: ISODate('2023-12-24T18:19:49.885Z'),
      lastHeartbeatRecv: ISODate('2023-12-24T18:19:49.885Z'),
      pingMs: Long('0'),
      lastHeartbeatMessage: '',
      syncSourceHost: '',
      syncSourceId: -1,
      infoMessage: '',
      configVersion: 1,
      configTerm: 1
    }
  ],
  ok: 1,
  '$clusterTime': {
    clusterTime: Timestamp({ t: 1703441987, i: 1 }),
    signature: {
      hash: Binary.createFromBase64('pAxRxe/6vwoMjWrLA/qH6RFGXpE=', 0),
      keyId: Long('7316227538898911237')
    }
  },
  operationTime: Timestamp({ t: 1703441987, i: 1 })
}

副本集用户管理

管理员账号

创建一个数据库管理员用户:

// 获取 MongoDB 中的 admin 数据库对象
mongo_repls [direct: primary] test> let admin = db.getSiblingDB("admin")

// 创建管理员用户
mongo_repls [direct: primary] test> admin.createUser({
  user: "root",
  pwd: passwordPrompt(),
  roles: [
    { role: "userAdminAnyDatabase", db: "admin" }, // 赋予用户在 admin 数据库中的用户管理权限
    "readWriteAnyDatabase",                        // 赋予用户在所有数据库的读写权限
    "dbAdminAnyDatabase",                          // 赋予用户在所有数据库的数据库管理权限
    { role : "backup", db : "admin" },             // 赋予用户在 admin 数据库中的数据备份权限
    { role : "restore", db : "admin" }             // 赋予用户在 admin 数据库中的数据恢复权限
  ]
})

使用新创建的用户重新连接到 MongoDB

root@mdb01:~# mongosh --port 27017 -u root -p
Enter password: ****

集群管理员账号

创建一个用于管理集群的用户:

mongo_repls [direct: primary] test> db.getSiblingDB("admin").createUser({
  user: "replicator",
  pwd: passwordPrompt(),
  roles: [ { role: "clusterAdmin", db: "admin" } ]
})

应用账号

创建一个普通应用用户:

// 使用 root 管理员用户登录
root@mdb01:~# mongosh --port 27017 -u root -p
Enter password: ****
Current Mongosh Log ID:        65893bc9891b59fcb2a98632
Connecting to:                mongodb://<credentials>@127.0.0.1:27017/?directConnection=true&serverSelectionTimeoutMS=2000&appName=mongosh+2.1.1
Using MongoDB:                7.0.4
Using Mongosh:                2.1.1

For mongosh info see: https://docs.mongodb.com/mongodb-shell/

// 创建并切换到 testdb 数据库
mongo_repls [direct: primary] test> use testdb
switched to db testdb

// 创建了一个名为 tt 的集合
mongo_repls [direct: primary] testdb> db.createCollection("tt")
{ ok: 1 }

// 向 tt 集合中插入了一个文档
mongo_repls [direct: primary] testdb> db.tt.insertOne({a: 1})
{
  acknowledged: true,
  insertedId: ObjectId('6589395f6d9cd30ac70b0c3b')
}

// 为 testdb 数据库创建一个名为 u_testdb_rw 应用用户
mongo_repls [direct: primary] testdb> db.createUser({
  user: "u_testdb_rw",
  pwd: passwordPrompt(),
  roles: [ { role: "readWrite", db: "testdb" } ]
})

// 使用 u_testdb_rw 用户登录 testdb 数据库
root@mdb01:~# mongosh --port 27017 -u u_testdb_rw testdb -p
Enter password: ****
Current Mongosh Log ID:        65893b86d4ee08022e7ab894
Connecting to:                mongodb://<credentials>@127.0.0.1:27017/testdb?directConnection=true&serverSelectionTimeoutMS=2000&appName=mongosh+2.1.1
Using MongoDB:                7.0.4
Using Mongosh:                2.1.1

For mongosh info see: https://docs.mongodb.com/mongodb-shell/

// 验证新用户的写权限:尝试在 tt 集合中插入新的文档
mongo_repls [direct: primary] testdb> db.tt.insertOne({b: 2})
{
  acknowledged: true,
  insertedId: ObjectId('65893ba1d4ee08022e7ab895')
}

// 验证新用户的读权限:查询 tt 集合中的所有文档
mongo_repls [direct: primary] testdb> db.tt.find()
[
  { _id: ObjectId('6589395f6d9cd30ac70b0c3b'), a: 1 },
  { _id: ObjectId('65893ba1d4ee08022e7ab895'), b: 2 }
]

集群功能测试

验证主备节点同步

MongoDB 复制集中,默认从节点 (secondary) 是不提供读取服务的,在从节点上设置读取偏好 (read preference),以允许在从节点上查询到数据。

// 使用 u_testdb_rw 用户进行登录:
root@mdb02:~# mongosh --port 27017 -u u_testdb_rw -p '****' testdb
Current Mongosh Log ID:        658953e991dd0c74e1ad15e2
Connecting to:                mongodb://<credentials>@127.0.0.1:27017/testdb?directConnection=true&serverSelectionTimeoutMS=2000&appName=mongosh+2.1.1
Using MongoDB:                7.0.4
Using Mongosh:                2.1.1

For mongosh info see: https://docs.mongodb.com/mongodb-shell/

To help improve our products, anonymous usage data is collected and sent to MongoDB periodically (https://www.mongodb.com/legal/privacy-policy).
You can opt-out by running the disableTelemetry() command.

// 在 MongoDB 复制集架构中,默认从节点是不提供读取服务的,所有的读取操作都会被路由到主节点进行
mongo_repls [direct: secondary] testdb> db.tt.find()
MongoServerError: not primary and secondaryOk=false - consider using db.getMongo().setReadPref() or readPreference in the connection string

// 可以改变这个默认行为,允许从节点读取数据,通过下面的命令来命令来设置读取偏好(read preference)
mongo_repls [direct: secondary] testdb> db.getMongo().setReadPref("secondaryPreferred")

// 现在就可以在从节点上查询到表中的数据了
mongo_repls [direct: secondary] testdb> db.tt.find()
[
  { _id: ObjectId('6589395f6d9cd30ac70b0c3b'), a: 1 },
  { _id: ObjectId('65893ba1d4ee08022e7ab895'), b: 2 }
]

// 请注意,在从节点读取数据时需要小心数据一致性问题。在复制集环境中,主节点和从节点的数据可能存在一点延迟(lag),
// 特别是在写入操作非常频繁的情况下。如果你的应用对数据一致性要求很高,建议你只从主节点读取数据。

使用 Go 操作复制集

使用 Go 程序连接到 MongoDB 副本集,在 testdb 数据库的 tt 集合中每秒将插入 1 条文档,共记插入 10 条,完毕后程序会断开与 MongoDB 的连接。

package main

import (
    "context"
    "fmt"
    "log"
    "time"

    "go.mongodb.org/mongo-driver/bson"
    "go.mongodb.org/mongo-driver/mongo"
    "go.mongodb.org/mongo-driver/mongo/options"
    "go.mongodb.org/mongo-driver/mongo/readpref"
)

func main() {
    // 设置 MongoDB 副本集连接信息
    uri := "mongodb://u_testdb_rw:test@mdb01:27017,mdb02:27017/testdb?replicaSet=mongo_repls"

    // 设置客户端选项
    clientOptions := options.Client().ApplyURI(uri)

    // 建立 MongoDB 客户端连接
    client, err := mongo.Connect(context.TODO(), clientOptions)
    if err != nil {
        panic(err)
    }

    // 检查连接是否成功
    err = client.Ping(context.TODO(), readpref.Primary())
    if err != nil {
        panic(err)
    }
    log.Println("Connected to MongoDB!")

    // 获取 testdb 数据库
    database := client.Database("testdb")

    // 获取 tt 集合
    collection := database.Collection("tt")

    // 插入十个文档
    for i := 1; i <= 10; i++ {
        // 文档内容
        doc := bson.M{
            "index":   i,
            "message": fmt.Sprintf("Hello, world! %d", i),
            "time":    time.Now(),
        }

        // 插入文档
        _, err := collection.InsertOne(context.TODO(), doc)
        if err != nil {
            log.Println("Insert failed:", err)
            continue
        }

        log.Println("Insert success for document:", i)

        // 休眠一秒
        time.Sleep(1 * time.Second)
    }

    // 断开和 MongoDB 的连接
    err = client.Disconnect(context.TODO())
    if err != nil {
        panic(err)
    }
    log.Println("Connection to MongoDB closed.")
}

验证副本集 HA 特性

为了验证 MongoDB 复制集的高可用性特性,我们构建了一个 Go 程序,该程序能周期性地、不间断地将数据插入 MongoDB 数据库。

package main

import (
    "context"
    "fmt"
    "log"
    "time"

    "go.mongodb.org/mongo-driver/mongo"
    "go.mongodb.org/mongo-driver/mongo/options"
    "go.mongodb.org/mongo-driver/mongo/readpref"
)

// 启动 Go 任务...
// 为了验证 MongoDB Replica Set 的 HA 特性,该 Go 程序将会一直不间断地向 MongoDB 数据库中的集合中插入新数据!
func main() {
    // 设置 MongoDB 连接字符串
    uri := "mongodb://u_testdb_rw:test@mdb01:27017,mdb02:27017/testdb?replicaSet=mongo_repls"
    clientOptions := options.Client().ApplyURI(uri)

    // 连接到 MongoDB
    client, err := mongo.Connect(context.TODO(), clientOptions)
    if err != nil {
        panic(err)
    }

    // 检查连接是否成功
    err = client.Ping(context.TODO(), readpref.Primary())
    if err != nil {
        panic(err)
    }

    // 获取 testdb 数据库和 tt 集合
    collection := client.Database("testdb").Collection("tt")

    // 每秒插入一个文档
    ticker := time.NewTicker(1 * time.Second)
    i := 1
    for t := range ticker.C {
        // 文档内容
        doc := map[string]interface{}{
            "message": fmt.Sprintf("Hello, world! %d", i),
            "time":    t,
        }

        // 插入文档
        _, err := collection.InsertOne(context.TODO(), doc)
        if err != nil {
            log.Println("Insert failed:", err)
            continue
        }

        log.Println("Insert success index:", i)
        i++
    }
}

在程序运行期间,我们手动关闭主节点 (Primary) 以模拟故障。在故障期间,Go 程序显示无法插入新的数据,这会在程序日志中体现出来。之后,从节点 (Secondary) 会自动升级为新的主节点并接管集群,恢复其对外提供服务的能力。随后,Go 程序将自动恢复,继续将新的数据不断写入 MongoDB。这个实验过程验证了 MongoDB 复制集在遇到主节点故障时,可以通过自我恢复,保证服务的高可用性,不影响持续的数据写入。

副本集常见操作

重新选举主节点

当故障节点恢复并重新加入到集群中时,你可能希望将其重新设为主节点。我们可以这样做:就是在当前的主节点上执行一个命令,让其主动放弃主节点角色,并触发一次新的节点选举。这样原故障节点就有机会被选为新的主节点。

// 让当前的 MongoDB 主节点(primary)在指定时间内(这里是 120 秒)放弃其主节点角色,并强制一次选举以决定新的主节点
mongo_repls [direct: primary] test> db.adminCommand({ replSetStepDown: 120 })
{
  ok: 1,
  'clusterTime': {
    operationTime: Timestamp({ t: 1703519155, i: 1 }),
    signature: {
      hash: Binary.createFromBase64('Hl+RMm3Y512Lrc150gVdVYTMhq0=', 0),
      keyId: Long('7316227538898911237')
    }
  },
  operationTime: Timestamp({ t: 1703519155, i: 1 })
}
mongo_repls [direct: secondary] test> 

副本集新增成员

MongoDB 副本集中,如果你想新增一个从成员,首先必须完成新成员的配置并成功启动其服务。在服务器运行正常的前提下,你可以通过下面的命令将新的从节点添加到副本集中。

// 增加新节点到副本集
mongo_repls [direct: primary] test> rs.add({ host: 'mdb02:27017' })

// 检查节点状态
mongo_repls [direct: primary] test> rs.status()

副本集删除成员

MongoDB 副本集中,如果某成员不再需要,或者需要进行更新换代,你可以使用如下的命令将其从副本集中删除。该操作需要你在副本集的主节点进行操作,当你试图执行 rs.remove() 操作来移除当前的主节点时,通常会遇到一个错误,因为 MongoDB 不允许直接移除当前的主节点。

// 从副本集中手动移除某个节点
mongo_repls [direct: primary] test> rs.remove("mdb02:27017")

// 检查节点状态
mongo_repls [direct: primary] test> rs.status()

MongoDB 分片集群

分片介绍

何时考虑使用分片

以下情境可以考虑使用 MongoDB 分片技术以提升性能和扩展性:

  • 数据量较大:当你的数据集大到足以超越单个服务器或复制集的存储能力时,分片能通过分散数据至多个服务器来有效处理此问题。因此,响应时间可得以改善,因为查询请求将在多个片区上并行执行。
  • 数据量/请求量持续增长:对于初期数据量较小的应用,几个复制集可能足够应对需求。然而,如预计到你的数据或请求量在未来会持续增长至超出单一片区或复制集的处理能力,及时启用分片的方案可能为你带来意想不到的好处。
  • 并发写入高:因为 MongoDB 的副本集只允许在主节点上进行写入操作,当写入操作的负载极高时,即使副本集的读性能通过读取副本节点得到了提高,写性能仍可能成为瓶颈。在这种情况下,通过在多个片区进行写入操作,分片可以提升整体的写入性能。

分片集群的特点

应用无感知: 应用程序通过 MongoDB 的查询路由器连接数据库,无需了解数据如何分布在各个片区,提供了与连接普通 MongoDB 服务器一样的体验。

自动数据均衡: 随着新片区的添加或某片区的数据量过大,MongoDB 会自动平衡各片区的数据,保证所有片区的数据量大致相等。

实时扩展能力: 分片集群运行后仍可随时扩展,增加新片区会触发 MongoDB 自动重新分布所有数据,以达到所有片区数据均衡,实现动态扩容。

物理架构

引自 ProcessOn 原作:www.processon.com/view/59ede9…

安装部署

略...

MongoDB 备份与恢复

安装数据库工具集

  1. 选择 MongoDB Database Tools

  1. 在你的服务器上下载并安装 MongoDB Database Tools
# 从 MongoDB 的官方网站下载 database tools 的 .deb 安装包
root@mdb:~# wget https://fastdl.mongodb.org/tools/db/mongodb-database-tools-debian11-x86_64-100.9.4.deb

# 使用 dpkg 命令安装下载好的 .deb 包
root@mdb:~# dpkg -i mongodb-database-tools-debian11-x86_64-100.9.4.deb
  1. 检查安装的 MongoDB Database Tools
# 检查工具安装
root@mdb:~# dpkg -l mongodb-database-tools
Desired=Unknown/Install/Remove/Purge/Hold
| Status=Not/Inst/Conf-files/Unpacked/halF-conf/Half-inst/trig-aWait/Trig-pend
|/ Err?=(none)/Reinst-required (Status,Err: uppercase=bad)
||/ Name                   Version      Architecture Description
+++-======================-============-============-===================================================================================
ii  mongodb-database-tools 100.9.4      amd64        mongodb-database-tools package provides tools for working with the MongoDB server:

# 检查相关命令集合
root@mdb:~# find $(echo $PATH | sed 's/:/ /g') -name "mongo*" ! -name "mongosh" ! -path "/usr/local/*"
/usr/bin/mongoimport
/usr/bin/mongorestore
/usr/bin/mongotop
/usr/bin/mongoexport
/usr/bin/mongodump
/usr/bin/mongostat
/usr/bin/mongofiles

备份和导出工具对比

mongodump /mongorestoremongoexport /mongoimport,都是 MongoDB 提供的用来导出和导入数据的工具,但它们之间有一些关键的区别:

命令数据格式数据恢复能力使用场景
mongodump mongorestore使用用 BSON 格式来存储数据,这是 MongoDB 内部使用的一种二进制表示格式,可以精确表示数据库中的所有数据类型。不仅可以导出和导入数据,还可以导出和导入数据库的元数据,如索引信息。因此,你可以用 mongorestore 恢复 mongodump 导出的备份,得到一个和原数据库完全一样的副本。更适合做数据备份和恢复,以及将数据从一个 MongoDB 服务器迁移到另一个服务器。
mongoexport mongoimport支持 JSON 格式和 CSV 格式,这些格式更加通用,可以被很多其他工具读取,但可能无法精确表示某些 MongoDB 特有的数据类型,如 DateObjectID 等。只能导出和导入数据,无法恢复索引等元数据。能让你更容易地与其他数据格式(如 JSON 和 CSV)交互,适合在 MongoDB 和其他系统之间转移数据,或者用于数仓 ETL(Extract, Transform, Load)数据处理操作。

mongodump / mongorestore

单实例备份与恢复

  1. 使用 root 用户登录到 shell,创建 "testdb" 数据库及 "u_testdb_rw" 用户:
root@mdb:~# mongosh --port 27017 -u root -p '****'
Current Mongosh Log ID:        658a8b35975b6002334bc239
Connecting to:                mongodb://<credentials>@127.0.0.1:27017/?directConnection=true&serverSelectionTimeoutMS=2000&appName=mongosh+2.1.1
Using MongoDB:                7.0.4
Using Mongosh:                2.1.1

For mongosh info see: https://docs.mongodb.com/mongodb-shell/

------
   The server generated these startup warnings when booting
   2023-12-25T23:45:36.124+08:00: You are running this process as the root user, which is not recommended
   2023-12-25T23:45:36.124+08:00: /sys/kernel/mm/transparent_hugepage/enabled is 'always'. We suggest setting it to 'never'
   2023-12-25T23:45:36.124+08:00: Soft rlimits for open file descriptors too low
------

// 创建 testdb 数据库
mongo_repls [direct: primary] test> use testdb
switched to db testdb

// 生成 100 条文档
mongo_repls [direct: primary] testdb> for (let i = 0; i < 100; i++) {
...   db.tt.insertOne({ n: i });
... }
{
  acknowledged: true,
  insertedId: ObjectId('658a8bdd81147d932008a380')
}

// 检查有 100 条数据
mongo_repls [direct: primary] testdb> db.tt.countDocuments()
100

// 为该数据库创建读写权限的用户
mongo_repls [direct: primary] testdb> db.createUser({
...   user: "u_testdb_rw",
...   pwd: passwordPrompt(),
...   roles: [ { role: "readWrite", db: "testdb" } ]
... })
Enter password
test
****{
  ok: 1,
  '$clusterTime': {
    clusterTime: Timestamp({ t: 1703578495, i: 1 }),
    signature: {
      hash: Binary.createFromBase64('s5xFSatIVrCcRdoS7Nti0ccIQeE=', 0),
      keyId: Long('7316227538898911237')
    }
  },
  operationTime: Timestamp({ t: 1703578495, i: 1 })
}

// 检查这个用户及权限
mongo_repls [direct: primary] testdb> db.getUser("u_testdb_rw")
{
  _id: 'testdb.u_testdb_rw',
  userId: UUID('d919ccbb-7c14-4f2e-9ac3-2e0182f86c49'),
  user: 'u_testdb_rw',
  db: 'testdb',
  roles: [ { role: 'readWrite', db: 'testdb' } ],
  mechanisms: [ 'SCRAM-SHA-1', 'SCRAM-SHA-256' ]
}
  1. 使用 "u_testdb_rw" 用户登录到 shell,验证其权限:
root@mdb:~# mongosh --port 27017 -u u_testdb_rw -p '****' testdb
Current Mongosh Log ID:        658a8bb381147d932008a37f
Connecting to:                mongodb://<credentials>@127.0.0.1:27017/testdb?directConnection=true&serverSelectionTimeoutMS=2000&appName=mongosh+2.1.1
Using MongoDB:                7.0.4
Using Mongosh:                2.1.1

For mongosh info see: https://docs.mongodb.com/mongodb-shell/

// 增加一条数据,验证其 write 权限
mongo_repls [direct: primary] testdb> db.tt.insertOne({ n: 100 })
{
  acknowledged: true,
  insertedId: ObjectId('658a8bdd81147d932008a380')
}

// 当前有 101 条数据
mongo_repls [direct: primary] testdb> db.tt.countDocuments()
101
  1. 使用 "u_testdb_rw" 用户进行 mongodump 数据库备份:
root@mdb:~# mkdir /data/mongodb/backup
root@mdb:~# cd /data/mongodb/backup

root@mdb:/backup# mongodump -u u_testdb_rw -p '****' --host localhost:27017 --db testdb --out ./bak-$(date +%Y%m%d)
2023-12-26T18:57:03.254+0800        writing testdb.tt to bak-20231226/testdb/tt.bson
2023-12-26T18:57:03.255+0800        done dumping testdb.tt (101 documents)

root@mdb:/backup# ll -h
total 0
drwxr-x--- 3 root root 20 Dec 26 18:57 bak-20231226

root@mdb:/backup# tree bak-20231226
bak-20231226/
└── testdb
    ├── tt.bson
    └── tt.metadata.json

1 directory, 2 files
  1. 使用 root 用户登录到 shell,删除 "testdb" 数据库:
root@mdb:~# mongosh --host localhost:27017 -u root -p '****' --eval 'db.getSiblingDB("testdb").dropDatabase()' > /dev/null 2> & 1
  1. 使用 "u_testdb_rw" 用户进行 mongorestore 数据库恢复:
root@mdb:/backup# mongorestore --host localhost:27017 -u u_testdb_rw -p '****' --db testdb /data/mongodb/backup/bak-$(date +%Y%m%d)/testdb
2023-12-26T19:01:10.812+0800        The --db and --collection flags are deprecated for this use-case; please use --nsInclude instead, i.e. with --nsInclude=${DATABASE}.${COLLECTION}
2023-12-26T19:01:10.812+0800        building a list of collections to restore from /data/mongodb/backup/bak-20231226/testdb dir
2023-12-26T19:01:10.812+0800        reading metadata for testdb.tt from /data/mongodb/backup/bak-20231226/testdb/tt.metadata.json
2023-12-26T19:01:10.822+0800        restoring testdb.tt from /data/mongodb/backup/bak-20231226/testdb/tt.bson
2023-12-26T19:01:10.837+0800        finished restoring testdb.tt (101 documents, 0 failures)
2023-12-26T19:01:10.837+0800        no indexes to restore for collection testdb.tt
2023-12-26T19:01:10.837+0800        101 document(s) restored successfully. 0 document(s) failed to restore.

# 检查数据库数据条目
root@mdb:/backup# mongosh --port 27017 -u u_testdb_rw -p '****' testdb --eval 'db = db.getSiblingDB("testdb"); db.tt.countDocuments()'
Current Mongosh Log ID:        658ab3564e0b61bf4b610fe3
Connecting to:                mongodb://<credentials>@127.0.0.1:27017/testdb?directConnection=true&serverSelectionTimeoutMS=2000&appName=mongosh+2.1.1
Using MongoDB:                7.0.4
Using Mongosh:                2.1.1

For mongosh info see: https://docs.mongodb.com/mongodb-shell/

101

副本集备份与恢复

当副本集备份时,可指定优先从副本集的 secondary 节点读取数据:

# root 用户备份数据库,需要配合使用 '--authenticationDatabase admin' 选项,用于指定存储认证信息的数据库
mongodump --host="mongo_repls/mdb01:27017,mdb02:27017" --readPreference=secondaryPreferred -u root -p '****' --authenticationDatabase admin --db testdb --out ./replica_bak

副本集恢复:

# root 用户恢复数据库,可以指定为其它数据库名字(不一定与原数据库保持一致)
mongorestore --host="mongo_repls/mdb01:27017,mdb02:27017" -u root -p '****' --authenticationDatabase admin --db newdb ./replica_bak

mongoexport / mongoimport

# 导出为 json 格式的文件
mongoexport --host localhost:27017 -u u_testdb_rw -p '****' -d testdb -c tt -o bak_tt.json

# 将 json 文件导入到集合
mongoimport --host localhost:27017 -u u_testdb_rw -p '****' -d testdb -c tt --file bak_tt.json

MongoDB 监控

待更新 ...

MongoDB 性能优化实践

如何应对副本集的性能问题?

在通常情况下,直接升级现有的硬件或优化配置可能是最直接有效的策略。例如,微调 MongoDB 服务的 CPU nice 值可以调整其在系统中的 CPU 使用优先级。如果 MongoDB 是部署在虚拟机中,那么增加虚拟机的 CPU、内存或磁盘资源可能变成易实行且有益的调整。然而,这些方法并非银弹,一味地增加硬件配置会造成额外的成本支出。如果数据量和请求负载持续快速增长,可能需要采取更高效的横向扩展策略,例如,考虑使用分片。

如何选择适量的硬盘和内存?

MongoDB 中,单独的副本集(或者节点)所能存储的数据量主要受限于服务器的硬盘容量。然而,实践中发现,当一个节点(主节点或备份节点)需要存储的数据大大超过其有效内存 (RAM) 时,性能可能开始下降,原因是 MongoDB 为优化读写性能,将常用的数据和索引保存在内存中。因此,配置硬件时,我们应尽量确保整个数据集(至少是频繁使用的数据)和所有的索引可以在内存中存储。这样做的好处是,大多数读写操作可以直接通过内存进行,而无需访问硬盘,从而实现最优的性能。然而,这不是硬性要求,实际操作需要综合考虑应用需求和工作负载的具体情况。有些应用可能需要较大的硬盘空间,而可以接受略微降低的内存管理性能。

如何判断数据库负载是否过高?

  • CPU使用率:当数据库执行大量写操作时,会占用大量 CPU 资源。如果在执行写操作时,你发现 CPU 使用率持续接近或达到 100%,很可能意味着你的写入负载过大。
  • 磁盘 I/O:大量写入请求可能导致磁盘 I/O 达到危险水平。你可以监控磁盘队列长度、磁盘使用率和磁盘延迟等指标,如果这些指标长时间维持在较高水平,可能意味着写入负载过大。
  • 内存压力MongoDB 会将热数据和索引存储在 RAM 中,以便快速访问,但大量的写入负载可能会使 RAM 资源迅速耗尽,进而降低写入性能。
  • 写入延迟:你还可以通过直接观察写入操作的延迟时间来判断。如果发现写入的延迟时间持续上升,这可能预示着你的写入负载已超出了当前系统的处理能力。

MongoDB 内存占用原理?

MongoDB 是一款高性能的数据库,它通过内存映射文件 (Memory-Mapped Files) 将数据和索引直接映射到进程的虚拟内存空间,使得对数据的读取和写入操作十分迅速。这种方式通过内存指针读取数据,通过修改内存写入数据,由操作系统决定何时以及在何处将数据写入硬盘。

为了优化性能,特别是在使用 WiredTiger 存储引擎的情况下,MongoDB 默认使用系统可用内存的一半作为其内部缓存。这意味着 MongoDB 将尽可能地将数据和索引保留在内存中,以提高查询效率和数据操作速度。在生产环境中,为了最大化查询性能,我们建议尽可能将你的应用经常访问的数据和索引(也被称为工作集)放入内存。

当然,随着业务量的增长,MongoDB 的内存占用可能呈线性增长。这是因为 MongoDB 会将经常访问的热点数据以及历史查询数据缓存到内存中,以提升后续请求的处理速度。在资源未受限制的情况下,MongoDB 甚至可能试图将所有数据和索引全部缓存到内存中。

MongoDB 3.4 版本开始,WiredTiger 默认的内部缓存大小是以下两者中较大的一个:RAM50%,或者 256 MB。设计这个阈值的目的是为了防止 MongoDB 占用过多系统内存。

然而,我们可以通过配置 wiredTigerCacheSizeGB 参数来限制 MongoDB 的内存使用。这个参数允许我们自定义缓存的上限,从而达到一个对我们的业务场景来说更合理的设置。你可以参考 MongoDB 官方文档 wiredTigerCacheSizeGB 来了解更多关于这个参数的信息。

相关参考

Blog www.mongodb.com/blog/post/4…
www.mongodb.com/blog/post/q…
Release www.mongodb.com/docs/v7.0/r…
Youtube www.youtube.com/watch?v=574…
Other www.instaclustr.com/blog/cassan…