MongoDB 面试题
15 道题- 分类
- 数据库
- 题目数
- 15 道
1 MongoDB 的核心架构与文档数据模型
答案:
MongoDB 是一款基于文档(Document)的分布式 NoSQL 数据库,采用 BSON(Binary JSON)作为数据存储格式,天然支持灵活 Schema 与嵌套结构,适用于半结构化与快速迭代的业务场景。
核心进程模型:
graph TD
Client["Client Driver
(mongosh / Driver)"]
mongos["mongos
Query Router"]
Config["Config Server
(副本集)"]
Shard1["Shard-1 (Replica Set)"]
Shard2["Shard-2 (Replica Set)"]
Shard3["Shard-3 (Replica Set)"]
Client -->|"读/写"| mongos
mongos -->|"查询元数据"| Config
mongos -->|"路由请求"| Shard1
mongos -->|"路由请求"| Shard2
mongos -->|"路由请求"| Shard3
核心组件职责:
| 组件 | 职责 | 部署形态 |
|---|---|---|
| mongod | 数据库核心进程,负责数据存储、复制、查询执行 | 单节点 / 复制集成员 / 分片成员 |
| mongos | 查询路由器,接收客户端请求并路由至对应 Shard | 无状态,建议多实例部署 |
| Config Server | 存储集群元数据(分片位置、Chunk 范围、用户权限) | 必须部署为复制集(CSRS) |
| Replica Set | 一组保持相同数据集的 mongod 节点,提供高可用与读扩展 | Primary + Secondary + Arbiter |
| Sharded Cluster | 数据按 Shard Key 水平拆分到多个 Replica Set | mongos + Config Server + N 个 Shard |
BSON 与文档结构:
// 一个典型的 BSON 文档示例
{
_id: ObjectId("507f1f77bcf86cd799439011"), // 主键,12 字节:时间戳(4) + 机器(3) + PID(2) + 计数器(3)
user_id: 10001,
username: "zhangsan",
profile: { // 嵌套文档
real_name: "张三",
age: 28,
address: {
city: "Beijing",
district: "Haidian"
}
},
tags: ["vip", "tech", "finance"], // 数组
created_at: ISODate("2024-01-15T08:30:00Z"),
balance: NumberDecimal("1234.56") // Decimal128 精确数值
}
BSON 类型系统:
| 类型 | 说明 | 使用场景 |
|---|---|---|
String | UTF-8 字符串 | 文本字段 |
Int32 / Int64 / Double | 数值类型 | 计数、金额 |
Decimal128 | 34 位十进制精度 | 财务金额,避免浮点误差 |
ObjectId | 12 字节唯一 ID | 默认 _id |
Date | 毫秒级时间戳 | 时间字段 |
Binary | 二进制数据 | 文件、加密数据 |
Object | 嵌套文档 | 复杂结构 |
Array | 数组 | 一对多内嵌 |
Boolean / Null | 布尔与空值 | 标志位 |
Timestamp | 内部时间戳 | oplog 内部使用 |
文档模型与关系模型对比:
| 维度 | 关系模型(MySQL) | 文档模型(MongoDB) |
|---|---|---|
| 数据单位 | 行(Row),固定 Schema | 文档(Document),动态 Schema |
| 关联方式 | JOIN(跨表) | 嵌入(Embed)或引用(Reference) |
| 扩展方式 | 垂直扩展 + 分库分表 | 水平扩展(Sharding 原生) |
| Schema 变更 | ALTER TABLE(锁表风险) | 直接插入新字段(无锁) |
| 事务能力 | 强 ACID | 4.0+ 多文档 ACID,4.4+ 跨分片事务 |
| 适用场景 | 强一致、复杂 JOIN、报表分析 | 半结构化、快速迭代、海量数据 |
适用场景:
- 内容管理(CMS):文章、评论、标签的嵌套结构天然适配
- 物联网(IoT):设备时序数据的高写入吞吐
- 用户画像 / 商品目录:字段动态、Schema 多变
- 实时分析:聚合管道 + 变更流实时处理
- 缓存层:替代 Redis 持久化方案
2 MongoDB 的 CRUD 操作与常用操作符
答案:
MongoDB 提供丰富的 CRUD 操作符,支持复杂条件查询、更新与聚合,所有操作均通过 db.collection.<action>(filter, update, options) 模式调用。
Insert 操作:
// 单文档插入
db.users.insertOne({
username: "zhangsan",
email: "[email protected]",
age: 28,
status: "active",
created_at: new Date()
})
// 多文档插入(有序,性能更优)
db.users.insertMany([
{ username: "lisi", age: 25, status: "active" },
{ username: "wangwu", age: 30, status: "inactive" }
], { ordered: false }) // ordered=false 失败不中断后续
// insertOne 返回结构
// {
// acknowledged: true,
// insertedId: ObjectId("...")
// }
Query 操作与常用操作符:
// 基础查询
db.users.find({ status: "active", age: { $gte: 18 } })
// 投影:仅返回指定字段
db.users.find(
{ status: "active" },
{ username: 1, email: 1, _id: 0 }
)
// 比较操作符
db.products.find({
price: { $gt: 100, $lte: 500 }, // 100 < price <= 500
stock: { $ne: 0 }, // stock != 0
category: { $in: ["electronics", "books"] }
})
// 逻辑操作符
db.users.find({
$and: [
{ age: { $gte: 18 } },
{ $or: [
{ status: "active" },
{ vip_level: { $gt: 3 } }
]}
]
})
// 元素操作符
db.users.find({
phone: { $exists: true, $type: "string" },
tags: { $size: 3 }, // 数组长度为 3
profile: { $elemMatch: { key: "city", value: "Beijing" } }
})
// 数组操作符
db.posts.find({ tags: { $all: ["mongodb", "nosql"] } }) // 包含所有元素
db.posts.find({ "comments.author": "zhangsan" }) // 数组元素匹配
Update 操作:
// $set 更新字段
db.users.updateOne(
{ _id: ObjectId("...") },
{
$set: { status: "inactive", last_login: new Date() },
$inc: { login_count: 1 }, // 字段自增
$push: { tags: "new_tag" }, // 数组追加
$addToSet: { roles: "admin" }, // 数组去重追加
$unset: { deprecated_field: "" } // 删除字段
}
)
// updateMany 批量更新
db.products.updateMany(
{ category: "books", stock: { $lt: 10 } },
{ $set: { low_stock_alert: true } }
)
// upsert:不存在则插入
db.users.updateOne(
{ username: "newuser" },
{ $set: { email: "[email protected]", created_at: new Date() } },
{ upsert: true }
)
// findAndModify:原子操作并返回文档
db.counters.findAndModify({
query: { _id: "order_id" },
update: { $inc: { seq: 1 } },
new: true, // 返回更新后的文档
upsert: true
})
Delete 操作:
// 删除单个文档
db.users.deleteOne({ _id: ObjectId("...") })
// 批量删除
db.logs.deleteMany({
created_at: { $lt: new Date(Date.now() - 30 * 24 * 3600 * 1000) } // 30 天前
})
// 危险操作:删除集合所有文档
db.users.deleteMany({}) // 等同于 TRUNCATE
聚合查询与分页:
// countDocuments vs estimatedDocumentCount
db.users.countDocuments({ status: "active" }) // 精确计数(扫描)
db.users.estimatedDocumentCount() // 估算(使用元数据,快)
// 排序、限制、跳过
db.users.find({ status: "active" })
.sort({ created_at: -1 }) // 降序
.limit(20)
.skip(0)
// 基于游标的高效分页(避免 skip 大偏移量)
const cursor = db.users.find({ status: "active" })
.sort({ _id: 1 })
.limit(20)
const lastId = cursor[19]._id
const nextPage = db.users.find({
status: "active",
_id: { $gt: lastId } // 游标分页
}).limit(20)
3 MongoDB 的索引机制与 explain() 执行计划分析
答案:
MongoDB 索引采用 B-Tree 结构(自 3.2 起引入 B+Tree 优化),合理使用索引可将查询性能从秒级降至毫秒级,是性能调优的核心手段。
索引类型全景:
graph TD
Index["MongoDB 索引体系"]
Index --> Single["单字段索引"]
Index --> Compound["复合索引"]
Index --> Multikey["多键索引
(Multikey)"]
Index --> Geo["地理空间索引
(2dsphere/2d)"]
Index --> Text["文本索引
(Text)"]
Index --> Hashed["哈希索引
(Hashed)"]
Index --> TTL["TTL 索引"]
Index --> Unique["唯一索引"]
Index --> Partial["部分索引"]
Index --> Sparse["稀疏索引"]
Index --> Wildcard["通配符索引
(4.2+)"]
单字段与复合索引:
// 单字段索引
db.users.createIndex({ email: 1 }) // 1: 升序, -1: 降序
db.users.createIndex({ created_at: -1 })
// 复合索引(遵循 ESR 原则:Equality, Sort, Range)
db.orders.createIndex(
{ user_id: 1, status: 1, created_at: -1 }
)
// 等值 user_id + 等值 status + 范围/排序 created_at 均可命中索引
// 唯一索引
db.users.createIndex({ email: 1 }, { unique: true })
// 部分索引:仅索引部分文档,节省空间
db.orders.createIndex(
{ created_at: -1 },
{ partialFilterExpression: { status: "active" } }
)
// 稀疏索引:跳过 null 字段
db.products.createIndex(
{ discontinued_at: 1 },
{ sparse: true } // 不索引 discontinued_at 为空的文档
)
特殊索引:
// TTL 索引:自动过期删除
db.sessions.createIndex(
{ created_at: 1 },
{ expireAfterSeconds: 3600 } // 1 小时后自动删除
)
// 文本索引:全文搜索
db.articles.createIndex(
{ title: "text", content: "text" },
{ weights: { title: 10, content: 1 } }
)
db.articles.find({ $text: { $search: "mongodb performance" } })
// 多键索引:数组字段自动创建
db.posts.createIndex({ tags: 1 }) // tags 为数组,自动 multikey
db.posts.find({ tags: "mongodb" }) // 命中索引
// 地理空间索引
db.locations.createIndex({ location: "2dsphere" })
db.locations.find({
location: {
$near: {
$geometry: { type: "Point", coordinates: [116.404, 39.915] },
$maxDistance: 5000
}
}
})
// 哈希索引(用于 Shard Key 哈希分片)
db.users.createIndex({ _id: "hashed" })
explain() 执行计划分析:
// 查询执行计划
db.orders.find({ user_id: 10001, status: "active" })
.sort({ created_at: -1 })
.explain("executionStats") // 详细统计
// 关键字段解读
explain 关键指标:
| 字段 | 含义 | 健康值 |
|---|---|---|
winningPlan.stage | 执行阶段 | IXSCAN(索引扫描)优于 COLLSCAN(全表扫描) |
executionStats.totalDocsExamined | 扫描文档数 | 越接近 nReturned 越好 |
executionStats.totalKeysExamined | 扫描索引键数 | 越接近 nReturned 越好 |
executionStats.nReturned | 返回文档数 | 与 totalDocsExamined 比值即索引效率 |
executionStats.executionTimeMillis | 总执行时间 | 业务可接受范围 |
executionStats.stage | 是否 SORT 内存排序 | IXSCAN + 无 SORT 内存为佳 |
rejectedPlans | 被拒绝的执行计划 | 多个候选时用于优化器诊断 |
// 反例:全表扫描 COLLSCAN(性能差)
{
"winningPlan": {
"stage": "COLLSCAN",
"filter": { "user_id": { "$eq": 10001 } }
},
"executionStats": {
"totalDocsExamined": 1000000,
"nReturned": 1,
"executionTimeMillis": 850
}
}
// 正例:索引扫描 IXSCAN(性能优)
{
"winningPlan": {
"stage": "FETCH",
"inputStage": {
"stage": "IXSCAN",
"indexName": "user_id_1_status_1_created_at_-1"
}
},
"executionStats": {
"totalKeysExamined": 1,
"totalDocsExamined": 1,
"nReturned": 1,
"executionTimeMillis": 2
}
}
索引优化策略:
| 策略 | 说明 |
|---|---|
| ESR 原则 | 复合索引字段顺序:Equality → Sort → Range |
| 覆盖查询 | 索引包含查询所需全部字段,避免回表 |
| 索引选择性 | 高基数(Cardinality)字段放前面 |
| 避免索引数组过大 | 多键索引对数组内每个元素都建立索引 |
| 监控索引使用 | db.collection.aggregate([{$indexStats:{}}]) 查看索引命中次数 |
| 删除冗余索引 | 重复前缀索引、命中率低的索引及时清理 |
覆盖查询示例:
// 创建覆盖索引
db.orders.createIndex({ user_id: 1, status: 1, total: 1 })
// 查询完全命中索引(无需回表)
db.orders.find(
{ user_id: 10001, status: "active" },
{ user_id: 1, status: 1, total: 1, _id: 0 } // 仅投影索引字段
)
// executionStats.stage = "IXSCAN"(无 FETCH 阶段),性能最佳
4 WiredTiger 存储引擎原理与 B+Tree
答案:
WiredTiger 是 MongoDB 3.2 起的默认存储引擎,采用 B+Tree 索引结构与 LSM 思想的数据组织方式,结合文档级并发控制(Document-Level Concurrency),提供高并发写入与压缩能力。
WiredTiger 核心架构:
graph TD
Client["Client Query"]
Client --> WT["WiredTiger Engine"]
subgraph WT["WiredTiger Storage Engine"]
BTree["B+Tree 索引
(内存 + 磁盘)"]
RowStore["Row Store
变长记录存储"]
Block["Block Manager
(WT_BLOCK)"]
Cache["Cache Pool
(默认 50% RAM - 1GB)"]
Evict["Eviction
(LRU/K)"]
Checkpoint["Checkpoint
(默认 60s)"]
Log["WiredTiger Log
(WAL)"]
BTree --> Cache
RowStore --> Cache
Cache --> Evict
Cache --> Block
Block --> Disk["Disk Files
*.wt"]
Cache --> Checkpoint
Client --> Log
Log --> Disk
end
B+Tree vs B-Tree:
| 特性 | B-Tree | B+Tree(WiredTiger) |
|---|---|---|
| 数据存储位置 | 内部节点 + 叶子节点 | 仅叶子节点 |
| 叶子节点链表 | 无 | 有(双向链表,支持范围扫描) |
| 内部节点 | 存储数据 + 指针 | 仅存储键 + 子节点指针 |
| 范围查询 | 需中序遍历 | 叶子节点顺序扫描,O(n) |
| 磁盘 I/O | 每次可能读取多个节点 | 内部节点更小,单次 I/O 可加载更多索引项 |
| 适用场景 | 点查询 | 点查询 + 范围查询(数据库首选) |
WiredTiger 写入流程:
1. 客户端发起 insert/update
2. WiredTiger 修改内存中的 B+Tree(copy-on-write)
3. 写入预写日志(WAL) → journal 文件
4. journal 落盘(fsync)→ 确认写入成功
5. 异步将脏页刷盘(eviction + checkpoint)
6. Checkpoint 创建一致性快照(默认 60 秒一次)
关键配置参数:
# mongod.conf(WiredTiger 配置段)
storage:
engine: wiredtiger
wiredTiger:
engineConfig:
cacheSizeGB: 8 # WiredTiger 缓存大小(默认 50% RAM - 1GB)
journalCompressor: snappy # WAL 压缩算法(none/snappy/zlib)
directoryForIndexes: false # 索引与数据分离目录
maxCacheOverflowFileSizeGB: 0 # 溢出文件大小(0 表示禁用磁盘溢出)
collectionConfig:
blockCompressor: snappy # 集合数据压缩(none/snappy/zlib)
indexConfig:
prefixCompression: true # 索引前缀压缩
indexConfig:
prefixCompression: true
document-level concurrency(文档级锁):
// WiredTiger 使用 MVCC 实现文档级并发控制
// 不同文档的写操作完全并行,无锁竞争
db.orders.updateOne({ _id: 1 }, { $set: { status: "paid" } })
db.orders.updateOne({ _id: 2 }, { $set: { status: "shipped" } })
// 上述两个操作可以并发执行,无需加锁
// 同一文档的并发写会串行化
// WiredTiger 在文档级别检测冲突,必要时短暂等待或重试
WiredTiger 性能调优矩阵:
| 参数 | 默认值 | 生产建议 | 说明 |
|---|---|---|---|
cacheSizeGB | 0.5 × RAM - 1 | 系统内存 50%(上限 16GB) | 越大热点缓存越多 |
blockCompressor | snappy | snappy(速度)/ zlib(压缩率) | snappy 速度优先 |
journalCompressor | snappy | snappy | 减少 WAL 写入量 |
checkpointSizeMB | 1024 | 根据数据量调整 | 控制 Checkpoint 期间写入量 |
eviction_target | 80 | 80 | 触发 Eviction 阈值 |
eviction_trigger | 95 | 95 | 强制 Eviction 阈值 |
eviction_dirty_target | 5 | 5 | 脏页 Eviction 阈值 |
eviction_dirty_trigger | 20 | 20 | 强制脏页 Eviction |
WiredTiger 与原 MMAPv1 引擎对比:
| 维度 | MMAPv1(3.2 之前) | WiredTiger(3.2+) |
|---|---|---|
| 并发模型 | 库级锁 / 集合级锁 | 文档级 MVCC |
| 压缩 | 不支持 | snappy / zlib(节省 50%+ 空间) |
| 内存管理 | mmap(OS 缓存) | 自管理缓存(更可控) |
| 故障恢复 | 重放 oplog | journal + Checkpoint |
| 空间回收 | 需 repair / resync | 文档更新不分配新空间 |
| 多核扩展 | 差(锁竞争) | 优(文档级并发) |
生产监控指标:
// WiredTiger 状态查看
db.serverStatus().wiredTiger
// 关注指标:
// - cache: "maximum bytes configured" / "bytes currently in the cache"
// - cache: "tracked dirty bytes in the cache"
// - cache: "pages read into cache" / "pages written from cache"
// - cache: "percentage overhead"
// - cache: "eviction server evicting pages"
// - "bytes belonging to page images in the cache"
5 复制集(Replica Set)架构与故障转移机制
答案:
MongoDB Replica Set 是一组维护相同数据集的 mongod 节点,提供数据冗余、高可用与读扩展能力,Primary 故障时自动触发选举(Election)机制选出新 Primary。
复制集架构:
graph TD
App["Application Driver"]
App -->|"Read/Write"| Primary["Primary
(可读写)"]
Primary -->|"oplog 复制"| Sec1["Secondary-1
(只读/Hidden)"]
Primary -->|"oplog 复制"| Sec2["Secondary-2
(只读/Hidden)"]
Primary -->|"oplog 复制"| Sec3["Secondary-3
(Arbiter)"]
subgraph Election["选举机制"]
Heartbeat["Heartbeat
(每 2s)"]
Priority["Priority 选举优先级"]
Vote["多数派投票 (Majority)"]
end
Sec1 -.-> Heartbeat
Sec2 -.-> Heartbeat
Sec3 -.-> Heartbeat
Heartbeat -.-> Vote
节点角色:
| 角色 | 职责 | 注意事项 |
|---|---|---|
| Primary | 接收所有写入操作,维护 oplog 权威副本 | 集群仅 1 个,宕机触发选举 |
| Secondary | 复制 Primary oplog 并应用,提供只读 | 默认 0 延迟,可调整 Priority |
| Arbiter | 投票节点,不存储数据 | 仅在偶数节点时用于打破平局 |
| Hidden | 不可被客户端读取的 Secondary | 用于备份 / 报表,不影响 Primary 选举 |
| Delayed | 延迟应用 oplog 的 Secondary | 灾难恢复(误删除防护) |
| Priority 0 | 不参与 Primary 选举的 Secondary | 通常为多数据中心灾备节点 |
复制集配置:
// 初始化复制集
rs.initiate({
_id: "rs0",
members: [
{ _id: 0, host: "mongo-1:27017", priority: 2 }, // 高优先级倾向
{ _id: 1, host: "mongo-2:27017", priority: 1 },
{ _id: 2, host: "mongo-3:27017", priority: 1 },
{ _id: 3, host: "mongo-arbiter:27017", arbiterOnly: true }
]
})
// 查看复制集状态
rs.status()
// 输出关键字段:
// - state: 1 (PRIMARY) / 2 (SECONDARY) / 7 (ARBITER)
// - members[].stateStr
// - members[].optimeDate: 最后应用的 oplog 时间
// - members[].health: 1 (健康) / 0 (宕机)
// - members[].electionDate: 上次选举时间
// 查看主从延迟
rs.printSecondaryReplicationInfo()
// 输出: "source: mongo-1, syncedTo: ..., lag: 0s"
oplog(操作日志):
oplog 是一组固定大小的 capped collection,存储在 local.oplog.rs,记录所有修改数据的操作。
关键特性:
- 幂等性:oplog 操作可重放(除 oplog 应用本身)
- 幂等写入:所有 oplog 操作都包含 _id
- 时序性:每条操作携带时间戳与唯一 ID
- 容量:默认 5% 磁盘(可通过 oplogSizeMB 调整)
- 复制单位:Secondary 持续拉取 Primary oplog 并应用
故障转移(Failover)流程:
sequenceDiagram
participant App as Application
participant P as Primary (mongo-1)
participant S1 as Secondary-1
participant S2 as Secondary-2
Note over P: 正常运行
P->>S1: 复制 oplog
P->>S2: 复制 oplog
Note over P: T+0s Primary 宕机
P--xApp: 连接断开
Note over S1,S2: T+10s 检测到 Primary 失联
S1->>S2: 发起选举
S2->>S1: 投票
S1->>S1: 获得多数票(2/3),成为新 Primary
Note over S1: T+12s 自我提升
S1-->>App: 重新接受写入
Note over P: 旧 Primary 重启
P->>S1: 以 Secondary 身份加入
S1->>P: 重新同步 oplog
选举参数配置:
// 复制集配置参数
cfg = rs.conf()
cfg.settings = {
electionTimeoutMillis: 10000, // 选举超时(默认 10s)
heartbeatIntervalMillis: 2000, // 心跳间隔
catchUpTimeoutMillis: 2000, // 新 Primary 追平数据超时
chainingAllowed: true, // 允许 Secondary 链式复制(4.0+)
// 多数派写入控制
getLastErrorDefaults: { w: 1, wtimeout: 0 },
// w: "majority" 表示多数节点确认后才返回
getLastErrorModes: [
{ name: "multi-region", loc: { "$in": ["us", "eu", "ap"] } }
]
}
rs.reconfig(cfg)
写入关注(Write Concern):
// 写入确认级别
db.orders.insertOne(
{ user_id: 10001, total: 99.9 },
{ writeConcern: { w: "majority", wtimeout: 5000 } } // 多数节点确认,5s 超时
)
// 写入关注级别
// w: 0 - 不等待确认(最快,可能丢失)
// w: 1 - Primary 确认(默认)
// w: 2 - 1 Primary + 1 Secondary 确认
// w: "majority" - 多数节点确认(推荐生产)
// w: <number> - 自定义数量
// 读关注(Read Concern)
db.orders.find({ user_id: 10001 })
.readConcern("majority") // 读取已确认的多数派数据
.readPreference("primaryPreferred") // 优先 Primary,失败读 Secondary
读偏好(Read Preference):
| 模式 | 行为 | 适用场景 |
|---|---|---|
primary | 仅 Primary 读取 | 强一致读 |
primaryPreferred | 优先 Primary,失败时 Secondary | 默认推荐,兼顾可用性 |
secondary | 仅 Secondary 读取 | 报表 / 备份,不影响 Primary |
secondaryPreferred | 优先 Secondary | 读多写少 |
nearest | 网络最近的节点 | 多数据中心部署 |
生产最佳实践:
| 实践 | 说明 |
|---|---|
| 节点数 >= 3 | 至少 3 数据节点 + 0~2 Arbiter,确保多数派选举 |
| 跨可用区部署 | Primary / Secondary 分布在不同 AZ |
| 隐藏节点备份 | Hidden Secondary 节点跑备份任务,不影响 Primary |
| 延迟节点防误删 | Delayed Secondary 延迟 1~24 小时,用于回滚 |
| w: “majority” | 生产环境使用 majority 写入关注 |
| Oplog 监控 | 监控 oplog window(oplog 可回放时长) |
6 分片集群(Sharding Cluster)原理与分片策略
答案:
MongoDB Sharded Cluster 将数据按 Shard Key 水平拆分到多个 Replica Set,实现存储与吞吐的水平扩展,支持千亿级文档与 PB 级数据量。
分片集群架构:
graph TD
App["Application"]
App --> M1["mongos-1"]
App --> M2["mongos-2"]
App --> M3["mongos-3"]
subgraph Config["Config Server (CSRS)"]
C1["Config-1
(Primary)"]
C2["Config-2"]
C3["Config-3"]
end
subgraph Shard1["Shard-1 (Replica Set)"]
S1P["Primary"]
S1S["Secondary"]
end
subgraph Shard2["Shard-2 (Replica Set)"]
S2P["Primary"]
S2S["Secondary"]
end
subgraph Shard3["Shard-3 (Replica Set)"]
S3P["Primary"]
S3S["Secondary"]
end
M1 -->|"元数据查询"| Config
M2 -->|"元数据查询"| Config
M3 -->|"元数据查询"| Config
M1 -->|"读写请求"| S1P
M1 -->|"读写请求"| S2P
M1 -->|"读写请求"| S3P
M2 -->|"读写请求"| S1P
M3 -->|"读写请求"| S3P
分片相关概念:
| 概念 | 说明 |
|---|---|
| Shard | 存储分片数据的 Replica Set,单个 Shard 可承载数 TB 数据 |
| Config Server | 集群元数据存储(CSRS,复制集模式),存储 chunk 映射、shard 信息 |
| mongos | 查询路由器,无状态,内存中缓存 Chunk 路由表 |
| Chunk | Shard 内连续范围的数据块,默认 128MB |
| Shard Key | 用于分片的字段,决定数据分布与查询路由 |
| Balancer | 后台进程,自动在 Shard 间迁移 Chunk 以均衡数据 |
分片策略对比:
| 策略 | 语法 | 数据分布 | 适用场景 |
|---|---|---|---|
| 范围分片(Range) | { user_id: 1 } | 按字段值范围连续分布 | 范围查询、单调递增字段 |
| 哈希分片(Hashed) | { user_id: "hashed" } | 哈希散列,均匀分布 | 写入密集、避免热点 |
| 复合分片 | { region: 1, user_id: 1 } | 先按 region 范围,再按 user_id 范围 | 多租户 + 高基数字段 |
| 带标签分片 | { region: 1 } + Zone | 按地理 / 业务隔离 | 跨数据中心、GDPR 合规 |
启用分片:
// 1. 启用分片
sh.enableSharding("mydb")
// 2. 选择 Shard Key 并创建索引
db.orders.createIndex({ user_id: 1 })
// 3. 对集合进行分片
sh.shardCollection(
"mydb.orders",
{ user_id: 1 } // 范围分片
)
// 或
sh.shardCollection(
"mydb.orders",
{ user_id: "hashed" } // 哈希分片
)
// 4. 复合分片
sh.shardCollection(
"mydb.orders",
{ region: 1, user_id: 1 } // 先 region,再 user_id
)
Chunk 拆分与迁移:
Chunk 生命周期:
1. 初始:每个 Shard 持有若干 Chunk
2. 拆分(Split):当 Chunk 数据量超过 chunkSize(默认 128MB)触发拆分
3. 迁移(Migrate):Balancer 检测到 Shard 间 Chunk 数量不均衡时
触发迁移,源 Shard 发送 Chunk 数据至目标 Shard
Chunk 大小配置:
- 默认 128MB(适合大多数场景)
- 调整:db.settings.save({ _id: "chunksize", value: 64 }) // 单位 MB
- 大 Chunk:减少元数据,但拆分不灵活
- 小 Chunk:迁移频繁(容易影响性能)
Balancer 均衡机制:
// 查看 Balancer 状态
sh.getBalancerState() // true / false
// 关闭 Balancer(运维窗口)
sh.stopBalancer()
// 启用 Balancer
sh.startBalancer()
// 立即触发一次均衡(默认自动)
sh.startBalancer(timeout, interval)
// 监控 Chunk 分布
db.adminCommand({ listShards: 1 })
use config
db.chunks.aggregate([
{ $group: { _id: "$shard", count: { $sum: 1 } } }
])
// 输出:[{ _id: "shard-1", count: 50 }, { _id: "shard-2", count: 30 }]
Shard Key 选择原则:
| 原则 | 说明 |
|---|---|
| 高基数 | 唯一值数量大(亿级以上),避免 Chunk 拆分困难 |
| 低频修改 | Shard Key 一旦选定不可修改,避免频繁更新 Shard Key |
| 查询命中 | 业务主要查询条件包含 Shard Key,避免 Scatter-Gather |
| 避免单调递增 | _id / ObjectId 哈希分片可避免写入热点 |
| 避免粗粒度 | 状态字段(status)等低基数不适合分片 |
分片查询路由:
// Targeted Query:包含 Shard Key 的查询,仅查询相关 Shard
db.orders.find({ user_id: 10001 })
// mongos 根据 user_id 路由到对应 Shard
// Broadcast Query:不包含 Shard Key 的查询,所有 Shard 都要查
db.orders.find({ status: "active" })
// mongos 向所有 Shard 广播,结果合并返回(性能差)
// 复合 Shard Key 部分命中
// Shard Key: { region: 1, user_id: 1 }
db.orders.find({ region: "us-east" }) // 部分命中,按 region 路由
db.orders.find({ user_id: 10001 }) // 不可路由,广播查询
生产注意事项:
| 问题 | 解决方案 |
|---|---|
| Jumbo Chunk(巨型块) | 拆分阈值受限(如唯一索引),手动 split + 强制迁移 |
| 热点 Shard | 选择哈希分片或高基数 Shard Key |
| Balancer 影响业务 | 业务低峰期启用,或使用窗口化调度(Balancing Window) |
| 不可分片集合 | 集合超过 16MB(4.4+ 之前为 16MB)会报错 |
| 跨分片事务 | 4.4+ 支持分布式事务,但性能与一致性需权衡 |
| config server 性能 | 监控元数据查询延迟,避免元数据成为瓶颈 |
7 聚合管道(Aggregation Pipeline)
答案:
聚合管道是 MongoDB 的数据分析框架,通过多阶段(Stage)管道对文档进行转换、过滤、统计与连接,类似于 Unix 管道的流式处理。
聚合管道架构:
graph LR
A["$match
过滤"] --> B["$project
投影"]
B --> C["$group
分组聚合"]
C --> D["$sort
排序"]
D --> E["$limit
限制"]
E --> F["$lookup
关联查询"]
F --> G["$out / $merge
输出"]
常用聚合阶段:
// 典型聚合管道
db.orders.aggregate([
// 1. 过滤:仅看 2024 年的订单
{
$match: {
created_at: { $gte: ISODate("2024-01-01"), $lt: ISODate("2025-01-01") },
status: "completed"
}
},
// 2. 关联查询:获取用户信息
{
$lookup: {
from: "users",
localField: "user_id",
foreignField: "_id",
as: "user_info"
}
},
// 3. 拆解数组:每个 user_info 元素作为独立文档
{ $unwind: "$user_info" },
// 4. 投影:仅保留必要字段
{
$project: {
_id: 0,
order_id: "$_id",
user_id: 1,
total: 1,
user_name: "$user_info.username",
user_city: "$user_info.profile.address.city"
}
},
// 5. 分组聚合:按城市统计 GMV
{
$group: {
_id: "$user_city",
total_gmv: { $sum: "$total" },
order_count: { $sum: 1 },
avg_order: { $avg: "$total" },
max_order: { $max: "$total" },
min_order: { $min: "$total" },
unique_users: { $addToSet: "$user_id" }
}
},
// 6. 计算字段:客单价
{
$addFields: {
unique_user_count: { $size: "$unique_users" }
}
},
// 7. 排序:GMV 降序
{ $sort: { total_gmv: -1 } },
// 8. 限制:Top 10 城市
{ $limit: 10 }
])
核心 Stage 详解:
| Stage | 功能 | 示例 |
|---|---|---|
$match | 过滤文档(类似 WHERE) | { $match: { status: "active" } } |
$project | 字段投影与重命名 | { $project: { name: 1, total: { $multiply: ["$price", "$qty"] } } } |
$group | 分组聚合 | { $group: { _id: "$category", total: { $sum: 1 } } } |
$sort | 排序 | { $sort: { created_at: -1 } } |
$limit / $skip | 分页 | { $limit: 20 } |
$lookup | 关联查询(类似 JOIN) | { $lookup: { from: "users", ... } } |
$unwind | 拆解数组字段 | { $unwind: "$tags" } |
$facet | 多管道并行 | 同时统计不同维度 |
$bucket | 分桶 | 按金额区间分组 |
$graphLookup | 递归查询 | 树形 / 图结构遍历 |
$merge / $out | 输出到集合 | 持久化聚合结果 |
$addFields / $set | 增加 / 修改字段 | { $addFields: { full_name: { $concat: ["$first", " ", "$last"] } } } |
$lookup 关联查询:
// 单条件关联
db.orders.aggregate([
{
$lookup: {
from: "users", // 关联集合
localField: "user_id", // 本集合字段
foreignField: "_id", // 关联集合字段
as: "user" // 输出数组字段名
}
}
])
// 嵌套 pipeline 关联(4.0+)
db.orders.aggregate([
{
$lookup: {
from: "users",
let: { uid: "$user_id" },
pipeline: [
{ $match: { $expr: { $eq: ["$_id", "$$uid"] } } },
{ $project: { username: 1, city: "$profile.address.city" } }
],
as: "user"
}
}
])
// 多条件关联 + 相关子查询
db.orders.aggregate([
{
$lookup: {
from: "products",
let: { items: "$items" },
pipeline: [
{ $match: { $expr: { $in: ["$_id", "$$items"] } } },
{ $project: { name: 1, price: 1 } }
],
as: "product_details"
}
}
])
$facet 多面分析:
// 单次查询返回多个维度的统计
db.products.aggregate([
{
$facet: {
// 价格区间分布
price_ranges: [
{
$bucket: {
groupBy: "$price",
boundaries: [0, 100, 500, 1000, 5000, 10000],
default: "10000+",
output: {
count: { $sum: 1 },
avg_rating: { $avg: "$rating" }
}
}
}
],
// Top 10 评分
top_rated: [
{ $sort: { rating: -1 } },
{ $limit: 10 },
{ $project: { name: 1, rating: 1, price: 1 } }
],
// 类别统计
by_category: [
{
$group: {
_id: "$category",
count: { $sum: 1 },
avg_price: { $avg: "$price" }
}
},
{ $sort: { count: -1 } }
]
}
}
])
聚合管道优化:
| 优化策略 | 说明 |
|---|---|
| $match 前置 | 尽早过滤,减少后续 Stage 处理量 |
| $project 前置 | 减少字段,降低内存占用 |
| 利用索引 | $match / $sort / $group 的字段需建索引 |
| allowDiskUse | 大数据量时允许使用磁盘临时文件 |
| 避免 $unwind 大数组 | 拆解后文档数可能爆炸性增长 |
| 分批处理 | 大批量聚合使用 $out / $merge 分批执行 |
// allowDiskUse 启用磁盘临时文件
db.orders.aggregate(
[...],
{ allowDiskUse: true }
)
// 聚合管道执行分析
db.orders.explain("executionStats").aggregate([...])
// 关注:
// - $cursor.queryPlanner.winningPlan
// - $cursor.executionStats.totalDocsExamined
// - $cursor.executionStats.executionTimeMillis
8 MongoDB 多文档 ACID 事务
答案:
MongoDB 4.0 引入多文档 ACID 事务(4.2+ 支持跨分片分布式事务),完全支持 ACID 四大事务特性,使 MongoDB 同时具备文档模型的灵活性与传统关系型数据库的事务保障。
事务能力演进:
| 版本 | 事务能力 | 限制 |
|---|---|---|
| 4.0 | 副本集内多文档 ACID 事务 | 不支持跨分片 |
| 4.2 | 跨分片分布式事务 | 性能开销较大 |
| 4.4 | 改进事务并发与性能 | — |
| 5.0+ | 性能持续优化 | — |
事务 ACID 特性:
| 特性 | 实现机制 |
|---|---|
| Atomicity(原子性) | WiredTiger 的 MVCC + 回滚机制,事务失败时回滚所有修改 |
| Consistency(一致性) | WiredTiger 检查约束 + Schema 验证 |
| Isolation(隔离性) | Snapshot Isolation(快照隔离),默认读已提交 |
| Durability(持久性) | 事务提交时强制 journal 刷盘 |
事务使用方式:
// 方式 1:回调 API(推荐,自动重试 TransientTransactionError)
const session = db.getMongo().startSession()
session.startTransaction({
readConcern: { level: "snapshot" },
writeConcern: { w: "majority" },
readPreference: "primary"
})
try {
const users = session.getDatabase("mydb").users
const orders = session.getDatabase("mydb").orders
// 业务逻辑:转账
users.updateOne(
{ _id: 10001 },
{ $inc: { balance: -100 } },
{ session }
)
users.updateOne(
{ _id: 10002 },
{ $inc: { balance: 100 } },
{ session }
)
orders.insertOne(
{ from: 10001, to: 10002, amount: 100, created_at: new Date() },
{ session }
)
session.commitTransaction()
console.log("事务提交成功")
} catch (error) {
// 区分可重试错误与不可重试错误
if (error.hasErrorLabel("TransientTransactionError")) {
// 临时错误(锁冲突、复制集选举),可重试
console.log("可重试的事务错误")
} else if (error.hasErrorLabel("UnknownTransactionCommitResult")) {
// 提交结果未知(网络问题),可重试提交
console.log("提交结果不确定,需重试 commit")
} else {
// 业务错误,回滚
console.log("业务错误,事务回滚")
}
session.abortTransaction()
} finally {
session.endSession()
}
// 方式 2:withTransaction 自动重试
session.withTransaction(
async () => {
const users = session.getDatabase("mydb").users
const orders = session.getDatabase("mydb").orders
users.updateOne({ _id: 10001 }, { $inc: { balance: -100 } }, { session })
users.updateOne({ _id: 10002 }, { $inc: { balance: 100 } }, { session })
orders.insertOne(
{ from: 10001, to: 10002, amount: 100 },
{ session }
)
},
{
readConcern: { level: "snapshot" },
writeConcern: { w: "majority" },
readPreference: "primary"
}
)
事务隔离级别:
| 隔离级别 | 行为 | 适用场景 |
|---|---|---|
readConcern: "local" | 读取节点本地最新数据(默认) | 低延迟读 |
readConcern: "majority" | 读取多数派已确认数据 | 避免脏读 |
readConcern: "snapshot" | 事务开始时的快照数据 | 事务可重复读 |
readConcern: "linearizable" | 线性化读,强一致 | 极端一致性需求 |
事务与性能:
事务开销分析:
1. 时间戳管理:
- 每个事务携带时间戳 t
- WiredTiger MVCC 通过 t 确定可见性
- 事务粒度越大,MVCC 维护成本越高
2. 锁竞争:
- 多文档事务会持有 WiredTiger 内部锁
- 锁冲突时等待时间随事务时长增加
- 长事务会导致 WiredTiger cache 压力
3. 网络开销:
- 事务开始 / 提交时需与所有相关节点通信
- 跨分片事务需更多节点协调
4. 磁盘 I/O:
- 事务回滚时产生 undo log
- 大量小事务的 undo log 累积
事务最佳实践:
| 实践 | 说明 |
|---|---|
| 事务粒度 | 事务应尽量短,仅包含必要操作 |
| 避免长事务 | 事务时长建议 < 60 秒,避免锁等待与缓存压力 |
| 错误重试 | 实现 TransientTransactionError 错误重试逻辑 |
| 合理 readConcern | 强一致需求用 snapshot,性能优先用 local |
| 避免跨分片事务 | 通过数据建模减少跨分片事务使用 |
| 事务超时 | 设置 transactionLifetimeLimitSeconds(默认 60s) |
| 监控事务 | db.serverStatus().transactions 监控事务状态 |
事务监控:
// 查看事务状态
db.serverStatus().transactions
// 关键字段:
// - "transactionsCollectionWriterCount"
// - "transactionsCollectionReaderCount"
// - "totalAborted": 已回滚事务数
// - "totalCommitted": 已提交事务数
// - "totalStarted": 总启动事务数
// - "currentActive": 当前活跃事务数
// - "currentInactive": 当前空闲事务数
// 查看活跃事务
db.adminCommand({
currentOp: 1,
$or: [
{ "op": "command", "command.$_internalId": { $exists: true } },
{ "op": "command", "command.commitTransaction": { $exists: true } }
]
})
事务与原子单文档操作对比:
// MongoDB 单文档操作天然原子(无需事务)
// 适用:嵌入式结构内的字段更新
db.users.updateOne(
{ _id: 10001, balance: { $gte: 100 } }, // 乐观锁条件
{ $inc: { balance: -100 } } // 条件性扣款
)
// 单文档原子操作性能更优,优先使用
// 多文档事务:跨集合 / 跨文档需要事务
// 如:账户 A 扣款 + 账户 B 加款 + 订单记录
// 必须使用事务保证三者一致
9 Change Streams 变更流与实时数据管道
答案:
Change Streams 是 MongoDB 3.6 引入的功能,基于 oplog 变更提供实时事件流,应用程序可以订阅集合的所有数据变更(insert / update / delete / drop 等),是实现 CDC(Change Data Capture)和实时数据管道的核心机制。
Change Streams 架构:
graph LR
App["Application"]
App -->|"watch()"| Mongo["MongoDB"]
Mongo -->|"订阅 oplog"| Oplog["local.oplog.rs"]
Oplog -->|"聚合变更事件"| CS["Change Stream"]
CS -->|"推送事件"| Consumer["Consumer
(Application / Kafka Connector)"]
Consumer --> Action["业务处理
数据同步/缓存更新/触发器"]
Change Stream 监听示例:
// Node.js 监听 orders 集合变更
const pipeline = [
{ $match: { "fullDocument.status": "active" } }, // 仅关注 active 订单
{ $project: { operationType: 1, fullDocument: 1, ns: 1, documentKey: 1 } }
]
const changeStream = db.collection("orders").watch(pipeline, {
fullDocument: "updateLookup", // update 时返回完整文档
resumeAfter: { _data: "..." }, // 断点续传起点
maxAwaitTimeMS: 1000, // 无变更时等待时间
batchSize: 100
})
changeStream.on("change", (change) => {
console.log("变更事件:", change.operationType)
// insert / update / replace / delete / drop / rename / invalidate
switch (change.operationType) {
case "insert":
handleNewOrder(change.fullDocument)
break
case "update":
handleOrderUpdate(change.documentKey, change.fullDocument)
break
case "delete":
handleOrderDelete(change.documentKey)
break
}
})
changeStream.on("error", (error) => {
// 错误处理(可恢复 vs 不可恢复)
if (error.code === 280) { // ChangeStreamHistoryLost
// Resume token 失效,需重新建立 watch
}
})
// 关闭流
changeStream.close()
事件结构:
// insert 事件
{
_id: { _data: "8265F8..." }, // resume token
operationType: "insert",
clusterTime: Timestamp(1700000000, 1),
ns: { db: "mydb", coll: "orders" },
documentKey: { _id: ObjectId("...") },
fullDocument: { _id: ..., user_id: 10001, total: 99.9, status: "active" }
}
// update 事件(需 fullDocument: "updateLookup")
{
_id: { _data: "..." },
operationType: "update",
clusterTime: Timestamp(...),
ns: { db: "mydb", coll: "orders" },
documentKey: { _id: ObjectId("...") },
updateDescription: {
updatedFields: { status: "shipped" },
removedFields: [],
truncatedArrays: []
},
fullDocument: { _id: ..., status: "shipped", ... } // 更新后完整文档
}
// delete 事件
{
_id: { _data: "..." },
operationType: "delete",
clusterTime: Timestamp(...),
ns: { db: "mydb", coll: "orders" },
documentKey: { _id: ObjectId("...") }
}
Change Stream 与 oplog 关系:
| 维度 | oplog | Change Stream |
|---|---|---|
| 访问层级 | 内部(local.oplog.rs) | 公开 API(任何客户端可订阅) |
| 权限要求 | 需 direct access | 需 readAnyDatabase 等权限 |
| 数据格式 | 二进制 BSON 内部格式 | 友好 JSON 格式(包含 ns / operationType) |
| 过滤能力 | 无(需应用层过滤) | 支持 $match 管道(性能更优) |
| 断点续传 | 需手动记录 ts | 内建 resumeToken |
| 分片集群 | 仅 Primary 节点 | 跨分片聚合全局事件 |
| 使用难度 | 高 | 低 |
Change Stream 高级用法:
// 1. 监听整个数据库
db.getSiblingDB("mydb").watch()
// 2. 监听整个集群
db.adminCommand({ aggregate: 1, pipeline: [...], cursor: {} })
// 3. 复杂过滤管道
const pipeline = [
{
$match: {
$and: [
{ "ns.coll": { $in: ["orders", "payments"] } },
{
$or: [
{ operationType: "insert", "fullDocument.total": { $gt: 1000 } },
{ operationType: "update", "updateDescription.updatedFields.status": "refunded" },
{ operationType: "delete" }
]
}
]
}
}
]
// 4. 断点续传(生产必备)
let resumeToken = null
const changeStream = db.orders.watch(pipeline, {
resumeAfter: resumeToken // 从上次中断点继续
})
changeStream.on("change", (change) => {
resumeToken = change._id // 保存 resume token
// 处理变更
processChange(change)
})
// 5. 配合 dead letter queue
changeStream.on("change", async (change) => {
try {
await processChange(change)
} catch (error) {
await deadLetterQueue.push({ change, error: error.message })
}
})
Change Stream 在 Kafka 生态的应用:
# MongoDB Kafka Connector 配置示例
# 从 MongoDB 同步变更到 Kafka
name: mongodb-source-orders
config:
connector.class: com.mongodb.kafka.connect.MongoSourceConnector
connection.uri: mongodb://mongo-1:27017,mongo-2:27017,mongo-3:27017/?replicaSet=rs0
database: mydb
collection: orders
# 复制集与 Change Stream 配置
change.stream.full.document: updateLookup
# 输出到 Kafka topic
topic.prefix: mongodb.
# Pipeline 过滤
pipeline: |
[
{ "$match": { "operationType": { "$in": ["insert", "update", "delete"] } } }
]
# 起始位置
startup.mode: resume
Change Stream 限制:
| 限制 | 说明 |
|---|---|
| 仅支持复制集 / 分片集群 | 单机模式不支持 |
| oplog 窗口依赖 | oplog 太小可能导致 Change Stream 失效(resume token 失效) |
| 权限要求 | 需 find + local 数据库 read 权限 |
| Capped Collection | Capped collection 删除事件可能不准确 |
| Renaming / Dropping | 集合重命名 / 删除时需重新建立 watch |
| 大事务延迟 | 事务中的变更在提交后才可见(避免读取未提交数据) |
10 GridFS 大文件存储
答案:
GridFS 是 MongoDB 内置的大文件存储规范,将大文件(默认超过 16MB 的单个 BSON 文档限制)拆分为多个 255KB 的 chunk 存储到两个集合中,提供透明的文件存取 API。
GridFS 存储结构:
graph TD
File["原始文件
(如 100MB 视频)"]
File --> Split["拆分为 255KB Chunk"]
Split --> C1["Chunk 1
(0-255KB)"]
Split --> C2["Chunk 2
(255-510KB)"]
Split --> C3["Chunk 3
(510KB+)"]
Split --> CN["Chunk N"]
C1 --> FilesColl["fs.files 集合
(文件元数据)"]
C2 --> ChunksColl["fs.chunks 集合
(二进制块)"]
C3 --> ChunksColl
CN --> ChunksColl
FilesColl --> Meta["{
_id: ObjectId,
filename: 'video.mp4',
length: 104857600,
chunkSize: 261120,
uploadDate: ISODate,
md5: '...',
metadata: {}
}"]
ChunksColl --> ChunkData["{
_id: ObjectId,
files_id: ObjectId('...'),
n: 0, // 块序号
data: BinData(...) // 二进制
}"]
GridFS 使用示例:
// Node.js GridFS Bucket API(推荐)
const { MongoClient, GridFSBucket } = require("mongodb")
const client = new MongoClient("mongodb://localhost:27017")
// 打开 GridFS Bucket
const db = client.db("mydb")
const bucket = new GridFSBucket(db, {
bucketName: "fs", // 集合前缀(fs.files / fs.chunks)
chunkSizeBytes: 261120 // 255KB(默认)
})
// 1. 上传文件
const fs = require("fs")
const uploadStream = bucket.openUploadStream("video.mp4", {
metadata: { category: "tutorial", uploaded_by: "zhangsan" }
})
fs.createReadStream("./local-video.mp4")
.pipe(uploadStream)
.on("error", (err) => console.error("上传失败:", err))
.on("finish", () => {
console.log("上传完成, fileId:", uploadStream.id)
})
// 2. 下载文件
const downloadStream = bucket.openDownloadStreamByName("video.mp4")
downloadStream.pipe(fs.createWriteStream("./downloaded.mp4"))
// 3. 通过 _id 下载
const fileId = ObjectId("...")
bucket.openDownloadStream(fileId)
.pipe(fs.createWriteStream("./by-id.mp4"))
// 4. 列出文件
const cursor = bucket.find({ "metadata.category": "tutorial" })
cursor.forEach(file => {
console.log(`${file.filename} - ${file.length} bytes`)
})
// 5. 删除文件
bucket.delete(fileId)
// 6. 范围下载(部分内容)
bucket.openDownloadStreamByName("video.mp4", {
start: 1024, // 起始字节
end: 2048 // 结束字节
})
GridFS 集合结构:
// fs.files 集合 - 文件元数据
{
_id: ObjectId("..."),
filename: "presentation.pptx",
length: 5242880, // 文件总大小
chunkSize: 261120, // chunk 大小
uploadDate: ISODate("2024-01-15T10:00:00Z"),
md5: "5d41402abc4b2a76b9719d911017c592",
contentType: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
metadata: {
uploaded_by: "zhangsan",
department: "engineering",
tags: ["internal", "training"]
},
aliases: ["deck.pptx"]
}
// fs.chunks 集合 - 二进制块
{
_id: ObjectId("..."),
files_id: ObjectId("..."), // 关联到 fs.files._id
n: 0, // chunk 序号(0 起始)
data: BinData(0, "...") // 255KB 二进制数据
}
GridFS 配置选项:
// 自定义 GridFS Bucket
const bucket = new GridFSBucket(db, {
bucketName: "attachments", // 自定义前缀
chunkSizeBytes: 1024 * 1024, // 1MB(适合大视频)
writeConcern: { w: "majority" }, // 多数派写入
readPreference: "primaryPreferred" // 读偏好
})
GridFS vs 对象存储对比:
| 维度 | GridFS | S3 / OSS / COS |
|---|---|---|
| 部署复杂度 | 复用 MongoDB 集群 | 需独立对象存储服务 |
| 单文件大小 | 理论无上限(实际受分片集群限制) | 单文件 5TB |
| 成本 | 数据库存储(昂贵) | 对象存储(便宜) |
| 数据一致性 | 强一致(MongoDB 事务保障) | 最终一致 |
| 访问延迟 | 较低(同集群) | 跨网络(高) |
| 适用场景 | 小型文件(< 1GB)、需事务 | 大文件、CDN 加速 |
| 流式读写 | 支持 | 支持 |
| 索引能力 | 强(自定义 metadata 索引) | 弱(仅按 key) |
| 跨区域复制 | 通过 MongoDB 复制集 | 原生支持 |
GridFS 适用场景:
| 场景 | 适用性 |
|---|---|
| 用户头像、缩略图(< 1MB) | 适合 |
| 小型文档附件(PDF/Excel < 10MB) | 适合 |
| 中等视频(10MB ~ 1GB) | 评估(建议用对象存储) |
| 大型视频 / 备份(> 1GB) | 不推荐(用对象存储 + CDN) |
| 需强一致与事务 | 适合(GridFS 文档可纳入事务) |
| 高频读 CDN 场景 | 不推荐(用对象存储) |
GridFS 性能优化:
| 优化点 | 方法 |
|---|---|
| Chunk Size 调优 | 大文件用 1MB~2MB chunk,减少 fs.chunks 文档数 |
| Index 优化 | { files_id: 1, n: 1 } 是默认索引,附加 metadata 字段索引 |
| 分离 Bucket | 冷热数据用不同 Bucket,便于归档 |
| 数据生命周期 | 通过 TTL 索引 + 后台任务定期清理 |
| CDN 配合 | 将 GridFS 文件转存到 CDN,降低 MongoDB 压力 |
// 优化索引
db.fs.chunks.createIndex({ files_id: 1, n: 1 }) // 默认索引
db.fs.files.createIndex({ "metadata.department": 1 }) // 业务查询索引
db.fs.files.createIndex({ uploadDate: -1 }) // 按时间清理
// 定期清理过期文件
db.fs.files.aggregate([
{ $match: { uploadDate: { $lt: new Date(Date.now() - 90 * 86400000) } } },
{ $group: { _id: null, file_ids: { $push: "$_id" } } }
]).forEach(result => {
result.file_ids.forEach(id => {
bucket.delete(id)
})
})
11 备份恢复策略:mongodump / mongorestore / oplog
答案:
MongoDB 备份恢复体系由逻辑备份(mongodump / mongorestore)、物理备份(文件系统快照)、oplog 增量备份(mongooplog)三种方式组成,分别适用于不同场景与 RPO(Recovery Point Objective)要求。
备份方式对比:
| 备份方式 | 原理 | RPO | 恢复粒度 | 影响 | 适用场景 |
|---|---|---|---|---|---|
| mongodump | 集合级 BSON 导出 | 备份时刻数据 | 集合 / 文档级 | 中(读压力) | 中小数据量、跨版本迁移 |
| 文件系统快照 | LVM/ZFS/Snapshot | 备份时刻数据 | 整库 | 低 | 大数据量、定期全量 |
| mongooplog | 备份后 oplog 增量 | 秒级 | 整库 | 极低 | 灾难恢复、误操作回滚 |
| MongoDB Atlas 备份 | 云服务托管 | 分钟级 | 整库 / 时间点 | 无自管 | 云上托管 |
| Sharded Cluster 备份 | 协调 mongodump | 备份时刻 | 数据库 | 中 | 分片集群 |
mongodump 逻辑备份:
# 全库备份
mongodump --host "rs0/mongo-1:27017,mongo-2:27017" \
--port 27017 \
--username backup_user \
--password "${MONGO_PASSWORD}" \
--authenticationDatabase admin \
--gzip \ # 启用压缩
--archive=mongo-backup-20240115.gz # 单文件归档
# 单数据库备份
mongodump --host rs0/mongo-1:27017 \
--db mydb \
--gzip \
--archive=mydb-backup-20240115.gz
# 单集合备份
mongodump --host rs0/mongo-1:27017 \
--db mydb \
--collection orders \
--gzip \
--archive=orders-backup-20240115.gz
# 指定字段查询备份(部分数据)
mongodump --host rs0/mongo-1:27017 \
--db mydb \
--collection orders \
--query '{"created_at": {"$gte": {"$date": "2024-01-01T00:00:00Z"}}}' \
--gzip \
--archive=orders-2024.gz
# 并行备份(加速)
mongodump --host rs0/mongo-1:27017 \
--numParallelCollections 4 \ # 4 集合并行
--gzip \
--archive=mongo-backup-20240115.gz
mongorestore 恢复:
# 恢复全库
mongorestore --host rs0/mongo-1:27017 \
--username restore_user \
--password "${MONGO_PASSWORD}" \
--authenticationDatabase admin \
--gzip \
--archive=mongo-backup-20240115.gz \
--drop # 恢复前删除已存在集合
# 恢复到不同数据库
mongorestore --host rs0/mongo-1:27017 \
--nsInclude "mydb.*" \
--nsFrom "mydb.*" --nsTo "mydb_restored.*" \
--gzip \
--archive=mongo-backup-20240115.gz
# 单集合恢复
mongorestore --host rs0/mongo-1:27017 \
--db mydb \
--collection orders \
--gzip \
--archive=orders-backup-20240115.gz
# 并行恢复
mongorestore --host rs0/mongo-1:27017 \
--numParallelCollections 4 \
--numInsertionWorkersPerCollection 8 \
--gzip \
--archive=mongo-backup-20240115.gz
# 索引重建(恢复时禁用索引构建,最后批量构建)
mongorestore --host rs0/mongo-1:27017 \
--noIndexRestore \
--gzip \
--archive=mongo-backup-20240115.gz
# 恢复后单独构建索引
mongosh "mongodb://rs0/mongo-1:27017" --eval "
db.orders.createIndex({ user_id: 1, created_at: -1 })
"
oplog 增量备份与 Point-In-Time Recovery:
# 1. 开启 oplog 监听(持续备份 oplog 到文件)
mongooplog --host rs0/mongo-1:27017 \
--from rs0/mongo-1:27017 \
--seconds 86400 \ # 监听 24 小时
> /backup/oplog-20240115.bson
# 2. 备份后应用 oplog(Point-In-Time Recovery)
# 假设 14:00 备份后,14:30 发生误操作,需要恢复到 14:25 状态
# 步骤 1:恢复 14:00 全量备份
mongorestore --host rs0/mongo-1:27017 \
--drop \
--archive=mongo-backup-1400.gz
# 步骤 2:应用 oplog 到 14:25
mongorestore --host rs0/mongo-1:27017 \
--oplogReplay \
--oplogLimit "1705305900:1" # oplog 时间戳(14:25 对应位置)
PITR(Point-In-Time Recovery)实战:
// 1. 查找目标时间点的 oplog 位置
// 查询 oplog,找到 14:25:00 之前的最后一个操作
use local
db.oplog.rs.find({
ts: { $lte: Timestamp(1705305900, 1) } // 14:25:00 的 Timestamp
}).sort({ ts: -1 }).limit(1)
// 2. 记录该 ts,作为 --oplogLimit 参数
// 3. 执行恢复(如上)
分片集群备份策略:
# 分片集群的备份需要协调每个 Shard + Config Server
# 1. 停止 Balancer(避免备份期间 Chunk 迁移)
mongosh "mongodb://mongos-1:27017" --eval "sh.stopBalancer()"
# 2. 备份 Config Server
mongodump --host config-1:27017 \
--db config \
--gzip \
--archive=config-backup.gz
# 3. 备份每个 Shard(可并行)
mongodump --host shard-1-primary:27017 \
--gzip \
--archive=shard1-backup.gz &
mongodump --host shard-2-primary:27017 \
--gzip \
--archive=shard2-backup.gz &
mongodump --host shard-3-primary:27017 \
--gzip \
--archive=shard3-backup.gz &
wait
# 4. 恢复后重启 Balancer
mongosh "mongodb://mongos-1:27017" --eval "sh.startBalancer()"
备份策略金字塔:
graph TD
L1["异地灾备
跨区域复制 S3 / OSS
每周 1 次 / 保留 3 个月"]
L2["物理快照
LVM/ZFS/CSI Snapshot
每天 1 次 / 保留 7 天"]
L3["逻辑全量 + oplog
mongodump + mongooplog
PITR 恢复
每天全量 + 持续 oplog / 保留 30 天"]
L1 --> L2 --> L3
备份验证 SOP:
| 步骤 | 频率 | 操作 |
|---|---|---|
| 备份完整性校验 | 每次备份后 | 校验归档文件 MD5 |
| 自动恢复测试 | 每周 | 备份恢复至独立测试环境,验证数据一致性 |
| PITR 演练 | 每月 | 模拟误操作,执行 PITR 恢复 |
| 恢复时间记录 | 每次演练 | 记录 RTO(恢复时间目标) |
| 备份监控告警 | 实时 | 备份失败立即告警 |
生产备份规范:
# Kubernetes CronJob 备份示例
apiVersion: batch/v1
kind: CronJob
metadata:
name: mongodb-backup
spec:
schedule: "0 2 * * *" # 每天凌晨 2 点
jobTemplate:
spec:
template:
spec:
containers:
- name: backup
image: mongo:7.0
command:
- /bin/sh
- -c
- |
mongodump --host mongo-primary:27017 \
--username $BACKUP_USER \
--password $BACKUP_PASSWORD \
--authenticationDatabase admin \
--gzip \
--archive=/backup/mongo-$(date +%Y%m%d).gz
# 上传到 S3
aws s3 cp /backup/mongo-$(date +%Y%m%d).gz \
s3://mongo-backups/daily/
# 清理 30 天前本地备份
find /backup -name "*.gz" -mtime +30 -delete
env:
- name: BACKUP_USER
valueFrom:
secretKeyRef:
name: mongo-credentials
key: username
- name: BACKUP_PASSWORD
valueFrom:
secretKeyRef:
name: mongo-credentials
key: password
restartPolicy: OnFailure
12 MongoDB 性能调优与慢查询分析
答案:
MongoDB 性能调优是一个系统性工程,需从 Schema 设计、索引策略、查询模式、WiredTiger 配置、硬件资源五个维度综合优化,核心目标是降低查询延迟与提升并发吞吐。
性能调优方法论:
graph TD
Start["发现性能问题"]
Start --> Profile1["1. 慢查询识别
profiler / slow query log"]
Profile1 --> Analyze["2. explain() 分析执行计划"]
Analyze --> Identify["3. 定位瓶颈
索引缺失/锁等待/网络"]
Identify --> Optimize["4. 优化
索引/Schema/查询/配置"]
Optimize --> Verify["5. 验证效果"]
Verify --> Profile1
Verify -->|未达预期| Profile1
Verify -->|达标| End["完成优化"]
1. 慢查询识别与 Profiler:
// 启用 Profiler(记录慢查询)
db.setProfilingLevel(2) // 记录所有操作
db.setProfilingLevel(1, { slowms: 100 }) // 记录 > 100ms 的操作(推荐)
// 查看 Profiler 状态
db.getProfilingStatus()
// 分析慢查询
db.system.profile.find({
millis: { $gt: 100 } // 慢查询阈值
}).sort({ ts: -1 }).limit(20).forEach(doc => {
print(JSON.stringify({
ts: doc.ts,
op: doc.op,
ns: doc.ns,
millis: doc.millis,
query: doc.command?.filter || doc.command?.query,
planSummary: doc.planSummary,
numYields: doc.numYield,
docsExamined: doc.docsExamined,
keysExamined: doc.keysExamined,
nreturned: doc.nreturned,
responseLength: doc.responseLength
}, null, 2))
})
// 慢查询关键指标
// docsExamined / nreturned → 索引效率(理想 = 1)
// keysExamined / nreturned → 索引选择性
// numYields → 锁等待次数
// millis → 总耗时
// 关闭 Profiler
db.setProfilingLevel(0)
2. 慢查询优化手段:
// 反例 1:全表扫描(COLLSCAN)
db.orders.find({ status: "active", created_at: { $gte: ISODate("2024-01-01") } })
// 优化:创建复合索引
db.orders.createIndex({ status: 1, created_at: -1 })
// ESR 原则:等值 (status) → 范围/排序 (created_at)
// 反例 2:未使用覆盖索引
db.users.find({ email: "[email protected]" }, { username: 1, email: 1, _id: 0 })
// 优化:覆盖索引
db.users.createIndex({ email: 1, username: 1 })
// 查询时只读取索引,无 FETCH 阶段
// 反例 3:正则表达式前缀模糊查询
db.users.find({ name: /zhang/i }) // 无法使用索引
db.users.find({ name: /^zhang/ }) // 前缀匹配,可使用索引
// 前缀 /^value/ 命中索引,包含 /value/ 不命中
// 反例 4:$ne / $nin 操作符(索引效率低)
db.orders.find({ status: { $ne: "deleted" } }) // 索引扫描大部分
// 优化:使用 $in 替代
db.orders.find({ status: { $in: ["active", "pending", "shipped"] } })
// 反例 5:$exists 检查
db.users.find({ phone: { $exists: true } }) // 索引效率低
// 优化:使用 $type 替代
db.users.find({ phone: { $type: "string" } })
3. Schema 设计优化:
// 反例:宽表 + 大量字段
{
_id: ObjectId("..."),
order_id: "O20240115-001",
user_basic: { name: "zhangsan", age: 28 }, // 用户基本信息
user_profile: { ... }, // 扩展信息
items: [...], // 订单商品
shipping: { ... }, // 物流信息
payment: { ... }, // 支付信息
...
}
// 文档过大(接近 16MB 上限),更新频繁导致局部更新失效
// 优化:反范式化拆分(按读写模式)
// 集合 1:orders(订单核心信息)
{
_id: ObjectId("..."),
order_id: "O20240115-001",
user_id: 10001,
status: "active",
total: 99.9,
created_at: ISODate("2024-01-15")
}
// 集合 2:order_items(订单商品)
{
_id: ObjectId("..."),
order_id: "O20240115-001",
product_id: 20001,
qty: 2,
price: 49.95
}
// 集合 3:order_shipping(物流信息)
{
_id: ObjectId("..."),
order_id: "O20240115-001",
carrier: "SF",
tracking_no: "SF1234567890"
}
4. WiredTiger 调优:
# mongod.conf 性能调优
storage:
wiredTiger:
engineConfig:
cacheSizeGB: 12 # 缓存 = (RAM - OS reserve) × 50%
journalCompressor: snappy
collectionConfig:
blockCompressor: snappy # 集合压缩
indexConfig:
prefixCompression: true # 索引前缀压缩
# 系统层面
net:
maxIncomingConnections: 2000 # 最大连接数
# 操作 Profiling
operationProfiling:
mode: slowOp
slowOpThresholdMs: 100 # 慢查询阈值
# 资源限制
processManagement:
fork: true
timeZoneInfo: /usr/share/zoneinfo
5. 硬件与操作系统调优:
| 调优点 | 建议 |
|---|---|
| 磁盘 I/O | NVMe SSD,IOPS > 10K;WAL / 数据库分离 |
| 内存 | 热数据 < WiredTiger cacheSize(推荐工作集占 60-70% 缓存) |
| CPU | 多核高频,WiredTiger 充分利用多核(文档级并发) |
| 网络 | 复制集节点间低延迟(< 5ms),避免跨 AZ 同步复制 |
| NUMA 架构 | 关闭 NUMA 或 numactl –interleave=all,避免跨 NUMA 内存访问 |
| 文件系统 | XFS(推荐)/ EXT4,禁用 atime,挂载选项 noatime |
| 内核参数 | vm.swappiness=1(避免 swap),vm.dirty_ratio=15 |
6. 性能监控关键指标:
// 1. 实时 QPS / 延迟
db.serverStatus().opcounters
// - insert / query / update / delete / command / getmore
// 2. 连接数
db.serverStatus().connections
// - current / available / totalCreated / active
// 3. 复制状态
db.serverStatus().repl
// - replSetName / ismaster / secondary / primary
// 4. WiredTiger 缓存
db.serverStatus().wiredTiger.cache
// - "bytes currently in the cache" / "maximum bytes configured"
// - "tracked dirty bytes in the cache"
// - "pages read into cache" / "pages written from cache"
// - "percentage overhead"
// 5. 内存使用
db.serverStatus().mem
// - resident / virtual / mapped
// 6. 锁统计
db.serverStatus().locks
// - globalLock.totalTime / currentQueue
7. 慢查询优化清单:
| 优化点 | 检查方法 |
|---|---|
| 索引覆盖查询 | explain().executionStats.totalDocsExamined == nReturned |
| 避免 COLLSCAN | explain().winningPlan.stage != "COLLSCAN" |
| 避免内存排序 | explain().winningPlan.inputStage.stage != "SORT" |
| 避免 Fetch 阶段 | 使用 projection 限制返回字段 |
| 避免大 Skip | 使用 cursor-based 分页替代 skip(N) |
| 避免大文档 | 单文档 < 200KB,避免文档爆炸 |
| 避免深度嵌套 | 嵌套层级 < 5 层,便于索引与查询 |
13 MongoDB 运维监控与诊断
答案:
MongoDB 运维监控体系包括内建命令、serverStatus 指标、Profiling 日志、Prometheus exporter 集成,需从运行状态、性能、复制、磁盘、连接、慢查询六个维度建立完整监控。
监控体系架构:
graph TD
subgraph Mongo["MongoDB Cluster"]
M1["mongod Primary"]
M2["mongod Secondary"]
M3["Config Server"]
end
subgraph Exporters["Metrics Exporters"]
MExp["mongodb_exporter
(Prometheus 格式)"]
BE["Bikeshed / mtools
(日志分析)"]
end
subgraph Stack["监控告警栈"]
Prom["Prometheus"]
Graf["Grafana"]
Alert["AlertManager"]
end
M1 -->|"暴露 /metrics"| MExp
M2 -->|"暴露 /metrics"| MExp
M3 -->|"暴露 /metrics"| MExp
M1 -->|"输出日志"| BE
MExp --> Prom
BE --> Prom
Prom --> Graf
Prom --> Alert
Alert -->|告警| OnCall["OnCall 通知"]
1. 内建监控命令:
// serverStatus:综合运行状态
db.serverStatus()
// 关键字段:
// - host / process / pid / uptime / version
// - storageEngine: { name: "wiredTiger" }
// - connections: { current, available, totalCreated, active }
// - mem: { resident, virtual, mapped }
// - opcounters: { insert, query, update, delete, command, getmore }
// - opLatencies: { reads, writes, commands }
// - repl: { replSetName, ismaster, secondary, primary }
// - wiredTiger.cache: { ... }
// - locks: { ... }
// dbStats:集合 / 数据库统计
db.stats()
// 关键字段:
// - collections / objects / avgObjSize / dataSize / storageSize
// - indexes / indexSize
// - fsUsedSize / fsTotalSize
// collStats:单集合详细统计
db.orders.stats(1024) // 1024 字节缩放
// 关键字段:
// - size / count / avgObjSize / maxSize
// - storageSize / totalIndexSize
// - wiredTiger: { ... } 详细存储信息
// currentOp:当前操作
db.currentOp({
$or: [
{ "op": { $in: ["query", "update", "insert", "remove"] } },
{ "command.findAndModify": { $exists: true } }
],
"secs_running": { $gt: 5 } // 超过 5 秒
})
// 终止慢查询
db.killOp(opid)
// top:按集合统计访问量
db.adminCommand({ top: 1 })
// hostInfo:服务器信息
db.hostInfo()
// - system / os / extra
2. 复制集监控:
// 复制集状态
rs.status()
// 关键字段:
// - set: 集群名
// - myState: 1 (PRIMARY) / 2 (SECONDARY)
// - members[]:
// - stateStr
// - health
// - optimeDate: 最后应用 oplog 时间
// - electionDate
// - syncSourceHost
// 主从延迟
rs.printSecondaryReplicationInfo()
// 输出:source: mongo-2, syncedTo: ..., lag: 2s
// oplog window:oplog 可回放时长
db.adminCommand({ replSetGetStatus: 1 }).members.map(m => ({
name: m.name,
state: m.stateStr,
lag_seconds: (Date.now() - m.optimeDate?.getTime()) / 1000
}))
3. Prometheus 集成:
# mongodb_exporter 部署
apiVersion: apps/v1
kind: Deployment
metadata:
name: mongodb-exporter
spec:
replicas: 1
selector:
matchLabels:
app: mongodb-exporter
template:
metadata:
labels:
app: mongodb-exporter
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "9216"
prometheus.io/path: "/metrics"
spec:
containers:
- name: exporter
image: percona/mongodb_exporter:0.40
args:
- "--mongodb.uri=mongodb://mongo-exporter:${PASSWORD}@mongo-primary:27017/admin"
- "--collect-all"
- "--compatible-mode"
ports:
- containerPort: 9216
env:
- name: PASSWORD
valueFrom:
secretKeyRef:
name: mongodb-credentials
key: password
关键 Prometheus 指标:
| 指标 | 说明 | 告警阈值 |
|---|---|---|
mongodb_ss_connections{conn_type="current"} | 当前连接数 | > 80% maxIncomingConnections |
mongodb_ss_opcounters_total | 操作计数器(rate 计算 QPS) | — |
mongodb_ss_mem_resident_bytes | 物理内存使用 | > 80% node memory |
mongodb_ss_wt_cache_bytes_currently_in_cache | WiredTiger 缓存使用 | > 90% cacheSize |
mongodb_ss_wt_cache_tracked_dirty_bytes | 脏页大小 | > 20% cacheSize |
mongodb_ss_repl_secondary_delay_seconds | 复制延迟 | > 30s |
mongodb_ss_repl_health | 节点健康状态 | != 1 |
mongodb_ss_opLatencies_reads_op_time | 读操作延迟 | > 100ms |
mongodb_ss_opLatencies_writes_op_time | 写操作延迟 | > 100ms |
mongodb_ss_storageEngine_dataSize_bytes | 数据大小 | 持续增长监控 |
PromQL 告警规则:
# Prometheus 告警规则示例
groups:
- name: mongodb_alerts
rules:
# 复制集 Primary 不可用
- alert: MongoDBPrimaryDown
expr: mongodb_rs_members_state == 1
for: 30s
labels:
severity: critical
annotations:
summary: "MongoDB Primary 不可用"
# 复制延迟
- alert: MongoDBReplicationLag
expr: mongodb_rs_members_optimeDate - mongodb_rs_members_primary_optimeDate > 30
for: 2m
labels:
severity: warning
annotations:
summary: "MongoDB 复制延迟 > 30s"
# WiredTiger 缓存使用率高
- alert: MongoDBHighCacheUsage
expr: mongodb_ss_wt_cache_bytes_currently_in_cache / mongodb_ss_wt_cache_maximum_bytes_configured > 0.9
for: 5m
labels:
severity: warning
annotations:
summary: "WiredTiger 缓存使用率 > 90%"
# 脏页率过高
- alert: MongoDBHighDirtyPages
expr: mongodb_ss_wt_cache_tracked_dirty_bytes / mongodb_ss_wt_cache_maximum_bytes_configured > 0.2
for: 10m
labels:
severity: warning
annotations:
summary: "WiredTiger 脏页率 > 20%"
# 连接数高
- alert: MongoDBHighConnections
expr: mongodb_ss_connections{conn_type="current"} > 1600
for: 5m
labels:
severity: warning
annotations:
summary: "MongoDB 连接数 > 1600"
# 备份缺失
- alert: MongoDBNoRecentBackup
expr: time() - mongodb_backup_last_success_time > 86400
for: 1h
labels:
severity: critical
annotations:
summary: "MongoDB 24 小时内无成功备份"
4. 慢查询日志分析:
# mtools:MongoDB 日志分析工具集
pip install mtools
# 常用 mloginfo / mlogfilter / mplotqueries
mloginfo mongo.log # 整体统计
mlogfilter mongo.log --slow 100 # 提取 > 100ms 慢查询
mlogfilter mongo.log --namespace mydb.orders # 提取特定集合
mplotqueries mongo.log --type scatter # 慢查询可视化
5. 故障排查 Checklist:
| 故障现象 | 排查步骤 |
|---|---|
| 查询慢 | 1. 开启 Profiler 2. explain() 分析 3. 检查索引 4. 检查 WiredTiger 缓存 |
| 写入慢 | 1. 检查 oplog 写入延迟 2. 检查磁盘 I/O (iostat) 3. 检查 writeConcern 4. 检查索引数量 |
| Primary 切换频繁 | 1. 检查节点健康 2. 检查网络分区 3. 检查选举参数 4. 检查资源 (CPU/Mem/Disk) |
| 复制延迟大 | 1. 检查 Secondary 资源 2. 检查网络 3. 检查 oplog 写入 4. 检查 Secondary 锁竞争 |
| 磁盘占满 | 1. 检查 oplog 大小 2. 检查索引膨胀 3. 检查文档删除后的空间回收 4. 考虑 compact / repair |
| 连接数耗尽 | 1. 检查应用连接池 2. 检查慢查询占用连接 3. 调整 maxIncomingConnections |
6. 性能基线与容量规划:
MongoDB 性能基线计算公式:
内存需求:
cacheSizeGB = (热数据 + 索引) × 1.3(30% 余量)
推荐:cacheSizeGB ≤ RAM × 50% - 1GB
系统内存建议:cacheSizeGB × 2.5
磁盘需求:
数据大小:原始 JSON × 1.5(BSON 开销) × 1.3(压缩后) × 1.2(索引开销)
加上 oplog(默认 5% 磁盘)
加上 WiredTiger checkpoint 临时空间(≈ 数据量 2%)
CPU 需求:
OLTP:每 1000 QPS 需约 2 vCPU
复杂聚合:每 100 QPS 需约 4 vCPU
网络需求:
复制流量:oplog 写入速率 × 副本数
客户端流量:峰值 QPS × 平均响应体大小
14 MongoDB vs MySQL 对比与场景选型
答案:
MongoDB 与 MySQL 是当前最主流的两种数据库引擎,分别代表文档数据库与关系型数据库的典型范式。理解两者差异是技术选型的基础。
核心对比矩阵:
| 维度 | MongoDB | MySQL |
|---|---|---|
| 数据模型 | 文档(BSON,动态 Schema) | 关系表(固定 Schema) |
| 扩展方式 | 水平扩展(Sharding 原生) | 垂直扩展 / 分库分表(应用层) |
| 事务能力 | 4.0+ 多文档 ACID,4.2+ 分布式事务 | 完整 ACID 事务 |
| JOIN 支持 | 弱($lookup 4.0+ 起支持) | 强(多表 JOIN 优化成熟) |
| 索引 | B+Tree、TTL、地理、文本、哈希 | B+Tree、HASH、FULLTEXT、SPATIAL |
| 查询语言 | MongoDB Query Language (JSON-like) | SQL |
| 一致性模型 | 默认最终一致(可配 w:majority 强一致) | 默认 ACID 强一致 |
| 写入性能 | 高(无 JOIN,文档级锁) | 中(行级锁,InnoDB 复杂事务) |
| 存储引擎 | WiredTiger | InnoDB(默认)/ MyISAM / Memory |
| 适用数据规模 | TB ~ PB 级 | GB ~ TB 级(分库分表后 PB 级) |
| 运维复杂度 | 中(复制集 + Sharding) | 中(主从 / MGR / 分库分表) |
| 生态成熟度 | 中(NoSQL 主流) | 极高(关系型事实标准) |
| 学习曲线 | 平缓(JSON-like) | 中(SQL 标准化) |
数据结构对比:
-- MySQL:用户表(关系型)
CREATE TABLE users (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(100) NOT NULL,
age INT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_email (email),
INDEX idx_created (created_at)
);
CREATE TABLE user_profiles (
user_id BIGINT PRIMARY KEY,
real_name VARCHAR(50),
address JSON, -- 5.7+ 支持 JSON 类型
FOREIGN KEY (user_id) REFERENCES users(id)
);
// MongoDB:用户文档(文档型)
db.users.insertOne({
username: "zhangsan",
email: "[email protected]",
age: 28,
created_at: new Date(),
profile: { // 嵌入文档
real_name: "张三",
address: {
city: "Beijing",
district: "Haidian"
}
},
tags: ["vip", "tech"] // 数组
})
// 索引
db.users.createIndex({ email: 1 }, { unique: true })
db.users.createIndex({ created_at: -1 })
事务能力对比:
-- MySQL:转账事务(标准 ACID)
START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE user_id = 10001;
UPDATE accounts SET balance = balance + 100 WHERE user_id = 10002;
INSERT INTO transfers (from_user, to_user, amount) VALUES (10001, 10002, 100);
COMMIT;
// MongoDB:转账事务(4.0+)
const session = db.getMongo().startSession()
session.startTransaction()
try {
const users = session.getDatabase("mydb").users
const transfers = session.getDatabase("mydb").transfers
users.updateOne({ _id: 10001 }, { $inc: { balance: -100 } }, { session })
users.updateOne({ _id: 10002 }, { $inc: { balance: 100 } }, { session })
transfers.insertOne({ from: 10001, to: 10002, amount: 100 }, { session })
session.commitTransaction()
} catch (e) {
session.abortTransaction()
} finally {
session.endSession()
}
JOIN 查询对比:
-- MySQL:JOIN 查询
SELECT u.id, u.username, COUNT(o.id) AS order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.created_at >= '2024-01-01'
GROUP BY u.id, u.username
HAVING order_count > 0
ORDER BY order_count DESC
LIMIT 10;
// MongoDB:$lookup 实现(性能通常弱于 MySQL JOIN)
db.users.aggregate([
{ $match: { created_at: { $gte: ISODate("2024-01-01") } } },
{
$lookup: {
from: "orders",
localField: "_id",
foreignField: "user_id",
as: "orders"
}
},
{
$project: {
username: 1,
order_count: { $size: "$orders" }
}
},
{ $match: { order_count: { $gt: 0 } } },
{ $sort: { order_count: -1 } },
{ $limit: 10 }
])
分库分表 vs Sharding 对比:
| 维度 | MySQL 分库分表(应用层) | MongoDB Sharding(原生) |
|---|---|---|
| 分片策略 | 应用代码中实现 | 数据库自动管理 |
| 数据迁移 | 业务停机 + DBA 手工 | Balancer 自动迁移 |
| 路由逻辑 | 中间件(ShardingSphere)或应用 | mongos 自动路由 |
| 跨分片查询 | 复杂(中间件) | 支持(性能有损失) |
| 跨分片事务 | 复杂(TCC / Saga) | 4.2+ 支持(但性能开销大) |
| 运维成本 | 高 | 中 |
典型场景选型:
| 场景 | 推荐 | 原因 |
|---|---|---|
| 金融核心账务 | MySQL | 强 ACID、严格一致性、复杂 JOIN、报表查询 |
| 电商订单(写多) | MongoDB | Schema 灵活、海量数据、高写入吞吐 |
| 用户画像 / 标签 | MongoDB | 字段动态、嵌套结构、宽表读取 |
| 内容管理(CMS) | MongoDB | 文章+评论+标签天然文档化 |
| 物联网时序数据 | MongoDB(5.0+ Time Series) | 高写入、TTL、聚合分析 |
| BI / 数据仓库 | MySQL / ClickHouse | 复杂 SQL、聚合分析、JOIN |
| 社交关系链 | MongoDB / Neo4j | 图结构、深度查询 |
| 缓存层 | MongoDB / Redis | 替代 Redis 持久化方案 |
| 传统 ERP / CRM | MySQL | 业务成熟、报表多、复杂事务 |
| 跨数据中心 | MongoDB(Zone Sharding) | 原生支持地理分片 |
选型决策树:
是否需要强事务 + 复杂 SQL + 报表分析?
├── 是 → 优先 MySQL(或其他 RDBMS)
└── 否 →
数据是否结构化固定?
├── 是 → MySQL
└── 否(Schema 多变)→
写入量是否 > 1万 QPS?
├── 是 → MongoDB(Sharding 水平扩展)
└── 否 →
是否需要灵活字段 / 嵌套结构?
├── 是 → MongoDB
└── 否 → MySQL
共存架构:
生产环境常见组合:
MySQL → 核心账务、交易、订单状态(强一致)
MongoDB → 商品详情、用户画像、日志、IoT 数据(半结构化)
Redis → 缓存层、Session、计数器
ClickHouse → OLAP 分析、日志分析
Elasticsearch → 全文搜索、日志检索
各司其职,通过数据同步工具(Debezium / DataX / Canal)打通
MongoDB 不适用的场景:
| 不适用场景 | 原因 |
|---|---|
| 强事务 + 复杂 JOIN 报表 | 关系模型更合适,MongoDB 聚合管道性能差 |
| 大量多对多关联查询 | MongoDB 引用模式查询性能随数据量下降 |
| 财务审计、监管严格 | 传统 RDBMS 工具链更成熟 |
| 大量复杂事务(每秒数千次跨文档事务) | MongoDB 事务性能开销大 |
| 团队 SQL 能力强、NoSQL 经验少 | 切换成本与风险 |
15 MongoDB 生产案例与故障排查
答案:
本节通过典型生产案例展示 MongoDB 在容量、性能、高可用、故障恢复等方面的实战经验与排查方法。
案例 1:WiredTiger 缓存不足导致全表扫描
现象:
某电商平台订单服务偶发查询延迟飙升至 5s+,业务高峰期出现报警。
监控数据:
- WiredTiger 缓存使用率持续 > 95%
- slow query 数量激增
- 多个查询 explain 显示 COLLSCAN
排查过程:
// 1. 确认缓存状态
db.serverStatus().wiredTiger.cache
// {
// "bytes currently in the cache": 13600000000, // 12.6GB
// "maximum bytes configured": 13800000000, // 12.9GB
// "tracked dirty bytes in the cache": 2700000000,
// "pages read into cache": 15230000, // 持续从磁盘读取
// "percentage overhead": 7
// }
// 2. 定位慢查询
db.system.profile.find({ millis: { $gt: 1000 } }).sort({ ts: -1 }).limit(10)
// 发现:db.orders.find({ status: "active" }) 频繁出现
// 3. 检查索引
db.orders.getIndexes()
// 已有:{ user_id: 1, created_at: -1 }
// 缺少:{ status: 1 }
// 4. 验证:执行计划
db.orders.find({ status: "active" }).explain("executionStats")
// winningPlan.stage = "COLLSCAN"
// totalDocsExamined = 5000000, nReturned = 80000
根因:
1. 业务变更新增"按订单状态查询"功能,但未创建 status 索引
2. status 字段低基数(仅 5 个值),命中索引时选择度低
3. 但因 orders 集合热数据 > WiredTiger cacheSize(12.9GB)
4. 缓存淘汰导致索引频繁失效
5. 查询退化为 COLLSCAN,性能急剧下降
解决方案:
// 1. 创建复合索引(ESR 原则:等值 status + 排序 created_at)
db.orders.createIndex(
{ status: 1, created_at: -1 },
{ background: true } // 后台构建,不阻塞
)
// 2. 优化查询条件(添加时间范围)
db.orders.find({
status: "active",
created_at: { $gte: ISODate("2024-01-01") }
}).sort({ created_at: -1 }).limit(20)
// 3. 扩容 WiredTiger 缓存
storage:
wiredTiger:
engineConfig:
cacheSizeGB: 16 // 从 12GB 扩容至 16GB
预防措施:
- 慢查询监控 + 自动告警(> 500ms 持续 1min)
- 索引审核流程(上线前 DBA 审核)
- Profiler 开启,记录慢查询
- 定期分析
db.collection.aggregate([{$indexStats: {}}])索引使用率
案例 2:分片集群 Jumbo Chunk 导致数据倾斜
现象:
某分片集群 Shard-1 数据量达 3TB(其他 Shard 仅 1TB),磁盘使用率告警。
查询 Shard-1 上订单的延迟显著高于其他 Shard。
排查过程:
// 1. 查看 Chunk 分布
use config
db.chunks.aggregate([
{ $group: { _id: { shard: "$shard", ns: "$ns" }, count: { $sum: 1 }, total: { $sum: "$size" } } },
{ $match: { "total": { $gt: 100 } } },
{ $sort: { "total": -1 } }
])
// 发现:shard-1 上某 Chunk 异常大(jumbo flag: true)
// 2. 查看 Jumbo Chunk
db.chunks.find({ jumbo: true }).count()
// 100+ jumbo chunks
// 3. 查看 Shard Key
db.collections.find({ _id: "mydb.orders" })
// key: { region: 1, user_id: 1 }
根因:
1. 业务初期数据按 region 范围分片
2. 单一 region("us-east")数据量激增(占总量 70%+)
3. region 内 user_id 范围分片
4. 但 us-east 内 user_id 集中在某段(业务用户群特征)
5. Shard Key 选择不当,导致数据倾斜
6. 出现 Jumbo Chunk(无法继续拆分)
解决方案:
// 步骤 1:紧急手动拆分 Jumbo Chunk
use config
// 查找 us-east 大 Chunk
db.chunks.find({
ns: "mydb.orders",
"min.region": "us-east",
jumbo: true
}).forEach(chunk => {
// 强制拆分(注意:在低峰期执行)
sh.splitAt("mydb.orders", chunk.max)
})
// 步骤 2:迁移拆分后的 Chunk
sh.startBalancer()
// 步骤 3:长期方案 - 调整 Shard Key
// 3.1 评估业务查询模式
// 3.2 业务侧实现应用层 Shard Key 写入新集群
// 3.3 双写双读切换
// 3.4 老集群下线
// 长期方案:使用 hashed 分片或更细粒度 Shard Key
// 例如:{ region: 1, user_id: "hashed" } // region 范围 + user_id 哈希
预防措施:
- 容量规划时考虑业务增长不均
- 选择 Shard Key 时充分考虑数据倾斜风险
- 监控 Chunk 分布与 Jumbo Chunk 数量
- 定期评估 Shard Key 设计
案例 3:Oplog 窗口不足导致 Change Stream 失效
现象:
某团队使用 Change Stream 同步订单数据到 Kafka,出现"resume token 失效"错误。
Change Stream 自动断开,应用程序告警。
排查过程:
// 1. 查看 oplog 状态
rs.printReplicationInfo()
// oplog 起始与结束时间窗口
// 2. 计算 oplog window
use local
db.oplog.rs.stats()
// {
// "maxSize": 51200000000, // 50GB
// "size": 51200000000,
// "count": 15000000,
// ...
// }
// 3. 查看 oplog window
db.adminCommand({ replSetGetStatus: 1 }).members.forEach(m => {
print(m.name, m.optimeDate)
})
// 输出:mongo-1 2024-01-15 10:00:00 mongo-2 2024-01-15 09:55:00
// oplog window = 5min(过短)
// 4. 查看 oplog 写入速率
db.oplog.rs.aggregate([
{ $group: { _id: null, avg_size: { $avg: "$size" } } }
])
根因:
1. oplogSizeMB 默认是 5% 磁盘空间
2. 业务高写入速率(10万 ops/s)导致 oplog 快速填满
3. Secondary 节点追赶不及时,oplog window 缩短
4. Change Stream 消费速率跟不上 oplog 产生速率
5. resume token 对应的 oplog 已被覆盖 → Change Stream 失效
解决方案:
// 1. 紧急:调整 oplog 大小(需重启)
// mongod.conf
replication:
oplogSizeMB: 102400 // 100GB(从 50GB 扩容)
// 重启 mongod(先 Secondary,再 Primary)
// 注意:oplogSizeMB 调整后需要重启生效
// 2. 优化 Change Stream 消费
// 2.1 增加消费者并发
changeStream.on("change", async (change) => {
// 异步处理
await processChangeAsync(change)
})
// 2.2 批量处理
let buffer = []
changeStream.on("change", (change) => {
buffer.push(change)
if (buffer.length >= 100) {
processBatch(buffer)
buffer = []
}
})
// 3. 监控告警
// oplog window < 1h 触发告警
// Change Stream 断开立即告警
预防措施:
- 监控 oplog window(
replSetGetStatus) - 监控 Change Stream lag
- 设置合理的 oplogSizeMB
- Change Stream 消费端实现断点续传 + 死信队列
案例 4:分片集群 Mongos 路由缓存不一致导致查询错误
现象:
业务反馈部分查询返回空数据,但实际数据存在。
多次重试后查询成功,问题偶发。
排查过程:
// 1. 检查 Balancer 状态
sh.status()
// 显示最近一次 Chunk 迁移
// 2. 查看 mongos 日志
// 出现 "could not find chunk" 错误
// 3. 查看 mongos 缓存
db.adminCommand({ getParameter: 1, userCacheInvalidationIntervalSecs: 1 })
// mongos 缓存更新默认 1s,业务端查询 < 1s 命中陈旧路由
根因:
1. Balancer 正在迁移 Chunk
2. mongos 缓存的路由信息未及时更新
3. 查询命中陈旧路由,定位到错误 Shard
4. 查询返回空或文档不存在
解决方案:
// 1. 临时 flush mongos 缓存(不重启)
db.adminCommand({ flushRouterConfig: 1 })
// 2. 优化 Balancer 时间窗口(业务低峰期迁移)
use config
db.settings.updateOne(
{ _id: "balancer" },
{ $set: { activeWindow: { start: "02:00", stop: "06:00" } } }
)
// 3. 应用层重试机制
async function findWithRetry(collection, filter, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
return await collection.find(filter).toArray()
} catch (err) {
if (err.codeName === "StaleConfig" && i < maxRetries - 1) {
await sleep(100) // 短暂等待
continue
}
throw err
}
}
}
// 4. 监控告警
// - StaleConfig 错误率
// - 路由缓存命中率
// - Balancer 迁移频率
预防措施:
- 业务低峰期执行 Balancer
- 应用层实现 StaleConfig 错误重试
- 监控 Chunk 迁移频率与持续时间
- 评估是否需要拆分大集合减少迁移频率
案例 5:副本集 Primary 频繁切换(Split-Brain 防护)
现象:
某 3 节点复制集在网络抖动时出现多次 Primary 切换,每次切换导致短暂写入失败。
排查过程:
// 1. 查看复制集状态历史
db.adminCommand({ replSetGetStatus: 1 }).members.forEach(m => {
print(m.name, "electionDate:", m.electionDate, "state:", m.stateStr)
})
// 2. 查看 oplog 窗口
rs.printReplicationInfo()
// 3. 查看节点日志
// 出现 "Stepped down" / "Heartbeat failure" / "Election"
根因:
1. 节点分布在 3 个 AZ,AZ 间网络抖动
2. 节点间心跳延迟 > electionTimeoutMillis(10s)
3. 触发不必要的选举
4. 新 Primary 提升后,旧 Primary 网络恢复又重新加入
5. 多次切换导致服务抖动
解决方案:
// 1. 调整选举参数
cfg = rs.conf()
cfg.settings = {
electionTimeoutMillis: 20000, // 增加到 20s(容忍网络抖动)
heartbeatIntervalMillis: 2000,
catchUpTimeoutMillis: 30000
}
rs.reconfig(cfg)
// 2. 优化节点分布
// - Primary 与多数派节点在同 AZ
// - 少数派节点用于灾备(Priority 0)
cfg.members = [
{ _id: 0, host: "mongo-az-a-1:27017", priority: 2 }, // 主 AZ 高优先级
{ _id: 1, host: "mongo-az-a-2:27017", priority: 2 },
{ _id: 2, host: "mongo-az-b-1:27017", priority: 1 }, // 备 AZ
{ _id: 3, host: "mongo-az-c-1:27017", priority: 0, votes: 0 } // DR
]
rs.reconfig(cfg)
// 3. 监控告警
// - Primary 切换次数(异常切换告警)
// - 心跳延迟
// - 选举耗时
预防措施:
- 节点分布:主节点与多数派在同一 AZ
- 调整 electionTimeoutMillis 容忍网络抖动
- 监控选举次数与频率
- 定期演练 Primary 切换(验证 Failover 流程)
生产运维 Checklist:
| 类别 | 检查项 | 频率 |
|---|---|---|
| 数据安全 | 备份完整性、PITR 可恢复性 | 每日 |
| 高可用 | Primary/Secondary 健康、心跳、选举 | 实时 |
| 性能 | 慢查询、缓存命中率、QPS/TPS | 实时 |
| 容量 | 磁盘使用率、文档数量、索引大小 | 每日 |
| 变更 | 索引变更、Schema 变更、配置变更 | 每次变更 |
| 安全 | 鉴权、加密、审计日志、网络隔离 | 每周 |
| 升级 | 补丁升级、版本升级、兼容性测试 | 按需 |
| 演练 | Failover、PITR 恢复、备份恢复 | 每月 |