From 016c47ba75e0bb5326ec283efde152be5907b381 Mon Sep 17 00:00:00 2001 From: hello-dd-code Date: Sat, 28 Feb 2026 16:39:53 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9EMySQL=E5=A4=87=E4=BB=BD?= =?UTF-8?q?=E6=81=A2=E5=A4=8D=E8=84=9A=E6=9C=AC=E5=B9=B6=E5=AE=8C=E6=88=90?= =?UTF-8?q?=E6=BC=94=E7=BB=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/ops/backup_recovery.md | 33 ++++++++++ .../backup_recovery_drill_2026-02-28.md | 28 +++++++++ scripts/ops/backup_mysql.sh | 61 +++++++++++++++++++ scripts/ops/restore_mysql.sh | 58 ++++++++++++++++++ 4 files changed, 180 insertions(+) create mode 100644 docs/ops/backup_recovery.md create mode 100644 docs/ops/reports/backup_recovery_drill_2026-02-28.md create mode 100755 scripts/ops/backup_mysql.sh create mode 100755 scripts/ops/restore_mysql.sh diff --git a/docs/ops/backup_recovery.md b/docs/ops/backup_recovery.md new file mode 100644 index 0000000..e23e612 --- /dev/null +++ b/docs/ops/backup_recovery.md @@ -0,0 +1,33 @@ +# 数据备份策略与恢复演练 + +对应 issue:`#12 [P1][T9] 备份策略与恢复演练` + +## 1. 备份策略 + +- 备份脚本:`scripts/ops/backup_mysql.sh` +- 备份格式:`_YYYYmmdd_HHMMSS.sql.gz` +- 默认保留:`KEEP_DAYS=7` +- 支持告警:`OPS_ALERT_WEBHOOK` + +推荐 cron(每天 02:00): + +```bash +0 2 * * * MYSQL_HOST=127.0.0.1 MYSQL_PORT=3306 MYSQL_USER=root MYSQL_PASSWORD=*** MYSQL_DATABASE=wx_service BACKUP_DIR=/var/backups/wx_service KEEP_DAYS=7 OPS_ALERT_WEBHOOK="https://example.com/webhook" /path/to/wx_service/scripts/ops/backup_mysql.sh >> /var/log/wx_service-backup.log 2>&1 +``` + +## 2. 恢复流程 + +- 恢复脚本:`scripts/ops/restore_mysql.sh` +- 为避免误操作,必须显式设置:`CONFIRM_RESTORE=1` + +示例: + +```bash +CONFIRM_RESTORE=1 MYSQL_HOST=127.0.0.1 MYSQL_PORT=3306 MYSQL_USER=root MYSQL_PASSWORD=*** MYSQL_DATABASE=wx_service /path/to/wx_service/scripts/ops/restore_mysql.sh /var/backups/wx_service/wx_service_20260228_020000.sql.gz +``` + +## 3. 演练建议 + +1. 先在演练库执行恢复,不要直接对生产库恢复。 +2. 恢复后执行关键表行数与抽样数据校验。 +3. 记录耗时、失败点、回滚步骤,形成固定 runbook。 diff --git a/docs/ops/reports/backup_recovery_drill_2026-02-28.md b/docs/ops/reports/backup_recovery_drill_2026-02-28.md new file mode 100644 index 0000000..abc9109 --- /dev/null +++ b/docs/ops/reports/backup_recovery_drill_2026-02-28.md @@ -0,0 +1,28 @@ +# 备份恢复演练记录(2026-02-28) + +对应 issue:`#12 [P1][T9] 备份策略与恢复演练` + +## 演练环境 + +- MySQL:`mysql:8.0` 临时容器 +- 数据库:`wx_service` +- 备份目录:`/tmp/wx_service_backup_drill` + +## 演练步骤 + +1. 初始化库并导入 `docs/sql/smoke.sql` +2. 插入一条样本数据到 `fa_smoke_log` +3. 执行备份脚本: + - `scripts/ops/backup_mysql.sh` +4. 清空 `fa_smoke_log` +5. 执行恢复脚本: + - `scripts/ops/restore_mysql.sh` +6. 对比恢复前后行数 + +## 演练结果 + +- 备份文件:`/tmp/wx_service_backup_drill/wx_service_20260228_163926.sql.gz` +- 清空后行数:`0` +- 恢复后行数:`1` + +结论:本次恢复演练成功,备份文件可用于恢复关键业务表数据。 diff --git a/scripts/ops/backup_mysql.sh b/scripts/ops/backup_mysql.sh new file mode 100755 index 0000000..456ac90 --- /dev/null +++ b/scripts/ops/backup_mysql.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +set -euo pipefail + +# 支持两种模式: +# 1) 本机 mysql/mysqldump: +# MYSQL_HOST MYSQL_PORT MYSQL_USER MYSQL_PASSWORD MYSQL_DATABASE +# 2) 容器内执行(推荐在无本地 mysql 客户端时使用): +# MYSQL_CONTAINER= + +MYSQL_HOST="${MYSQL_HOST:-127.0.0.1}" +MYSQL_PORT="${MYSQL_PORT:-3306}" +MYSQL_USER="${MYSQL_USER:-root}" +MYSQL_PASSWORD="${MYSQL_PASSWORD:-}" +MYSQL_DATABASE="${MYSQL_DATABASE:-wx_service}" +MYSQL_CONTAINER="${MYSQL_CONTAINER:-}" + +BACKUP_DIR="${BACKUP_DIR:-/var/backups/wx_service}" +KEEP_DAYS="${KEEP_DAYS:-7}" +OPS_ALERT_WEBHOOK="${OPS_ALERT_WEBHOOK:-}" +ALERT_TITLE="${ALERT_TITLE:-[wx_service] 数据备份失败}" + +send_alert() { + local message="$1" + if [[ -z "${OPS_ALERT_WEBHOOK}" ]]; then + echo "ALERT: ${message}" >&2 + return + fi + curl -fsS -X POST "${OPS_ALERT_WEBHOOK}" \ + -H "Content-Type: application/json" \ + -d "{\"title\":\"${ALERT_TITLE}\",\"message\":\"${message}\"}" >/dev/null || true +} + +dump_cmd() { + if [[ -n "${MYSQL_CONTAINER}" ]]; then + if [[ -n "${MYSQL_PASSWORD}" ]]; then + docker exec "${MYSQL_CONTAINER}" mysqldump -h127.0.0.1 -P"${MYSQL_PORT}" -u"${MYSQL_USER}" "-p${MYSQL_PASSWORD}" --single-transaction --quick --set-gtid-purged=OFF "${MYSQL_DATABASE}" + else + docker exec "${MYSQL_CONTAINER}" mysqldump -h127.0.0.1 -P"${MYSQL_PORT}" -u"${MYSQL_USER}" --single-transaction --quick --set-gtid-purged=OFF "${MYSQL_DATABASE}" + fi + return + fi + + if [[ -n "${MYSQL_PASSWORD}" ]]; then + mysqldump -h"${MYSQL_HOST}" -P"${MYSQL_PORT}" -u"${MYSQL_USER}" "-p${MYSQL_PASSWORD}" --single-transaction --quick --set-gtid-purged=OFF "${MYSQL_DATABASE}" + else + mysqldump -h"${MYSQL_HOST}" -P"${MYSQL_PORT}" -u"${MYSQL_USER}" --single-transaction --quick --set-gtid-purged=OFF "${MYSQL_DATABASE}" + fi +} + +mkdir -p "${BACKUP_DIR}" +timestamp="$(date +%Y%m%d_%H%M%S)" +outfile="${BACKUP_DIR}/${MYSQL_DATABASE}_${timestamp}.sql.gz" + +if ! dump_cmd | gzip -9 > "${outfile}"; then + send_alert "MySQL 备份失败,数据库=${MYSQL_DATABASE}" + exit 1 +fi + +find "${BACKUP_DIR}" -type f -name "${MYSQL_DATABASE}_*.sql.gz" -mtime +"${KEEP_DAYS}" -delete || true +echo "backup created: ${outfile}" + diff --git a/scripts/ops/restore_mysql.sh b/scripts/ops/restore_mysql.sh new file mode 100755 index 0000000..5afdb8a --- /dev/null +++ b/scripts/ops/restore_mysql.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +set -euo pipefail + +# 用法: +# CONFIRM_RESTORE=1 ./scripts/ops/restore_mysql.sh /path/to/backup.sql.gz +# +# 支持两种模式: +# 1) 本机 mysql 客户端 +# 2) MYSQL_CONTAINER=(在容器内执行 mysql) + +if [[ "${CONFIRM_RESTORE:-0}" != "1" ]]; then + echo "restore blocked: set CONFIRM_RESTORE=1 to continue" >&2 + exit 1 +fi + +if [[ $# -lt 1 ]]; then + echo "usage: $0 " >&2 + exit 1 +fi + +backup_file="$1" +if [[ ! -f "${backup_file}" ]]; then + echo "backup file not found: ${backup_file}" >&2 + exit 1 +fi + +MYSQL_HOST="${MYSQL_HOST:-127.0.0.1}" +MYSQL_PORT="${MYSQL_PORT:-3306}" +MYSQL_USER="${MYSQL_USER:-root}" +MYSQL_PASSWORD="${MYSQL_PASSWORD:-}" +MYSQL_DATABASE="${MYSQL_DATABASE:-wx_service}" +MYSQL_CONTAINER="${MYSQL_CONTAINER:-}" + +run_mysql() { + if [[ -n "${MYSQL_CONTAINER}" ]]; then + if [[ -n "${MYSQL_PASSWORD}" ]]; then + docker exec -i "${MYSQL_CONTAINER}" mysql -h127.0.0.1 -P"${MYSQL_PORT}" -u"${MYSQL_USER}" "-p${MYSQL_PASSWORD}" "${MYSQL_DATABASE}" + else + docker exec -i "${MYSQL_CONTAINER}" mysql -h127.0.0.1 -P"${MYSQL_PORT}" -u"${MYSQL_USER}" "${MYSQL_DATABASE}" + fi + return + fi + + if [[ -n "${MYSQL_PASSWORD}" ]]; then + mysql -h"${MYSQL_HOST}" -P"${MYSQL_PORT}" -u"${MYSQL_USER}" "-p${MYSQL_PASSWORD}" "${MYSQL_DATABASE}" + else + mysql -h"${MYSQL_HOST}" -P"${MYSQL_PORT}" -u"${MYSQL_USER}" "${MYSQL_DATABASE}" + fi +} + +if [[ "${backup_file}" == *.gz ]]; then + zcat "${backup_file}" | run_mysql +else + cat "${backup_file}" | run_mysql +fi + +echo "restore completed: ${backup_file}" +