Files
awoooi/scripts/backup/backup-signoz.sh

338 lines
14 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.
#!/bin/bash
# =============================================================================
# WOOO AIOps - SigNoz ClickHouse native backup orchestrator
# Version: 2.0.0
#
# ClickHouse is backed up online through its native BACKUP engine. This script
# never reads or archives the live ClickHouse/SQLite Docker volumes. SigNoz
# metadata/SQLite remains an explicit application-consistent export gap and is
# recorded in the Restic payload without being misreported as completed.
# =============================================================================
set -euo pipefail
source "$(dirname "$0")/common.sh"
SERVICE="signoz"
LOCAL_REPO="${BACKUP_BASE}/signoz"
DUMP_DIR="/tmp/signoz-backup-$$"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
CLICKHOUSE_NATIVE_BACKUP_SCRIPT="${CLICKHOUSE_NATIVE_BACKUP_SCRIPT:-${SCRIPT_DIR}/clickhouse-native-backup.sh}"
COLLECTOR_NAME="${SIGNOZ_COLLECTOR_NAME:-signoz-otel-collector}"
COLLECTOR_DOCKER_TIMEOUT_SECONDS="${COLLECTOR_DOCKER_TIMEOUT_SECONDS:-10}"
COLLECTOR_CONTINUITY_POLL_SECONDS="${COLLECTOR_CONTINUITY_POLL_SECONDS:-1}"
TIMEOUT_KILL_AFTER_SECONDS="${TIMEOUT_KILL_AFTER_SECONDS:-30}"
BACKUP_SKIP_RETENTION_CLEANUP="${BACKUP_SKIP_RETENTION_CLEANUP:-0}"
RESTIC_RECEIPT_ROOT="${SIGNOZ_BACKUP_RECEIPT_ROOT:-/backup/logs/signoz-backup}"
RESTIC_INIT_TIMEOUT_SECONDS="${SIGNOZ_RESTIC_INIT_TIMEOUT_SECONDS:-300}"
RESTIC_BACKUP_TIMEOUT_SECONDS="${SIGNOZ_RESTIC_BACKUP_TIMEOUT_SECONDS:-1800}"
RESTIC_READBACK_TIMEOUT_SECONDS="${SIGNOZ_RESTIC_READBACK_TIMEOUT_SECONDS:-120}"
RESTIC_CHECK_TIMEOUT_SECONDS="${SIGNOZ_RESTIC_CHECK_TIMEOUT_SECONDS:-1800}"
BACKUP_ID_SEED="$(date -u '+%Y%m%dT%H%M%SZ')-$$"
TRACE_ID="${TRACE_ID:-signoz-backup-${BACKUP_ID_SEED}}"
RUN_ID="${RUN_ID:-signoz-${BACKUP_ID_SEED}}"
WORK_ITEM_ID="${WORK_ITEM_ID:-SIGNOZ-BACKUP}"
export TRACE_ID RUN_ID WORK_ITEM_ID
COLLECTOR_ID_BEFORE=""
COLLECTOR_STARTED_AT_BEFORE=""
COLLECTOR_RESTARTS_BEFORE=""
COLLECTOR_HEALTH_BEFORE=""
COLLECTOR_OBSERVER_PID=""
COLLECTOR_OBSERVATIONS="${DUMP_DIR}/collector-continuity.tsv"
CLEANUP_COMPLETE=0
FAILURE_NOTIFIED=0
run_collector_docker() {
timeout --kill-after="${TIMEOUT_KILL_AFTER_SECONDS}" \
"${COLLECTOR_DOCKER_TIMEOUT_SECONDS}" docker "$@"
}
collector_identity_state() {
run_collector_docker inspect --format $'{{.Id}}\t{{.State.Running}}\t{{.State.StartedAt}}\t{{.RestartCount}}\t{{if (index .State "Health")}}{{(index .State "Health").Status}}{{else}}none{{end}}' \
"${COLLECTOR_NAME}" 2>/dev/null
}
notify_failure_once() {
local detail="$1"
if [ "${FAILURE_NOTIFIED}" -eq 1 ]; then
return 0
fi
FAILURE_NOTIFIED=1
notify_clawbot "failed" "${SERVICE}" "${detail}" || true
}
observe_collector_continuity() {
local epoch observation
while true; do
epoch="$(date +%s)"
if observation="$(collector_identity_state)"; then
printf '%s\t%s\n' "${epoch}" "${observation}" >> "${COLLECTOR_OBSERVATIONS}"
else
printf '%s\tunknown\tunknown\tunknown\tunknown\tunknown\n' "${epoch}" >> "${COLLECTOR_OBSERVATIONS}"
fi
sleep "${COLLECTOR_CONTINUITY_POLL_SECONDS}"
done
}
start_collector_observer() {
: > "${COLLECTOR_OBSERVATIONS}"
observe_collector_continuity &
COLLECTOR_OBSERVER_PID=$!
}
stop_collector_observer() {
if [ -n "${COLLECTOR_OBSERVER_PID}" ] \
&& kill -0 "${COLLECTOR_OBSERVER_PID}" 2>/dev/null; then
kill -TERM "${COLLECTOR_OBSERVER_PID}" 2>/dev/null || true
wait "${COLLECTOR_OBSERVER_PID}" 2>/dev/null || true
fi
COLLECTOR_OBSERVER_PID=""
}
verify_collector_continuity() {
local observation final_id final_running final_started_at final_restarts final_health
stop_collector_observer
[ -s "${COLLECTOR_OBSERVATIONS}" ] || {
log_error "${COLLECTOR_NAME} continuity receipt 為空"
return 1
}
if awk -F '\t' -v expected_id="${COLLECTOR_ID_BEFORE}" \
-v expected_started="${COLLECTOR_STARTED_AT_BEFORE}" \
-v expected_restarts="${COLLECTOR_RESTARTS_BEFORE}" \
-v expected_health="${COLLECTOR_HEALTH_BEFORE}" \
'$2 == "unknown" || $3 != "true" || $2 != expected_id || \
$4 != expected_started || $5 != expected_restarts || $6 != expected_health { exit 1 }' \
"${COLLECTOR_OBSERVATIONS}"; then
:
else
log_error "${COLLECTOR_NAME} 在 native backup window 發生停止或 identity drift"
return 1
fi
observation="$(collector_identity_state)" || return 1
IFS=$'\t' read -r final_id final_running final_started_at final_restarts final_health <<< "${observation}"
[ "${final_id}" = "${COLLECTOR_ID_BEFORE}" ] \
&& [ "${final_running}" = "true" ] \
&& [ "${final_started_at}" = "${COLLECTOR_STARTED_AT_BEFORE}" ] \
&& [ "${final_restarts}" = "${COLLECTOR_RESTARTS_BEFORE}" ] \
&& [ "${final_health}" = "${COLLECTOR_HEALTH_BEFORE}" ]
}
cleanup() {
local exit_code=$?
if [ "${CLEANUP_COMPLETE}" -eq 1 ]; then
return "${exit_code}"
fi
CLEANUP_COMPLETE=1
trap '' HUP INT TERM
stop_collector_observer
rm -rf -- "${DUMP_DIR}"
return "${exit_code}"
}
on_signal() {
local signal_number="$1"
exit "$((128 + signal_number))"
}
run_native_clickhouse_backup() {
local mode="$1"
[ -f "${CLICKHOUSE_NATIVE_BACKUP_SCRIPT}" ] \
&& [ -x "${CLICKHOUSE_NATIVE_BACKUP_SCRIPT}" ] \
&& [ ! -L "${CLICKHOUSE_NATIVE_BACKUP_SCRIPT}" ] \
|| {
log_error "ClickHouse native backup adapter 缺失、不可執行或為 symlink"
return 1
}
CLICKHOUSE_NATIVE_OUTPUT_DIR="${DUMP_DIR}" \
TRACE_ID="${TRACE_ID}" RUN_ID="${RUN_ID}" WORK_ITEM_ID="${WORK_ITEM_ID}" \
"${CLICKHOUSE_NATIVE_BACKUP_SCRIPT}" "${mode}"
}
write_metadata_gap_receipt() {
cat > "${DUMP_DIR}/signoz-metadata-gap.json" <<EOF
{"trace_id":"${TRACE_ID}","run_id":"${RUN_ID}","work_item_id":"${WORK_ITEM_ID}","asset":"signoz-metadata-sqlite","terminal":"pending_application_consistent_export","raw_volume_read":false,"raw_volume_archive":false,"completion_claim":false}
EOF
log_warn "SigNoz metadata/SQLite 尚缺 application-consistent export本 run 未讀取 live volume"
}
write_restic_receipt() {
local snapshot_id="$1"
local artifact_hash="$2"
local manifest_hash="$3"
local inventory_hash="$4"
local receipt_dir receipt_tmp receipt_file
[[ "${RESTIC_RECEIPT_ROOT}" == /* ]] && [ ! -L "${RESTIC_RECEIPT_ROOT}" ] || return 1
mkdir -p "${RESTIC_RECEIPT_ROOT}"
receipt_dir="${RESTIC_RECEIPT_ROOT}/${RUN_ID}"
[ ! -L "${receipt_dir}" ] || return 1
mkdir -m 700 -p "${receipt_dir}"
receipt_file="${receipt_dir}/restic-snapshot.json"
receipt_tmp="${receipt_file}.partial.$$"
printf '{"trace_id":"%s","run_id":"%s","work_item_id":"%s","service":"signoz","snapshot_id":"%s","artifact_sha256":"%s","manifest_sha256":"%s","source_inventory_sha256":"%s","repository_scope":"local_same_failure_domain","restic_check_read_data_subset":"1%%","offsite_verified":false,"metadata_sqlite_terminal":"pending_application_consistent_export","completion_claim":"clickhouse_native_only"}\n' \
"${TRACE_ID}" "${RUN_ID}" "${WORK_ITEM_ID}" "${snapshot_id}" \
"${artifact_hash}" "${manifest_hash}" "${inventory_hash}" > "${receipt_tmp}"
chmod 600 "${receipt_tmp}"
mv "${receipt_tmp}" "${receipt_file}"
}
main() {
local start_time end_time duration snapshot_id snapshot_json tags
local initial_observation initial_running final_id final_running
local final_started_at final_restarts final_health
local artifact_file manifest_file inventory_file artifact_hash manifest_hash inventory_hash
start_time="$(date +%s)"
trap cleanup EXIT
trap 'on_signal 1' HUP
trap 'on_signal 2' INT
trap 'on_signal 15' TERM
log_info "========== 開始 SigNoz ClickHouse native 備份 =========="
[[ "${COLLECTOR_CONTINUITY_POLL_SECONDS}" =~ ^[0-9]+([.][0-9]+)?$ ]] \
&& [[ "${COLLECTOR_CONTINUITY_POLL_SECONDS}" != "0" ]] \
&& [[ "${COLLECTOR_CONTINUITY_POLL_SECONDS}" != "0.0" ]] || {
notify_failure_once "COLLECTOR_CONTINUITY_POLL_SECONDS 無效"
exit 1
}
for timeout_value in "${RESTIC_INIT_TIMEOUT_SECONDS}" "${RESTIC_BACKUP_TIMEOUT_SECONDS}" \
"${RESTIC_READBACK_TIMEOUT_SECONDS}" "${RESTIC_CHECK_TIMEOUT_SECONDS}"; do
[[ "${timeout_value}" =~ ^[0-9]+$ ]] && [ "${timeout_value}" -gt 0 ] || {
notify_failure_once "Restic timeout contract 無效"
exit 1
}
done
mkdir -p "${DUMP_DIR}"
# Check-mode must pass before the backup window. It writes only durable
# receipts and does not submit BACKUP or create a server artifact.
if ! run_native_clickhouse_backup --check; then
log_error "ClickHouse native BACKUP check-mode 失敗;沒有 volume tar fallback"
notify_failure_once "SigNoz ClickHouse native BACKUP runtime contract 未就緒"
exit 1
fi
initial_observation="$(collector_identity_state)" || {
notify_failure_once "無法讀取 ${COLLECTOR_NAME} 初始 identity/state"
exit 1
}
IFS=$'\t' read -r COLLECTOR_ID_BEFORE initial_running COLLECTOR_STARTED_AT_BEFORE \
COLLECTOR_RESTARTS_BEFORE COLLECTOR_HEALTH_BEFORE <<< "${initial_observation}"
[ -n "${COLLECTOR_ID_BEFORE}" ] && [ "${initial_running}" = "true" ] || {
notify_failure_once "${COLLECTOR_NAME} 初始狀態不是 running"
exit 1
}
[ -n "${COLLECTOR_STARTED_AT_BEFORE}" ] \
&& [[ "${COLLECTOR_RESTARTS_BEFORE}" =~ ^[0-9]+$ ]] \
&& [[ "${COLLECTOR_HEALTH_BEFORE}" =~ ^(healthy|none)$ ]] || {
notify_failure_once "${COLLECTOR_NAME} 初始 runtime metadata 不完整"
exit 1
}
start_collector_observer
# Online native backup: no collector stop and no live-volume read.
if ! run_native_clickhouse_backup --apply; then
notify_failure_once "SigNoz ClickHouse native BACKUP 失敗"
exit 1
fi
if ! verify_collector_continuity; then
notify_failure_once "${COLLECTOR_NAME} no-downtime continuity verifier 失敗"
exit 1
fi
log_success "${COLLECTOR_NAME} no-downtime continuity verifier 通過"
write_metadata_gap_receipt
artifact_file="$(find "${DUMP_DIR}" -maxdepth 1 -type f -name 'clickhouse-native-*.zip' -print)"
manifest_file="$(find "${DUMP_DIR}" -maxdepth 1 -type f -name 'clickhouse-native-*.zip.manifest.json' -print)"
inventory_file="$(find "${DUMP_DIR}" -maxdepth 1 -type f -name 'clickhouse-native-*.source-inventory.tsv' -print)"
[ "$(printf '%s\n' "${artifact_file}" | awk 'NF {count++} END {print count+0}')" -eq 1 ] \
&& [ "$(printf '%s\n' "${manifest_file}" | awk 'NF {count++} END {print count+0}')" -eq 1 ] \
&& [ "$(printf '%s\n' "${inventory_file}" | awk 'NF {count++} END {print count+0}')" -eq 1 ] || {
log_error "ClickHouse native artifact set 不唯一或不完整"
exit 1
}
artifact_hash="$(sha256sum "${artifact_file}" | awk '{print $1}')"
manifest_hash="$(sha256sum "${manifest_file}" | awk '{print $1}')"
inventory_hash="$(sha256sum "${inventory_file}" | awk '{print $1}')"
if [ ! -d "${LOCAL_REPO}/data" ]; then
log_info "初始化 Restic 倉庫: ${LOCAL_REPO}"
timeout --kill-after="${TIMEOUT_KILL_AFTER_SECONDS}" "${RESTIC_INIT_TIMEOUT_SECONDS}" \
restic -r "${LOCAL_REPO}" init --password-file "${RESTIC_PASSWORD_FILE}" 2>&1 || {
log_error "Restic 倉庫初始化失敗"
exit 1
}
fi
log_info "建立 ClickHouse native artifact 的 Restic 備份..."
tags="$(build_tags "${SERVICE}")"
timeout --kill-after="${TIMEOUT_KILL_AFTER_SECONDS}" "${RESTIC_BACKUP_TIMEOUT_SECONDS}" \
restic -r "${LOCAL_REPO}" backup "${DUMP_DIR}" \
--password-file "${RESTIC_PASSWORD_FILE}" ${tags} \
--tag "trace:${TRACE_ID}" --tag "run:${RUN_ID}" \
--tag "work-item:${WORK_ITEM_ID}" 2>&1
snapshot_json="$(timeout --kill-after="${TIMEOUT_KILL_AFTER_SECONDS}" \
"${RESTIC_READBACK_TIMEOUT_SECONDS}" restic -r "${LOCAL_REPO}" snapshots \
--tag "run:${RUN_ID}" --latest 1 --json \
--password-file "${RESTIC_PASSWORD_FILE}" 2>/dev/null)"
snapshot_id="$(printf '%s\n' "${snapshot_json}" \
| sed -n 's/.*"short_id":"\([^"]*\)".*/\1/p' | head -1)"
[[ "${snapshot_id}" =~ ^[A-Za-z0-9]+$ ]] || {
log_error "Restic same-run snapshot durable readback 缺失"
exit 1
}
timeout --kill-after="${TIMEOUT_KILL_AFTER_SECONDS}" "${RESTIC_CHECK_TIMEOUT_SECONDS}" \
restic -r "${LOCAL_REPO}" check --read-data-subset=1% \
--password-file "${RESTIC_PASSWORD_FILE}" 2>&1 || {
log_error "Restic repository/data-subset verifier 失敗"
exit 1
}
write_restic_receipt "${snapshot_id}" "${artifact_hash}" \
"${manifest_hash}" "${inventory_hash}" || {
log_error "Restic durable receipt writeback 失敗"
exit 1
}
log_success "Restic 備份完成: ${snapshot_id}"
if [ "${BACKUP_SKIP_RETENTION_CLEANUP}" = "1" ]; then
log_info "略過 SignOz retention cleanup (BACKUP_SKIP_RETENTION_CLEANUP=1)"
else
cleanup_old_backups "${LOCAL_REPO}"
fi
# Final readback is separate from the continuity observer receipt.
initial_observation="$(collector_identity_state)" || {
notify_failure_once "${COLLECTOR_NAME} final readback 失敗"
exit 1
}
IFS=$'\t' read -r final_id final_running final_started_at final_restarts final_health <<< "${initial_observation}"
[ "${final_id}" = "${COLLECTOR_ID_BEFORE}" ] \
&& [ "${final_running}" = "true" ] \
&& [ "${final_started_at}" = "${COLLECTOR_STARTED_AT_BEFORE}" ] \
&& [ "${final_restarts}" = "${COLLECTOR_RESTARTS_BEFORE}" ] \
&& [ "${final_health}" = "${COLLECTOR_HEALTH_BEFORE}" ] || {
notify_failure_once "${COLLECTOR_NAME} final identity/start/restart/health verifier 失敗"
exit 1
}
end_time="$(date +%s)"
duration=$((end_time - start_time))
log_success "========== SigNoz ClickHouse native 備份完成 (${duration}s) =========="
notify_clawbot "warning" "${SERVICE}" \
"ClickHouse native backup 完成metadata/SQLite application-consistent export 仍 pending" \
"${duration}"
}
main "$@"