fix(backup): 整库逻辑备份按库容错,默认跳过 RDS 管理库 rdsadmin

单库 pg_dump 失败(权限/编码/管理库)不再中止整次备份:清掉半个目录、记进 MANIFEST.failed_databases、
轮转照常执行、最后按失败情况给退出码。云主机实跑:33 秒 / 742.6 MiB / 8 库 / 失败 0。
This commit is contained in:
姚章浩
2026-10-09 12:25:12 +08:00
parent a1be19bb0f
commit 19e0596e1d
2 changed files with 22 additions and 8 deletions
+3 -1
View File
@@ -9,7 +9,7 @@
|---|---|---|
| 导入前留档(PG 行 + ES 文档) | ✅ 已建:`scripts/target_kb_snapshot.sh` 云内执行,8 张表 + 该库在 ES 的全部文档(只读 scroll,gzip JSONL,按 `_id` 可重建)+ `MANIFEST.json`(行数/字节/sha256/每库 ES 文档数)+ `export.sql` + `README.md`。实测 **2 分 52 秒**(两个库 13.1 万条 ES 文档 / 692 MB,字节留云主机);`PULL_ES=1` 拉回本机则 **19 分 40 秒** | `$KB_BACKUP_ROOT/target-kb-snapshots/<UTC>-<kb8>/`(云主机全量;本地镜像 PG 行 + 清单) |
| 发布前硬门禁 | ✅ 已上线(`internal/releasesnapshot`):写目标库之前必须存在覆盖该库、24h 内、**PG 行与 ES 文档两层都齐备且哈希可校验**的留档,否则 `release_target_snapshot_missing`(不可重试),目标库一行不碰 | 单测 10(门禁)+ 6(发布器);`go run ./cmd/guardcheck $KB_BACKUP_ROOT/target-kb-snapshots <kb_id>` 实测:两个绑定库 PASS、未绑定库 BLOCK |
| 自管整库逻辑备份 | ✅ 已建:`scripts/logical-backup.sh`(`pg_dump -Fd -j4 -Z6` 目录格式,可并行/按表恢复;按天轮转保留 7 份 + 每月一份) | `$KB_BACKUP_ROOT/rds-logical/<UTC>/<库>.dir` + `MANIFEST.json` |
| 自管整库逻辑备份 | ✅ 已建并在云主机实跑:`scripts/logical-backup.sh`(`pg_dump -Fd -j4 -Z6` 目录格式,可并行/按表恢复;按天轮转保留 7 份 + 每月一份)。实测 **33 秒 / 742.6 MiB / 214 文件 / 失败 0**,覆盖该实例全部 8 个库(`weknora_test`、`weknora_prod`、`weknora_drill74`、`dobotai_prod/test`、`knowledge_adapter_prod/test`、`postgres`);RDS 管理库 `rdsadmin` 默认跳过,单库失败只记账不中止 | `$KB_BACKUP_ROOT/rds-logical/<UTC>/<库>.dir` + `MANIFEST.json`(含 `failed_databases`) |
| ES 快照 | ✅ 已建:仓库 `kb_backup`(容器内 `path.repo=/usr/share/elasticsearch/backup`,宿主 `$KB_BACKUP_ROOT/es-snapshots`)→ 首个快照 `initial-20261009t024209z` **36 秒 / 4.7 GB / 0 分片失败**;SLM `kb-hourly`(留 3 天)+ `kb-daily`(留 7 天)已各成功执行 | `GET _snapshot/kb_backup/_all`、`_slm/policy/*` |
| OSS 对象 | ✅ 目标桶 `dobot-weknora-test` 已开启**版本控制**(覆盖/删除可按 VersionId 回滚);未配跨区域复制 | `GET /?versioning` = `Enabled` |
| RDS 托管备份 | ⏳ **待控制台确认**:库里 `archive_mode=on` 但 `archive_command` 为空、`pg_stat_archiver.archived_count=0`;当前连接 `is_superuser=off`(看不到备节点)→ 系列与日志备份开关只能看控制台 | 见下节 |
@@ -82,6 +82,8 @@ docker run --rm --network host postgres:17-alpine psql "$TARGET_DB_URL" \
- **ES 快照仓与留档在同一台云主机、同一块盘**上:整机故障会一起丢。异地副本需把
`es-snapshots/` 与 `target-kb-snapshots/` 同步到 OSS(需一把可写备份桶的 AK,属授权项)。
- 自管逻辑备份**不能替代 PITR**:它只能回到"备份那一刻",且恢复要靠 `pg_restore` 手工执行。
它按库产出目录格式归档(`pg_restore -d <库> -j4 <库>.dir`,跨实例加 `--no-owner --no-acl`),
且 **同一实例上的生产库(`weknora_prod`)也在其中** —— 备份目录权限按 0700/0600 保管。
- 留档默认不含 `chunks` 行:向量在 ES 里、分块行可由源端重发;要"整库行级可离线重建"才需
`INCLUDE_CHUNKS=1`(实测本机隧道约 24 分钟、云内 44 秒内)。
- `es_documents.jsonl.gz` 的 sha256 **每次导出都不同**(scroll 顺序非确定),判据是清单里的
+19 -7
View File
@@ -21,7 +21,7 @@
# JOBS 并行度(默认 4)
# KEEP_DAILY 保留最近几份每日备份(默认 7)
# KEEP_MONTHS 额外保留最近几个自然月各一份(默认 3)
# EXCLUDE_DBS 逗号分隔的跳过列表(默认空)
# EXCLUDE_DBS 逗号分隔的跳过列表(默认 rdsadmin:RDS 管理库,普通账号导不出来)
set -euo pipefail
# 备份目录契约:由 scripts/backup_layout.sh 写到 /etc/knowledge-governance/backup.env。
@@ -39,7 +39,7 @@ KEEP_DAILY="${KEEP_DAILY:-7}"
KEEP_MONTHS="${KEEP_MONTHS:-3}"
DRY_RUN="${DRY_RUN:-0}"
DBS="${DBS:-}"
EXCLUDE_DBS="${EXCLUDE_DBS:-}"
EXCLUDE_DBS="${EXCLUDE_DBS:-rdsadmin}"
STAMP="$(date -u +%Y%m%dT%H%M%SZ)"
env_value() {
@@ -89,6 +89,7 @@ docker run --rm -e PGURL="$PGURL" "$PG_IMAGE" sh -c \
[ -s "$TARGET/globals.err" ] || rm -f "$TARGET/globals.err"
IFS=',' read -r -a DB_ARRAY <<<"$DBS"
FAILED_DBS=""
for db in "${DB_ARRAY[@]}"; do
db="$(printf '%s' "$db" | tr -d ' ')"
[ -n "$db" ] || continue
@@ -99,17 +100,23 @@ print(f"postgres://{u.quote(user)}:{u.quote(password)}@{host}:{port}/{name}")
PY
)"
echo " - $db ..."
docker run --rm -e DB_URL="$DB_URL" -v "$TARGET:/backup" "$PG_IMAGE" sh -c \
"pg_dump -Fd -j $JOBS -Z 6 --no-sync -d \"\$DB_URL\" -f /backup/${db}.dir"
# pg_dump -Fd 不接受已有目录;目录名冲突说明本次重复执行,直接报错退出
if docker run --rm -e DB_URL="$DB_URL" -v "$TARGET:/backup" "$PG_IMAGE" sh -c \
"pg_dump -Fd -j $JOBS -Z 6 --no-sync -d \"\$DB_URL\" -f /backup/${db}.dir"; then
:
else
# 一个库失败(权限/编码/管理库)不能把整次备份作废:清掉半个目录并记账
echo " 失败,跳过并记账: $db" >&2
rm -rf "$TARGET/${db}.dir"
FAILED_DBS="${FAILED_DBS}${db} "
fi
done
# dump 之后写清单与说明(先写清单,再算哈希,避免把哈希写进自己的输入)
DURATION="$(( $(date +%s) - START_SECONDS ))"
python3 - "$TARGET" "$STAMP" "$STARTED_AT" "$DURATION" "$DBS" "$PG_IMAGE" "$JOBS" <<'PY'
python3 - "$TARGET" "$STAMP" "$STARTED_AT" "$DURATION" "$DBS" "$PG_IMAGE" "$JOBS" "$FAILED_DBS" <<'PY'
import hashlib, json, os, pathlib, sys
target = pathlib.Path(sys.argv[1]); stamp, started, duration = sys.argv[2], sys.argv[3], int(sys.argv[4])
databases, image, jobs = sys.argv[5], sys.argv[6], int(sys.argv[7])
databases, image, jobs = sys.argv[5], sys.argv[6], int(sys.argv[7]); failed = sys.argv[8].split()
files = []
for path in sorted(target.rglob("*")):
if path.is_dir() or path.name in ("MANIFEST.json",):
@@ -126,6 +133,7 @@ manifest = {
"started_at": started,
"duration_seconds": duration,
"databases": [name for name in databases.split(",") if name],
"failed_databases": failed,
"format": "pg_dump -Fd (directory, parallel, zlib)",
"pg_dump_image": image,
"parallel_jobs": jobs,
@@ -187,4 +195,8 @@ for path in stamps:
print("kept:", len([p for p in stamps if p.name in keep]))
PY
if [ -n "$FAILED_DBS" ]; then
echo "done(有失败的库): $TARGET | failed: $FAILED_DBS" >&2
exit 1
fi
echo "done: $TARGET"