复制能让另一个成员接替故障节点,却会忠实复制误删除和错误更新。备份保存的是可回到过去的恢复材料;只有把材料恢复出来并通过验收,才算拥有可用的备份。本节会把纸舟书店归档,恢复到另一个数据库,并检查文档、索引和验证规则。
这三个词经常被放在一起,但用途不同:
mongoexport 生成 JSON 并不等于完整备份。订单中的日期、Decimal128、ObjectId、索引和 schema validation 都需要额外处理。恢复 MongoDB 数据库时,应优先使用与目标相符的备份产品或 Database Tools。
MongoDB Database Tools 独立于服务器发布。课程使用的镜像中可以分别查看版本:
docker exec paperboat-mongo mongod --version | head -n 1
docker exec paperboat-mongo mongodump --version | head -n 1db version v8.0.26
mongodump version: 100.17.0具体补丁号会随镜像更新时间变化。关键是记录服务器、工具、备份时间、来源拓扑和恢复要求,而不是假设所有版本都能任意互换。官方说明建议让恢复目标使用与来源相同的主版本或相同的 feature compatibility version。
两个指标能把“多备份一点”变成可验证要求:
小型项目可以从定时逻辑备份开始。订单量和 RPO 要求提高后,应评估 Atlas 连续备份、文件系统快照、存储快照或能提供时间点恢复的方案。选择工具之前,先写清 RPO、RTO、数据规模、拓扑和合规要求。
mongodump 会读取业务数据,也会消耗 CPU、磁盘和网络。不要在高峰期未经评估地对大型生产库运行,更不要把它当作任何规模下都合适的唯一方案。
本节使用三个选项:
--archive 把多个 BSON 与元数据流装进一个文件,便于传输。--gzip 压缩归档,恢复时也必须带上 --gzip。备份文件可能包含全部业务数据,应按敏感数据管理:限制读取权限、在传输和静态存储时加密、设置保留周期,并让删除策略覆盖旧副本。
先查看即将备份的文档数量,建立恢复对照。下面只读取三个核心集合:
docker exec paperboat-mongo mongosh --quiet --eval '
const d = db.getSiblingDB("bookstore");
printjson({
books: d.books.countDocuments({}),
customers: d.customers.countDocuments({}),
orders: d.orders.countDocuments({})
})
'在课程数据集上,结果是:
{
books: 4,
customers: 4,
orders: 7
}接着执行 mongodump。归档先写入容器内的 /tmp,避免不同镜像用户对共享目录权限不一致:
docker exec paperboat-mongo sh -lc '
set -eu
rm -f /tmp/bookstore.archive
mongodump \
--uri="mongodb://127.0.0.1:27017/?directConnection=true&replicaSet=rs0" \
--db=bookstore \
--archive=/tmp/bookstore.archive \
--gzip
test -s /tmp/bookstore.archive
ls -lh /tmp/bookstore.archive
'set -e 让任意命令失败时立即返回非零状态;test -s 要求归档存在且不为空。不要为了输出整洁而先加 --quiet,否则权限或路径错误更容易被忽略。
mongodump 会列出正在写入的集合,并以类似信息结束:
writing bookstore.books to archive '/tmp/bookstore.archive'
writing bookstore.customers to archive '/tmp/bookstore.archive'
writing bookstore.orders to archive '/tmp/bookstore.archive'
done dumping bookstore.books (4 documents)
done dumping bookstore.customers (4 documents)
done dumping bookstore.orders (7 documents)
-rw-r--r-- 1 root root 3.8K ... /tmp/bookstore.archive文件大小会因索引、验证规则、压缩版本和文档内容变化,不应把 3.8K 当成固定验收值。真正的最低门槛是:命令退出码为零,而且归档非空。
现在把归档复制到项目的 backups 目录。文件名带 UTC 时间,避免新备份无意覆盖旧备份:
mkdir -p backups
backup_file="backups/bookstore-$(date -u +%Y%m%dT%H%M%SZ).archive.gz"
docker cp paperboat-mongo:/tmp/bookstore.archive "$backup_file"
printf '%s\n' "$backup_file" > backups/latest-course-backup.txtSuccessfully copied 5.63kB to .../backups/bookstore-20260715T083000Z.archive.gzlatest-course-backup.txt 记录本次课程归档的精确相对路径。这样即使关闭终端,恢复步骤也不依赖只存在于某个 shell 进程中的变量。
再计算摘要并保存在相邻文件中:
if command -v shasum >/dev/null 2>&1; then
shasum -a 256 "$backup_file" | tee "$backup_file.sha256"
else
sha256sum "$backup_file" | tee "$backup_file.sha256"
fi9f84b8... backups/bookstore-20260715T083000Z.archive.gz先列出文件,再让 shasum 重新计算并比对:
ls -lh "$backup_file" "$backup_file.sha256"-rw-r--r-- ... bookstore-20260715T083000Z.archive.gz
-rw-r--r-- ... bookstore-20260715T083000Z.archive.gz.sha256if command -v shasum >/dev/null 2>&1; then
shasum -a 256 -c "$backup_file.sha256"
else
sha256sum -c "$backup_file.sha256"
fibackups/bookstore-20260715T083000Z.archive.gz: OKOK 只表示当前文件与生成摘要时相同。macOS 通常提供 shasum,Linux 与 WSL 常见的是 sha256sum,条件分支会选择实际存在的工具。若攻击者能同时替换归档和摘要,普通哈希无法证明来源;更严格的流程需要受保护的签名、不可变存储和访问审计。
第一次验证备份时,不要直接覆盖唯一的 bookstore。我们使用命名空间映射,把:
bookstore.books → bookstore_restore.books
bookstore.customers → bookstore_restore.customers
bookstore.orders → bookstore_restore.orders这样可以对照源和恢复结果。--drop 只删除目标归档中将要恢复的集合,避免上一次演练残留数据让计数虚高。
先从指针文件重新读取归档路径,并验证它仍是课程约定的文件名且文件非空,再复制回容器。这段命令可以在新的终端会话中独立执行:
backup_file="$(cat backups/latest-course-backup.txt)"
case "$backup_file" in
backups/bookstore-*.archive.gz) ;;
*) echo "归档路径不符合课程约定" >&2; exit 1 ;;
esac
test -s "$backup_file"
test -s "$backup_file.sha256"
docker cp "$backup_file" paperboat-mongo:/tmp/bookstore-restore.archiveSuccessfully copied 5.63kB to paperboat-mongo:/tmp/bookstore-restore.archive执行恢复,并把源命名空间重写到 bookstore_restore:
docker exec paperboat-mongo mongorestore \
--uri="mongodb://127.0.0.1:27017/?directConnection=true&replicaSet=rs0" \
--archive=/tmp/bookstore-restore.archive \
--gzip \
--nsFrom='bookstore.*' \
--nsTo='bookstore_restore.*' \
--drop关键输出如下:
restoring bookstore_restore.books from archive
restoring bookstore_restore.customers from archive
restoring bookstore_restore.orders from archive
restoring indexes for collection bookstore_restore.books
restoring indexes for collection bookstore_restore.orders
15 document(s) restored successfully. 0 document(s) failed to restore.15 正好等于基线中的 4 + 4 + 7,0 failed 表示工具没有报告文档恢复失败。但这仍不是最终验收:我们还要从数据库中读取恢复结果。
一次有意义的恢复检查应覆盖:
只看 mongorestore 的退出码,无法发现备份时间不对、缺少业务集合或恢复了错误环境的数据。
下面同时读取源库与恢复库的数量:
docker exec paperboat-mongo mongosh --quiet --eval '
const source = db.getSiblingDB("bookstore");
const restored = db.getSiblingDB("bookstore_restore");
printjson({
source: {
books: source.books.countDocuments({}),
customers: source.customers.countDocuments({}),
orders: source.orders.countDocuments({})
},
restored: {
books: restored.books.countDocuments({}),
customers: restored.customers.countDocuments({}),
orders: restored.orders.countDocuments({})
}
})
'{
source: { books: 4, customers: 4, orders: 7 },
restored: { books: 4, customers: 4, orders: 7 }
}接着检查索引名称和 orders 的 validator:
docker exec paperboat-mongo mongosh --quiet --eval '
const d = db.getSiblingDB("bookstore_restore");
printjson({
bookIndexes: d.books.getIndexes().map(i => i.name),
customerIndexes: d.customers.getIndexes().map(i => i.name),
orderIndexes: d.orders.getIndexes().map(i => i.name),
hasOrderValidator:
Boolean(d.getCollectionInfos({ name: "orders" })[0]?.options?.validator)
})
'{
bookIndexes: [
'_id_',
'category_1_price_1',
'published_1_category_1_price_1_id_1',
'published_1_price_1_id_1'
],
customerIndexes: [
'_id_',
'email_unique'
],
orderIndexes: [
'_id_'
],
hasOrderValidator: true
}索引名称取决于前面章节实际创建的索引。验收时应把期望清单放进版本控制,并检查关键唯一索引、复合索引和 validator,而不只比较“索引数量大于零”。
最后抽查一条订单的金额不变量:
docker exec paperboat-mongo mongosh --quiet --eval '
const order = db.getSiblingDB("bookstore_restore").orders.findOne(
{ _id: "order-1001" },
{ _id: 1, total: 1, status: 1, "items.quantity": 1 }
);
printjson(order)
'输出应包含目标订单、状态和条目数量。实际项目还可以重新计算 items 小计、对账库存,或让 API 指向恢复库运行一组只读冒烟检查。
对正在写入的副本集,连续读取多个集合时,数据可能在备份过程中变化。mongodump --oplog 可以在执行完整副本集成员备份时同时捕获期间的 oplog 变化,恢复时配合 --oplogReplay 获得更一致的时间点。
它有重要限制:--oplog 不能与只选择某个数据库或集合的选项随意组合,恢复工具版本和服务器版本也要匹配。大型生产系统还要考虑负载、备份窗口、分片集群协调和时间点恢复,不能把本节的单库归档直接放大成通用生产方案。
如果把容器外目录或命名卷挂到 /backup,容器中的 mongodump 进程可能没有写权限。最危险的处理方式是:
mongodump --quiet ... || true它同时隐藏错误输出和退出状态,后续流程可能把空文件上传成“成功备份”。更可靠的脚本应满足:
set -euo pipefail 或等价的错误处理。mongodump 的退出码。工具安静并不等于任务成功。备份流程应该主动失败,并把失败暴露给监控。
确认验收记录已经保存后,删除恢复数据库和容器内临时文件:
docker exec paperboat-mongo mongosh --quiet --eval '
printjson(db.getSiblingDB("bookstore_restore").dropDatabase())
'{ ok: 1, dropped: 'bookstore_restore' }docker exec paperboat-mongo rm -f \
/tmp/bookstore.archive \
/tmp/bookstore-restore.archive如果这些归档只为练习,继续删除 backups 中刚生成的文件;真实备份则应转移到受控、加密并有保留策略的存储,而不是长期留在项目目录。
docker exec paperboat-mongo mongosh --quiet --eval '
print(db.getMongo().getDBNames().includes("bookstore_restore"))
'falsefalse 表示恢复演练没有留下额外数据库,原始 bookstore 仍然存在。
这一节的完成标准不是“生成了一个文件”,而是归档非空、摘要可校验、15 个文档恢复成功、三个集合数量吻合,并确认关键索引与 validator 一起恢复。