Files
hfb_sys/scripts/archive-binlog.sh
T
2026-08-16 23:57:19 +08:00

218 lines
9.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 归档。每分钟检查活动 binlog 的 Position,仅在有新增已提交事务时
# 轮转;每个对象以 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 { }'
}
master_status() {
local mysql_cid="$1"
docker exec "${mysql_cid}" sh -c \
'MYSQL_PWD="$MYSQL_ROOT_PASSWORD" mysql -uroot --protocol=socket -N -B -e "SHOW MASTER STATUS;"' \
2>/dev/null | awk 'NR == 1 { print $1 "\t" $2 }'
}
state_active_binlog() {
local state_dir="$1" key="$2" value
[[ -f "${state_dir}/active-binlog.env" ]] || return
value="$(awk -F= -v key="${key}" '$1 == key { print substr($0, index($0, "=") + 1); exit }' "${state_dir}/active-binlog.env")"
printf '%s' "${value}"
}
write_active_binlog_state() {
local state_dir="$1" filename="$2" position="$3"
{
printf 'filename=%s\n' "${filename}"
printf 'position=%s\n' "${position}"
printf 'observed_at=%s\n' "$(date -Iseconds)"
} > "${state_dir}/active-binlog.env"
}
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 force="${1:-}" backup_dir oss_uri mysql_cid server_uuid state_dir first_binlog last_index first_index expected_index filename index
local current_status current_file current_pos previous_file previous_pos final_status final_file final_pos
if [[ -n "${force}" && "${force}" != '--force' ]]; then
printf '用法: %s archive [--force]\n' "$0" >&2
exit 1
fi
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}"
current_status="$(master_status "${mysql_cid}")"
IFS=$'\t' read -r current_file current_pos <<< "${current_status}"
[[ -n "${current_file}" && "${current_pos}" =~ ^[0-9]+$ ]] || { backup_error '无法读取当前 binlog 位点'; exit 1; }
backup_binlog_index "${current_file}" >/dev/null
previous_file="$(state_active_binlog "${state_dir}" filename || true)"
previous_pos="$(state_active_binlog "${state_dir}" position || true)"
if [[ "${force}" == '--force' || -z "${previous_file}" || -z "${previous_pos}" || "${current_file}" != "${previous_file}" || "${current_pos}" != "${previous_pos}" ]]; then
docker exec "${mysql_cid}" sh -c \
'MYSQL_PWD="$MYSQL_ROOT_PASSWORD" mysql -uroot --protocol=socket -e "FLUSH BINARY LOGS;"' >/dev/null
else
backup_log "活动 binlog 无新增已提交事务,跳过轮转"
fi
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}")
final_status="$(master_status "${mysql_cid}")"
IFS=$'\t' read -r final_file final_pos <<< "${final_status}"
[[ -n "${final_file}" && "${final_pos}" =~ ^[0-9]+$ ]] || { backup_error '无法读取归档后的 binlog 位点'; exit 1; }
write_active_binlog_state "${state_dir}" "${final_file}" "${final_pos}"
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 "${2:-}" ;;
verify) verify ;;
*)
printf '用法: %s {archive|verify}\n' "$0" >&2
exit 1
;;
esac