fix(backup): 整库逻辑备份按库容错,默认跳过 RDS 管理库 rdsadmin
单库 pg_dump 失败(权限/编码/管理库)不再中止整次备份:清掉半个目录、记进 MANIFEST.failed_databases、 轮转照常执行、最后按失败情况给退出码。云主机实跑:33 秒 / 742.6 MiB / 8 库 / 失败 0。
This commit is contained in:
@@ -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 顺序非确定),判据是清单里的
|
||||
|
||||
@@ -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"
|
||||
Reference in New Issue
Block a user