diff --git a/mariadb-backup/mariadb-backup-logic-per-database.sh b/mariadb-backup/mariadb-backup-logic-per-database.sh index 62916ed..462c92e 100644 --- a/mariadb-backup/mariadb-backup-logic-per-database.sh +++ b/mariadb-backup/mariadb-backup-logic-per-database.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Backup logico per-database con retry globale e per-database, atomicità e cleanup +# Backup logico per-database con retry globale e retry per-database # Sicuro per cluster Galera ad alto carico # Autore: Marco + Copilot @@ -12,212 +12,197 @@ set -euo pipefail # Aumenta i file descriptor disponibili (mariadb-dump può aprirne molti) ulimit -n 65536 -# Directory base dove verranno create le cartelle dei backup logici +# Directory base dei backup logici BACKUP_BASE=/var/backups/tscale01 -# Prefisso per distinguere i backup logici da altri tipi di backup +# Prefisso per distinguere i backup logici PREFIX=eqn-bck-logic- -# File di log condiviso per tutti i backup MariaDB +# File di log condiviso LOGFILE=/var/log/mariadb-backup.log -# Lockfile usato da flock per evitare esecuzioni concorrenti dello script +# Lockfile per evitare esecuzioni concorrenti LOCKFILE=/var/lock/mariadb-backup.lock -# Nome breve del server, utile per distinguere i log in ambienti multi-nodo +# Nome breve del server per i log SERVER_NAME=$(hostname -s) -# Numero massimo di tentativi globali dell’intero backup logico +# Numero massimo di tentativi globali dell’intero backup GLOBAL_MAX_RETRIES=5 -# Secondi di attesa tra un tentativo globale e il successivo +# 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) +# Numero massimo di tentativi per ogni database DB_MAX_RETRIES=10 -# Secondi di attesa tra un tentativo e l’altro per lo stesso database +# 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) +# Percorso del client mariadb MYSQL_BIN=$(command -v mariadb || true) -# Percorso di mariadb-dump (per eseguire i dump logici) +# Percorso di mariadb-dump MYSQLDUMP_BIN=$(command -v mariadb-dump || true) -# 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 +# Opzioni ottimizzate per Galera MYSQLDUMP_OPTS="--user=root --single-transaction --quick --skip-lock-tables" -# Assicura che il logfile esista +# Assicura che il logfile esista e abbia permessi sicuri touch "$LOGFILE" -# Permessi restrittivi sul logfile (solo root può leggere/scrivere) chmod 600 "$LOGFILE" -# Funzione che esegue un singolo tentativo completo di backup logico +############################################################################### +# FUNZIONE PRINCIPALE: esegue un singolo tentativo completo di backup logico +############################################################################### run_backup() { - # Timestamp usato per nominare la directory del backup + + # Timestamp per la directory del backup TIMESTAMP=$(date +%Y%m%d-%H%M%S) - # Directory di destinazione per questo tentativo di backup + # Directory di destinazione del backup TARGET="$BACKUP_BASE/${PREFIX}${TIMESTAMP}" - # File temporaneo per catturare stderr di mariadb-dump + # File temporaneo per catturare errori di mariadb-dump TMPLOG=$(mktemp /tmp/mariadb-backup-logic.XXXXXX) - # Crea la directory del backup e imposta owner e permessi + # Crea la directory del backup con permessi sicuri mkdir -p "$TARGET" chown mysql:mysql "$TARGET" chmod 750 "$TARGET" - # Separatore visivo nel logfile - echo "---------------------" >> "$LOGFILE" - # Log di inizio backup logico + # Log di inizio backup echo "[$(date '+%F %T')] [$SERVER_NAME] START logic backup $TARGET" >> "$LOGFILE" - # Elenco dei database utente (esclude schemi di sistema) + # 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 globale di fallimento (0 = tutto ok, 1 = almeno un DB fallito) + # Flag di fallimento globale FAILED=0 - # Elenco dei database che hanno avuto errori FAILED_DBS="" - # Ciclo su ogni database utente + # Ciclo su ogni database for DB in $DB_LIST; do - # File di destinazione per il dump di questo database + + # File di destinazione del dump DUMPFILE="$TARGET/${DB}.sql" - # Log di inizio dump per il singolo database + + # Log di inizio dump echo "[$(date '+%F %T')] [$SERVER_NAME] START dump database '$DB' -> $DUMPFILE" >> "$LOGFILE" - # Contatore dei tentativi per questo database + # Tentativi per questo database ATTEMPT=1 - # Flag di successo per questo database (0 = non riuscito, 1 = riuscito) SUCCESS=0 # Ciclo di retry per il singolo database while [ "$ATTEMPT" -le "$DB_MAX_RETRIES" ]; do - # Pulisce il file temporaneo degli errori + + # Pulisce il file temporaneo : >"$TMPLOG" - # Esegue il dump del database, redirezionando stderr su TMPLOG + # Esegue il dump if $MYSQLDUMP_BIN $MYSQLDUMP_OPTS "$DB" >"$DUMPFILE" 2>"$TMPLOG"; then - # Dump riuscito: log e segna successo + # Dump riuscito 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 + # Dump fallito echo "[$(date '+%F %T')] [$SERVER_NAME] ERROR dumping database '$DB' (attempt $ATTEMPT). See $TMPLOG" >> "$LOGFILE" tail -n 50 "$TMPLOG" >> "$LOGFILE" - # Se l’errore è un deadlock 1213, consideriamo il problema temporaneo e riproviamo + # 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: non ha senso riprovare questo database + # Errore non recuperabile → interrompe retry echo "[$(date '+%F %T')] [$SERVER_NAME] NON-RETRYABLE error for '$DB'" >> "$LOGFILE" - # Esce dal ciclo di retry per questo database break fi fi - # Incrementa il contatore dei tentativi per questo database + # Incrementa tentativo ATTEMPT=$((ATTEMPT + 1)) done - # Se dopo tutti i tentativi il database non è stato dumpato con successo + # Se il database non è stato dumpato correttamente 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 done - # Calcola la dimensione apparente (somma dei byte logici dei file) + # Calcolo dimensioni apparenti e su disco 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 + # 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 tutti i database sono stati dumpati correttamente + # Se tutto è andato bene 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 + # Fallimento → pulizia directory 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 } +############################################################################### +# BLOCCO PRINCIPALE CON FLOCK E RETRY GLOBALE +############################################################################### ( - # FLOCK: evita che due istanze dello script girino contemporaneamente + # Evita esecuzioni concorrenti flock -n 9 || { echo "[$(date '+%F %T')] [$SERVER_NAME] SKIP: another backup is running" >> "$LOGFILE" exit 0 } - # Ciclo di retry globale: ripete l’intero backup logico fino a GLOBAL_MAX_RETRIES volte + # Ciclo di retry globale for GLOBAL_ATTEMPT in $(seq 1 $GLOBAL_MAX_RETRIES); do - # Log del tentativo globale corrente + + # Separatore visivo per ogni tentativo globale + echo "---------------------" >> "$LOGFILE" + + # Log del tentativo globale echo "[$(date '+%F %T')] [$SERVER_NAME] GLOBAL attempt $GLOBAL_ATTEMPT/$GLOBAL_MAX_RETRIES" >> "$LOGFILE" - # Esegue un tentativo completo di backup logico + # Esegue un tentativo completo 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 + # Se non è l’ultimo tentativo → retry 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 + # Fallimento definitivo echo "[$(date '+%F %T')] [$SERVER_NAME] GLOBAL RESULT: FAILED after $GLOBAL_MAX_RETRIES attempts" >> "$LOGFILE" exit 1 fi fi done - # Rotazione dei backup logici: rimuove le directory più vecchie di 7 giorni + # Rotazione: elimina backup logici più vecchi di 7 giorni TO_DELETE=$(find "$BACKUP_BASE" -maxdepth 1 -type d -name "${PREFIX}*" -mtime +7) - # 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"