gestione errore non-retryable nei backup logici
This commit is contained in:
@@ -1,163 +1,223 @@
|
||||
#!/bin/bash
|
||||
# Backup logico per-database con retry e cleanup automatico
|
||||
# Backup logico per-database con retry globale e per-database, atomicità e cleanup
|
||||
# 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 (mysqldump può aprirne molti)
|
||||
# Aumenta i file descriptor disponibili (mariadb-dump può aprirne molti)
|
||||
ulimit -n 65536
|
||||
|
||||
# Directory base dei backup
|
||||
# Directory base dove verranno create le cartelle dei backup logici
|
||||
BACKUP_BASE=/var/backups/tscale01
|
||||
|
||||
# Prefisso per distinguere i backup logici di questo nodo
|
||||
# Prefisso per distinguere i backup logici da altri tipi di backup
|
||||
PREFIX=eqn-bck-logic-
|
||||
|
||||
# Timestamp per la directory del backup
|
||||
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
|
||||
|
||||
# Directory finale del backup
|
||||
TARGET="$BACKUP_BASE/${PREFIX}${TIMESTAMP}"
|
||||
|
||||
# File di log generale
|
||||
# File di log condiviso per tutti i backup MariaDB
|
||||
LOGFILE=/var/log/mariadb-backup.log
|
||||
|
||||
# Lockfile per evitare esecuzioni concorrenti
|
||||
# Lockfile usato da flock per evitare esecuzioni concorrenti dello script
|
||||
LOCKFILE=/var/lock/mariadb-backup.lock
|
||||
|
||||
# Nome del server per i log (utile in ambienti multi-nodo)
|
||||
# Nome breve del server, utile per distinguere i log in ambienti multi-nodo
|
||||
SERVER_NAME=$(hostname -s)
|
||||
|
||||
# Binari necessari
|
||||
# Numero massimo di tentativi globali dell’intero backup logico
|
||||
GLOBAL_MAX_RETRIES=5
|
||||
|
||||
# Secondi di attesa tra un tentativo globale e il successivo
|
||||
GLOBAL_RETRY_SLEEP=30
|
||||
|
||||
# Numero massimo di tentativi per ogni singolo database (in caso di deadlock 1213)
|
||||
DB_MAX_RETRIES=10
|
||||
|
||||
# Secondi di attesa tra un tentativo e l’altro per lo stesso database
|
||||
DB_RETRY_SLEEP=20
|
||||
|
||||
# Percorso del client mariadb (per eseguire query come SHOW DATABASES)
|
||||
MYSQL_BIN=$(command -v mariadb || true)
|
||||
|
||||
# Percorso di mariadb-dump (per eseguire i dump logici)
|
||||
MYSQLDUMP_BIN=$(command -v mariadb-dump || true)
|
||||
|
||||
# Opzioni dump ottimizzate per Galera (no lock, snapshot consistente)
|
||||
# Opzioni di mariadb-dump ottimizzate per Galera:
|
||||
# - --single-transaction: snapshot consistente senza lock globali
|
||||
# - --quick: stream delle righe senza caricare tutto in RAM
|
||||
# - --skip-lock-tables: evita lock sulle tabelle
|
||||
MYSQLDUMP_OPTS="--user=root --single-transaction --quick --skip-lock-tables"
|
||||
|
||||
# Numero massimo di retry per ogni database (deadlock = retry)
|
||||
MAX_RETRIES=10
|
||||
|
||||
# Secondi di attesa tra un retry e l'altro
|
||||
RETRY_SLEEP=30
|
||||
|
||||
# Assicura che il logfile esista e abbia permessi sicuri
|
||||
# Assicura che il logfile esista
|
||||
touch "$LOGFILE"
|
||||
chown root:root "$LOGFILE"
|
||||
# Permessi restrittivi sul logfile (solo root può leggere/scrivere)
|
||||
chmod 600 "$LOGFILE"
|
||||
|
||||
(
|
||||
# FLOCK: evita che due backup partano insieme
|
||||
flock -n 9 || { echo "[$(date '+%F %T')] [$SERVER_NAME] SKIP: another backup is running" >> "$LOGFILE"; exit 0; }
|
||||
# Funzione che esegue un singolo tentativo completo di backup logico
|
||||
run_backup() {
|
||||
# Timestamp usato per nominare la directory del backup
|
||||
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
|
||||
|
||||
# Controllo binari
|
||||
if [ -z "$MYSQL_BIN" ]; then
|
||||
echo "[$(date '+%F %T')] [$SERVER_NAME] ERROR: mariadb client not found in PATH" >> "$LOGFILE"
|
||||
exit 1
|
||||
fi
|
||||
# Directory di destinazione per questo tentativo di backup
|
||||
TARGET="$BACKUP_BASE/${PREFIX}${TIMESTAMP}"
|
||||
|
||||
if [ -z "$MYSQLDUMP_BIN" ]; then
|
||||
echo "[$(date '+%F %T')] [$SERVER_NAME] ERROR: mariadb-dump not found in PATH" >> "$LOGFILE"
|
||||
exit 1
|
||||
fi
|
||||
# File temporaneo per catturare stderr di mariadb-dump
|
||||
TMPLOG=$(mktemp /tmp/mariadb-backup-logic.XXXXXX)
|
||||
|
||||
# Crea directory del backup
|
||||
mkdir -p "$TARGET"
|
||||
chown mysql:mysql "$TARGET"
|
||||
chmod 750 "$TARGET"
|
||||
# Crea la directory del backup e imposta owner e permessi
|
||||
mkdir -p "$TARGET"
|
||||
chown mysql:mysql "$TARGET"
|
||||
chmod 750 "$TARGET"
|
||||
|
||||
# File temporaneo per catturare errori del dump
|
||||
TMPLOG=$(mktemp /tmp/mariadb-backup-logic.XXXXXX)
|
||||
# Separatore visivo nel logfile
|
||||
echo "---------------------" >> "$LOGFILE"
|
||||
# Log di inizio backup logico
|
||||
echo "[$(date '+%F %T')] [$SERVER_NAME] START logic backup $TARGET" >> "$LOGFILE"
|
||||
|
||||
echo "---------------------" >> "$LOGFILE"
|
||||
echo "[$(date '+%F %T')] [$SERVER_NAME] START logic backup (per-database) $TARGET" >> "$LOGFILE"
|
||||
# Elenco dei database utente (esclude schemi di sistema)
|
||||
DB_LIST=$($MYSQL_BIN -N -e "SHOW DATABASES" | grep -vE '^(information_schema|performance_schema|mysql|sys)$' || true)
|
||||
|
||||
# Elenco database utente (escludiamo schemi di sistema)
|
||||
DB_LIST=$($MYSQL_BIN -N -e "SHOW DATABASES" | grep -vE '^(information_schema|performance_schema|mysql|sys)$' || true)
|
||||
# Flag globale di fallimento (0 = tutto ok, 1 = almeno un DB fallito)
|
||||
FAILED=0
|
||||
# Elenco dei database che hanno avuto errori
|
||||
FAILED_DBS=""
|
||||
|
||||
if [ -z "$DB_LIST" ]; then
|
||||
echo "[$(date '+%F %T')] [$SERVER_NAME] WARNING: no user databases found to dump" >> "$LOGFILE"
|
||||
fi
|
||||
# Ciclo su ogni database utente
|
||||
for DB in $DB_LIST; do
|
||||
# File di destinazione per il dump di questo database
|
||||
DUMPFILE="$TARGET/${DB}.sql"
|
||||
# Log di inizio dump per il singolo database
|
||||
echo "[$(date '+%F %T')] [$SERVER_NAME] START dump database '$DB' -> $DUMPFILE" >> "$LOGFILE"
|
||||
|
||||
FAILED=0
|
||||
FAILED_DBS=""
|
||||
# Contatore dei tentativi per questo database
|
||||
ATTEMPT=1
|
||||
# Flag di successo per questo database (0 = non riuscito, 1 = riuscito)
|
||||
SUCCESS=0
|
||||
|
||||
# Loop su ogni database
|
||||
for db in $DB_LIST; do
|
||||
DUMPFILE="$TARGET/${db}.sql"
|
||||
echo "[$(date '+%F %T')] [$SERVER_NAME] START dump database '$db' -> $DUMPFILE" >> "$LOGFILE"
|
||||
# Ciclo di retry per il singolo database
|
||||
while [ "$ATTEMPT" -le "$DB_MAX_RETRIES" ]; do
|
||||
# Pulisce il file temporaneo degli errori
|
||||
: >"$TMPLOG"
|
||||
|
||||
attempt=1
|
||||
success=0
|
||||
# Esegue il dump del database, redirezionando stderr su TMPLOG
|
||||
if $MYSQLDUMP_BIN $MYSQLDUMP_OPTS "$DB" >"$DUMPFILE" 2>"$TMPLOG"; then
|
||||
# Dump riuscito: log e segna successo
|
||||
echo "[$(date '+%F %T')] [$SERVER_NAME] END dump database '$DB' (attempt $ATTEMPT)" >> "$LOGFILE"
|
||||
SUCCESS=1
|
||||
# Esce dal ciclo di retry per questo database
|
||||
break
|
||||
else
|
||||
# Dump fallito: log dell’errore e ultime righe del TMPLOG
|
||||
echo "[$(date '+%F %T')] [$SERVER_NAME] ERROR dumping database '$DB' (attempt $ATTEMPT). See $TMPLOG" >> "$LOGFILE"
|
||||
tail -n 50 "$TMPLOG" >> "$LOGFILE"
|
||||
|
||||
# Retry intelligente in caso di deadlock (errore 1213)
|
||||
while [ "$attempt" -le "$MAX_RETRIES" ]; do
|
||||
: >"$TMPLOG" # pulisce il file temporaneo
|
||||
# Se l’errore è un deadlock 1213, consideriamo il problema temporaneo e riproviamo
|
||||
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: non ha senso riprovare questo database
|
||||
echo "[$(date '+%F %T')] [$SERVER_NAME] NON-RETRYABLE error for '$DB'" >> "$LOGFILE"
|
||||
# Esce dal ciclo di retry per questo database
|
||||
break
|
||||
fi
|
||||
fi
|
||||
|
||||
if $MYSQLDUMP_BIN $MYSQLDUMP_OPTS "$db" >"$DUMPFILE" 2>"$TMPLOG"; then
|
||||
echo "[$(date '+%F %T')] [$SERVER_NAME] END dump database '$db' (attempt $attempt)" >> "$LOGFILE"
|
||||
success=1
|
||||
break
|
||||
else
|
||||
echo "[$(date '+%F %T')] [$SERVER_NAME] ERROR dumping database '$db' (attempt $attempt). See $TMPLOG" >> "$LOGFILE"
|
||||
tail -n 50 "$TMPLOG" >> "$LOGFILE"
|
||||
# Incrementa il contatore dei tentativi per questo database
|
||||
ATTEMPT=$((ATTEMPT + 1))
|
||||
done
|
||||
|
||||
# Se è un deadlock, riproviamo
|
||||
if grep -q "Error 1213" "$TMPLOG"; then
|
||||
echo "[$(date '+%F %T')] [$SERVER_NAME] RETRY for database '$db' in ${RETRY_SLEEP}s due to deadlock (1213)" >> "$LOGFILE"
|
||||
sleep "$RETRY_SLEEP"
|
||||
else
|
||||
# Errori non recuperabili → stop retry
|
||||
echo "[$(date '+%F %T')] [$SERVER_NAME] NON-RETRYABLE error for database '$db'" >> "$LOGFILE"
|
||||
break
|
||||
# Se dopo tutti i tentativi il database non è stato dumpato con successo
|
||||
if [ "$SUCCESS" -ne 1 ]; then
|
||||
# Segna che almeno un database è fallito
|
||||
FAILED=1
|
||||
# Aggiunge il nome del database alla lista dei falliti
|
||||
FAILED_DBS="$FAILED_DBS $DB"
|
||||
fi
|
||||
fi
|
||||
|
||||
attempt=$((attempt + 1))
|
||||
done
|
||||
|
||||
# Se dopo i retry non è andata → segna come fallito
|
||||
if [ "$success" -ne 1 ]; then
|
||||
FAILED=1
|
||||
FAILED_DBS="$FAILED_DBS $db"
|
||||
# Calcola la dimensione apparente (somma dei byte logici dei file)
|
||||
SIZE_APPARENT_BYTES=$(find "$TARGET" -type f -printf '%s\n' | awk '{s+=$1} END{print s+0}')
|
||||
# Converte la dimensione apparente in formato leggibile (es. MiB, GiB)
|
||||
SIZE_APPARENT_HUMAN=$(numfmt --to=iec --suffix=B "$SIZE_APPARENT_BYTES")
|
||||
|
||||
# Calcola lo spazio effettivo su disco (byte allocati)
|
||||
SIZE_DISK_BYTES=$(du -s --block-size=1 "$TARGET" | cut -f1)
|
||||
# Converte la dimensione su disco in formato leggibile
|
||||
SIZE_DISK_HUMAN=$(numfmt --to=iec --suffix=B "$SIZE_DISK_BYTES")
|
||||
|
||||
# Log delle dimensioni del backup
|
||||
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 tutti i database sono stati dumpati correttamente
|
||||
if [ "$FAILED" -eq 0 ]; then
|
||||
# Log di successo complessivo
|
||||
echo "[$(date '+%F %T')] [$SERVER_NAME] RESULT: OK" >> "$LOGFILE"
|
||||
# Rimuove il file temporaneo degli errori
|
||||
rm -f "$TMPLOG"
|
||||
# Ritorna 0 (successo) al chiamante (ciclo globale)
|
||||
return 0
|
||||
else
|
||||
# Log di fallimento complessivo con elenco dei database problematici
|
||||
echo "[$(date '+%F %T')] [$SERVER_NAME] RESULT: FAILED, databases with errors:$FAILED_DBS" >> "$LOGFILE"
|
||||
# Log della pulizia della directory incompleta
|
||||
echo "[$(date '+%F %T')] [$SERVER_NAME] CLEANUP: removing incomplete backup directory $TARGET" >> "$LOGFILE"
|
||||
# Rimuove la directory del backup incompleto (atomicità: o tutto o niente)
|
||||
rm -rf "$TARGET"
|
||||
# Rimuove il file temporaneo degli errori
|
||||
rm -f "$TMPLOG"
|
||||
# Ritorna 1 (fallimento) al chiamante (ciclo globale)
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# Calcolo dimensioni del backup
|
||||
size_apparent_bytes=$(find "$TARGET" -type f -printf '%s\n' 2>/dev/null | awk '{s+=$1} END{print s+0}')
|
||||
size_apparent_human=$(numfmt --to=iec --suffix=B "$size_apparent_bytes" 2>/dev/null || echo "${size_apparent_bytes}B")
|
||||
(
|
||||
# FLOCK: evita che due istanze dello script girino contemporaneamente
|
||||
flock -n 9 || {
|
||||
echo "[$(date '+%F %T')] [$SERVER_NAME] SKIP: another backup is running" >> "$LOGFILE"
|
||||
exit 0
|
||||
}
|
||||
|
||||
size_disk_bytes=$(du -s --block-size=1 "$TARGET" 2>/dev/null | cut -f1 || echo 0)
|
||||
size_disk_human=$(numfmt --to=iec --suffix=B "$size_disk_bytes" 2>/dev/null || echo "${size_disk_bytes}B")
|
||||
# Ciclo di retry globale: ripete l’intero backup logico fino a GLOBAL_MAX_RETRIES volte
|
||||
for GLOBAL_ATTEMPT in $(seq 1 $GLOBAL_MAX_RETRIES); do
|
||||
# Log del tentativo globale corrente
|
||||
echo "[$(date '+%F %T')] [$SERVER_NAME] GLOBAL attempt $GLOBAL_ATTEMPT/$GLOBAL_MAX_RETRIES" >> "$LOGFILE"
|
||||
|
||||
echo "[$(date '+%F %T')] [$SERVER_NAME] SIZE apparent: $size_apparent_human ($size_apparent_bytes bytes) for $TARGET" >> "$LOGFILE"
|
||||
echo "[$(date '+%F %T')] [$SERVER_NAME] SIZE on-disk: $size_disk_human ($size_disk_bytes bytes) for $TARGET" >> "$LOGFILE"
|
||||
# Esegue un tentativo completo di backup logico
|
||||
if run_backup; then
|
||||
# Se il tentativo ha successo, log e uscita dal ciclo globale
|
||||
echo "[$(date '+%F %T')] [$SERVER_NAME] GLOBAL RESULT: SUCCESS" >> "$LOGFILE"
|
||||
break
|
||||
else
|
||||
# Se il tentativo fallisce e non siamo all’ultimo tentativo globale
|
||||
if [ "$GLOBAL_ATTEMPT" -lt "$GLOBAL_MAX_RETRIES" ]; then
|
||||
# Log del prossimo retry globale e attesa
|
||||
echo "[$(date '+%F %T')] [$SERVER_NAME] GLOBAL RETRY in ${GLOBAL_RETRY_SLEEP}s" >> "$LOGFILE"
|
||||
sleep "$GLOBAL_RETRY_SLEEP"
|
||||
else
|
||||
# Se anche l’ultimo tentativo globale fallisce, log e exit 1
|
||||
echo "[$(date '+%F %T')] [$SERVER_NAME] GLOBAL RESULT: FAILED after $GLOBAL_MAX_RETRIES attempts" >> "$LOGFILE"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# Risultato finale con cleanup in caso di fallimento
|
||||
if [ "$FAILED" -eq 0 ]; then
|
||||
echo "[$(date '+%F %T')] [$SERVER_NAME] RESULT: OK, all databases dumped successfully" >> "$LOGFILE"
|
||||
else
|
||||
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"
|
||||
exit 1
|
||||
fi
|
||||
# Rotazione dei backup logici: rimuove le directory più vecchie di 7 giorni
|
||||
TO_DELETE=$(find "$BACKUP_BASE" -maxdepth 1 -type d -name "${PREFIX}*" -mtime +7)
|
||||
|
||||
rm -f "$TMPLOG"
|
||||
|
||||
# Rotazione: elimina backup più vecchi di 7 giorni
|
||||
TO_DELETE=$(find "$BACKUP_BASE" -maxdepth 1 -type d -name "${PREFIX}*" -mtime +7 -print 2>/dev/null || true)
|
||||
|
||||
if [ -n "$TO_DELETE" ]; then
|
||||
echo "[$(date '+%F %T')] [$SERVER_NAME] ROTATE: removing old logic backups:" >> "$LOGFILE"
|
||||
echo "$TO_DELETE" >> "$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
|
||||
|
||||
echo " " >> "$LOGFILE"
|
||||
# Se ci sono directory da cancellare
|
||||
if [ -n "$TO_DELETE" ]; then
|
||||
# Log delle directory che verranno rimosse
|
||||
echo "[$(date '+%F %T')] [$SERVER_NAME] ROTATE: removing old logic backups:" >> "$LOGFILE"
|
||||
echo "$TO_DELETE" | tr '\n' '\0' | xargs -0 -r rm -rf --
|
||||
else
|
||||
# Log se non c’è nulla da ruotare
|
||||
echo "[$(date '+%F %T')] [$SERVER_NAME] ROTATE: no logic backups to remove (<=7 days)" >> "$LOGFILE"
|
||||
fi
|
||||
|
||||
# File descriptor 9 associato al LOCKFILE per flock
|
||||
) 9>"$LOCKFILE"
|
||||
|
||||
Reference in New Issue
Block a user