Se domattina il server non c'è più: quanto perdi e quanto ci metti a ripartire
Immagina uno studio con quaranta persone che ha messo in casa un assistente sui propri documenti: un database con utenti e pratiche, una cache, un indice per la ricerca semantica e i file del modello. Una mattina il disco non risponde. Due numeri decidono se è un incidente o una crisi: quanto lavoro hai perso (RPO, Recovery Point Objective) e quanto tempo passa prima che l'assistente risponda di nuovo (RTO, Recovery Time Objective). Non sono numeri tecnici: li sceglie chi risponde dell'attività, e la tecnica serve a rispettarli e a dimostrare che si possono rispettare.
C'è anche una ragione normativa per averli per iscritto. Il GDPR, all'articolo 32 sulla sicurezza del trattamento, elenca tra le misure «la capacità di ripristinare tempestivamente la disponibilità e l'accesso dei dati personali in caso di incidente fisico o tecnico» e «una procedura per testare, verificare e valutare regolarmente l'efficacia delle misure tecniche e organizzative»; la direttiva NIS2, all'articolo 21 paragrafo 2, cita fra le misure di gestione del rischio la continuità operativa, come la gestione del backup e il ripristino in caso di disastro. NIS2 vale solo per i soggetti che rientrano nel suo ambito: se la tua azienda ci rientra va stabilito caso per caso, e questa non è consulenza legale. Il testo che interessa tutti è la seconda frase: un backup mai provato non soddisfa una «procedura per testare».
Questo articolo mette in fila le scelte per quel caso costruito: come si ragiona su RPO e RTO con un programma, che cosa configurare in PostgreSQL e Redis (con i parametri verificati sulla documentazione ufficiale), come si controlla che i backup esistano e siano integri con un programma eseguito, come si cifrano, e come si prova il ripristino. Ci sono due premesse nella serie: quanto carico regge il server, nell'articolo su Ollama da 1 a 50 utenti, e che cosa va protetto in termini di dati personali, in Privacy by design. La tracciabilità di chi fa che cosa, che completa il quadro, è nel prossimo articolo sull'audit log.
RPO e RTO: due numeri che cambiano per ogni componente
Non esiste un solo RPO per «il sistema». Il database dell'assistente contiene lavoro che nessuno può rifare; una cache si riempie da sola; un indice vettoriale è un dato derivato, che si ricostruisce dal database ricalcolando gli embedding. Il criterio per classificare ogni componente è una domanda sola: se lo perdo, da dove lo rigenero e quanto costa farlo? Dove la risposta è «da nessuna parte» servono backup frequenti, dove è «da un'altra copia in qualche ora» basta una copia notturna o nessuna.
Il programma qui sotto mette il ragionamento in numeri per il caso costruito. La prima parte elenca l'RPO nel caso peggiore: con l'archiviazione dei WAL e un archive_timeout di 300 secondi si perdono al più cinque minuti, senza si perde fino a una copia notturna, cioè un giorno. La seconda parte somma i passi del ripristino: quelli in sequenza si sommano, quelli che possono andare in parallelo contano per il più lungo. Tutti i tempi sono ipotesi, da sostituire con quelli che misuri nella prova di ripristino più avanti.
"""RPO e RTO di un assistente interno con quattro componenti. Tutti i tempi sono IPOTESI da sostituire con le misure delle prove di ripristino."""
# RPO: quanto lavoro puoi perdere. Dipende dal meccanismo, non da quanto "frequente" sembra il backup.
rpo_minuti = {
"database, con archiviazione WAL": 5, # archive_timeout = 300 s: i segmenti non pieni vengono chiusi ogni 5 minuti
"database, solo copia notturna": 24 * 60, # se salti la WAL perdi fino a un giorno
"cache e sessioni": 24 * 60, # ricostruibili: si accetta
"indice vettoriale": 24 * 60, # derivato dal database: si ricostruisce
}
print("RPO nel caso peggiore")
for nome, minuti in rpo_minuti.items():
print(f" {nome:34s} {minuti:5d} min")
# RTO: ripristino in sequenza, i passi che possono andare in parallelo contano una volta sola (il più lungo)
sequenza = [
("decidere e dichiarare il disastro", 10),
("provisioning o accensione della macchina", 15),
("ripristino base backup del database", 20),
("riproduzione dei WAL fino al punto scelto", 10),
]
parallelo = [
("ricostruzione dell'indice vettoriale", 90),
("riscaldamento della cache", 5),
]
tot_seq = sum(m for _, m in sequenza)
tot_par = max(m for _, m in parallelo)
print()
print(f"RTO per il solo database in servizio: {tot_seq} min")
print(f"RTO per ricerca semantica completa: {tot_seq + tot_par} min")
obiettivo = 60
print(f"obiettivo di {obiettivo} min: database {'rispettato' if tot_seq <= obiettivo else 'NON rispettato'}, "
f"ricerca completa {'rispettato' if tot_seq + tot_par <= obiettivo else 'NON rispettato'}")
# Output:
# RPO nel caso peggiore
# database, con archiviazione WAL 5 min
# database, solo copia notturna 1440 min
# cache e sessioni 1440 min
# indice vettoriale 1440 min
#
# RTO per il solo database in servizio: 55 min
# RTO per ricerca semantica completa: 145 min
# obiettivo di 60 min: database rispettato, ricerca completa NON rispettato
Il risultato dice una cosa che il solo obiettivo «un'ora» nasconde: il database torna in 55 minuti e l'obiettivo è rispettato, ma l'assistente con la ricerca semantica completa torna in 145, perché la ricostruzione dell'indice (90 minuti nell'ipotesi) è il passo più lungo e dipende da quanti documenti hai da rielaborare. Hai due strade: accettare che la ricerca semantica torni più tardi del resto, dichiarandolo nel piano, oppure includere l'indice nei backup per non doverlo ricostruire. Dato che non so quanti documenti hai, non so quale convenga: lo dice la tua misura.
Il database: base backup, WAL e la prova che il ripristino funziona
La documentazione di PostgreSQL descrive l'archiviazione continua così: una copia completa della cartella dati (il base backup, che si ottiene con pg_basebackup) più la serie dei segmenti WAL generati dopo. Con le due cose puoi riportare il database a qualunque istante successivo al base backup, non solo al momento della copia. Il parametro che rende significativo l'RPO è archive_timeout: l'archiviazione scatta solo per i segmenti completi, e se il traffico è basso la pausa fra una transazione e la sua copia al sicuro può essere lunga; impostando il timeout, il server chiude un segmento ogni intervallo, e fissa un limite all'età dei dati non archiviati.
# Schema da adattare, NON eseguito: nessun server PostgreSQL è stato avviato per questo articolo.
# postgresql.conf del server primario (parametri della documentazione ufficiale, sezione "Continuous Archiving")
wal_level = replica
archive_mode = on
archive_command = 'test ! -f /mnt/archivio/wal/%f && cp %p /mnt/archivio/wal/%f'
archive_timeout = 300 # secondi: chiude e archivia un segmento anche se non è pieno
Il comando di archiviazione dell'esempio è quello della documentazione. La riga che conta è test ! -f ... && cp: il comando deve fallire, cioè restituire un codice diverso da zero, se il file esiste già, perché PostgreSQL non deve mai sovrascrivere un segmento archiviato e deve ritentare finché non ha successo. Un archive_command che restituisce zero senza aver copiato niente è il modo più silenzioso di perdere un backup. Per sapere se l'archiviazione sta funzionando serve guardare la vista pg_stat_archiver, che riporta l'ultimo segmento archiviato e l'ultimo fallito.
-- Schema da adattare, NON eseguito. Da lanciare ogni pochi minuti dal monitoraggio.
-- Se last_failed_time è più recente di last_archived_time, l'archiviazione dei WAL è ferma.
SELECT last_archived_wal, last_archived_time, last_failed_wal, last_failed_time
FROM pg_stat_archiver;
La regola di lettura: se last_failed_time è più recente di last_archived_time, l'archivio è fermo e il tuo RPO reale sta crescendo di minuto in minuto, qualunque cosa dica la configurazione. Va messa sotto monitoraggio con un allarme, non guardata a mano. Il ripristino, a sua volta, segue le istruzioni della sezione della documentazione sul recupero da archivio continuo: si estrae il base backup, si indicano restore_command e un punto di arrivo, si crea il file recovery.signal e si avvia il server, che riproduce i WAL fino al punto scelto. Lo schema qui sotto è da adattare e non eseguito: serviva avviare un server PostgreSQL.
# Schema da adattare, NON eseguito. Ripristino da base backup + WAL (documentazione PostgreSQL, "Recovering Using a Continuous Archive Backup").
# 1. Fermare il server e svuotare la cartella dati; estrarre il base backup nella cartella dati
# 2. Dire al server dove trovare i WAL archiviati e fino a quando riprodurli (postgresql.conf o postgresql.auto.conf)
restore_command = 'cp /mnt/archivio/wal/%f %p'
recovery_target_time = '2026-11-12 14:55:00+01'
# 3. Creare il file che avvia il recupero e far partire il server
touch "$PGDATA/recovery.signal"
pg_ctl -D "$PGDATA" start
Il punto di arrivo (recovery_target_time) è la differenza fra un ripristino e un disastro nel disastro. Se qualcuno ha cancellato per errore una tabella alle 14:56, il ripristino all'ultimo istante disponibile riporta anche la cancellazione; scegliendo le 14:55 recuperi lo stato prima dell'errore. Per questo il backup non si sostituisce con una replica: una replica copia in tempo reale anche la cancellazione. Quella in streaming serve per un altro obiettivo, riaccendere in fretta quando il server si ferma, ed è descritta nella sezione sui server di riserva della stessa documentazione: sul server di riserva si crea il file standby.signal e si indica primary_conninfo.
# Schema da adattare, NON eseguito. Replica in streaming (documentazione PostgreSQL, "Log-Shipping Standby Servers").
# Sul server di riserva: creare il file vuoto standby.signal nella cartella dati, poi in postgresql.conf
primary_conninfo = 'host=192.0.2.10 port=5432 user=replica application_name=riserva'
restore_command = 'cp /mnt/archivio/wal/%f %p' # facoltativo: legge anche dall'archivio se perde il collegamento
# Controllo sul primario: la replica è in corso e quanto è indietro
# SELECT client_addr, state, replay_lsn FROM pg_stat_replication;
# Promozione della riserva a primario, quando il primario è perso:
# pg_ctl promote -D "$PGDATA" (oppure SELECT pg_promote();)
Il parametro standby_mode, che molte guide ancora riportano, è stato rimosso in PostgreSQL 12 e sostituito dal file standby.signal, come dicono le note di rilascio di quella versione: se lo trovi in una guida, quella guida è vecchia. Con la riserva puoi promuoverla a primario con pg_ctl promote o con pg_promote(), come dice la documentazione. La replica asincrona ha un ritardo: i dati scritti sul primario e non ancora arrivati alla riserva si perdono nel passaggio, quindi l'RPO di una promozione è il ritardo di replica al momento del guasto, non zero. La vista pg_stat_replication mostra lo stato e la posizione, e il ritardo va monitorato come quello dell'archivio.
Redis: prima decidi che cos'è, cache o archivio
Se Redis nel tuo sistema tiene solo cache e contatori che si ricostruiscono, la persistenza è un comfort. Se tiene anche sessioni, code di lavori o l'indice di ricerca, la persistenza è un requisito. La documentazione ufficiale descrive due meccanismi: l'RDB (istantanee a intervalli) e l'AOF (registro di ogni scrittura). Dice anche una cosa utile per i backup: il file RDB non viene mai modificato dopo che è stato prodotto, e viene scritto con un nome temporaneo e rinominato in modo atomico, quindi copiarlo mentre il server funziona è sicuro. Il suggerimento della documentazione è una copia oraria in una cartella e una giornaliera in un'altra, con find che elimina le più vecchie.
# Schema da adattare, NON eseguito. redis.conf del primario.
save 900 1 # esempio: istantanea RDB se c'è almeno 1 modifica in 900 s
save 300 10 # oppure 10 modifiche in 300 s
save 60 10000 # oppure 10000 modifiche in 60 s
appendonly yes
appendfsync everysec # fsync una volta al secondo: si possono perdere fino a ~1 secondo di scritture
# Sulla replica, per promuoverla a primario in caso di guasto (da redis-cli):
# REPLICAOF NO ONE
I tre save dicono: istantanea se c'è almeno una modifica in 900 secondi, o dieci in 300, o diecimila in sessanta. appendfsync everysec fa scrivere il registro su disco una volta al secondo: la documentazione dice che è abbastanza veloce e che in caso di disastro si può perdere un secondo di dati. Da Redis 7 l'AOF non è più un file solo ma un insieme di file (un file base e file incrementali) in una cartella: se copi a mano un vecchio appendonly.aof stai copiando la cosa sbagliata. Sulla replica, per promuoverla, il comando è REPLICAOF NO ONE, che la documentazione descrive come il modo per tornare primario; il vecchio nome SLAVEOF è deprecato.
Non ho verificato come un backup RDB tratti l'indice di ricerca vettoriale, e il testo non lo afferma: nella prova di ripristino controlla con il tuo comando di lettura dello stato dell'indice (FT.INFO restituisce informazioni e statistiche) che il numero di documenti sia quello atteso. Se non lo è, la strada è ricostruire dal database. In nessun caso usare FLUSHDB come rimedio rapido: cancella tutte le chiavi della base di dati selezionata, non solo l'indice.
Modelli e indice: dati derivati, con una dipendenza nascosta
I file dei modelli non contengono stato tuo e si possono riscaricare. Ma «si possono riscaricare» è una dipendenza dal registro dei modelli nel momento del disastro, e dalla tua banda: il tempo è la dimensione del file diviso la velocità della linea. Per un modello da 7,25 miliardi di parametri a 4 bit, l'articolo sul dimensionamento calcola un minimo teorico di circa 3,4 GiB: il tempo di scaricamento si calcola con la tua linea. Se il ripristino deve funzionare anche quando il registro è irraggiungibile, tieni una copia dei file dei modelli che usi nell'archivio dei backup, con la loro impronta, e annota la versione esatta: il modello giudice di RAG evaluation e quello di generazione devono tornare identici, o i punteggi di prima non sono più confrontabili.
L'indice vettoriale è derivato dal database, ma ricostruirlo costa tempo (nel programma, 90 minuti di ipotesi) e dipende dal modello di embedding: se il modello cambia, gli embedding non sono più compatibili e va ricostruito tutto. Per questo conviene registrare con ogni backup il nome e la versione del modello di embedding usato. La configurazione dei servizi (unit di systemd, Nginx, parametri di database) non è dato: è codice, e il suo backup è un repository versionato, non uno script che copia cartelle.
Un backup non verificato è un'ipotesi
Il guasto più comune dei backup non è l'incendio ma il silenzio: lo script gira, scrive un file da zero byte, e nessuno se ne accorge per sei mesi. Tre controlli automatici, poco costosi, lo prevengono: ogni file ha l'impronta registrata nel manifesto e coincide; il backup più recente non è più vecchio dell'RPO; nessun file è molto più piccolo della mediana. Il programma esegue i tre controlli su una cartella temporanea in cui costruisce sette backup di esempio, poi li danneggia in tre modi tipici: un file troncato, un file alterato senza cambiare la dimensione, un file sparito. L'orologio è un parametro, così l'uscita è la stessa a ogni esecuzione.
"""Verifica di una cartella di backup: manifesto SHA-256, freschezza rispetto all'RPO, dimensioni anomale."""
import hashlib
import os
import tempfile
from datetime import datetime, timedelta
from statistics import median
ADESSO = datetime(2026, 11, 13, 6, 0, 0) # passato come parametro: nessuna dipendenza dall'orologio
def sha256(percorso):
h = hashlib.sha256()
with open(percorso, "rb") as f:
for blocco in iter(lambda: f.read(65536), b""):
h.update(blocco)
return h.hexdigest()
def controlla(cartella, manifesto, adesso, rpo, tolleranza=0.5):
problemi = []
dimensioni = {}
for nome, atteso in manifesto.items():
p = os.path.join(cartella, nome)
if not os.path.exists(p):
problemi.append(f"{nome}: MANCANTE")
continue
dimensioni[nome] = os.path.getsize(p)
if sha256(p) != atteso:
problemi.append(f"{nome}: impronta diversa dal manifesto")
if dimensioni:
mediana = median(dimensioni.values())
for nome, d in dimensioni.items():
if d < mediana * tolleranza:
problemi.append(f"{nome}: {d} byte contro mediana {mediana:.0f}")
piu_recente = max(datetime.fromtimestamp(os.path.getmtime(os.path.join(cartella, n))) for n in dimensioni)
if adesso - piu_recente > rpo:
problemi.append(f"ultimo backup di {adesso - piu_recente} fa: oltre l'RPO di {rpo}")
return problemi
with tempfile.TemporaryDirectory() as d:
manifesto = {}
for giorno in range(7):
nome = f"db_{giorno}.sql.gz"
contenuto = (f"riga {giorno} " * 2000).encode()
with open(os.path.join(d, nome), "wb") as f:
f.write(contenuto)
manifesto[nome] = hashlib.sha256(contenuto).hexdigest()
quando = (ADESSO - timedelta(days=6 - giorno, hours=3)).timestamp()
os.utime(os.path.join(d, nome), (quando, quando))
print("1) cartella integra:", controlla(d, manifesto, ADESSO, timedelta(hours=26)) or "nessun problema")
# Un file troncato (disco pieno a metà scrittura) e uno alterato senza cambiare la dimensione
with open(os.path.join(d, "db_6.sql.gz"), "wb") as f:
f.write(b"x" * 300)
t6 = (ADESSO - timedelta(hours=3)).timestamp()
os.utime(os.path.join(d, "db_6.sql.gz"), (t6, t6)) # il file riscritto riprende il suo orario: l'esempio non dipende dall'orologio
alterato = os.path.join(d, "db_2.sql.gz")
dati = bytearray(open(alterato, "rb").read())
dati[10] ^= 0xFF
open(alterato, "wb").write(bytes(dati))
os.remove(os.path.join(d, "db_4.sql.gz"))
print("2) dopo i danni:")
for p in controlla(d, manifesto, ADESSO, timedelta(hours=26)):
print(" ", p)
print("3) stesso stato, tre giorni dopo:")
for p in controlla(d, manifesto, ADESSO + timedelta(days=3), timedelta(hours=26)):
if "RPO" in p:
print(" ", p)
# Output:
# 1) cartella integra: nessun problema
# 2) dopo i danni:
# db_2.sql.gz: impronta diversa dal manifesto
# db_4.sql.gz: MANCANTE
# db_6.sql.gz: impronta diversa dal manifesto
# db_6.sql.gz: 300 byte contro mediana 14000
# 3) stesso stato, tre giorni dopo:
# ultimo backup di 3 days, 3:00:00 fa: oltre l'RPO di 1 day, 2:00:00
La prima riga conferma che la cartella integra non produce falsi allarmi. Dopo i danni, il controllo dell'impronta trova il file alterato (db_2), che il controllo sulla dimensione non avrebbe visto, e il file troncato (db_6), che viene segnalato due volte, per impronta e per dimensione; il file mancante compare come tale. La terza prova sposta l'orologio avanti di tre giorni e il controllo di freschezza scatta. Quello che il programma non può dire è se il contenuto sia ripristinabile: l'impronta prova che il file è quello scritto, non che sia un backup valido. Solo la prova di ripristino lo dimostra.
Cifrare i backup: la chiave non sta dove stanno i backup
Un backup contiene gli stessi dati personali del sistema vivo, e in più sta in un posto che spesso ha protezioni più deboli. Lo stesso articolo 32 del GDPR cita la cifratura dei dati personali fra le misure appropriate. La regola che pesa più della scelta dello strumento è la gestione della chiave: se sta solo sul server che hai perso, i backup cifrati sono una pila di byte inutile; se sta insieme ai backup, la cifratura non protegge niente. La chiave va conservata altrove, e va provata quanto il backup.
Il programma che segue usa GnuPG con cifratura simmetrica AES-256 in una cartella temporanea con un portachiavi isolato, cifra un file, lo decifra e confronta l'impronta con l'originale, poi prova una chiave sbagliata. La passphrase è di prova e non va riusata: in produzione sta nel gestore dei segreti. La dimensione del file cifrato non viene stampata perché varia da una esecuzione all'altra.
#!/bin/bash
# Cifratura simmetrica di un backup con GnuPG e prova di ripristino. Ambiente isolato in una cartella temporanea.
set -euo pipefail
LAVORO=$(mktemp -d)
export GNUPGHOME="$LAVORO/gnupg"; mkdir -m 700 "$GNUPGHOME"
printf 'chiave-di-prova-da-non-riusare' > "$LAVORO/passphrase" # in produzione sta fuori dal server dei backup
head -c 100000 /dev/zero | tr '\0' 'a' > "$LAVORO/db.sql"
sha256sum "$LAVORO/db.sql" | cut -d' ' -f1 > "$LAVORO/db.sha256"
gpg --batch --quiet --pinentry-mode loopback --passphrase-file "$LAVORO/passphrase" \
--symmetric --cipher-algo AES256 --output "$LAVORO/db.sql.gpg" "$LAVORO/db.sql"
echo "cifrato: nel file non compare il testo in chiaro (occorrenze di 'aaaa': $(grep -c aaaa "$LAVORO/db.sql.gpg" || true))"
gpg --batch --quiet --pinentry-mode loopback --passphrase-file "$LAVORO/passphrase" \
--decrypt --output "$LAVORO/ripristinato.sql" "$LAVORO/db.sql.gpg"
if [ "$(sha256sum "$LAVORO/ripristinato.sql" | cut -d' ' -f1)" = "$(cat "$LAVORO/db.sha256")" ]; then
echo "ripristino: impronta identica all'originale"
fi
printf 'chiave-sbagliata' > "$LAVORO/sbagliata"
if gpg --batch --quiet --pinentry-mode loopback --passphrase-file "$LAVORO/sbagliata" \
--decrypt --output /dev/null "$LAVORO/db.sql.gpg" 2>/dev/null; then echo "ERRORE: chiave sbagliata accettata"; else echo "chiave sbagliata: decifratura rifiutata"; fi
rm -rf "$LAVORO"
# Output:
# cifrato: nel file non compare il testo in chiaro (occorrenze di 'aaaa': 0)
# ripristino: impronta identica all'originale
# chiave sbagliata: decifratura rifiutata
L'uscita mostra le tre cose che servono a una procedura di cifratura: nel file cifrato non compare il testo in chiaro, il ripristino restituisce un file con impronta identica, e la chiave sbagliata viene rifiutata. È uno strumento di esempio, non una raccomandazione unica: restic, che cifra e deduplica i dati nel repository, usa AES-256 in modalità CTR con un codice di autenticazione Poly1305-AES per l'integrità, come descrive la sua documentazione, e ha il vantaggio dei backup incrementali. Non l'ho eseguito qui. Qualunque strumento scegli, prova il ripristino con la sola chiave che hai custodito altrove.
La prova di ripristino: il solo modo di conoscere il tuo RTO
Il GDPR chiede una procedura per testare regolarmente, e il tuo RTO, finché non l'hai misurato, è un augurio. Una prova di ripristino si fa su una macchina di prova, con un cronometro, seguendo la procedura scritta e non la memoria di chi l'ha scritta. L'ordine dei passi conta, e il cronometro segna il tempo di ciascuno: sono i numeri che sostituiscono le ipotesi del primo programma.
- Accendi una macchina pulita, senza accesso ai dati di produzione se non tramite l'archivio dei backup, e inizia a contare.
- Recupera la chiave dal luogo dove la tieni fuori dal server e decifra l'ultimo base backup.
- Ripristina il database con base backup e WAL fino a un punto scelto e verifica che il numero di record di una tabella nota sia quello atteso.
- Ripristina Redis dall'ultima copia dell'RDB e controlla lo stato dell'indice; se non è integro, ricostruiscilo dal database e annota quanto ci metti.
- Riporta i modelli dall'archivio dei backup o dal registro e verifica le impronte.
- Esegui sul sistema ripristinato una decina di domande del golden dataset e confronta i punteggi con l'ultima misura.
- Scrivi il tempo di ogni passo e che cosa è andato storto, poi aggiorna il piano e le ipotesi dei programmi.
L'ultimo passo è quello che quasi tutti saltano. L'elenco è in ordine perché ogni passo dipende dal precedente: senza chiave non decifri, senza database non c'è nulla da interrogare. Le domande del golden dataset, nate nell'articolo sulla valutazione, servono qui come prova funzionale: un sistema che si avvia ma risponde diversamente da prima non è ripristinato. Se la prova richiede più dell'RTO dichiarato, non hai un problema di script ma un problema di piano, e la correzione può essere cambiare l'obiettivo, non solo il sistema.
Domande frequenti
Una replica sostituisce il backup?
No. La replica copia anche un errore, come una tabella cancellata, nel momento in cui avviene. Serve a ripartire in fretta se il server si ferma, non a tornare indietro nel tempo, che è ciò che fanno base backup e archivio dei WAL.
Che RPO scelgo per un assistente sui documenti?
Dipende da quanto lavoro puoi permetterti di rifare. Per il database, con l'archiviazione dei WAL e un archive_timeout di 300 secondi, il caso peggiore è di circa cinque minuti. Per cache e indice, che si ricostruiscono, di solito si accetta una copia giornaliera o nessuna.
Ogni quanto devo provare il ripristino?
La normativa chiede una verifica regolare e non fissa la cadenza. Una prova a ogni cambiamento importante dell'infrastruttura e almeno una volta a trimestre è una scelta ragionevole, ma è una scelta tua, da scrivere nel piano.
Posso copiare il file dell'RDB mentre Redis lavora?
Sì: la documentazione ufficiale spiega che il file RDB non viene modificato dopo la produzione e che viene rinominato in modo atomico a istantanea completata, quindi la copia a server acceso è sicura.
Che cosa sai fare adesso
Sai dire quanto lavoro perdi e quanto tempo serve a ripartire, componente per componente, e sai far cadere gli obiettivi quando una misura li smentisce. Sai configurare l'archiviazione dei WAL di PostgreSQL con un limite di età dei dati non archiviati, sai che una replica non sostituisce un backup e che il punto di ripristino si sceglie. Sai controllare che i backup esistano, siano freschi e integri, cifrarli senza perdere la chiave e provare il ripristino con il cronometro. Il passo dopo è dimostrare, con un registro, che cosa il sistema ha fatto: audit log e tracciabilità.
Nella stessa serie trovi anche il conto dei costi di un server in proprio, i casi in cui conviene un servizio gestito e il riepilogo con la scheda di decisione.
Fonti
- PostgreSQL - Continuous Archiving and Point-in-Time Recovery - archive_command, restore_command, recovery.signal, consultata il 2026-11-13
- PostgreSQL - Write Ahead Log (parametri di configurazione) - archive_timeout, consultata il 2026-11-13
- PostgreSQL - Log-Shipping Standby Servers - standby.signal, primary_conninfo, promozione, consultata il 2026-11-13
- PostgreSQL - The Cumulative Statistics System - colonne di pg_stat_archiver, consultata il 2026-11-13
- PostgreSQL - Release notes 12 - rimozione di standby_mode e introduzione di standby.signal e recovery.signal, consultata il 2026-11-13
- Redis - Persistence - RDB, AOF, appendfsync, copia sicura dell'RDB, AOF a più file da Redis 7, consultata il 2026-11-13
- Redis - REPLICAOF - REPLICAOF NO ONE, consultata il 2026-11-13
- restic - Design e riferimenti - cifratura AES-256 CTR con Poly1305-AES, consultata il 2026-11-13
- Regolamento (UE) 2016/679 (GDPR), articolo 32 - ripristino tempestivo, cifratura, procedura di verifica, consultata il 2026-11-13
- Direttiva (UE) 2022/2555 (NIS2), articolo 21 - continuità operativa, backup e ripristino in caso di disastro, consultata il 2026-11-13
I programmi dell'articolo sono stati eseguiti con Python 3.14.4 e GnuPG 2.4.8, due volte ciascuno con uscita identica; tutti i tempi e i dati sono ipotesi dichiarate o esempi costruiti. Gli schemi PostgreSQL, Redis e le procedure di ripristino non sono stati eseguiti perché non è stato avviato alcun server.

Commenti
Caricamento commenti...
Accedi per commentare