#!/usr/bin/env bash # backup-full.sh —— 每日全量备份编排脚本(02:00 运行) # 内容:MySQL xtrabackup 全量 + PostgreSQL pg_dump + Marzban SQLite # 备份 → age 加密 → 上传 S3 set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" source "${SCRIPT_DIR}/lib.sh" TIMESTAMP="$(date +%Y%m%d_%H%M%S)" START="$(timer_start)" log_info "========== 全量备份开始 ${TIMESTAMP} ==========" ERRORS=0 # ────────────────────────────────────────────────────── # 1. MySQL 全量备份(xtrabackup) # ────────────────────────────────────────────────────── mysql_full_backup() { local target="${BACKUP_DIR}/mysql/full/${TIMESTAMP}" local archive="${BACKUP_DIR}/mysql/full/${TIMESTAMP}.tar.gz" local s3_key="mysql/full/${TIMESTAMP}.tar.gz" log_info "[MySQL] 开始 xtrabackup 全量备份" mkdir -p "${target}" if ! mysql_ping; then log_warn "[MySQL] 无法连接 MySQL,跳过 MySQL 备份" return 1 fi # xtrabackup --backup:连接 MySQL 并备份 InnoDB 数据文件 xtrabackup \ --backup \ --host="${MYSQL_HOST}" \ --port="${MYSQL_PORT}" \ --user="${MYSQL_USER}" \ --password="${MYSQL_PASSWORD}" \ --target-dir="${target}" \ --compress \ --compress-threads=2 \ ${MYSQL_DATADIR:+--datadir="${MYSQL_DATADIR}"} \ 2>&1 | tee "${target}/xtrabackup-backup.log" # xtrabackup --prepare:回放 redo log,使备份一致(无需 MySQL 服务) xtrabackup \ --prepare \ --target-dir="${target}" \ 2>&1 | tee "${target}/xtrabackup-prepare.log" # 打包(xtrabackup --compress 已压缩,这里仅归档) log_info "[MySQL] 打包备份目录" tar -czf "${archive}" -C "$(dirname "${target}")" "$(basename "${target}")" # 更新 latest-full 引用(供增量备份使用) echo "${target}" > "${BACKUP_DIR}/mysql/latest-full.txt" # 清除上一次增量引用 rm -f "${BACKUP_DIR}/mysql/latest-inc.txt" # 加密 + 上传 encrypt_and_upload "${archive}" "${s3_key}" # 清理本地中间文件(保留原始目录供后续增量) rm -f "${archive}" # 清理 7 天前的全量备份目录 find "${BACKUP_DIR}/mysql/full" -maxdepth 1 -type d -mtime +7 \ -not -path "${BACKUP_DIR}/mysql/full" \ -exec rm -rf {} + 2>/dev/null || true log_ok "[MySQL] 全量备份完成: ${target}" } # ────────────────────────────────────────────────────── # 2. PostgreSQL 全量备份(pg_dump) # ────────────────────────────────────────────────────── postgres_full_backup() { log_info "[PostgreSQL] 开始 pg_dump 全量备份" if ! pg_ping; then log_warn "[PostgreSQL] 无法连接 PostgreSQL,跳过 PG 备份" return 1 fi # 支持多数据库(逗号分隔) IFS=',' read -ra DBS <<< "${PG_DATABASES:-postgres}" for DB in "${DBS[@]}"; do DB="$(echo "${DB}" | tr -d ' ')" local archive="${BACKUP_DIR}/postgres/full_${DB}_${TIMESTAMP}.pgdump" local s3_key="postgres/full_${DB}_${TIMESTAMP}.pgdump" log_info "[PostgreSQL] 备份数据库: ${DB}" PGPASSWORD="${PG_PASSWORD:-}" pg_dump \ --host="${PG_HOST}" \ --port="${PG_PORT}" \ --username="${PG_USER:-postgres}" \ --format=custom \ --compress=6 \ --no-password \ "${DB}" > "${archive}" encrypt_and_upload "${archive}" "${s3_key}" rm -f "${archive}" log_ok "[PostgreSQL] ${DB} 备份完成" done } # ────────────────────────────────────────────────────── # 3. Marzban SQLite 备份 # ────────────────────────────────────────────────────── marzban_backup() { local marzban_db="${MARZBAN_DB_PATH:-/var/lib/marzban/db.sqlite3}" local archive="${BACKUP_DIR}/marzban/marzban_${TIMESTAMP}.sqlite3.gz" local s3_key="marzban/marzban_${TIMESTAMP}.sqlite3.gz" if [ ! -f "${marzban_db}" ]; then log_warn "[Marzban] SQLite 文件不存在: ${marzban_db},跳过" return 0 fi log_info "[Marzban] 备份 SQLite: ${marzban_db}" # 使用 sqlite3 online backup,避免损坏 WAL sqlite3 "${marzban_db}" ".backup '${BACKUP_DIR}/marzban/marzban_${TIMESTAMP}.sqlite3'" gzip -9 "${BACKUP_DIR}/marzban/marzban_${TIMESTAMP}.sqlite3" encrypt_and_upload "${archive}" "${s3_key}" rm -f "${archive}" # 清理 30 天前的 SQLite 备份 find "${BACKUP_DIR}/marzban" -name "*.gz" -mtime +30 -delete 2>/dev/null || true log_ok "[Marzban] SQLite 备份完成" } # ────────────────────────────────────────────────────── # 执行 # ────────────────────────────────────────────────────── mysql_full_backup || { log_error "[MySQL] 全量备份失败"; ERRORS=$(( ERRORS + 1 )); } postgres_full_backup || { log_error "[PostgreSQL] 全量备份失败"; ERRORS=$(( ERRORS + 1 )); } marzban_backup || { log_error "[Marzban] 备份失败"; ERRORS=$(( ERRORS + 1 )); } ELAPSED=$(timer_elapsed "${START}") DURATION=$(format_duration "${ELAPSED}") if [ "${ERRORS}" -eq 0 ]; then log_ok "========== 全量备份完成,耗时 ${DURATION} ==========" notify "INFO" "全量备份成功,耗时 ${DURATION}" else log_error "========== 全量备份完成(${ERRORS} 个失败),耗时 ${DURATION} ==========" notify "ERROR" "全量备份完成,但有 ${ERRORS} 个失败,请检查日志" exit 1 fi