Files
hfb_sys/scripts/archive-binlog.sh
T
2026-08-16 22:38:31 +08:00

175 lines
7.2 KiB
Bash
Executable File
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
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
}
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 "${cipher_file}" "${remote_dir}/binlog.enc"
backup_oss_upload "${work_dir}/${object_id}.enc.sha256" "${remote_dir}/binlog.enc.sha256"
# complete.env 最后上传,恢复程序只使用有该文件的 binlog。
backup_oss_upload "${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