Files
bash-scripts/mariadb-backup/mariadb-backup-logic-per-database.sh
2026-04-02 11:43:32 +02:00

210 lines
7.3 KiB
Bash
Raw Permalink 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
# Backup logico per-database con retry globale e retry per-database
# Sicuro per cluster Galera ad alto carico
# Autore: Marco + Copilot
# Uscita immediata in caso di:
# - comando con exit code != 0 (set -e)
# - uso di variabile non definita (set -u)
# - errore in una pipe (set -o pipefail)
set -euo pipefail
# Aumenta i file descriptor disponibili (mariadb-dump può aprirne molti)
ulimit -n 65536
# Directory base dei backup logici
BACKUP_BASE=/var/backups/tscale01/mariadb
# Prefisso per distinguere i backup logici
PREFIX=backup-logic-
# File di log condiviso
LOGFILE=/var/log/mariadb-backup.log
# Lockfile per evitare esecuzioni concorrenti
LOCKFILE=/var/lock/mariadb-backup.lock
# Nome breve del server per i log
SERVER_NAME=$(hostname -s)
# Numero massimo di tentativi globali dellintero backup
GLOBAL_MAX_RETRIES=5
# Attesa tra un tentativo globale e il successivo
GLOBAL_RETRY_SLEEP=30
# Numero massimo di tentativi per ogni database
DB_MAX_RETRIES=10
# Attesa tra un tentativo e laltro per lo stesso database
DB_RETRY_SLEEP=20
# Percorso del client mariadb
MYSQL_BIN=$(command -v mariadb || true)
# Percorso di mariadb-dump
MYSQLDUMP_BIN=$(command -v mariadb-dump || true)
# Opzioni ottimizzate per Galera
MYSQLDUMP_OPTS="--user=root --single-transaction --quick --skip-lock-tables"
# Assicura che il logfile esista e abbia permessi sicuri
touch "$LOGFILE"
chmod 600 "$LOGFILE"
###############################################################################
# FUNZIONE PRINCIPALE: esegue un singolo tentativo completo di backup logico
###############################################################################
run_backup() {
# Timestamp per la directory del backup
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
# Directory di destinazione del backup
TARGET="$BACKUP_BASE/${PREFIX}${TIMESTAMP}"
# File temporaneo per catturare errori di mariadb-dump
TMPLOG=$(mktemp /tmp/mariadb-backup-logic.XXXXXX)
# Crea la directory del backup con permessi sicuri
mkdir -p "$TARGET"
chown mysql:mysql "$TARGET"
chmod 750 "$TARGET"
# Log di inizio backup
echo "[$(date '+%F %T')] [$SERVER_NAME] START logic backup $TARGET" >> "$LOGFILE"
# Elenco dei database utente (escludiamo schemi di sistema)
DB_LIST=$($MYSQL_BIN -N -e "SHOW DATABASES" | grep -vE '^(information_schema|performance_schema|mysql|sys)$' || true)
# Flag di fallimento globale
FAILED=0
FAILED_DBS=""
# Ciclo su ogni database
for DB in $DB_LIST; do
# File di destinazione del dump
DUMPFILE="$TARGET/${DB}.sql"
# Log di inizio dump
echo "[$(date '+%F %T')] [$SERVER_NAME] START dump database '$DB' -> $DUMPFILE" >> "$LOGFILE"
# Tentativi per questo database
ATTEMPT=1
SUCCESS=0
# Ciclo di retry per il singolo database
while [ "$ATTEMPT" -le "$DB_MAX_RETRIES" ]; do
# Pulisce il file temporaneo
: >"$TMPLOG"
# Esegue il dump
if $MYSQLDUMP_BIN $MYSQLDUMP_OPTS "$DB" >"$DUMPFILE" 2>"$TMPLOG"; then
# Dump riuscito
echo "[$(date '+%F %T')] [$SERVER_NAME] END dump database '$DB' (attempt $ATTEMPT)" >> "$LOGFILE"
SUCCESS=1
break
else
# Dump fallito
echo "[$(date '+%F %T')] [$SERVER_NAME] ERROR dumping database '$DB' (attempt $ATTEMPT). See $TMPLOG" >> "$LOGFILE"
tail -n 50 "$TMPLOG" >> "$LOGFILE"
# Deadlock 1213 → retry
if grep -q "Error 1213" "$TMPLOG"; then
echo "[$(date '+%F %T')] [$SERVER_NAME] RETRY for '$DB' in ${DB_RETRY_SLEEP}s due to deadlock (1213)" >> "$LOGFILE"
sleep "$DB_RETRY_SLEEP"
else
# Errore non recuperabile → interrompe retry
echo "[$(date '+%F %T')] [$SERVER_NAME] NON-RETRYABLE error for '$DB'" >> "$LOGFILE"
break
fi
fi
# Incrementa tentativo
ATTEMPT=$((ATTEMPT + 1))
done
# Se il database non è stato dumpato correttamente
if [ "$SUCCESS" -ne 1 ]; then
FAILED=1
FAILED_DBS="$FAILED_DBS $DB"
fi
done
# Calcolo dimensioni apparenti e su disco
SIZE_APPARENT_BYTES=$(find "$TARGET" -type f -printf '%s\n' | awk '{s+=$1} END{print s+0}')
SIZE_APPARENT_HUMAN=$(numfmt --to=iec --suffix=B "$SIZE_APPARENT_BYTES")
SIZE_DISK_BYTES=$(du -s --block-size=1 "$TARGET" | cut -f1)
SIZE_DISK_HUMAN=$(numfmt --to=iec --suffix=B "$SIZE_DISK_BYTES")
# Log delle dimensioni
echo "[$(date '+%F %T')] [$SERVER_NAME] SIZE apparent: $SIZE_APPARENT_HUMAN ($SIZE_APPARENT_BYTES bytes)" >> "$LOGFILE"
echo "[$(date '+%F %T')] [$SERVER_NAME] SIZE on-disk: $SIZE_DISK_HUMAN ($SIZE_DISK_BYTES bytes)" >> "$LOGFILE"
# Se tutto è andato bene
if [ "$FAILED" -eq 0 ]; then
echo "[$(date '+%F %T')] [$SERVER_NAME] RESULT: OK" >> "$LOGFILE"
rm -f "$TMPLOG"
return 0
else
# Fallimento → pulizia directory
echo "[$(date '+%F %T')] [$SERVER_NAME] RESULT: FAILED, databases with errors:$FAILED_DBS" >> "$LOGFILE"
echo "[$(date '+%F %T')] [$SERVER_NAME] CLEANUP: removing incomplete backup directory $TARGET" >> "$LOGFILE"
rm -rf "$TARGET"
rm -f "$TMPLOG"
return 1
fi
}
###############################################################################
# BLOCCO PRINCIPALE CON FLOCK E RETRY GLOBALE
###############################################################################
(
# Evita esecuzioni concorrenti
flock -n 9 || {
echo "[$(date '+%F %T')] [$SERVER_NAME] SKIP: another backup is running" >> "$LOGFILE"
exit 0
}
# Ciclo di retry globale
for GLOBAL_ATTEMPT in $(seq 1 $GLOBAL_MAX_RETRIES); do
# Separatore visivo per ogni tentativo globale
echo "" >> "$LOGFILE"
echo "---------------------" >> "$LOGFILE"
# Log del tentativo globale
echo "[$(date '+%F %T')] [$SERVER_NAME] GLOBAL attempt $GLOBAL_ATTEMPT/$GLOBAL_MAX_RETRIES" >> "$LOGFILE"
# Esegue un tentativo completo
if run_backup; then
echo "[$(date '+%F %T')] [$SERVER_NAME] GLOBAL RESULT: SUCCESS" >> "$LOGFILE"
break
else
# Se non è lultimo tentativo → retry globale
if [ "$GLOBAL_ATTEMPT" -lt "$GLOBAL_MAX_RETRIES" ]; then
echo "[$(date '+%F %T')] [$SERVER_NAME] GLOBAL RETRY in ${GLOBAL_RETRY_SLEEP}s" >> "$LOGFILE"
sleep "$GLOBAL_RETRY_SLEEP"
else
# Fallimento definitivo
echo "[$(date '+%F %T')] [$SERVER_NAME] GLOBAL RESULT: FAILED after $GLOBAL_MAX_RETRIES attempts" >> "$LOGFILE"
exit 1
fi
fi
done
# Rotazione: elimina backup logici più vecchi di 7 giorni
TO_DELETE=$(find "$BACKUP_BASE" -maxdepth 1 -type d -name "${PREFIX}*" -mtime +7)
if [ -n "$TO_DELETE" ]; then
echo "[$(date '+%F %T')] [$SERVER_NAME] ROTATE: removing old logic backups:" >> "$LOGFILE"
echo "$TO_DELETE" | tr '\n' '\0' | xargs -0 -r rm -rf --
else
echo "[$(date '+%F %T')] [$SERVER_NAME] ROTATE: no logic backups to remove (<=7 days)" >> "$LOGFILE"
fi
) 9>"$LOCKFILE"