From 19e0596e1d2d8bd00848993c8dfd18552e3939ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A7=9A=E7=AB=A0=E6=B5=A9?= Date: Fri, 9 Oct 2026 12:25:12 +0800 Subject: [PATCH] =?UTF-8?q?fix(backup):=20=E6=95=B4=E5=BA=93=E9=80=BB?= =?UTF-8?q?=E8=BE=91=E5=A4=87=E4=BB=BD=E6=8C=89=E5=BA=93=E5=AE=B9=E9=94=99?= =?UTF-8?q?=EF=BC=8C=E9=BB=98=E8=AE=A4=E8=B7=B3=E8=BF=87=20RDS=20=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E5=BA=93=20rdsadmin?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 单库 pg_dump 失败(权限/编码/管理库)不再中止整次备份:清掉半个目录、记进 MANIFEST.failed_databases、 轮转照常执行、最后按失败情况给退出码。云主机实跑:33 秒 / 742.6 MiB / 8 库 / 失败 0。 --- docs/PRODUCTION_BACKUP_RECOVERY.md | 4 +++- scripts/logical-backup.sh | 26 +++++++++++++++++++------- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/docs/PRODUCTION_BACKUP_RECOVERY.md b/docs/PRODUCTION_BACKUP_RECOVERY.md index ff09c2a..f835ed1 100644 --- a/docs/PRODUCTION_BACKUP_RECOVERY.md +++ b/docs/PRODUCTION_BACKUP_RECOVERY.md @@ -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/-/`(云主机全量;本地镜像 PG 行 + 清单) | | 发布前硬门禁 | ✅ 已上线(`internal/releasesnapshot`):写目标库之前必须存在覆盖该库、24h 内、**PG 行与 ES 文档两层都齐备且哈希可校验**的留档,否则 `release_target_snapshot_missing`(不可重试),目标库一行不碰 | 单测 10(门禁)+ 6(发布器);`go run ./cmd/guardcheck $KB_BACKUP_ROOT/target-kb-snapshots ` 实测:两个绑定库 PASS、未绑定库 BLOCK | -| 自管整库逻辑备份 | ✅ 已建:`scripts/logical-backup.sh`(`pg_dump -Fd -j4 -Z6` 目录格式,可并行/按表恢复;按天轮转保留 7 份 + 每月一份) | `$KB_BACKUP_ROOT/rds-logical//<库>.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//<库>.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 顺序非确定),判据是清单里的 diff --git a/scripts/logical-backup.sh b/scripts/logical-backup.sh index b9c8441..c7813ba 100644 --- a/scripts/logical-backup.sh +++ b/scripts/logical-backup.sh @@ -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" \ No newline at end of file