备份与恢复演练
学习目标
备份数据库、媒体和部署环境,验证文件完整性,在新的 Compose 项目与卷中恢复并比对;理解备份成功与业务可恢复不是同一件事。
为什么数据库文件不是完整备份
数据库记录封面路径,图片内容在媒体卷;只备份其中一部分,恢复后会丢失关联内容。部署密钥和邮件设置也影响账号与会话,但包含秘密的配置必须限制访问,异地保存时加密。备份不能放进公开 Git 仓库。
本章采用维护窗口,停止所有业务写入后保存一致的数据库与媒体。mysqldump 的 single-transaction 适合 InnoDB 一致性快照,却不能保证与同时变化的文件系统天然同步,也不能抵消并发结构变更,因此维护窗口要覆盖其他写入者和定时任务。
具体怎么做
脚本要求 Bash、Docker Compose、Python 3.9+、gzip 和 tar,适用于 Linux/macOS 运维环境;Windows 可在 WSL 中执行。先保存脚本并检查语法,然后只在练习部署操作:
bash -n deploy/backup.sh
bash -n deploy/restore-drill.sh
docker compose stop web
bash deploy/backup.sh
docker compose up -d web
备份命令失败时也要先检查原因和写入状态,再决定恢复服务。脚本用 077 umask 建立私有目录,保存 SQL 压缩包、媒体压缩包、site.env 和 SHA256SUMS;校验和检测文件损坏,不证明备份来源可信或数据完整。
本章新增或替换的完整文件
以下路径均相对于练习项目根目录。替换同名文件的全部内容;未列出的文件保持上一章版本。
deploy/backup.sh
#!/usr/bin/env bash
set -euo pipefail
umask 077
cd "$(dirname "$0")/.."
# 一致性要求:先进入维护窗口并停止 web,禁止其他应用写这个数据库和媒体卷。
mkdir -p backups
snapshot=$(mktemp -d "backups/$(date -u +%Y%m%dT%H%M%SZ)-XXXXXX")
trap 'echo "备份失败;未完成目录:$snapshot" >&2' ERR
docker compose exec -T db sh -c 'MYSQL_PWD="$MYSQL_PASSWORD" exec mysqldump -u"$MYSQL_USER" --single-transaction --no-tablespaces --skip-lock-tables --set-gtid-purged=OFF "$MYSQL_DATABASE"' | gzip > "$snapshot/database.sql.gz"
docker compose run --rm --no-deps -T web tar -C /app/media -czf - . > "$snapshot/media.tar.gz"
cp .env "$snapshot/site.env"
gzip -t "$snapshot/database.sql.gz"
tar -tzf "$snapshot/media.tar.gz" > /dev/null
python3 - "$snapshot" <<'PYTHON'
from pathlib import Path
import hashlib, sys
def digest_file(path):
digest = hashlib.sha256()
with path.open("rb") as handle:
for block in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(block)
return digest.hexdigest()
root = Path(sys.argv[1])
lines = []
for name in ("database.sql.gz", "media.tar.gz", "site.env"):
digest = digest_file(root / name)
lines.append(f"{digest} {name}")
(root / "SHA256SUMS").write_text("\n".join(lines) + "\n")
PYTHON
printf '%s\n' "备份完成:$snapshot"
deploy/restore-drill.sh
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")/.."
snapshot=${1:?用法:bash deploy/restore-drill.sh 备份目录 zhixing-restore-唯一编号}
target=${2:?必须提供独立恢复项目名}
[[ "$target" =~ ^zhixing-restore-[a-z0-9-]+$ ]] || { echo "只允许 zhixing-restore- 前缀" >&2; exit 1; }
[[ -z "$(docker volume ls -q --filter "label=com.docker.compose.project=$target")" ]] || { echo "目标卷已存在,拒绝覆盖" >&2; exit 1; }
[[ -z "$(docker compose -p "$target" ps -aq)" ]] || { echo "目标容器已存在,拒绝覆盖" >&2; exit 1; }
python3 - "$snapshot" <<'PYTHON'
import hashlib, sys, tarfile
from pathlib import Path
def digest_file(path):
digest = hashlib.sha256()
with path.open("rb") as handle:
for block in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(block)
return digest.hexdigest()
root = Path(sys.argv[1])
expected_names = {"database.sql.gz", "media.tar.gz", "site.env"}
rows = [line.split(" ", 1) for line in (root / "SHA256SUMS").read_text().splitlines()]
assert {name for digest, name in rows} == expected_names
for digest, name in rows:
assert digest_file(root / name) == digest, name
with tarfile.open(root / "media.tar.gz") as archive:
for item in archive:
assert not item.name.startswith("/") and ".." not in Path(item.name).parts
assert item.isdir() or item.isfile(), "拒绝链接等特殊归档成员"
PYTHON
# 使用当前已配置的独立演练凭据;数据库名与备份内表一致。绝不自动覆盖本机 .env。
docker compose -p "$target" up -d --wait db
# init 是一次性容器;等待它成功完成。
docker compose -p "$target" run --rm init
gzip -dc "$snapshot/database.sql.gz" | docker compose -p "$target" exec -T db sh -c 'MYSQL_PWD="$MYSQL_PASSWORD" exec mysql -u"$MYSQL_USER" "$MYSQL_DATABASE"'
cat "$snapshot/media.tar.gz" | docker compose -p "$target" run --rm --no-deps -T web tar -C /app/media -xzf -
docker compose -p "$target" run --rm --no-deps web python manage.py migrate --check
docker compose -p "$target" run --rm --no-deps web python manage.py shell -c 'from blog.models import Post; print("恢复文章数:", Post.objects.count())'
printf '%s\n' "演练完成。保留独立卷供人工比对;确认后只清理项目 ${target}。"
如何确认成功
记下脚本输出的实际目录,例如 backups/20261101T120000Z-ABC123,不要直接复制这个示例路径。用一个从未使用的目标名执行恢复:
bash deploy/restore-drill.sh backups/实际备份目录 zhixing-restore-drill01
脚本拒绝覆盖已有目标卷和容器,只启动独立数据库与初始化服务,不发布站点端口。它校验归档路径与哈希,恢复数据库及媒体,检查迁移,并输出恢复文章数量。当前 .env 用来创建演练数据库账号,备份中的 site.env 仅保存供核对,不会自动覆盖现有环境。
继续比对原库和演练库的文章、用户、评论数量,抽样正文与图片;用 docker compose -p zhixing-restore-drill01 run --rm --no-deps web python manage.py audit_media 检查缺失文件(该命令在下一章提供)。需要浏览器访问时为演练环境另配端口,不能占用线上端口。确认完成后只清理明确的演练项目,删除卷前核对项目名。
恢复策略练习
写出你能接受的 RPO(最多丢多少时间的数据)和 RTO(多久恢复)。据此决定备份频率、保留数量、异地副本和定期演练。至少保留一个不在当前服务器上的加密副本,并监控备份失败;不要把“文件存在”当成成功条件。
常见问题
恢复演练成功后需要自动切换线上吗? 不需要,演练环境独立存在,真正切换有单独的停写和验收流程。为什么脚本不自动删除旧备份? 初学阶段先保证可恢复,保留策略应在确认异地副本和容量后制定。修改数据库密码会影响恢复吗? 数据导入和数据库连接账号是两层,演练用当前凭据,应用密钥是否保留则要按实际恢复目标判断。
官方参考
登录后可保存阅读位置、标记章节完成,并在其他设备继续学习。
登录并保存学习进度 →联系站长
微信:VIPdjango。技术问题建议先在 社区讨论 中交流,方便更多人参与和查阅。
章节讨论 0