重写在线备份链路

This commit is contained in:
yml2213
2026-08-16 22:32:07 +08:00
parent f48da14ed2
commit 7438fb6725
10 changed files with 999 additions and 608 deletions
+175
View File
@@ -0,0 +1,175 @@
#!/usr/bin/env bash
set -euo pipefail
umask 077
# 分钟级 binlog 归档。每次轮转后只归档已关闭文件;每个对象以
# server_uuid、原始文件名和原始 sha256 命名,避免实例重建后的编号碰撞。
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=backup-common.sh
source "${SCRIPT_DIR}/backup-common.sh"
MYSQL_DATA_DIR='/var/lib/mysql'
work_dir=''
cleanup() {
if [[ -n "${work_dir}" && -d "${work_dir}" ]]; then
rm -rf -- "${work_dir}"
fi
}
trap cleanup EXIT
require_archive_config() {
backup_require_env BACKUP_DIR >/dev/null
backup_require_env BACKUP_OSS_URI >/dev/null
backup_require_env BACKUP_PASSPHRASE >/dev/null
backup_require_env BACKUP_KMS_KEY_ID >/dev/null
}
closed_binlogs() {
local mysql_cid="$1"
docker exec "${mysql_cid}" sh -c \
'MYSQL_PWD="$MYSQL_ROOT_PASSWORD" mysql -uroot --protocol=socket -N -B -e "SHOW BINARY LOGS;"' \
2>/dev/null | awk 'NR > 1 { print previous } { previous = $1 } END { }'
}
state_last_index() {
local state_dir="$1"
local value
[[ -f "${state_dir}/last-archived.env" ]] || return
value="$(awk -F= '$1 == "binlog_index" { print $2; exit }' "${state_dir}/last-archived.env")"
[[ "${value}" =~ ^[0-9]+$ ]] && printf '%s' "${value}"
}
write_state() {
local state_dir="$1" filename="$2" index="$3" raw_sha="$4" cipher_sha="$5" remote_dir="$6"
{
printf 'filename=%s\n' "${filename}"
printf 'binlog_index=%s\n' "${index}"
printf 'raw_sha256=%s\n' "${raw_sha}"
printf 'cipher_sha256=%s\n' "${cipher_sha}"
printf 'remote_dir=%s\n' "${remote_dir}"
} > "${state_dir}/${filename}.${raw_sha}.state"
cp "${state_dir}/${filename}.${raw_sha}.state" "${state_dir}/last-archived.env"
}
archive_one() {
local mysql_cid="$1" state_dir="$2" server_uuid="$3" oss_uri="$4" filename="$5"
local source_path source_sha raw_file raw_sha cipher_file cipher_sha cipher_size binlog_index object_id remote_dir
source_path="${MYSQL_DATA_DIR}/${filename}"
if ! docker exec "${mysql_cid}" test -f "${source_path}" >/dev/null 2>&1; then
backup_error "待归档 binlog 已从 MySQL 数据目录消失:${filename}PITR 链路已断裂"
exit 1
fi
raw_file="${work_dir}/${filename}"
docker cp "${mysql_cid}:${source_path}" "${raw_file}"
raw_sha="$(backup_sha256 "${raw_file}")"
source_sha="$(docker exec "${mysql_cid}" sha256sum "${source_path}" | awk '{print $1}')"
[[ "${source_sha}" == "${raw_sha}" ]] || {
backup_error "binlog 拷贝校验失败:${filename}"
exit 1
}
if compgen -G "${state_dir}/${filename}.${raw_sha}.state" >/dev/null; then
return
fi
binlog_index="$(backup_binlog_index "${filename}")"
object_id="${filename}.${raw_sha}"
remote_dir="${oss_uri}/mysql/binlog/${server_uuid}/${object_id}"
cipher_file="${work_dir}/${object_id}.enc"
backup_encrypt_file "${raw_file}" "${cipher_file}"
cipher_sha="$(backup_sha256 "${cipher_file}")"
cipher_size="$(backup_file_size "${cipher_file}")"
printf '%s %s\n' "${cipher_sha}" 'binlog.enc' > "${work_dir}/${object_id}.enc.sha256"
{
printf 'format_version=1\n'
printf 'server_uuid=%s\n' "${server_uuid}"
printf 'source_binlog=%s\n' "${filename}"
printf 'binlog_index=%s\n' "${binlog_index}"
printf 'raw_sha256=%s\n' "${raw_sha}"
printf 'payload=binlog.enc\n'
printf 'payload_sha256=%s\n' "${cipher_sha}"
printf 'payload_size_bytes=%s\n' "${cipher_size}"
printf 'created_at=%s\n' "$(date -Iseconds)"
} > "${work_dir}/${object_id}.complete.env"
backup_oss_upload_kms "${cipher_file}" "${remote_dir}/binlog.enc"
backup_oss_upload_kms "${work_dir}/${object_id}.enc.sha256" "${remote_dir}/binlog.enc.sha256"
# complete.env 最后上传,恢复程序只使用有该文件的 binlog。
backup_oss_upload_kms "${work_dir}/${object_id}.complete.env" "${remote_dir}/complete.env"
write_state "${state_dir}" "${filename}" "${binlog_index}" "${raw_sha}" "${cipher_sha}" "${remote_dir}"
rm -f -- "${raw_file}" "${cipher_file}" "${work_dir}/${object_id}.enc.sha256" "${work_dir}/${object_id}.complete.env"
}
archive() {
backup_require_cmd docker
backup_require_cmd ossutil
backup_require_cmd openssl
require_archive_config
local backup_dir oss_uri mysql_cid server_uuid state_dir first_binlog last_index first_index expected_index filename index
backup_dir="$(backup_require_env BACKUP_DIR)"
backup_validate_dir "${backup_dir}"
oss_uri="$(backup_require_env BACKUP_OSS_URI)"
backup_validate_oss_uri "${oss_uri}"
backup_lock_or_exit "${backup_dir}/.binlog.lock"
mysql_cid="$(backup_mysql_container_id)"
server_uuid="$(backup_server_uuid)"
state_dir="${backup_dir}/binlog-state/${server_uuid}"
work_dir="${backup_dir}/.tmp-binlog-$$"
mkdir -p "${state_dir}" "${work_dir}"
docker exec "${mysql_cid}" sh -c \
'MYSQL_PWD="$MYSQL_ROOT_PASSWORD" mysql -uroot --protocol=socket -e "FLUSH BINARY LOGS;"' >/dev/null
first_binlog="$(docker exec "${mysql_cid}" sh -c \
'MYSQL_PWD="$MYSQL_ROOT_PASSWORD" mysql -uroot --protocol=socket -N -B -e "SHOW BINARY LOGS;"' | head -1 | awk '{print $1}')"
[[ -n "${first_binlog}" ]] || { backup_error '无法读取 binlog 列表'; exit 1; }
last_index="$(state_last_index "${state_dir}" || true)"
first_index="$(backup_binlog_index "${first_binlog}")"
if [[ -n "${last_index}" && "${first_index}" -gt $((last_index + 1)) ]]; then
backup_error "发现未归档 binlog 缺口:已归档至序号 ${last_index},当前最早为 ${first_index}"
exit 1
fi
expected_index="${last_index}"
while IFS= read -r filename; do
[[ -n "${filename}" ]] || continue
index="$(backup_binlog_index "${filename}")"
if [[ -n "${expected_index}" && "${index}" -le "${expected_index}" ]]; then
continue
fi
if [[ -n "${expected_index}" && "${index}" -ne $((expected_index + 1)) ]]; then
backup_error "发现未归档 binlog 缺口:期望序号 $((expected_index + 1)),实际为 ${index}"
exit 1
fi
archive_one "${mysql_cid}" "${state_dir}" "${server_uuid}" "${oss_uri}" "${filename}"
expected_index="${index}"
done < <(closed_binlogs "${mysql_cid}")
backup_log "binlog 归档完成,实例 ${server_uuid}"
}
verify() {
backup_require_cmd ossutil
require_archive_config
local backup_dir server_uuid state_dir state_file remote_dir
backup_dir="$(backup_require_env BACKUP_DIR)"
backup_validate_dir "${backup_dir}"
backup_lock_or_exit "${backup_dir}/.binlog.lock"
server_uuid="$(backup_server_uuid)"
state_dir="${backup_dir}/binlog-state/${server_uuid}"
[[ -d "${state_dir}" ]] || { backup_error '尚未产生 binlog 归档状态'; exit 1; }
while IFS= read -r -d '' state_file; do
remote_dir="$(awk -F= '$1 == "remote_dir" { print substr($0, index($0, "=") + 1); exit }' "${state_file}")"
[[ -n "${remote_dir}" ]] || { backup_error "归档状态不完整:${state_file}"; exit 1; }
ossutil stat "${remote_dir}/binlog.enc" >/dev/null
ossutil stat "${remote_dir}/binlog.enc.sha256" >/dev/null
ossutil stat "${remote_dir}/complete.env" >/dev/null
done < <(find "${state_dir}" -maxdepth 1 -name '*.state' -print0)
backup_log '全部本地 binlog 归档状态均有远端完整对象'
}
case "${1:-archive}" in
archive) archive ;;
verify) verify ;;
*)
printf '用法: %s {archive|verify}\n' "$0" >&2
exit 1
;;
esac