// Script de backfill în batch-uri mici pentru a evita blocarea DB-ului
async function backfill() {
const BATCH_SIZE = 5000;
let lastId = 0;
let hasMore = true;
while (hasMore) {
// Luăm doar ID-urile care au nevoie de update
const rows = await db.query(
'SELECT id, old_column FROM users WHERE id > $1 AND new_column IS NULL ORDER BY id ASC LIMIT $2',
[lastId, BATCH_SIZE]
);
if (rows.length === 0) {
hasMore = false;
break;
}
// Updatăm fiecare rând în mod controlat
for (const row of rows) {
const transformedValue = transformData(row.old_column);
await db.query(
'UPDATE users SET new_column = $1 WHERE id = $2',
[transformedValue, row.id]
);
lastId = row.id;
}
console.log(`Progres: am procesat până la ID-ul ${lastId}`);
// Lăsăm baza de date să respire ca să nu moară CPU-ul în producție
await new Promise(resolve => setTimeout(resolve, 100));
}
}Am văzut prea des baze de date blocate în producție din cauza unui simplu ALTER TABLE pe o tabelă masivă. Dacă lucrezi la un sistem care nu își permite downtime, trebuie să uiți definitiv de migrările clasice dintr-un singur pas. Astăzi îți arăt cum fac eu asta folosind pattern-ul în patru pași: Expand and Contract.
De ce crapă abordarea clasică?
La un proiect trecut, aveam o tabelă de users cu vreo 14 milioane de rânduri și un trafic constant de 600 de request-uri pe secundă. Trebuia să redenumim o coloană și să-i schimbăm tipul din varchar în jsonb pentru a stoca niște setări flexibile.
Un simplu ALTER TABLE ar fi însemnat un lock exclusiv pe tabelă. Asta bloca toate scrierile. În doar două minute, coada de conexiuni s-a umplut, serverul de API a început să dea erori 504, iar baza de date era în genunchi. Dacă baza ta are peste câteva sute de mii de rânduri și trafic constant, modificările structurale directe sunt o rețetă sigură pentru dezastru.
Pattern-ul în 4 pași (Expand & Contract)
Ca să evităm blocajele, împărțim migrarea în patru faze distincte, rulate pe parcursul mai multor deploy-uri de cod.
1. Add (Expand)
Adaugi noua coloană ca fiind opțională (nullable). Nu te atingi de cea veche. Deoarece coloana acceptă valori nule, baza de date doar își updatează catalogul de sistem (în PostgreSQL sau MySQL 8). Operațiunea durează câteva milisecunde și nu blochează tabela.
2. Dual Write (Scrierea în ambele)
Faci deploy la o versiune de cod care scrie în ambele coloane. Când vine un request de salvare, salvezi valoarea atât în coloana veche, cât și în cea nouă. Citirile se fac în continuare din coloana veche. În acest moment, toate înregistrările noi vor avea datele actualizate în ambele locuri.
3. Backfill (Migrarea datelor vechi)
Acesta este pasul unde mulți se grăbesc și strică totul. Trebuie să copiezi datele din coloana veche în cea nouă pentru rândurile create înainte de Pasul 2.
Niciodată să nu rulezi un simplu UPDATE tabela SET noua_coloana = vechea_coloana. Vei bloca tabela ore în șir. Soluția este un script de backfill care rulează în background și face update-ul în batch-uri mici (de exemplu, câte 5000 de rânduri o dată), cu o scurtă pauză între ele.
4. Flip & Cleanup (Contract)
După ce backfill-ul s-a terminat și ești 100% sigur că datele sunt identice, finalizezi procesul:
- Schimbi codul de citire să folosească doar coloana nouă.
- Scoți scrierea în coloana veche din cod.
- (Opțional) Adaugi constrângerea
NOT NULLpe noua coloană (folosindNOT VALIDși apoiVALIDATE CONSTRAINTîn Postgres, ca să eviți lock-ul lung). - Ștergi coloana veche (
DROP COLUMN).
Trade-off-ul sincer
Nimic nu e gratis pe lumea asta. Acest pattern îți garantează zero downtime, dar vine cu costuri mari de workflow. În loc de o singură comandă SQL rulată în pipeline, acum ai nevoie de cel puțin 3 deploy-uri diferite de cod, monitorizare atentă în timpul backfill-ului și cod temporar pe care trebuie să-ți aduci aminte să-l ștergi ulterior.
Pentru o tabelă mică de configurări e o pierdere totală de timp. În schimb, pentru tabelele core ale afacerii tale, este singura cale responsabilă.
Voi cum gestionați migrările astea grele? Ați încercat să automatizați procesul de backfill sau mergeți tot pe scripturi rulate manual din CLI?