Perché il tuo mysqldump notturno probabilmente non si ripristina

Perché il tuo mysqldump notturno probabilmente non si ripristina

C'è una domanda che faccio in ogni subentro su un'infrastruttura con database, e la risposta è quasi sempre la stessa. La domanda: "quando avete ripristinato un backup l'ultima volta?". La risposta: un silenzio, seguito da "beh, il backup gira ogni notte". Sono due affermazioni che non si toccano nemmeno. Il job notturno che produce un file è una speranza; il backup è un'altra cosa, è un file di cui hai dimostrato che si ripristina, e fra le due c'è un catalogo di fallimenti silenziosi che ho visto materializzarsi tutti, almeno una volta, su macchine reali: dump troncati da mesi con l'exit code perso in una pipe, snapshot internamente incoerenti presi su un database in piena attività, stored procedure e trigger che nel dump non sono mai entrati perché nessuno sapeva che vanno chiesti esplicitamente, file perfetti che si rifiutano di importare sul server di emergenza per un DEFINER che punta a un utente inesistente. In questo articolo faccio due cose, nell'ordine giusto: prima il catalogo dei cinque modi in cui un mysqldump tradisce senza produrre nessun errore visibile, perché la paranoia informata è la base del metodo; poi il tutorial completo del collaudo di ripristino automatico, il job settimanale che ripristina davvero il dump in un container usa e getta, esegue le verifiche e ti scrive solo se qualcosa non torna. È la differenza fra sperare e sapere, e si costruisce in un pomeriggio.

TL;DR

> - Troncato: una pipe mysqldump | gzip senza set -o pipefail inghiotte l'errore: il file esiste, corto. Verifica: l'ultima riga di un dump sano è -- Dump completed.

  • Incoerente: su InnoDB senza --single-transaction il dump fotografa tabelle in momenti diversi. Sempre --single-transaction per dati transazionali.
  • Incompleto: routine, trigger ed eventi NON sono tutti inclusi di default: servono --routines --triggers --events. Gli utenti e i permessi non ci sono comunque: vanno salvati a parte.
  • Non importabile: i DEFINER dentro viste e routine bloccano il restore su un server con utenti diversi.
  • Il collaudo che scopre tutto: ogni settimana, restore automatico in un container MariaDB effimero + conteggi, checksum e verifica di freschezza, con mail solo in caso di problemi.

Perché un file che esiste non è un backup?

Perché tutte le modalità di fallimento interessanti producono comunque un file. Vediamole una per una, in ordine di frequenza sul campo. La prima è il troncamento silenzioso: lo script classico mysqldump ... | gzip > dump.sql.gz dentro un cron ha una proprietà velenosa, l'exit status della pipeline è quello di gzip, non di mysqldump. Se il dump muore a metà, per un timeout, un errore di permessi su una tabella, la connessione caduta, gzip comprime felicemente il parziale e chiude con successo: file presente, data giusta, contenuto monco. In bash la contromisura è set -o pipefail in testa allo script, sempre; e la verifica strutturale è che un dump completo termina con la riga -- Dump completed on ..., che è il sigillo da controllare prima di fidarsi.

La seconda è l'incoerenza interna. Un dump preso su un database attivo senza le opzioni giuste legge le tabelle in sequenza: gli ordini fotografati alle 3:00, le righe di magazzino alle 3:07, e nel mezzo l'applicazione ha scritto. Il risultato importa senza errori e contiene riferimenti che non tornano. Su tabelle InnoDB la soluzione esiste dal secolo scorso e si chiama --single-transaction: uno snapshot consistente a inizio dump, senza lock prolungati; ne ho scritto in dettaglio nella guida a mysqldump senza lock su server di produzione, che di questo articolo è il prerequisito.

La terza è l'incompletezza per default: mysqldump non include tutto quello che pensi. Le stored procedure e le function viaggiano solo con --routines, gli eventi schedulati solo con --events, e chi ha applicazioni che vivono anche di logica nel database può scoprire, il giorno del ripristino, di avere tutte le tabelle e nessun comportamento. La documentazione ufficiale di mysqldump elenca i default, ed è una lettura che consiglio di fare una volta con il proprio script accanto. E c'è un'assenza strutturale che nessun flag risolve: utenti e permessi non fanno parte del dump di un database; se il piano di emergenza è "ricreo il server e importo", senza un export dei grant il ripristino si ferma al primo login fallito dell'applicazione. Il rimedio costa poco: sulle versioni moderne di MariaDB esiste il dump delle tabelle di sistema pensato apposta, e in alternativa un giro scriptato di SHOW GRANTS per ogni utente applicativo produce un file di poche righe che nel giorno dell'emergenza vale oro; l'importante è che quel file esista, viaggi con i dump e abbia la stessa retention.

La quarta è il dump che non si importa altrove: il file è perfetto, ma contiene clausole DEFINER=utente@host dentro viste, trigger e routine, e sul server di emergenza quell'utente non esiste; l'import si blocca o, peggio, procede lasciando fuori gli oggetti incriminati. È il fallimento più beffardo perché si manifesta solo nel momento esatto in cui stai ripristinando per davvero, sotto pressione, su una macchina diversa da quella di origine. Le bonifiche possibili sono due, e conviene sceglierne una adesso: filtrare le clausole DEFINER all'uscita del dump, così gli oggetti vengono ricreati con l'identità di chi importa, oppure garantire che la procedura di emergenza ricrei gli utenti attesi prima dell'import; la peggiore delle opzioni è non aver deciso, che è lo stato di default. La quinta è la rotazione cieca: la retention che cancella per età, combinata con un job rotto da settimane, converge matematicamente verso l'archivio che contiene solo dump difettosi. Ognuna di queste cinque, e lo dico per esperienza diretta di subentri, supera indenne qualunque controllo del tipo "il file esiste ed è più grande di X".

Questa consapevolezza è il motivo per cui nei parchi che gestisco il backup non è un job ma un ciclo con il collaudo dentro: nel mio profilo professionale trovi come imposto continuità e disaster recovery per le PMI, dove la regola è che nessun backup si considera esistente finché non è stato ripristinato da un automatismo o da una mano umana.

Il collaudo automatico: restore settimanale in un container usa e getta

Passiamo al tutorial. L'idea è semplice e potente: una volta a settimana, uno script prende l'ultimo dump prodotto dal backup reale, lo ripristina in un MariaDB effimero dentro un container, esegue una batteria di verifiche e manda una mail solo se qualcosa non torna. Il container è perfetto per questo lavoro: nasce pulito, non tocca il database di produzione, muore alla fine, e l'unico requisito sulla macchina è Docker o Podman. La versione dell'immagine va fissata alla stessa major del server di produzione (nel mio caso tipico MariaDB 11.8, la LTS che Debian 13 porta nei repository), perché collaudare il ripristino su una versione diversa da quella su cui ripristineresti davvero è un test di qualcos'altro.

#!/usr/bin/env bash
# /usr/local/bin/collaudo-restore.sh - via systemd timer, settimanale
set -euo pipefail

DUMP=$(ls -1t /var/backups/mysql/*.sql.gz | head -1)
MAILTO="[email protected]"
CONT="restore-test-$$"

fallisci() { echo "$1" | mail -s "COLLAUDO RESTORE FALLITO su $(hostname)" "$MAILTO"; exit 1; }

# 1. Sigillo di completezza: il dump termina come deve?
zcat "$DUMP" | tail -1 | grep -q "Dump completed" || fallisci "Dump troncato: $DUMP"

# 2. Restore in un container effimero
docker run -d --name "$CONT" -e MARIADB_ROOT_PASSWORD=collaudo mariadb:11.8 >/dev/null
trap 'docker rm -f "$CONT" >/dev/null 2>&1' EXIT
sleep 25   # attesa avvio; in produzione: healthcheck loop

zcat "$DUMP" | docker exec -i "$CONT" mariadb -uroot -pcollaudo \
    || fallisci "Import fallito: $DUMP"

# 3. Verifiche di sostanza (adatta nomi di db e tabelle sentinella)
Q() { docker exec "$CONT" mariadb -uroot -pcollaudo -N -e "$1"; }
ORDINI=$(Q "SELECT COUNT(*) FROM gestionale.ordini")
[ "$ORDINI" -gt 1000 ] || fallisci "Conteggio ordini sospetto: $ORDINI"

FRESCO=$(Q "SELECT MAX(created_at) >= NOW() - INTERVAL 2 DAY FROM gestionale.ordini")
[ "$FRESCO" = "1" ] || fallisci "Dati non freschi: ultimo ordine troppo vecchio"

ROUTINE=$(Q "SELECT COUNT(*) FROM information_schema.routines WHERE routine_schema='gestionale'")
[ "$ROUTINE" -ge 5 ] || fallisci "Routine mancanti nel dump: trovate $ROUTINE"

Le tre verifiche finali sono il cuore del metodo e vanno adattate al tuo schema, ma la logica è generale. Il conteggio su una tabella sentinella smaschera i dump troncati che il sigillo testuale non basta a escludere. La verifica di freschezza sul timestamp più recente smaschera il job che gira ma sta salvando sempre lo stesso vecchio dump, o che punta al database sbagliato: è il controllo che più spesso trova qualcosa, perché fotografa l'intera catena e non solo l'ultimo anello. E il conteggio delle routine smaschera l'incompletezza da default, procedure e trigger che il tuo script non stava esportando. A queste si può aggiungere, per i database dove la deriva silenziosa è un rischio concreto, un confronto di CHECKSUM TABLE su tabelle statiche fra produzione e restore. Il tutto agganciato a un timer systemd settimanale con la notifica di fallimento via OnFailure, lo stesso pattern di monitoraggio essenziale che ho descritto nel pezzo sul monitorare un VPS senza agent e senza SaaS: silenzio quando tutto va, dettagli quando qualcosa si rompe.

C'è un sottoprodotto del collaudo che vale quanto le verifiche stesse: il tempo. Se lo script cronometra l'import, con il valore scritto in un log a ogni esecuzione, ogni settimana ottieni gratis la misura aggiornata del tuo tempo di ripristino, che è il numero da confrontare con l'obiettivo di recovery che hai promesso, a te stesso o ai clienti. E la serie storica di quei tempi è un allarme precoce che nessun altro controllo dà: un import che sei mesi fa durava otto minuti e oggi ne dura venticinque racconta la crescita del database, e ti dice con largo anticipo quando il dump logico smetterà di stare nella finestra, cioè quando sarà ora di cambiare strategia, da posizione di calma invece che di emergenza.

Il collaudo automatico non sostituisce la prova di ripristino completa fatta da un umano, con i tempi cronometrati e la procedura scritta: la integra. Il robot verifica ogni settimana che il materiale sia sano; l'umano verifica ogni tanto che con quel materiale, sotto pressione, si riesca davvero a tornare in piedi. Servono entrambi, e il secondo senza il primo lavora su sabbia.

I confini del metodo, detti onestamente

Questo impianto copre la classe di database per cui mysqldump è lo strumento giusto: fino a qualche decina di gigabyte, con finestre notturne in cui un dump logico completa in tempi civili. Oltre, il dump logico smette di essere sensato prima per il restore che per il backup, un import da centinaia di gigabyte si misura in ore o giorni, e la strada giusta diventa il backup fisico incrementale, che ho trattato nella guida a XtraBackup con recovery point granulare. E il collaudo del database è un tassello di una strategia, non la strategia: la copia fuori macchina, la cifratura, la retention a generazioni e la regola che almeno una copia non sia raggiungibile dalle credenziali del server compromesso restano il perimetro dentro cui questo articolo vive, quello della strategia completa di backup per VPS unmanaged. Il dump verificato dentro una strategia sbagliata è un ottimo materiale nel posto sbagliato. Un ultimo confine riguarda la granularità temporale: il dump notturno fotografa un istante al giorno, quindi il tuo punto di ripristino peggiore è ieri notte, e tutto ciò che è successo dopo è perso per costruzione. Per i database dove perdere una giornata di scritture non è accettabile, la risposta è l'archiviazione del log binario accanto ai dump, che permette il ripristino a un punto arbitrario nel tempo: un altro livello di complessità, da adottare quando il valore dei dati lo giustifica e non prima, ma da conoscere per scegliere con gli occhi aperti.

Chiudo con il punto di metodo, che è poi il motivo per cui questo pezzo esiste: la fiducia nei backup non è uno stato d'animo, è un sottoprodotto della verifica ripetuta. Ogni settimana che il collaudo passa, la tua stima onesta della capacità di ripristino si aggiorna su dati, non su speranze; la settimana che fallisce, hai trovato in un martedì tranquillo il problema che altrimenti avresti scoperto nella peggiore notte dell'anno. Se il tuo backup notturno non ha mai visto un ripristino, oggi è il giorno giusto per dargliene uno, e se preferisci arrivarci con una base collaudata, script, timer e verifiche già rodate su parchi reali, adattata al tuo schema in poche ore, contattami per una consulenza: di tutti gli interventi che faccio, il collaudo dei backup è quello con il miglior rapporto fra costo e sonni tranquilli.

Ultima modifica: