刘光辉
昨天 bb638871a7fb692d80f1b7a758f991dc0879002c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
#!/usr/bin/env bash
# ─────────────────────────────────────────────────────────────────────────────
# JNPF PostgreSQL 定时备份
#   - docker exec pg_dump 逐库导出(custom 格式 -Fc,已压缩、可 pg_restore 选择性还原)
#   - 备份产物落 volume 外的宿主目录(BACKUP_DIR),容器/卷损坏也不丢
#   - 每份导出用 pg_restore -l 校验可读性
#   - 保留最近 RETENTION_DAYS 天,更早的自动删除
#
# 用法:
#   ./backup.sh                     # 备份全部三库
#   DATABASES="jnpf_init" ./backup.sh
#
# 参数(环境变量):
#   CONTAINER        PG 容器名        默认 cx-pg-customer1(2026-08-08 客户A改名,交付环境用时显式传 CONTAINER=cx-postgres)
#   PGUSER           超级用户         默认 postgres
#   DATABASES        待备份库(空格分隔) 默认 "jnpf_init jnpf_flow jnpf_xxljob nacos_config"
#   BACKUP_DIR       宿主备份目录     默认 /data/pg-backup(务必在 volume 外)
#   RETENTION_DAYS   保留天数         默认 14
#
# ── cron 定时(现场 crontab -e,每天 02:00)─────────────────────────────────
#   0 2 * * * /opt/jnpf/offline/migration/backup.sh >> /var/log/jnpf-pg-backup.log 2>&1
#   注意:宿主文件存储(.env JNPF_DATA_DIR 下 jnpf-resources/,上传附件所在)不在 PG 里,
#   需另行纳入文件级备份(rsync/tar 均可)
#   还原示例:
#     gunzip -c jnpf_init_YYYYMMDD_HHMMSS.dump.gz > /tmp/x.dump   # 若已 gzip
#     docker exec -i cx-pg-customer1 pg_restore -U postgres -d jnpf_init --clean --if-exists < /tmp/x.dump
#   (-Fc 自身已压缩,本脚本不再额外 gzip;下方 .dump 可直接 pg_restore)
# ─────────────────────────────────────────────────────────────────────────────
set -euo pipefail
 
CONTAINER="${CONTAINER:-cx-pg-customer1}"
PGUSER="${PGUSER:-postgres}"
# nacos_config 一并备份(配置中心内容只存 PG,codex review 2026-07-17 补)
DATABASES="${DATABASES:-jnpf_init jnpf_flow jnpf_xxljob nacos_config}"
# Windows/Git Bash 护栏:默认 /data/pg-backup 会映射进 Git 安装目录(静默备份到错误位置),
# 故 MSYS 环境必须显式传 BACKUP_DIR(如 /d/pg-backup)
case "$(uname -s)" in
  MINGW*|MSYS*) [ -n "${BACKUP_DIR:-}" ] || {
    printf '\033[1;31m[backup][err]\033[0m %s\n' \
      "Windows/Git Bash 下必须显式指定 BACKUP_DIR,如:BACKUP_DIR=/d/pg-backup $0" >&2
    exit 1
  } ;;
esac
BACKUP_DIR="${BACKUP_DIR:-/data/pg-backup}"
RETENTION_DAYS="${RETENTION_DAYS:-14}"
TS="$(date +%Y%m%d_%H%M%S)"
 
log()  { printf '\033[1;34m[backup]\033[0m %s\n' "$*"; }
warn() { printf '\033[1;33m[backup][warn]\033[0m %s\n' "$*"; }
die()  { printf '\033[1;31m[backup][err]\033[0m %s\n' "$*" >&2; exit 1; }
 
docker inspect "$CONTAINER" >/dev/null 2>&1 || die "容器 $CONTAINER 不在"
mkdir -p "$BACKUP_DIR"
 
fail=0
for db in $DATABASES; do
  out="$BACKUP_DIR/${db}_${TS}.dump"
  log "pg_dump ${db} → $(basename "$out")"
  # -Fc custom 格式;失败则删残缺文件并计入失败
  if docker exec "$CONTAINER" pg_dump -U "$PGUSER" -Fc -d "$db" > "$out" 2>/dev/null; then
    # 校验:pg_restore -l 能列出目录即视为可读
    if docker exec -i "$CONTAINER" pg_restore -l < "$out" >/dev/null 2>&1; then
      log "  ✓ ${db}  $(du -h "$out" | cut -f1)  校验通过"
    else
      warn "  ✗ ${db} 导出文件 pg_restore -l 校验失败"; fail=$((fail+1))
    fi
  else
    warn "  ✗ ${db} pg_dump 失败"; rm -f "$out"; fail=$((fail+1))
  fi
done
 
log "清理 ${RETENTION_DAYS} 天前的旧备份 …"
find "$BACKUP_DIR" -maxdepth 1 -name '*.dump' -type f -mtime "+${RETENTION_DAYS}" -print -delete | while read -r f; do
  log "  删除 $(basename "$f")"
done
 
log "当前备份目录占用:$(du -sh "$BACKUP_DIR" 2>/dev/null | cut -f1)"
[ "$fail" = 0 ] || die "有 ${fail} 个库备份失败,检查日志"
log "备份完成 ✓(${BACKUP_DIR})"