MongoDB 生产优化实战:慢查询分析、内存调优与安全加固

MongoDB 生产环境调优:慢查询分析与 Profiling、WiredTiger 内存调优、连接池与安全配置

MongoDB 作为文档型数据库的领导者,在现代应用架构中扮演着越来越重要的角色。然而,从开发环境迁移到生产环境时,许多团队都会遇到性能瓶颈、连接泄漏、安全漏洞等问题。本文将从实战角度出发,系统讲解 MongoDB 生产环境的慢查询分析、explain 深度解读、WiredTiger 内存调优、连接池配置、安全加固以及备份监控策略。

1. 慢查询分析:Profiling 与 currentOp

生产环境中最常见的问题就是慢查询拖垮整个集群。MongoDB 提供了完整的 Profiling 机制来捕获和分析慢查询。

1.1 开启 Profiling

Profiling 有三个级别:

  • 0:关闭(默认)
  • 1:仅记录慢查询(超过 slowms 阈值的查询)
  • 2:记录所有操作
// 以 admin 身份连接到目标数据库
db = db.getSiblingDB('production_db');

// 开启 Profiling,记录超过 100ms 的查询
db.setProfilingLevel(1, { slowms: 100 });

// 确认 Profiling 状态
db.getProfilingStatus();
// 输出:{ "was" : 1, "slowms" : 100, "sampleRate" : 1.0 }

// 关闭 Profiling
db.setProfilingLevel(0);

// 记录所有操作(仅在调试时使用,生产环境慎用)
db.setProfilingLevel(2);

对于高并发场景,可以开启采样模式,只记录 10% 的慢查询,减少 Profiler 本身的开销:

// 采样率 10%,记录超过 200ms 的查询
db.setProfilingLevel(1, { slowms: 200, sampleRate: 0.1 });

// 查看采样设置
db.getProfilingStatus();
// 输出:{ "was" : 1, "slowms" : 200, "sampleRate" : 0.1 }

1.2 分析 system.profile 集合

慢查询记录存储在 database.system.profile 固定集合(capped collection)中,默认大小为 1MB。生产环境建议扩大容量,否则旧记录会被快速覆盖。

// 创建更大的 Profiling 集合(比如 256MB),先关闭 Profiling
db.setProfilingLevel(0);

// 删除旧的 profile 集合
db.system.profile.drop();

// 创建新的固定集合,256MB,最大 10000 条记录
db.createCollection("system.profile", { capped: true, size: 256*1024*1024, max: 10000 });

// 重新开启 Profiling
db.setProfilingLevel(1, { slowms: 100 });

下面是分析慢查询的核心脚本:

// 查询最近最耗时的操作,按执行时间降序
db.system.profile.find().sort({ millis: -1 }).limit(10).forEach(op => {
    print("============================================================");
    print("操作类型:", op.op);
    print("命名空间:", op.ns);
    print("执行耗时(毫秒):", op.millis);
    print("扫描文档数:", op.docsExamined);
    print("返回文档数:", op.nreturned || op.nresponse);
    print("索引扫描数:", op.keysExamined);
    print("查询条件:", JSON.stringify(op.query || op.command, null, 2));
    print("执行计划:", JSON.stringify(op.planSummary));
    print("时间戳:", op.ts);
    print("============================================================");
});

// 统计最慢的集合和查询模式
db.system.profile.aggregate([
    { $match: { op: { $in: ["query", "command"] } } },
    { $group: {
        _id: { ns: "$ns", planSummary: "$planSummary" },
        avgTime: { $avg: "$millis" },
        maxTime: { $max: "$millis" },
        count: { $sum: 1 }
    }},
    { $sort: { avgTime: -1 } },
    { $limit: 10 }
]).forEach(r => {
    printjson(r);
});

1.3 使用 db.currentOp() 诊断实时性能问题

currentOp 可以查看当前正在执行的操作,是定位卡死查询和锁竞争的利器。

// 查看所有正在进行的操作
db.currentOp();

// 查看运行时间超过 5 秒的操作
db.currentOp({ "secs_running": { $gt: 5 }, "active": true });

// 查看正在等待锁的操作
db.currentOp({ "waitForLock": true });

// 查看特定集合上的操作
db.currentOp({ "ns": /production_db\.orders/ });

// 查看阻塞其他操作的长事务
db.adminCommand({
    currentOp: true,
    "secs_running": { $gt: 60 },
    "active": true
});

一旦发现有害查询,可以立即终止:

// 先找到 opid
db.currentOp({ "ns": "production_db.orders", "secs_running": { $gt: 30 } });

// 终止特定操作(使用 admin 数据库)
db.getSiblingDB('admin').killOp(12345678);

在生产环境中,建议将 slowOpSampleRate 设置为合理的值,并结合日志分析:

// 查看当前服务器参数
db.adminCommand({ getParameter: "*" }).slowOpSampleRate;

// 动态修改慢查询日志采样率
db.adminCommand({ setParameter: 1, slowOpSampleRate: 1.0 });

2. explain() 深度解读:执行计划分析

MongoDB 的 explain() 是索引优化的核心工具。它提供三个详细级别:queryPlannerexecutionStatsallPlansExecution

2.1 explain 三种模式对比

模式实际执行返回内容
queryPlanner仅查询计划
executionStats实际执行统计
allPlansExecution所有候选计划统计
// queryPlanner 模式:不真正执行,只看到候选计划
db.orders.find({ status: "shipped" }).explain("queryPlanner");

// executionStats 模式:真正执行,看到实际扫描和返回数量
db.orders.find({ status: "shipped" }).explain("executionStats");

// allPlansExecution 模式:执行所有候选计划(最详细,开销也最大)
db.orders.find({ status: "shipped" }).explain("allPlansExecution");

2.2 executionStats 核心指标解读

const plan = db.orders.find({ status: "shipped", amount: { $gt: 1000 } })
    .sort({ createdAt: -1 })
    .limit(50)
    .explain("executionStats");

printjson(plan);

关键输出字段的含义:

// 核心执行统计
plan.executionStats.totalDocsExamined;   // 扫描的文档总数
plan.executionStats.totalKeysExamined;   // 扫描的索引键总数
plan.executionStats.nReturned;           // 返回的文档数
plan.executionStats.executionTimeMillis; // 执行耗时(毫秒)
plan.executionStats.executionStages.stage; // 根阶段类型

// 理想的执行计划应该满足:
// totalDocsExamined ≈ nReturned(每个返回文档只扫描一次)
// totalKeysExamined ≈ nReturned(索引精确匹配)
// executionTimeMillis 应该远小于业务容忍阈值

2.3 读懂 winningPlan

const explain = db.orders.find({ userId: "u12345", status: "pending" }).explain("executionStats");

// 查看获胜的查询计划
const winningPlan = explain.queryPlanner.winningPlan;

// 典型的 COLLSCAN(全表扫描)
// winningPlan.stage === "COLLSCAN"  
// 意味着没有使用索引,必须扫描全部文档

// 典型的 IXSCAN(索引扫描)
// winningPlan.stage === "FETCH"
// winningPlan.inputStage.stage === "IXSCAN"
// winningPlan.inputStage.indexName === "userId_1_status_1"
// 一个使用了复合索引的良好执行计划
{
    "stage": "LIMIT",
    "limitAmount": 50,
    "inputStage": {
        "stage": "FETCH",
        "inputStage": {
            "stage": "IXSCAN",
            "indexName": "status_1_createdAt_-1",
            "keyPattern": { "status": 1, "createdAt": -1 },
            "direction": "forward",
            "indexBounds": {
                "status": ["[\"shipped\", \"shipped\"]"],
                "createdAt": ["[MaxKey, MinKey]"]
            }
        }
    }
}

2.4 识别和解决低效查询

// CASE 1: 内存排序(SORT 阶段但没有使用索引排序)
// 如果在 executionStages 中看到 SORT stage,说明发生了内存排序
// 解决方案:创建包含排序字段的复合索引

db.orders.createIndex({ status: 1, createdAt: -1 });

// CASE 2: 回表过多(FETCH 阶段扫描了大量文档但返回很少)
// 当 totalDocsExamined >> nReturned 时发生
// 解决方案:使用覆盖索引(covered query)

db.orders.createIndex({ status: 1, createdAt: -1, userId: 1 });
// 查询只返回索引中的字段,可以完全避免 FETCH
db.orders.find({ status: "shipped" }, { userId: 1, createdAt: 1, _id: 0 })
    .sort({ createdAt: -1 });

// CASE 3: 索引前缀缺失导致全表扫描
// 查询 { email: "x@y.com" } 但索引是 { userId: 1, email: 1 }
// 由于缺少 userId 条件,无法使用该索引

// 解决方案:为 email 单独创建索引,或调整索引顺序
db.users.createIndex({ email: 1 });
// 使用 update 的 explain 查看更新操作的执行计划
db.orders.explain("executionStats").update(
    { status: "pending", createdAt: { $lt: new Date(Date.now() - 7*24*60*60*1000) } },
    { $set: { status: "cancelled" } },
    { multi: true }
);

3. 内存调优:WiredTiger 缓存与内存压力

WiredTiger 是 MongoDB 的默认存储引擎,其内部缓存大小直接影响查询性能。

3.1 缓存大小计算与配置

默认情况下,WiredTiger 缓存大小为 max(0.5 * (RAM - 1GB), 256MB),即最多使用物理内存的一半(减去 1GB)。这个默认值对大多数场景是合理的,但在特定业务负载下需要调整。

# mongod.conf
storage:
  wiredTiger:
    engineConfig:
      # 显式设置缓存大小(GB 或 MB)
      cacheSizeGB: 8
      # 或者使用 cacheSizeMB: 8192
      
      # 是否压缩集合数据
      collectionConfig:
        blockCompressor: snappy
      
      # 索引前缀压缩
      indexConfig:
        prefixCompression: true
# 启动时通过命令行参数指定缓存大小
mongod --wiredTigerCacheSizeGB 12 \
       --dbpath /var/lib/mongodb \
       --logpath /var/log/mongodb/mongod.log

3.2 内存调优黄金法则

生产环境的内存分配原则:

# 计算公式(适用于 Linux)
# WiredTiger Cache = (RAM - OS 预留 - 其他进程内存) * 0.6 ~ 0.7

# 举例:32GB RAM 的服务器
# OS 和内核预留:~2GB
# mongod 进程本身(连接、线程等):~2-4GB
# 其他进程(mongos、监控 agent):按需预留
# 推荐 WiredTiger Cache:16-20GB

# 对于索引密集型工作负载,可以适当降低缓存,给文件系统缓存留更多空间
# 因为 MongoDB 会利用操作系统的文件系统缓存来缓存压缩后的页面

3.3 内存压力指标与诊断

// 查看服务器状态和内存使用情况
db.serverStatus().wiredTiger.cache;
const cacheStats = db.serverStatus().wiredTiger.cache;

// 核心监控指标
const metrics = {
    // 当前缓存中的数据量(字节)
    bytesCurrentlyInCache: cacheStats["bytes currently in the cache"],
    
    // 缓存中脏数据量,过高说明写入压力大
    dirtyBytes: cacheStats["tracked dirty bytes in the cache"],
    
    // 缓存页面读入次数
    pagesReadIntoCache: cacheStats["pages read into cache"],
    
    // 缓存页面写出次数
    pagesWrittenFromCache: cacheStats["pages written from cache"],
    
    // 从缓存中淘汰的页面数
    pagesEvicted: cacheStats["unmodified pages evicted"],
    
    // 缓存无法满足的读请求(需要从磁盘加载)
    cacheMisses: cacheStats["pages read into cache requiring cache overflow repair"]
};

printjson(metrics);
// 计算缓存命中率(近似)
const status = db.serverStatus();
const wiredTiger = status.wiredTiger.cache;
const pagesRequested = wiredTiger["pages requested from the cache"];
const pagesRead = wiredTiger["pages read into cache"];

const hitRate = pagesRequested > 0 
    ? ((pagesRequested - pagesRead) / pagesRequested * 100).toFixed(2)
    : 100;

print("缓存命中率:", hitRate + "%");
// 低于 95% 通常意味着缓存过小或工作集远超内存

3.4 工作集(Working Set)分析

// 查看各集合的大小和文档数量,评估工作集
db.getCollectionNames().forEach(coll => {
    const stats = db[coll].stats();
    print(`${coll}: 文档数=${stats.count}, 数据大小=${(stats.size/1024/1024).toFixed(2)}MB, 索引大小=${(stats.totalIndexSize/1024/1024).toFixed(2)}MB`);
});
# 使用 mongostat 实时监控内存和 I/O
mongostat --host mongo-primary.example.com:27017 --username admin --password 'securePass' --authenticationDatabase admin

# 关键列说明:
# insert/query/update/delete: 每秒操作数
# getmore: cursor 获取更多数据
# command: 每秒命令数
# dirty: WT 缓存脏页百分比(超过 20% 需关注)
# used: WT 缓存使用率(接近 100% 时性能下降)
# flushes: checkpoint 频率
# vsize/res: 虚拟/物理内存
# qr|qw: 读/写队列长度(>0 说明有排队)
# ar|aw: 活跃读/写客户端数

4. 连接池优化:驱动层配置

不合理的连接池配置是导致应用延迟和 MongoDB 服务器资源耗尽的常见原因。

4.1 连接池核心参数

// Node.js MongoDB 驱动连接池配置
const { MongoClient } = require('mongodb');

const client = new MongoClient(uri, {
    // 最大连接数(默认 100)
    maxPoolSize: 200,
    
    // 最小保留连接数(默认 0)
    minPoolSize: 10,
    
    // 连接最大空闲时间(毫秒),超过将被关闭(默认 0 表示不限制)
    maxIdleTimeMS: 60000,
    
    // 等待可用连接的最大时间(默认 0 表示无限等待,生产环境必须设置)
    waitQueueTimeoutMS: 5000,
    
    // 连接超时时间(默认 10000ms)
    serverSelectionTimeoutMS: 30000,
    
    // 单个操作的超时时间(MongoDB 4.4+)
    timeoutMS: 30000,
    
    // socket 连接超时
    connectTimeoutMS: 10000,
    
    // socket 读取超时
    socketTimeoutMS: 0, // 0 表示不限制,长查询场景适用
    
    // 心跳检测间隔
    heartbeatFrequencyMS: 10000,
    
    // 连接在关闭前可以复用的最大次数(防泄漏)
    maxConnecting: 2 // 同时建立的连接数
});
# Python PyMongo 连接池配置
from pymongo import MongoClient

client = MongoClient(
    "mongodb://user:pass@host1:27017,host2:27017/db?replicaSet=rs0",
    maxPoolSize=200,
    minPoolSize=10,
    maxIdleTimeMS=60000,
    waitQueueTimeoutMS=5000,
    serverSelectionTimeoutMS=30000,
    connectTimeoutMS=10000,
    socketTimeoutMS=30000,
    heartbeatFrequencyMS=10000
)

4.2 连接数规划公式

# 生产环境连接数计算
# 单个 MongoDB 服务器的最大连接数由系统限制和配置决定
# MongoDB 默认 maxConns 为 64000(受 ulimit -n 限制)

# 建议公式:
# maxPoolSize = (server_max_connections * 0.8) / application_instance_count

# 举例:
# MongoDB 服务器 maxConns = 64000
# 应用实例数 = 20
# 每个实例连接池 maxPoolSize = (64000 * 0.8) / 20 = 2560
# 保守设置通常为 100-500,取决于并发量

# 查看当前连接数
db.serverStatus().connections;
# 输出:{ "current": 245, "available": 63755, "totalCreated": 18500, "active": 32 }

4.3 连接泄漏排查

// 监控连接增长趋势
db.serverStatus().connections;
# 定时记录连接数,发现泄漏
# 每 30 秒采集一次连接数
while true; do
    mongo --quiet --eval "printjson(db.serverStatus().connections)" \
          mongodb://admin:pass@localhost:27017/admin
    sleep 30
done
// 查看当前连接的客户端来源和状态
db.currentOp({ "active": true }).forEach(op => {
    if (op.client) {
        print(`客户端: ${op.client}, 应用: ${op.appName || 'unknown'}, 操作: ${op.op}`);
    }
});

应用端最佳实践:

// Node.js:确保在应用关闭时正确关闭连接池
process.on('SIGINT', async () => {
    console.log('正在关闭 MongoDB 连接池...');
    await client.close();
    process.exit(0);
});

// 连接字符串中直接指定连接池参数
const uri = 'mongodb://user:pass@host:27017/db?' +
    'maxPoolSize=150&' +
    'minPoolSize=10&' +
    'waitQueueTimeoutMS=5000&' +
    'maxIdleTimeMS=30000';

5. 连接字符串安全:SRV、TLS 与 authSource

生产环境的连接字符串必须包含安全相关的配置项。

5.1 标准安全连接字符串

// 基础安全连接:指定用户名、密码和认证数据库
const uri = 'mongodb://appuser:AppPassword123@mongo1.example.com:27017,mongo2.example.com:27017/production_db?replicaSet=rs0&authSource=admin';

// authSource=admin 表示用户凭证存储在 admin 数据库中
// 如果不指定 authSource,驱动默认在连接的数据库中查找用户

5.2 SRV 连接字符串

SRV 记录允许将多个节点的信息存储在 DNS 中,简化连接字符串:

# DNS SRV 记录示例(需要在 DNS 中配置)
# _mongodb._tcp.cluster0.example.com.  SRV  0 0 27017 mongo1.example.com.
# _mongodb._tcp.cluster0.example.com.  SRV  0 0 27017 mongo2.example.com.
# _mongodb._tcp.cluster0.example.com.  SRV  0 0 27017 mongo3.example.com.

# TXT 记录可以存储默认的连接字符串选项
# cluster0.example.com.  TXT  "replicaSet=rs0&authSource=admin"
// 使用 SRV 连接字符串
const uri = 'mongodb+srv://appuser:AppPassword123@cluster0.example.com/production_db?retryWrites=true&w=majority';

// 驱动会:
// 1. 查询 DNS SRV 记录获取所有节点
// 2. 查询 DNS TXT 记录获取默认参数
// 3. 合并显式参数后建立连接

5.3 TLS/SSL 配置

// 启用 TLS(推荐生产环境使用)
const fs = require('fs');
const { MongoClient } = require('mongodb');

const client = new MongoClient(
    'mongodb://appuser:pass@mongo1.example.com:27017/production_db?' +
    'replicaSet=rs0&authSource=admin',
    {
        tls: true,
        tlsCAFile: '/etc/ssl/certs/ca.crt',
        tlsCertificateKeyFile: '/etc/ssl/certs/client.pem',
        tlsAllowInvalidHostnames: false, // 生产环境必须为 false
        tlsAllowInvalidCertificates: false // 生产环境必须为 false
    }
);
# 使用 mongosh 通过 TLS 连接
mongosh \
    "mongodb://appuser:pass@mongo1.example.com:27017/admin" \
    --tls \
    --tlsCAFile /etc/ssl/certs/ca.crt \
    --tlsCertificateKeyFile /etc/ssl/certs/client.pem
# mongod.conf TLS 配置
net:
  tls:
    mode: requireTLS
    certificateKeyFile: /etc/ssl/certs/server.pem
    CAFile: /etc/ssl/certs/ca.crt
    allowConnectionsWithoutCertificates: false
    disabledProtocols: "TLS1_0,TLS1_1"

5.4 完整的生产级连接字符串

const productionUri = 'mongodb://prod_app:SecureP@ssw0rd@' +
    'mongo1.db.internal:27017,' +
    'mongo2.db.internal:27017,' +
    'mongo3.db.internal:27017/' +
    'ecommerce_db?' +
    'replicaSet=rs0' +
    '&authSource=admin' +
    '&readPreference=primaryPreferred' +
    '&w=majority' +
    '&retryWrites=true' +
    '&maxPoolSize=200' +
    '&minPoolSize=20' +
    '&waitQueueTimeoutMS=5000' +
    '&serverSelectionTimeoutMS=30000' +
    '&connectTimeoutMS=10000' +
    '&socketTimeoutMS=30000' +
    '&heartbeatFrequencyMS=10000' +
    '&appname=order-service';

6. 安全加固:认证、授权与网络隔离

MongoDB 默认不启用认证,这在线上环境中是重大安全隐患。

6.1 启动认证并创建用户

# 首次启动时不启用认证(仅首次配置)
mongod --dbpath /var/lib/mongodb --port 27017

# 连接后创建管理员用户
mongosh --port 27017
// 在 admin 数据库中创建超级管理员
use admin;

db.createUser({
    user: "adminRoot",
    pwd: "StrongAdminPassword123!",
    roles: [
        { role: "userAdminAnyDatabase", db: "admin" },
        { role: "dbAdminAnyDatabase", db: "admin" },
        { role: "readWriteAnyDatabase", db: "admin" },
        { role: "clusterAdmin", db: "admin" }
    ]
});

// 创建应用专用用户(最小权限原则)
use production_db;

db.createUser({
    user: "app_user",
    pwd: "AppSecurePass456!",
    roles: [
        { role: "readWrite", db: "production_db" }
    ]
});

// 创建只读用户(用于报表、监控)
db.createUser({
    user: "report_user",
    pwd: "ReadOnlyPass789!",
    roles: [
        { role: "read", db: "production_db" }
    ]
});

// 创建负责变更管理的用户(可以创建索引等)
db.createUser({
    user: "db_admin",
    pwd: "DbAdminPass012!",
    roles: [
        { role: "dbAdmin", db: "production_db" },
        { role: "readWrite", db: "production_db" }
    ]
});

6.2 启用认证和网络安全配置

# /etc/mongod.conf
security:
  authorization: enabled
  # keyFile 用于副本集成员间认证(副本集/分片集群必须)
  keyFile: /etc/mongodb/keyfile
  # 使用 keyFile 时 TLS 内部认证更安全
  # clusterAuthMode: x509

net:
  port: 27017
  # 仅绑定内网地址,禁止公网访问
  bindIp: 127.0.0.1,10.0.1.10
  
  # 或者使用 Unix socket(同一主机通信)
  # unixDomainSocket:
  #   enabled: true
  #   pathPrefix: /var/run/mongodb
  
  tls:
    mode: requireTLS
    certificateKeyFile: /etc/ssl/mongodb-server.pem
    CAFile: /etc/ssl/ca.crt
# 生成 keyFile(用于副本集成员认证)
openssl rand -base64 756 > /etc/mongodb/keyfile
chmod 400 /etc/mongodb/keyfile
chown mongodb:mongodb /etc/mongodb/keyfile

6.3 角色管理与权限审计

use admin;

// 查看所有用户
db.getUsers();

// 查看特定用户的权限
db.getUser("app_user");

// 撤销不必要的权限
db.revokeRolesFromUser("app_user", [
    { role: "dbAdmin", db: "production_db" }
]);

// 添加额外权限
db.grantRolesToUser("report_user", [
    { role: "read", db: "analytics_db" }
]);

// 创建自定义角色(细粒度控制)
db.createRole({
    role: "orderAppRole",
    privileges: [
        {
            resource: { db: "production_db", collection: "orders" },
            actions: [ "find", "insert", "update", "remove" ]
        },
        {
            resource: { db: "production_db", collection: "customers" },
            actions: [ "find", "insert" ]
        },
        {
            resource: { db: "production_db", collection: "" },
            actions: [ "listCollections" ]
        }
    ],
    roles: []
});

// 将自定义角色赋给用户
db.grantRolesToUser("app_user", [
    { role: "orderAppRole", db: "admin" }
]);

6.4 网络层安全加固

# 使用防火墙限制访问(ufw 示例)
sudo ufw default deny incoming
sudo ufw allow from 10.0.0.0/8 to any port 27017
sudo ufw allow from 172.16.0.0/12 to any port 27017
sudo ufw allow from 192.168.0.0/16 to any port 27017
sudo ufw enable

# iptables 规则示例
iptables -A INPUT -p tcp --dport 27017 -s 10.0.0.0/8 -j ACCEPT
iptables -A INPUT -p tcp --dport 27017 -j DROP

# 云安全组规则示例(AWS Security Group)
# Type: Custom TCP, Port: 27017, Source: sg-xxxxx(应用服务器安全组)
# 不要开放 0.0.0.0/0
# Docker Compose 网络隔离示例
version: '3.8'
services:
  mongo:
    image: mongo:7.0
    container_name: mongo-primary
    networks:
      - backend
    ports:
      # 仅在需要外部工具访问时映射,否则不暴露
      - "127.0.0.1:27017:27017"
    environment:
      MONGO_INITDB_ROOT_USERNAME: adminRoot
      MONGO_INITDB_ROOT_PASSWORD_FILE: /run/secrets/mongo_root_pass
    secrets:
      - mongo_root_pass
    volumes:
      - mongo_data:/data/db
      - ./mongod.conf:/etc/mongod.conf:ro
      - ./keyfile:/etc/mongodb/keyfile:ro
    command: ["mongod", "--config", "/etc/mongod.conf"]

networks:
  backend:
    internal: true  # 禁止外部直接访问

volumes:
  mongo_data:

secrets:
  mongo_root_pass:
    file: ./secrets/root_pass.txt

7. 备份策略:mongodump、mongorestore 与 Oplog

数据备份是数据库运维的生命线。MongoDB 提供了多种备份方案,需要根据 RPO(恢复点目标)和 RTO(恢复时间目标)选择。

7.1 逻辑备份:mongodump / mongorestore

# 全库备份(通过认证)
mongodump \
    --host mongo-primary.example.com \
    --port 27017 \
    --username backup_user \
    --password 'BackupPass123!' \
    --authenticationDatabase admin \
    --out /backup/mongodb/$(date +%Y%m%d_%H%M%S)

# 只备份特定数据库
mongodump \
    --uri "mongodb://backup_user:pass@localhost:27017/production_db?authSource=admin" \
    --out /backup/mongodb/$(date +%Y%m%d)

# 只备份特定集合
mongodump \
    --db production_db \
    --collection orders \
    --query '{ "createdAt": { "$gte": { "$date": "2026-08-01T00:00:00Z" } } }' \
    --out /backup/mongodb/orders_incremental

# 使用压缩(减少磁盘占用和传输时间)
mongodump \
    --uri "mongodb://backup_user:pass@localhost:27017/?authSource=admin" \
    --archive=/backup/mongodb/full_backup_$(date +%Y%m%d).gz \
    --gzip
# 恢复备份
mongorestore \
    --host mongo-primary.example.com \
    --username restore_user \
    --password 'RestorePass123!' \
    --authenticationDatabase admin \
    /backup/mongodb/20260813_020000/

# 从压缩归档恢复
mongorestore \
    --uri "mongodb://restore_user:pass@localhost:27017/?authSource=admin" \
    --archive=/backup/mongodb/full_backup_20260813.gz \
    --gzip \
    --drop  # 恢复前删除目标集合

# 恢复单个集合到不同名称的集合
mongorestore \
    --db production_db \
    --collection orders_new \
    /backup/mongodb/20260813/production_db/orders.bson

7.2 Oplog 备份:实现近实时恢复

# Oplog 是 MongoDB 的变更日志,可以通过 mongodump 备份
dump_oplog() {
    local OUTDIR="/backup/mongodb/oplog_$(date +%Y%m%d_%H%M%S)"
    mongodump \
        --host mongo-primary.example.com \
        --username backup_user \
        --password 'BackupPass123!' \
        --authenticationDatabase admin \
        --db local \
        --collection oplog.rs \
        --out "$OUTDIR"
    echo "Oplog 备份完成: $OUTDIR"
}

# 每隔 15 分钟增量备份 oplog(保留最近 4 小时)
*/15 * * * * /usr/local/bin/backup_oplog.sh
# 基于 oplog 的时间点恢复(Point-in-Time Recovery)
# 1. 先恢复到完整备份
mongorestore --db production_db /backup/mongodb/20260813_000000/production_db/

# 2. 应用 oplog 到特定时间点
mongorestore \
    --oplogReplay \
    --oplogLimit "1723488000:1" \
    /backup/mongodb/oplog_20260813_001500/local/oplog.rs.bson

7.3 物理备份:文件系统级备份

对于 WiredTiger 存储引擎,可以使用文件系统快照或物理复制。

# LVM 快照备份(需要数据在 LVM 上)
# 1. 在 Primary 上锁定写入(副本集建议在 Secondary 上执行)
mongosh --eval "db.fsyncLock()"

# 2. 创建 LVM 快照
lvcreate --size 50G --snapshot --name mongo_snap /dev/vg0/mongo_data

# 3. 解锁
mongosh --eval "db.fsyncUnlock()"

# 4. 挂载快照并复制数据
mkdir -p /mnt/mongo_snap
mount /dev/vg0/mongo_snap /mnt/mongo_snap
rsync -avz /mnt/mongo_snap/ /backup/mongodb/physical/$(date +%Y%m%d)/
umount /mnt/mongo_snap
lvremove -y /dev/vg0/mongo_snap
# 云存储快照(AWS EBS 示例)
# 1. 锁定数据库
mongosh --host mongo-secondary.internal:27017 --eval "db.fsyncLock()"

# 2. 创建 EBS 快照
aws ec2 create-snapshot \
    --volume-id vol-xxxxxxxxxxxxx \
    --description "MongoDB daily backup $(date +%Y-%m-%d)"

# 3. 解锁
mongosh --host mongo-secondary.internal:27017 --eval "db.fsyncUnlock()"

7.4 自动化备份脚本

#!/bin/bash
# /usr/local/bin/mongodb_backup.sh

set -euo pipefail

BACKUP_DIR="/backup/mongodb"
DATE=$(date +%Y%m%d_%H%M%S)
RETENTION_DAYS=30
MONGO_URI="mongodb://backup_user:BackupPass123!@localhost:27017/?authSource=admin"
S3_BUCKET="s3://company-mongodb-backups"

# 创建备份目录
mkdir -p "$BACKUP_DIR"

# 全量逻辑备份
mongodump \
    --uri "$MONGO_URI" \
    --archive="$BACKUP_DIR/full_backup_$DATE.gz" \
    --gzip \
    --oplog

# 上传到 S3
aws s3 cp "$BACKUP_DIR/full_backup_$DATE.gz" "$S3_BUCKET/full/"

# 清理本地旧备份
find "$BACKUP_DIR" -name "full_backup_*.gz" -mtime +$RETENTION_DAYS -delete

# 清理 S3 旧备份
aws s3 ls "$S3_BUCKET/full/" | awk '{print $4}' | sort -r | tail -n +31 | \
    xargs -I {} aws s3 rm "$S3_BUCKET/full/{}"

echo "备份完成: full_backup_$DATE.gz"

8. 日志轮转与监控

完善的日志和监控体系是提前发现问题、快速定位故障的基础。

8.1 MongoDB 日志轮转配置

# mongod.conf 日志配置
systemLog:
  destination: file
  path: /var/log/mongodb/mongod.log
  logAppend: true
  logRotate: reopen  # 支持通过 SIGUSR1 或命令轮转
  
  # 组件日志级别(生产环境建议保持默认或 warn)
  component:
    command:
      verbosity: 0
    control:
      verbosity: 0
    geo:
      verbosity: 0
    index:
      verbosity: 0
    network:
      verbosity: 0
    query:
      verbosity: 0  # 调为 1 可记录慢查询详情
    replication:
      verbosity: 0
    storage:
      verbosity: 0
    journal:
      verbosity: 0
    write:
      verbosity: 0
# 手动触发日志轮转
mongosh --eval "db.adminCommand({ logRotate: 1 })"

# 或者使用 SIGUSR1 信号
kill -SIGUSR1 $(pgrep mongod)
# logrotate 配置(Linux 系统级管理)
# /etc/logrotate.d/mongodb
/var/log/mongodb/mongod.log {
    daily
    rotate 30
    compress
    delaycompress
    missingok
    notifempty
    create 0640 mongodb mongodb
    sharedscripts
    postrotate
        /bin/kill -SIGUSR1 $(cat /var/lib/mongodb/mongod.lock 2>/dev/null) 2>/dev/null || true
    endscript
}

8.2 Prometheus 监控 exporter

# docker-compose.yml - MongoDB Exporter
version: '3.8'
services:
  mongodb-exporter:
    image: percona/mongodb_exporter:0.40
    container_name: mongo_exporter
    command:
      - "--mongodb.uri=mongodb://exporter:ExporterPass@mongo-primary:27017/admin?ssl=false"
      - "--collect-all"
      - "--discovering-mode"
    environment:
      MONGODB_USER: exporter
      MONGODB_PASSWORD: ExporterPass
    ports:
      - "9216:9216"
    networks:
      - monitoring

  prometheus:
    image: prom/prometheus:v2.53
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
      - prometheus_data:/prometheus
    ports:
      - "9090:9090"
    networks:
      - monitoring

  grafana:
    image: grafana/grafana:10.4
    volumes:
      - grafana_data:/var/lib/grafana
    ports:
      - "3000:3000"
    networks:
      - monitoring

networks:
  monitoring:

volumes:
  prometheus_data:
  grafana_data:
# prometheus.yml 配置
global:
  scrape_interval: 15s

scrape_configs:
  - job_name: 'mongodb'
    static_configs:
      - targets: ['mongodb-exporter:9216']
    metrics_path: /metrics
    scrape_interval: 15s

  - job_name: 'mongodb-node'
    static_configs:
      - targets: ['node-exporter:9100']
# Prometheus 查询示例:

# MongoDB 连接使用率
(mongodb_connections{state="current"} / mongodb_connections{state="available"}) * 100

# WT 缓存使用率
mongodb_ss_wt_cache_bytes_currently_in_the_cache / 
mongodb_ss_wt_cache_maximum_bytes_configured * 100

# 慢查询率(每分钟)
rate(mongodb_ss_opcounters_query[1m])

# 复制滞后时间
mongodb_mongod_replset_member_replication_lag

# 磁盘使用率
(mongodb_ss_disk_partition_space_used_bytes / 
 mongodb_ss_disk_partition_space_total_bytes) * 100

8.3 MongoDB Atlas 监控集成

如果使用 MongoDB Atlas,可以直接利用其内置监控和 alert 功能:

# Atlas CLI 监控查询示例
atlas metrics databases describe production_db \
    --projectId 5f3e... \
    --granularity PT1M \
    --period PT1H

# Atlas 告警可以通过 API 或控制台配置
# 关键告警项:
# - Query Targeting: Scanned Objects / Returned > 1000
# - Connection Pool utilization > 80%
# - System CPU (User) > 80%
# - Replication lag > 60 seconds
# - Disk space utilization > 85%
# Datadog MongoDB 集成(另一种商业监控方案)
# /etc/datadog-agent/conf.d/mongo.d/conf.yaml
init_config:

instances:
  - hosts:
      - mongo-primary.example.com:27017
    username: datadog
    password: 'DatadogPass123!'
    database: admin
    options:
      authSource: admin
      serverSelectionTimeoutMS: 30000
    replica_check: true
    collections_indexes_stats: true
// 自定义健康检查脚本
const { MongoClient } = require('mongodb');

async function healthCheck() {
    const client = new MongoClient(process.env.MONGO_URI);
    try {
        await client.connect();
        
        const admin = client.db('admin');
        const status = await admin.command({ serverStatus: 1 });
        
        const checks = {
            status: 'healthy',
            uptime: status.uptime,
            connections: status.connections.current,
            connectionsAvailable: status.connections.available,
            wtCacheUsed: status.wiredTiger.cache['bytes currently in the cache'],
            wtCacheMax: status.wiredTiger.cache['maximum bytes configured'],
            qpCounters: status.opcounters,
            timestamp: new Date()
        };
        
        // 检查告警阈值
        if (checks.connections / (checks.connections + checks.connectionsAvailable) > 0.8) {
            checks.status = 'warning';
            checks.message = 'Connection pool high utilization';
        }
        
        console.log(JSON.stringify(checks, null, 2));
        process.exit(0);
    } catch (err) {
        console.error(JSON.stringify({ status: 'unhealthy', error: err.message }));
        process.exit(1);
    } finally {
        await client.close();
    }
}

healthCheck();

总结

MongoDB 生产环境优化是一个系统工程,需要从查询层、内存层、连接层、安全层、备份层和监控层全面考虑:

  1. 慢查询分析:善用 setProfilingLevelsystem.profiledb.currentOp(),建立慢查询治理流程。
  2. 执行计划:掌握 explain("executionStats"),关注 totalDocsExaminednReturned 的比例,避免全表扫描和内存排序。
  3. 内存调优:合理配置 WiredTiger Cache(通常为物理内存的 50%-70%),监控 bytes currently in the cachedirty bytes
  4. 连接池:根据并发量设置 maxPoolSizeminPoolSize,务必配置 waitQueueTimeoutMS 防止雪崩。
  5. 连接安全:使用 TLS、SRV 记录、authSource 和最小权限原则,拒绝明文传输和公网暴露。
  6. 安全加固:启用认证、创建角色分离的用户、限制 bindIp 和网络访问,定期审计权限。
  7. 备份策略:逻辑备份(mongodump)+ Oplog 增量备份实现近实时恢复,物理备份(快照)用于快速恢复。
  8. 日志监控:配置 logrotate 避免磁盘占满,使用 Prometheus + Grafana 或 Atlas 建立可视化监控体系。

只有在生产环境部署前完成上述所有优化和加固工作,才能确保 MongoDB 集群在高负载下的稳定性和安全性。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「database」更多文章

  1. 缓存架构演进之路:从单机 Redis 到亿级分布式多级缓存体系
  2. Redis 7.x 重大新特性与架构升级深度解析
  3. Redis 消息队列深度对比:Pub/Sub、Streams 与 Kafka/RabbitMQ 选型指南