#!/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 dell’intero 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 l’altro 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 è l’ultimo 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"