Modifiche allo schema senza downtime con il pattern expand/contract
Pianifica e rilascia modifiche allo schema senza downtime con il pattern expand/contract, backfill sicuri, release compatibili, verifiche e rollback.

Perché le modifiche allo schema causano interruzioni
Le modifiche allo schema causano interruzioni quando le versioni dell'applicazione, i worker in background e il database non concordano più su quali strutture e valori siano validi. Il problema può essere evidente, per esempio quando ogni richiesta restituisce un errore, oppure graduale, con latenza delle query in aumento, scritture non riuscite, ritardo delle repliche e una coda di job da rieseguire.
Un deployment in produzione raramente modifica tutti i processi nello stesso istante. I rilasci progressivi lasciano in esecuzione insieme istanze vecchie e nuove dell'applicazione. I worker di lunga durata possono mantenere una build precedente per ore, i client mobili possono restare attivi per mesi e job di reportistica o integrazione possono usare le tabelle senza passare dall'applicazione principale. Tutti condividono un unico database.
I problemi più comuni includono:
- Il nuovo codice scrive in una colonna prima che sia completata la migrazione che la crea.
- Il vecchio codice legge una tabella o una colonna che una release successiva ha rinominato o rimosso.
- Una riscrittura della tabella, un backfill o la creazione di un indice consuma abbastanza I/O e CPU da rallentare il traffico normale.
- Un comando sullo schema attende un lock mentre le richieste si accumulano dietro di esso.
- Un nuovo vincolo rifiuta scritture provenienti da un processo non ancora aggiornato.
La parte pericolosa spesso è l'acquisizione del lock, non il tempo di esecuzione previsto. Un rapido ALTER TABLE può restare in attesa dietro una transazione lunga. Mentre attende, le query successive possono mettersi in coda dietro il lock sullo schema in sospeso, trasformando una piccola migrazione in un blocco dell'intera applicazione.
L'assenza di downtime richiede che ogni stato intermedio del database resti utilizzabile da ogni versione dell'applicazione che può ancora essere in esecuzione. Aggiungi prima strutture compatibili, sposta traffico e dati in passaggi controllati e rimuovi il percorso precedente solo quando il suo ultimo consumer è sparito.
Questo lavoro è giustificato per sistemi con traffico attivo, deployment progressivi, obiettivi di disponibilità rigorosi o procedure di ripristino costose. Un piccolo strumento interno con un database poco usato può essere gestito meglio con una finestra di manutenzione testata. La decisione deve riflettere il costo di un'interruzione e la complessità operativa della migrazione.
Expand/contract in parole semplici
Il pattern expand/contract trasforma una modifica incompatibile in una sequenza di release compatibili. Il database supporta temporaneamente due rappresentazioni mentre codice e dati passano da quella vecchia a quella nuova.
La sequenza ha tre parti:
- Espandi aggiungendo colonne, tabelle, indici o vincoli senza rimuovere ciò che serve al codice corrente.
- Transizione distribuendo codice compatibile, spostando i dati storici e indirizzando letture e scritture verso la nuova rappresentazione.
- Contrai eliminando il vecchio codice e gli oggetti del database dopo che la verifica ha dimostrato che non vengono usati.
Immagina una tabella PostgreSQL che memorizza il nome di una persona in full_name, mentre l'applicazione richiede campi distinti first_name e last_name. L'espansione aggiunge colonne nullable mantenendo full_name. Una release compatibile scrive le rappresentazioni necessarie durante la transizione. Un backfill separa i valori esistenti, con una politica esplicita per i nomi che non possono essere divisi in modo affidabile. Le letture vengono spostate solo quando i nuovi campi sono sufficientemente completi. In seguito, la contrazione rimuove full_name.
Questo ordine si adatta ai deployment progressivi perché la vecchia build trova ancora full_name e quella nuova trova tutte e tre le colonne. Mantiene anche un percorso di rollback dell'applicazione. Se la nuova release si comporta male, la build precedente può continuare a funzionare perché le sue dipendenze dallo schema non sono state rimosse.
Il rollback del database è diverso dal rollback dell'applicazione. Invertire una migrazione dopo una trasformazione dei dati può eliminare informazioni o ripristinare un valore obsoleto. Durante la transizione, è preferibile riportare il traffico dell'applicazione alla rappresentazione nota lasciando in sede gli oggetti additivi del database. Correggi la migrazione in avanti dopo che l'incidente si è stabilizzato.
Il pattern non significa che ogni modifica richieda codice di doppia scrittura. Aggiungere una colonna opzionale usata solo dal nuovo codice può richiedere una migrazione additiva e un deployment. Ridenominazioni, modifiche della rappresentazione, suddivisioni di tabelle e cambiamenti ai campi obbligatori richiedono in genere più fasi, poiché altrimenti due versioni dell'applicazione non possono condividere lo schema in sicurezza.
Classifica la modifica prima di scegliere i passaggi
Il piano di migrazione deve corrispondere ai rischi effettivi dell'operazione in termini di lock, riscrittura, compatibilità e conversione dei dati. Considerare ogni ALTER TABLE equivalente porta a procedure inutilmente complesse o a una release non sicura.
Le modifiche additive sono in genere le più semplici. Una colonna nullable, una tabella separata o un indice creato con un metodo online spesso possono essere introdotti prima che il codice dell'applicazione li usi. Il comando richiede comunque un lock, quindi testa il suo comportamento su una tabella e un carico transazionale simili alla produzione.
Le modifiche distruttive includono l'eliminazione o la ridenominazione di colonne, la restrizione dei tipi, la sostituzione di tabelle e l'aggiunta di vincoli più severi. Queste modifiche invalidano un presupposto del codice esistente. Inseriscile nella fase di contrazione, dopo aver rimosso riferimenti dal codice e consumer esterni.
Le operazioni che modificano dati meritano una valutazione specifica. Convertire timestamp, normalizzare numeri di telefono, unire record o dividere testo libero può comportare perdita di informazioni. Definisci come verranno gestiti valori non validi e ambigui prima di avviare il backfill. Se una trasformazione non può essere invertita, conserva l'origine finché il risultato non ha superato controlli a livello di business.
Una revisione preliminare utile copre cinque domande:
- Quale lock richiede ogni istruzione e per quanto tempo può attenderlo o mantenerlo?
- L'operazione riscriverà la tabella, genererà molto WAL o aumenterà il ritardo delle repliche?
- Quali applicazioni, job, report e consumer di change data capture usano gli oggetti interessati?
- Le release attuali e proposte possono funzionare con ogni stato transitorio?
- Quale segnale sospende l'operazione e quale stato preciso resta dopo l'interruzione?
Esegui la migrazione esatta su dati con volume e distribuzione realistici. Una tabella di test con mille righe ordinate dice poco di una tabella di produzione con centinaia di milioni di righe, tuple ampie, righe morte, valori distribuiti in modo irregolare e transazioni lunghe.
Espandi in sicurezza in PostgreSQL
Un'espansione PostgreSQL sicura usa brevi modifiche ai metadati, attese per i lock limitate e operazioni online separate quando il database le richiede. Aggiungi la nuova struttura prima di distribuire codice che ne dipende.
L'aggiunta di una colonna nullable senza valore predefinito è di solito una breve operazione sui metadati:
BEGIN;
SET LOCAL lock_timeout = '2s';
SET LOCAL statement_timeout = '30s';
ALTER TABLE customers
ADD COLUMN phone_e164 text;
COMMIT;
Il timeout evita che la release attenda indefinitamente dietro una transazione aperta. Se il lock non può essere acquisito rapidamente, lascia fallire la migrazione, analizza il blocco e riprova in un momento più sicuro. Non riprovare automaticamente in un ciclo serrato, perché richieste di lock ripetute possono continuare a disturbare il traffico di produzione.
Le versioni moderne di PostgreSQL possono aggiungere una colonna con un valore predefinito costante senza scrivere subito quel valore in ogni riga esistente. Questa ottimizzazione non rende innocuo ogni valore predefinito. Un'espressione volatile può richiedere una riscrittura e ALTER TABLE necessita comunque di un breve lock ACCESS EXCLUSIVE. Verifica il comportamento per la versione PostgreSQL distribuita e per l'espressione esatta, senza affidarti a una regola generale.
Un normale CREATE INDEX può bloccare le scritture. Usa la creazione concorrente quando la tabella deve restare scrivibile:
CREATE INDEX CONCURRENTLY idx_customers_phone_e164
ON customers (phone_e164);
CREATE INDEX CONCURRENTLY non può essere eseguito dentro un blocco di transazione. Richiede più tempo, svolge lavoro aggiuntivo e può attendere transazioni precedenti, ma inserimenti, aggiornamenti ed eliminazioni normali possono continuare. Consuma comunque CPU, I/O e WAL, quindi monitora la latenza del database e le repliche mentre è in esecuzione.
Una creazione concorrente non riuscita può lasciare un indice non valido. Controlla lo stato dell'indice prima di riprovare, quindi rimuovi o ricrea deliberatamente l'oggetto non valido. Gli strumenti di migrazione che racchiudono ogni file in una transazione devono supportare una modalità non transazionale per le operazioni concorrenti sugli indici.
Spesso è più semplice introdurre nuove tabelle che eseguire trasformazioni sul posto. Per una relazione uno-a-molti o molti-a-molti, aggiungi la tabella di destinazione e i relativi indici mantenendo la colonna di origine. Rimanda l'eliminazione dell'origine finché non sono stati spostati nuove scritture, dati storici, letture e consumer a valle.
Le modifiche dei tipi richiedono ulteriore attenzione. Alcune riguardano solo i metadati, altre riscrivono ogni riga o acquisiscono un lock restrittivo troppo a lungo. Per una conversione rischiosa, aggiungi una colonna con il tipo di destinazione, popolala in batch, cambia l'accesso dell'applicazione e rimuovi l'originale in seguito. Questo offre anche al team un punto in cui registrare gli errori di conversione invece di far riuscire o fallire in blocco un grande ALTER COLUMN TYPE.
Distribuisci codice che resta compatibile
Il codice dell'applicazione compatibile tollera valori transitori mancanti e non richiede una migrazione distruttiva durante lo stesso rollout. L'espansione del database deve essere completata prima che la prima istanza dell'applicazione inizi a usare il nuovo oggetto.
La doppia scrittura è utile quando entrambe le rappresentazioni devono restare aggiornate. Esegui entrambe le scritture nella stessa transazione del database ogni volta che è possibile. Una seconda scrittura asincrona può fallire dopo che la prima ha avuto successo, creando una divergenza che le letture successive potrebbero mostrare.
La logica di doppia scrittura richiede anche un'unica fonte di autorità. Se phone_e164 deriva da phone, definisci quale input prevale quando vengono forniti entrambi e applica la stessa normalizzazione nei gestori API, nei worker, nelle importazioni e negli strumenti amministrativi. Altrimenti due percorsi di codice apparentemente corretti possono memorizzare risultati diversi.
Le letture dovrebbero passare più tardi delle scritture. Mantieni le letture sul campo consolidato mentre le nuove scritture popolano entrambe le forme e il backfill gestisce le righe storiche. Dopo la verifica, distribuisci un percorso di lettura che preferisce il nuovo campo e usa il valore precedente solo secondo una regola di fallback definita. Misura l'uso del fallback. Un fallback silenzioso può nascondere dati incompleti per sempre.
Una sequenza di release tipica è:
- La release 1 aggiunge i nuovi oggetti del database senza cambiare il comportamento dell'applicazione.
- La release 2 scrive le rappresentazioni transitorie continuando a usare le letture consolidate.
- La release 3 sposta le letture dopo il superamento di backfill e controlli di coerenza.
- La release 4 smette di mantenere la vecchia rappresentazione dopo la scadenza dei criteri di rollback.
- La release 5 rimuove i vecchi riferimenti del codice, seguita in seguito dalla pulizia del database.
Mantieni separati i contratti API pubblici dalle modifiche fisiche dello schema. La ridenominazione di una colonna del database non richiede di rinominare subito un campo nelle risposte web, mobile o di integrazione. Modifica quei contratti secondo una propria politica di compatibilità, soprattutto quando i client non possono essere aggiornati insieme al server.
Fai l'inventario di ogni writer. I gestori HTTP sono solo una fonte di modifiche. Consumer di code, job pianificati, script di importazione, strumenti di riparazione dati, trigger di database e operazioni amministrative dirette possono continuare a produrre righe nel vecchio formato. Quando possibile, assegna un nome applicativo alle connessioni al database e registra l'uso dei percorsi transitori, così un processo trascurato diventa visibile.
I processi di lunga durata possono conservare presupposti obsoleti tramite istruzioni preparate, metadati in cache o un livello di mappatura oggetto-relazionale. Testa i riavvii progressivi e il comportamento del pool di connessioni prima della contrazione. Un processo che non ha generato traffico di recente può comunque fallire alla prima esecuzione di un job raro.
Esegui il backfill senza sovraccaricare il database
Un backfill sicuro aggiorna batch piccoli e riprendibili e rallenta quando la salute della produzione peggiora. Inizia solo dopo che i writer attivi possono mantenere la nuova rappresentazione.
Scegli i batch in base al tempo trascorso e all'impatto sul database, non a un numero universale di righe. Mille righe strette possono terminare in millisecondi, mentre mille righe con valori grandi o trasformazioni costose possono generare I/O significativo. Parti con cautela e punta a transazioni che terminano in pochi secondi. Esegui il commit tra un batch e l'altro, così lock e vecchie versioni delle righe non si accumulano in una sola transazione.
PostgreSQL non supporta direttamente ORDER BY e LIMIT in un semplice UPDATE. Seleziona un batch in una common table expression, quindi aggiorna quelle righe:
WITH batch AS (
SELECT id
FROM my_table
WHERE id > $1
AND new_col IS NULL
ORDER BY id
LIMIT 1000
)
UPDATE my_table AS target
SET new_col = transform_expression(target.old_col)
FROM batch
WHERE target.id = batch.id
AND target.new_col IS NULL
RETURNING target.id;
L'applicazione registra il maggiore id completato come cursore. L'aggiornamento condizionale rende idempotenti le riesecuzioni, quindi un crash dopo il commit non corrompe le righe già elaborate. Salva l'avanzamento con sufficiente attenzione affinché il cursore non possa avanzare oltre un batch non confermato.
Un cursore id crescente evita di scandire ripetutamente l'inizio della tabella, ma non intercetta correzioni tardive o righe inserite al di sotto del cursore. Completa con un passaggio di recupero su tutti i valori NULL rimanenti. Se gli identificatori non sono ordinati o le righe possono passare da uno stato di idoneità a un altro, usa una tabella di lavoro o un altro checkpoint esplicito invece di presumere che una scansione in avanti sia completa.
Più worker possono acquisire righe con FOR UPDATE SKIP LOCKED, ma il parallelismo aumenta la pressione di scrittura e complica il monitoraggio dell'avanzamento. Non combinare righe saltate con un cursore che avanza definitivamente oltre di esse. Una coda di identificatori acquisiti o una scansione ripetuta dei criteri di idoneità è più sicura per worker paralleli.
Regola la velocità in base a misurazioni di produzione come latenza delle query, connessioni attive, attese per lock, generazione WAL, ritardo nel replay delle repliche e crescita delle righe morte. Sospendi quando viene superata una soglia, poi riprendi dal checkpoint. Attese fisse sono semplici, ma il feedback dal database risponde meglio ai cambiamenti nel traffico.
Evita di modificare ogni riga quando solo alcune richiedono interventi. Filtra sul nuovo campo, sullo stato di origine o su un indicatore di migrazione. Se la trasformazione è costosa, calcolala fuori dalla transazione di aggiornamento quando la coerenza lo consente, poi esegui una breve scrittura condizionale. Conserva un conteggio e un campione dei valori rifiutati invece di inventare dati in silenzio.
Autovacuum e le repliche devono assorbire il lavoro dopo ogni aggiornamento. Un backfill può terminare con successo sul primario mentre le repliche restano molto indietro o il bloat della tabella peggiora le query successive. I limiti di velocità devono considerare questo costo ritardato, non solo il tempo di esecuzione immediato del batch.
Verifica i dati e il traffico di produzione
Una migrazione è pronta per la contrazione solo quando controlli sui dati, telemetria dell'applicazione e prove sulle dipendenze concordano sul fatto che il nuovo percorso è autorevole. Un contatore di job completati non dimostra da solo la correttezza.
Inizia da completezza e coerenza. IS DISTINCT FROM di PostgreSQL confronta i valori gestendo esplicitamente NULL, a differenza di <>, che produce un risultato sconosciuto quando uno dei due lati è NULL:
SELECT count(*)
FROM customers
WHERE normalize_phone(phone) IS DISTINCT FROM phone_e164;
Non eseguire ripetutamente un conteggio sull'intera tabella non indicizzato su una tabella molto grande e occupata. Usa una convalida controllata una tantum, intervalli di identificatori limitati, campioni o un processo di verifica temporaneo che avanza nella tabella. Il metodo corretto dipende dal costo di un errore e dalla capacità residua disponibile nel database.
La verifica deve includere:
- Non restano valori mancanti imprevisti nelle righe che richiedono il nuovo campo.
- Il nuovo valore corrisponde alla trasformazione concordata, inclusi input non validi e vuoti.
- Nuove righe e aggiornamenti restano coerenti dopo il completamento del passaggio sui dati storici.
- L'uso delle letture di fallback ha raggiunto la soglia pianificata, in genere zero per il traffico controllato dal server.
- Tassi di errore, latenza delle query, lock e ritardo delle repliche restano entro i limiti della release.
Confronta anche gli esiti di business, non solo le colonne. Se una migrazione modifica prezzi, autorizzazioni, stato dell'account o identificatori, convalida totali e invarianti su cui gli utenti fanno affidamento. Due colonne possono corrispondere meccanicamente e contenere entrambe una regola di business sbagliata.
Osserva un intero ciclo operativo prima della pulizia. L'intervallo corretto dipende dal comportamento reale del sistema, non da una regola fissa di una settimana. Può dover includere elaborazioni di fine mese, un job di fatturazione poco frequente, tentativi ritardati di code o la durata massima di un vecchio client mobile. Registra le prove che ogni consumer è stato spostato.
Esegui un canary dello spostamento delle letture se l'architettura dell'applicazione lo consente. Invia una piccola parte del traffico al nuovo percorso di lettura, confronta i risultati e amplia gradualmente. Mantieni semplice l'azione di rollback: reindirizza le letture alla rappresentazione consolidata senza invertire il backfill.
Aggiungi vincoli quando i dati sono pronti
I vincoli devono diventare rigorosi solo dopo che tutti i writer sono conformi e i dati esistenti sono stati convalidati. Applicare NOT NULL, un check o una chiave esterna durante l'espansione può bloccare il traffico o rifiutare scritture da un processo precedente.
PostgreSQL può aggiungere un vincolo CHECK come NOT VALID, applicando la regola alle righe nuove o modificate senza scandire subito tutte le righe storiche. Convalidalo separatamente dopo il backfill:
ALTER TABLE customers
ADD CONSTRAINT customers_phone_e164_present
CHECK (phone_e164 IS NOT NULL) NOT VALID;
ALTER TABLE customers
VALIDATE CONSTRAINT customers_phone_e164_present;
Una volta riuscita la convalida, le versioni PostgreSQL che lo supportano possono usare questa prova per impostare la colonna su NOT NULL, evitando un'altra scansione completa della tabella. L'alterazione finale richiede comunque un lock forte sulla tabella, quindi usa un lock timeout limitato e un piano di ripetizione:
ALTER TABLE customers
ALTER COLUMN phone_e164 SET NOT NULL;
ALTER TABLE customers
DROP CONSTRAINT customers_phone_e164_present;
Il check temporaneo può restare se è utile, ma conservare vincoli equivalenti aggiunge confusione al catalogo senza cambiare la regola.
Le chiavi esterne possono seguire una sequenza simile con NOT VALID e VALIDATE CONSTRAINT. Le nuove scritture vengono controllate dopo la creazione del vincolo, mentre la convalida storica avviene in seguito. Aggiungi intenzionalmente l'indice di supporto quando il comportamento di eliminazione o aggiornamento sulla relazione referenziata causerebbe altrimenti scansioni costose.
La convalida dell'applicazione dovrebbe precedere l'applicazione del vincolo nel database, ma non la sostituisce. Il codice produce errori più chiari per gli utenti, mentre il database protegge i dati scritti da ogni percorso. Durante il rollout, osserva le violazioni dei vincoli per individuare un writer sfuggito all'audit delle dipendenze.
Contrai il vecchio percorso in sicurezza
La fase di contrazione deve rimuovere le dipendenze dell'applicazione prima di rimuovere gli oggetti del database. Quando telemetria e verifica stabiliscono che il nuovo percorso è autorevole, la pulizia può procedere tramite release separate.
Per prima cosa smetti di leggere il vecchio campo e rimuovi la logica di fallback. Poi disattiva le sue scritture e osserva la produzione abbastanza a lungo da intercettare percorsi rari. Rimuovi feature flag, trigger, viste di compatibilità, script di riparazione e job pianificati che menzionano la vecchia rappresentazione. Cerca nel codice sorgente esportato e nel codice di migrazione, ma controlla anche report, query di integrazione e configurazioni di change data capture esterne al repository principale.
Un ordine di pulizia sicuro è:
- Rimuovi le letture di fallback e conferma che non compaiano più nella telemetria.
- Interrompi le vecchie scritture ed elimina il codice di sincronizzazione.
- Rimuovi i riferimenti dell'applicazione da tutte le versioni distribuibili.
- Elimina indici e vincoli obsoleti con il metodo online appropriato.
- Rimuovi la vecchia colonna o tabella in una release del database successiva.
L'eliminazione di una colonna PostgreSQL è soprattutto una modifica al catalogo, ma richiede comunque un lock ACCESS EXCLUSIVE. Un'istruzione breve può quindi attendere dietro una transazione lunga e bloccare il lavoro successivo. Applica un lock timeout, controlla prima le transazioni di lunga durata e pianifica il tentativo in un periodo meno rischioso.
Usa DROP INDEX CONCURRENTLY per un indice obsoleto quando bloccare le scritture sarebbe inaccettabile. Come la creazione concorrente, non può essere eseguito dentro un blocco di transazione e ha limitazioni che gli strumenti di migrazione devono gestire.
Non combinare la pulizia del codice e l'eliminazione fisica in una sola release. La separazione permette all'applicazione ripulita di funzionare su un database che contiene ancora l'oggetto inutilizzato. Se emerge un problema nell'applicazione, il rollback resta possibile senza ricreare lo schema o ricostruire i dati.
Prima di eliminare una tabella, verifica la proprietà di sequenze, viste, funzioni, autorizzazioni, trigger, pubblicazioni di replica e query esterne. Evita CASCADE come scorciatoia in una migrazione di produzione, perché può rimuovere dipendenze che non facevano parte della modifica prevista.
Gestisci rollback e passaggi non riusciti
La pianificazione del rollback deve definire un'azione sicura per ogni fase invece di affidarsi a un'unica migrazione generica verso il basso. Oggetti additivi, spostamento dei dati, cambio delle letture ed eliminazione hanno proprietà di ripristino diverse.
Se l'espansione non riesce ad acquisire il lock, lascia invariata l'applicazione e riprova dopo aver risolto la transazione bloccante. Se la creazione concorrente di un indice fallisce, verifica se ha lasciato un indice non valido e pulisci quello specifico oggetto prima di un nuovo tentativo.
Se un backfill genera carico, sospendilo. I batch idempotenti già confermati possono restare in sede. Riduci la dimensione o la frequenza dei batch, intervieni sulla trasformazione costosa e riprendi dal checkpoint. Annullare milioni di aggiornamenti corretti di solito aumenta il rischio senza aiutare il recupero della produzione.
Se un nuovo percorso di lettura restituisce risultati errati, reindirizza le letture alla vecchia rappresentazione mantenendo i nuovi dati per la diagnosi. Continua con la doppia scrittura solo se sai che è corretta. Quando il writer stesso è difettoso, disabilitalo o esegui il rollback dell'applicazione prima di riparare le righe interessate.
Dopo la contrazione, il recupero può richiedere il ripristino dei dati e non soltanto il deployment di una build precedente. Definisci esplicitamente il punto di non ritorno. Crea il backup o lo snapshot previsto dalla politica di ripristino del sistema, prova il ripristino prima della release e conserva il vecchio oggetto per l'intervallo di conservazione concordato quando il costo di archiviazione lo consente.
I comandi sullo schema possono essere transazionali, ma gli effetti esterni non sono sempre inclusi. Operazioni concorrenti sugli indici, messaggi in coda, modifiche della cache e deployment dell'applicazione non condividono un'unica transazione atomica. Il runbook dovrebbe descrivere lo stato osservabile dopo ogni errore parziale e il comando che consente di proseguire in sicurezza.
Evita le trappole più comuni nelle migrazioni
La maggior parte delle migrazioni senza downtime non riuscite applica il nuovo stato troppo presto oppure dimentica un consumer del vecchio stato. Le seguenti trappole meritano una revisione esplicita prima dell'approvazione.
- Aggiungere
NOT NULLmentre una vecchia istanza dell'applicazione può ancora omettere il campo. - Eseguire un grande backfill in un'unica transazione, trattenendo troppo a lungo lock e versioni delle righe.
- Rinominare una colonna come se fosse una modifica additiva, anche se il vecchio codice usa ancora il suo nome originale.
- Spostare le letture prima che tutti i percorsi di scrittura e le righe storiche popolino la nuova rappresentazione.
- Considerare un deployment riuscito come prova che report, worker, repliche e integrazioni siano compatibili.
Un altro errore sottile deriva dalla sincronizzazione bidirezionale. Un trigger copia old_col in new_col, mentre il codice dell'applicazione copia new_col di nuovo in old_col. Differenze nella normalizzazione o nell'ordine dei trigger possono creare cicli, sovrascrivere valori intenzionali o rendere poco chiara l'autorità. Preferisci una sola direzione e documenta quale rappresentazione è autorevole durante ogni release.
I valori predefiniti possono nascondere aggiornamenti mancanti nei writer. Se una nuova colonna obbligatoria riceve un valore predefinito vuoto o generico, il vecchio codice sembra compatibile mentre memorizza dati semanticamente non validi. Usa una transizione nullable quando l'assenza fornisce informazioni diagnostiche utili, quindi applica la regola reale dopo che ogni writer fornisce un valore significativo.
Un feature flag non rende sicuro da solo un comando incompatibile sullo schema. Un percorso di codice disabilitato può comunque essere caricato, preparato o eseguito da un processo precedente. L'oggetto del database deve restare finché nessuna versione distribuibile o attiva lo riferisce.
Anche la responsabilità della migrazione conta. Assegna una persona o un team alla transizione fino alla contrazione, comprese le date di verifica e rimozione. Altrimenti colonne temporanee, flag e job di sincronizzazione possono restare per mesi, aumentando il costo di ogni modifica successiva.
Sostituisci una colonna telefonica senza downtime
Sostituire customers.phone con customers.phone_e164 normalizzato richiede una colonna additiva, una politica di conversione definita, codice compatibile, un backfill limitato, un cambio delle letture e una pulizia posticipata. La politica di conversione deve precedere SQL, perché non tutti i valori memorizzati possono essere normalizzati automaticamente.
Inizia classificando i valori esistenti. I numeri validi possono essere convertiti quando è noto il contesto nazionale richiesto. I valori vuoti possono diventare NULL. Numeri ambigui o non validi dovrebbero finire in un report delle eccezioni invece di essere indovinati. Decidi se il prodotto richiede che ogni cliente abbia un numero di telefono, poiché questo determina se NOT NULL sarà appropriato in seguito.
Aggiungi la colonna con un lock timeout breve:
BEGIN;
SET LOCAL lock_timeout = '2s';
ALTER TABLE customers
ADD COLUMN phone_e164 text;
COMMIT;
Distribuisci codice che normalizza il nuovo input e scrive phone e phone_e164 in un'unica transazione. Mantieni inizialmente le letture su phone. Aggiorna ogni writer, inclusi importazioni di account, strumenti di assistenza, job dei worker e test che creano fixture di clienti.
Esegui il backfill delle righe idonee in transazioni brevi. Registra l'ultimo identificatore elaborato, il numero convertito, quello saltato e il motivo per ogni categoria di errore. Limita la velocità del job in base alla latenza di produzione e al ritardo delle repliche. Dopo il completamento del passaggio in avanti, riesegui la scansione per i valori NULL idonei, così intercetterai inserimenti simultanei o righe mancate dopo un riavvio.
Esegui controlli di coerenza usando le stesse regole di normalizzazione dell'applicazione, poi verifica manualmente prefissi internazionali, interni, valori vuoti, record di contatto duplicati e vecchi dati importati. Un conteggio delle righe dimostra la copertura, non la correttezza di un numero telefonico.
Distribuisci un percorso di lettura che restituisce phone_e164 quando presente e usa phone solo per un'eccezione registrata. Monitora l'uso del fallback e gli errori di normalizzazione. Risolvi le eccezioni rimanenti invece di lasciare che il fallback diventi un comportamento permanente.
Quando il nuovo campo è autorevole, rimuovi il fallback e smetti di scrivere phone. Osserva job rari e traffico di integrazione per un ciclo operativo appropriato. Aggiungi il vincolo convalidato solo se la regola di prodotto lo richiede.
Infine, rimuovi i riferimenti del codice a phone. Elimina separatamente i suoi indici o vincoli, quindi rimuovi la colonna in una migrazione successiva con un'attesa del lock limitata. Se il cambio delle letture fallisce in qualsiasi momento prima di quell'eliminazione, ripristina il comportamento dell'applicazione mentre entrambe le colonne restano disponibili.
Questo esempio mette in luce anche un problema di dominio che i meccanismi dello schema non possono risolvere: dividere o normalizzare dati inseriti da persone non sempre è privo di perdite. Il piano di migrazione deve conservare le eccezioni e consentire a un responsabile di risolverle.
Controlla ogni release prima della distribuzione
Una checklist di release dovrebbe dimostrare la compatibilità, limitare l'impatto sulla produzione e indicare l'azione di ripristino per la fase corrente. Conserva le prove insieme alla modifica, così un operatore non deve ricostruire l'intento durante un incidente.
Prima del deployment, conferma quanto segue:
- La versione dell'applicazione funziona con lo stato del database prima e dopo questa release.
- Lock timeout e statement timeout sono impostati per i comandi sullo schema che potrebbero attendere dietro il traffico.
- Il job di backfill o convalida dispone di controlli per avanzamento, sospensione, ripresa e limitazione della velocità.
- Le dashboard coprono errori, latenza, lock, carico del database, WAL e ritardo delle repliche.
- L'azione di rollback è stata testata senza dipendere da un oggetto già rimosso.
Registra condizioni di completamento esplicite. Alcuni esempi: zero nuovi errori di coerenza per un intero ciclo di job, zero letture di fallback dal traffico controllato dal server, aggiornamento di ogni consumer noto e una query di convalida controllata riuscita. La percentuale di completamento è utile durante un backfill, ma il 100% elaborato non equivale al 100% corretto.
Rivedi l'ordine della migrazione indipendentemente dalla revisione del codice. Un insieme corretto di SQL e modifiche all'applicazione può comunque fallire se il deployment li esegue nella sequenza sbagliata. Indica quale passaggio può iniziare solo dopo il completamento di un altro.
Le condizioni di arresto dovrebbero essere numeriche quando possibile. Definisci latenza delle query, attesa per lock, ritardo delle repliche, tasso di errore e durata dei batch accettabili. Quando viene superata una soglia, l'operatore deve sapere se sospendere un job, annullare un'istruzione in attesa o reindirizzare le letture senza cercare una nuova approvazione durante l'incidente.
La migrazione è completa solo quando la nuova rappresentazione gestisce letture e scritture, i dati storici hanno superato la verifica, il vecchio oggetto è stato rimosso e l'infrastruttura operativa temporanea non c'è più.
Rendi il processo ripetibile
Un runbook di migrazione riutilizzabile trasforma expand/contract in un normale lavoro di release, con responsabili nominati e passaggi misurabili. Deve essere abbastanza breve da poterlo seguire durante un deployment live e abbastanza specifico da descrivere gli stati di errore parziale.
Usa cinque sezioni nel runbook:
- Espansione: operazioni esatte sullo schema, lock previsti, timeout e requisiti delle transazioni.
- Compatibilità: codice interessato, writer, reader, flag, client e ordine di deployment.
- Backfill: politica di trasformazione, batching, checkpoint, limitazione della velocità e gestione delle eccezioni.
- Verifica: controlli SQL, invarianti di business, telemetria e soglie di completamento.
- Contrazione: rimozione delle dipendenze, periodo di osservazione, pulizia fisica e limiti di ripristino.
Assegna un responsabile e una data di completamento prevista a ogni oggetto transitorio. Tieni traccia di colonne, indici, flag, trigger e job nello stesso posto. La pulizia fa parte della migrazione, non è manutenzione facoltativa.
Per i team che sviluppano con Koder.ai, Planning Mode può aiutare a definire queste fasi e checkpoint prima di iniziare modifiche in produzione. L'esportazione del codice sorgente permette inoltre di sottoporre SQL di migrazione e logica di compatibilità alla stessa revisione del resto del codice applicativo. Koder.ai supporta deployment, hosting, snapshot e rollback, ma il rollback dell'applicazione non va considerato un'inversione automatica di una trasformazione dei dati già confermata. Mantieni la compatibilità dello schema finché il piano di ripristino del database non dipende più dalla vecchia rappresentazione.
Quando possibile, pianifica il lavoro con molte scritture nei periodi di minor traffico, ma non affidarti solo alla tempistica come controllo di sicurezza. Transazioni limitate, limitazione basata sul feedback, avanzamento osservabile e un'azione di sospensione testata rendono gestibile una migrazione online quando traffico o dati si comportano diversamente dal previsto.
Domande frequenti
Perché una modifica allo schema può causare un'interruzione del servizio?
Le modifiche allo schema interrompono la produzione quando le versioni vecchie e nuove dell'applicazione si aspettano strutture di database diverse. Durante un deployment progressivo, le due versioni possono essere in esecuzione contemporaneamente, quindi rimuovere o rinominare una colonna troppo presto può causare errori di lettura o scrittura.
Che cos'è il pattern di migrazione expand/contract?
Expand/contract divide una modifica incompatibile in fasi sicure. Prima aggiungi la nuova struttura, poi sposti codice e dati su di essa e solo in seguito rimuovi la struttura precedente, quando nessun consumer la usa più.
Come posso rinominare o sostituire una colonna del database senza downtime?
Aggiungi prima la nuova colonna e conserva quella vecchia. Distribuisci codice che funziona con entrambi i campi, esegui il backfill delle righe esistenti in piccoli batch, sposta le letture dopo la convalida e rimuovi la vecchia colonna in una release successiva.
Posso aggiungere una colonna PostgreSQL senza bloccare il traffico?
Nella maggior parte dei casi sì. In PostgreSQL, aggiungere una colonna nullable senza valore predefinito è spesso una breve modifica ai metadati, ma richiede comunque un lock sulla tabella. Imposta un lock timeout breve affinché la migrazione fallisca invece di aspettare una transazione lunga.
Come posso creare un indice senza bloccare le scritture?
Usa CREATE INDEX CONCURRENTLY quando la tabella deve restare scrivibile. L'operazione richiede più tempo e aumenta il carico sul database, inoltre non può essere eseguita dentro un blocco di transazione. Durante l'esecuzione monitora latenza, WAL e ritardo delle repliche.
Quando un'applicazione dovrebbe scrivere sia nel campo vecchio sia in quello nuovo?
Scrivi entrambi i valori nella stessa transazione del database quando le due rappresentazioni devono restare aggiornate. Definisci quale campo prevale se i valori non coincidono e applica le stesse regole di normalizzazione in API, worker, importazioni e strumenti di assistenza.
Come eseguire in sicurezza il backfill di una grande tabella PostgreSQL?
Elabora batch brevi e riprendibili, eseguendo il commit dopo ogni batch. Salva un checkpoint, aggiorna solo le righe che richiedono ancora interventi e rallenta o sospendi il job quando aumentano latenza delle query, attese per lock, volume WAL o ritardo delle repliche.
Come capisco se un backfill è completo e corretto?
Non spostare le letture solo perché il backfill è terminato. Verifica che esistano i valori richiesti, confronta le rappresentazioni vecchia e nuova, monitora le letture di fallback e conferma che le nuove scritture restino coerenti dopo il passaggio sui dati storici.
Quando dovrei aggiungere NOT NULL, vincoli CHECK o chiavi esterne?
Aggiungi vincoli rigorosi dopo che i dati esistenti hanno superato la convalida e tutti gli scrittori attivi forniscono valori validi. PostgreSQL consente di aggiungere alcuni vincoli come NOT VALID, applicarli alle nuove righe e convalidare separatamente le righe storiche.
Quando è sicuro rimuovere il vecchio percorso dello schema?
Rimuovi prima le letture di fallback, poi interrompi le vecchie scritture e osserva il sistema per un intero ciclo operativo. Quando applicazioni, job, report, integrazioni e client non fanno più riferimento al vecchio oggetto, rimuovi il codice correlato e cancella la colonna o la tabella in una release successiva.