210 lines
7.3 KiB
Bash
210 lines
7.3 KiB
Bash
#!/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"
|