From 35fe37fb0cdfe78926d1572e9399880968def518 Mon Sep 17 00:00:00 2001 From: liuwenxin <1009266660@qq.com> Date: Mon, 19 Oct 2026 11:22:39 +0800 Subject: [PATCH] =?UTF-8?q?feat(ops):=20=E5=A2=9E=E9=87=8F=E5=A4=87?= =?UTF-8?q?=E4=BB=BD=E2=80=94=E2=80=94binlog=20=E5=BD=92=E6=A1=A3=E8=84=9A?= =?UTF-8?q?=E6=9C=AC=E4=B8=8E=E5=85=A8=E9=87=8F=E4=BD=8D=E7=82=B9=E8=AE=B0?= =?UTF-8?q?=E5=BD=95=EF=BC=88#19=20=E7=AD=94=E5=A4=8D=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - backup_incremental.sh:归档已写完 binlog(tar 拷出+gzip),状态文件对齐序号,有更新才产出,保留 30 天 - backup_full.sh:补 .pos 位点文件(SHOW MASTER STATUS),增量重放的起点锚 - 手册新增增量章节:恢复顺序=全量→binlog 重放,含 mysqlbinlog 缺席镜像的说明 Co-Authored-By: Claude Code --- docs/备份恢复手册.md | 31 ++++++++++++ scripts/backup_full.sh | 9 ++++ scripts/backup_incremental.sh | 91 +++++++++++++++++++++++++++++++++++ 3 files changed, 131 insertions(+) create mode 100644 scripts/backup_incremental.sh diff --git a/docs/备份恢复手册.md b/docs/备份恢复手册.md index 197f4e8..0b51780 100644 --- a/docs/备份恢复手册.md +++ b/docs/备份恢复手册.md @@ -31,6 +31,36 @@ bash scripts/restore_drill.sh --drop-exec # 核对通过后再加此参数删 - 行数对比覆盖 16 张关键表(users/roles/param_*/business*/personnel/products/customers/shipments*/receipts/approval_*/system_settings) - 全部 ✓ 即备份可恢复;出现 ✗ 时临时库保留供排查 +## 二·五、增量备份(澄清 #19 答复:每日全量 + 有更新时增量补充) + +全量之间的数据变更由 **binlog 归档**覆盖(MySQL 8 默认开启 binlog,ROW 格式、服务端保留 30 天): + +```bash +bash scripts/backup_incremental.sh +``` + +- 产物:`db/backups/binlog.NNNNNN.gz`(原始 binlog 文件逐个压缩) +- **有更新时才产出**:状态文件 `db/backups/.incremental_state` 记录上次归档到的文件,无新变更时直接退出 +- 只归档「已写完」的 binlog(正在写的最后一个留到下次收取) +- 保留策略同全量:自动清理 30 天前的增量包 + +**配套**:`backup_full.sh` 每次全量会同时产出位点文件 `payroll_*.sql.gz.pos`(binlog 文件名 + 位点),标记「该全量覆盖到此」——恢复重放从位点之后开始。 + +**恢复顺序**(真出事时):最新全量 → 依次 `gunzip` 其后的增量包 → 用 `mysqlbinlog` 重放(镜像内无此客户端,宿主机或临时容器执行): + +```bash +# 解出增量包后重放(示例:从全量位点之后开始) +gunzip -c db/backups/binlog.000003.gz > /tmp/binlog.000003 +mysqlbinlog --start-position=<全量位点> /tmp/binlog.000003 | \ + docker exec -i ys-mysql-dev sh -c 'mysql -uroot -p"$MYSQL_ROOT_PASSWORD" payroll' +``` + +**建议定时**:增量与全量同节奏(每日 02:30 全量后接一次增量;变更频繁的日子可在中午加跑一次): + +```cmd +schtasks /create /tn "ys-payroll-backup-incr" /tr "\"C:\Program Files\Git\bin\bash.exe\" -lc \"bash scripts/backup_incremental.sh\"" /sc daily /st 02:50 /ru SYSTEM +``` + ## 三、生产恢复(真出事了,七步) > ⚠️ 生产恢复是**高危操作**:先把容器停了再恢复,避免写冲突。下面假设恢复到**全新空数据卷**(最干净路径)。 @@ -68,3 +98,4 @@ schtasks /create /tn "ys-payroll-health" /tr "\"C:\Program Files\Git\bin\bash.ex - 备份含**全部业务数据与参数种子**,属敏感文件——`db/backups/` 已加入 `.gitignore`,**不要**把它提交到版本库 - 恢复演练产生的 `payroll_drill_*` 库会占磁盘,核对完及时 `--drop-exec` 清理 - 备份脚本与恢复脚本都只依赖 Docker(无需宿主机装 MySQL 客户端) +- 增量重放的 `mysqlbinlog` 是唯一需要宿主机(或临时容器)具备的客户端——官方 mysql:8.0 镜像内不含;重放前务必确认 binlog 序号连续(中间断号说明有未归档文件,先跑一次增量归档) diff --git a/scripts/backup_full.sh b/scripts/backup_full.sh index aa1eead..f64a4c1 100644 --- a/scripts/backup_full.sh +++ b/scripts/backup_full.sh @@ -49,8 +49,17 @@ fi SIZE="$(du -h "$OUT" | cut -f1)" echo "备份完成:$(basename "$OUT") ($SIZE)" +# 记录 binlog 位点:增量备份(backup_incremental.sh)与恢复重放的对齐锚点—— +# 从「此位点之后」的 binlog 开始重放即为完整增量链 +docker exec "$CONTAINER" sh -c \ + 'mysql -uroot -p"$MYSQL_ROOT_PASSWORD" -N -e "SHOW MASTER STATUS"' > "${OUT}.pos" 2>/dev/null +if [[ -s "${OUT}.pos" ]]; then + echo "binlog 位点:$(awk '{print $1, "pos="$2}' "${OUT}.pos") → $(basename "$OUT").pos" +fi + # 保留 30 天 find "$BACKUP_DIR" -maxdepth 1 -name 'payroll_*.sql.gz' -mtime +30 -print -delete +find "$BACKUP_DIR" -maxdepth 1 -name 'payroll_*.pos' -mtime +30 -print -delete echo "已清理 30 天前的旧备份" echo "" diff --git a/scripts/backup_incremental.sh b/scripts/backup_incremental.sh new file mode 100644 index 0000000..795eaf3 --- /dev/null +++ b/scripts/backup_incremental.sh @@ -0,0 +1,91 @@ +#!/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]}"