#!/usr/bin/env bash # ============================================================================= # 系统业务库备份脚本 (backup-system-db.sh) # # 作用: # 1. 按环境 (dev/test/main) 定位 Docker 容器与数据库; # 2. 采用 pg_dump -Fc 导出系统业务库(含 task_runs、工作流治理等); # 3. 生成 manifest.json 记录 RPO 基准元数据与 SHA256。 # # 用法: # ./scripts/backup-system-db.sh <目标备份目录> # ============================================================================= set -eo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" show_help() { cat < <目标备份目录> 示例: $0 dev ./backups/system_dev_20260912 $0 test /tmp/agent_system_test_backup EOF exit 0 } if [[ $# -lt 2 ]] || [[ "$1" == "--help" ]] || [[ "$1" == "-h" ]]; then show_help fi ENV_NAME="$1" TARGET_DIR="$2" case "${ENV_NAME}" in dev) CONTAINER_NAME="shuimu-context-pg-dev" DB_NAME="agent_system_dev" DB_USER="app" DB_PASS="dev_secret_db_pass" ;; test) CONTAINER_NAME="shuimu-system-pg-test" DB_NAME="agent_system_test" DB_USER="app" DB_PASS="test_secret_db_pass" ;; main) CONTAINER_NAME="shuimu-system-pg-main" DB_NAME="agent_system_main" DB_USER="app" DB_PASS="prod_highly_secure_db_pass" ;; *) echo "[错误] 未知环境: ${ENV_NAME}(仅支持 dev/test/main)" exit 1 ;; esac mkdir -p "${TARGET_DIR}" TARGET_DIR="$(cd "${TARGET_DIR}" && pwd)" if ! command -v docker >/dev/null 2>&1; then echo "[错误] 未找到 docker 命令" exit 1 fi if ! docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then echo "[警告] 容器 ${CONTAINER_NAME} 未运行,尝试启动 ${ENV_NAME} 环境..." if [[ -f "${ROOT_DIR}/scripts/env-service.sh" ]]; then "${ROOT_DIR}/scripts/env-service.sh" "${ENV_NAME}" start else docker compose -f "${ROOT_DIR}/docker-compose.${ENV_NAME}.yml" up -d system-db fi fi echo "[等待] 检查 ${ENV_NAME} 系统库连接..." for i in {1..30}; do if docker exec -i "${CONTAINER_NAME}" pg_isready -U "${DB_USER}" -d "${DB_NAME}" >/dev/null 2>&1; then break fi sleep 1 if [ "$i" -eq 30 ]; then echo "[错误] 数据库连接超时" exit 1 fi done echo "[统计] 采集 ${ENV_NAME} 系统库指标..." TASK_RUNS=$(docker exec -i "${CONTAINER_NAME}" psql -U "${DB_USER}" -d "${DB_NAME}" -t -A -c \ "SELECT count(*) FROM task_runs;" 2>/dev/null || echo "0") PROJECTS=$(docker exec -i "${CONTAINER_NAME}" psql -U "${DB_USER}" -d "${DB_NAME}" -t -A -c \ "SELECT count(*) FROM projects;" 2>/dev/null || echo "0") DB_SIZE=$(docker exec -i "${CONTAINER_NAME}" psql -U "${DB_USER}" -d "${DB_NAME}" -t -A -c \ "SELECT pg_size_pretty(pg_database_size('${DB_NAME}'));" 2>/dev/null || echo "0 MB") GOOSE_VER=$(docker exec -i "${CONTAINER_NAME}" psql -U "${DB_USER}" -d "${DB_NAME}" -t -A -c \ "SELECT version_id FROM goose_db_version ORDER BY id DESC LIMIT 1;" 2>/dev/null || echo "0") DUMP_FILENAME="system_db_${ENV_NAME}.dump" TMP_CONTAINER_DUMP="/tmp/${DUMP_FILENAME}" DEST_DUMP_PATH="${TARGET_DIR}/${DUMP_FILENAME}" echo "[导出] pg_dump -Fc ${DB_NAME} ..." START_TIME=$(date +%s) docker exec "${CONTAINER_NAME}" pg_dump -U "${DB_USER}" -d "${DB_NAME}" -Fc -Z 6 -f "${TMP_CONTAINER_DUMP}" docker cp "${CONTAINER_NAME}:${TMP_CONTAINER_DUMP}" "${DEST_DUMP_PATH}" docker exec "${CONTAINER_NAME}" rm -f "${TMP_CONTAINER_DUMP}" DUMP_DURATION=$(( $(date +%s) - START_TIME )) DUMP_SIZE_HUMAN=$(ls -lh "${DEST_DUMP_PATH}" | awk '{print $5}') DUMP_SIZE_BYTES=$(stat -c "%s" "${DEST_DUMP_PATH}" 2>/dev/null || stat -f "%z" "${DEST_DUMP_PATH}" 2>/dev/null || echo "0") if command -v sha256sum >/dev/null 2>&1; then DUMP_SHA256=$(sha256sum "${DEST_DUMP_PATH}" | awk '{print $1}') elif command -v shasum >/dev/null 2>&1; then DUMP_SHA256=$(shasum -a 256 "${DEST_DUMP_PATH}" | awk '{print $1}') else DUMP_SHA256="unavailable" fi NOW_ISO=$(date -u +"%Y-%m-%dT%H:%M:%SZ") MANIFEST_FILE="${TARGET_DIR}/manifest.json" cat < "${MANIFEST_FILE}" { "manifest_version": "1.0", "created_at": "${NOW_ISO}", "environment": "${ENV_NAME}", "rpo_target_minutes": 60, "database": { "container_name": "${CONTAINER_NAME}", "database_name": "${DB_NAME}", "user": "${DB_USER}", "dump_file": "${DUMP_FILENAME}", "dump_sha256": "${DUMP_SHA256}", "dump_size_bytes": ${DUMP_SIZE_BYTES}, "dump_size_human": "${DUMP_SIZE_HUMAN}", "goose_version": ${GOOSE_VER:-0}, "task_runs_count": ${TASK_RUNS:-0}, "projects_count": ${PROJECTS:-0}, "database_size": "${DB_SIZE}" } } EOF echo "============================================================" echo ">>> [SUCCESS] 系统库备份完成 (${ENV_NAME})" echo ">>> Dump: ${DEST_DUMP_PATH} (${DUMP_SIZE_HUMAN}, ${DUMP_DURATION}s)" echo ">>> Manifest: ${MANIFEST_FILE}" echo ">>> 恢复演练: ./scripts/restore-system-db-drill.sh \"${TARGET_DIR}\"" echo "============================================================"