PayrollManagementSystem/scripts/backup_incremental.sh
liuwenxin 35fe37fb0c feat(ops): 增量备份——binlog 归档脚本与全量位点记录(#19 答复)
- backup_incremental.sh:归档已写完 binlog(tar 拷出+gzip),状态文件对齐序号,有更新才产出,保留 30 天
- backup_full.sh:补 .pos 位点文件(SHOW MASTER STATUS),增量重放的起点锚
- 手册新增增量章节:恢复顺序=全量→binlog 重放,含 mysqlbinlog 缺席镜像的说明

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-19 11:22:39 +08:00

92 lines
3.4 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# 衍水工资系统 —— MySQL 增量备份(binlog 归档)
#
# 口径(澄清 #19 业务答复 2026-10-19):每日全量 + 有更新时增量补充备份。
# 增量 = 归档 MySQL binlog:自上次归档之后产生的新 binlog 文件逐个拷出压缩;
# 没有新变更时不产出文件(「有更新时」才补充)。
#
# 恢复顺序:最新全量(backup_full.sh 产物)→ 依次重放其后的 binlog 增量包
# (用 mysqlbinlog 重放,见 docs/备份恢复手册.md 增量章节)。
#
# 实现说明:mysql:8.0 官方镜像不含 mysqlbinlog 客户端,故不走
# --read-from-remote-server,直接从数据目录 tar 拷出原始 binlog 文件;
# 只归档「已写完」的文件(SHOW BINARY LOGS 列表最后一个为当前写入中,
# 留到下次收取,配合 30 天 binlog 过期窗口余量充足)。
#
# 用法:在项目根目录执行 bash scripts/backup_incremental.sh
# 产物:db/backups/binlog_NNNNNN.gz(NNNNNN 为 binlog 序号),保留 30 天。
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT"
if [[ -f docker/.env ]]; then
# shellcheck disable=SC1091
source docker/.env
else
echo "ERROR: docker/.env 不存在(请先创建 docker/.env 并填入 MySQL 凭据)" >&2
exit 1
fi
: "${MYSQL_DATABASE:?docker/.env 缺 MYSQL_DATABASE}"
BACKUP_DIR="$ROOT/db/backups"
STATE_FILE="$BACKUP_DIR/.incremental_state"
mkdir -p "$BACKUP_DIR"
# 容器名固定(compose 显式 container_name),与 docker/docker-compose.dev.yml 保持一致
CONTAINER="ys-mysql-dev"
if ! docker inspect "$CONTAINER" >/dev/null 2>&1; then
echo "ERROR: 容器 $CONTAINER 未运行(先 docker compose up -d)" >&2
exit 1
fi
# 列出 binlog:文件名 序号 大小(mysql -N 无表头,直接出数据行)
mapfile -t BINLOGS < <(
docker exec "$CONTAINER" sh -c \
'mysql -uroot -p"$MYSQL_ROOT_PASSWORD" -N -e "SHOW BINARY LOGS"' 2>/dev/null \
| awk '{print $1}'
)
if [[ ${#BINLOGS[@]} -eq 0 ]]; then
echo "ERROR: SHOW BINARY LOGS 无输出(binlog 未开启或凭据不对)" >&2
exit 1
fi
if [[ ${#BINLOGS[@]} -eq 1 ]]; then
echo "仅有写入中的 binlog(${BINLOGS[0]}),无已写完文件可归档"
exit 0
fi
COMPLETED=("${BINLOGS[@]:0:${#BINLOGS[@]}-1}") # 去掉写入中的最后一个
LAST="$(cat "$STATE_FILE" 2>/dev/null || echo "")"
NEW=()
for f in "${COMPLETED[@]}"; do
# 与状态文件比较序号:无状态(首次)全收;有状态只收更大的
if [[ -z "$LAST" ]] || (( ${f##*.} > ${LAST##*.} )); then
NEW+=("$f")
fi
done
if [[ ${#NEW[@]} -eq 0 ]]; then
echo "无新增变更(上次归档至 ${LAST},无需补充增量)"
exit 0
fi
STAMP="$(date +%Y%m%d_%H%M%S)"
echo "归档 ${#NEW[@]} 个 binlog(${NEW[0]} ~ ${NEW[-1]})"
for f in "${NEW[@]}"; do
OUT="$BACKUP_DIR/${f}.gz"
docker exec "$CONTAINER" sh -c "tar cf - -C /var/lib/mysql '$f'" | gzip > "$OUT"
if [[ ! -s "$OUT" ]]; then
echo "ERROR: $OUT 为空" >&2
rm -f "$OUT"
exit 1
fi
echo " $(basename "$OUT") ($(du -h "$OUT" | cut -f1))"
done
echo "$STAMP ${NEW[-1]}" > "$STATE_FILE"
# 保留 30 天(与全量一致;binlog 服务端默认 30 天过期,先于服务端消失无妨,
# 全量 + 其后增量链完整即满足恢复目标)
find "$BACKUP_DIR" -maxdepth 1 -name 'binlog.*.gz' -mtime +30 -print -delete
echo "增量归档完成,最新至 ${NEW[-1]}"