// Exemplu simplu de batch backfill pentru a evita blocarea bazei de date
async function backfillUsers() {
const batchSize = 1000;
let lastId = 0;
let hasMore = true;
while (hasMore) {
// Luăm utilizatorii în batch-uri pe baza ID-ului (mult mai rapid decât OFFSET)
const users = await db.query(
'SELECT id, old_id FROM users WHERE id > $1 ORDER BY id ASC LIMIT $2',
[lastId, batchSize]
);
if (users.length === 0) {
hasMore = false;
break;
}
for (const user of users) {
const newUuid = generateUuidFromOldId(user.old_id);
await db.query(
'UPDATE users SET new_uuid = $1 WHERE id = $2',
[newUuid, user.id]
);
}
lastId = users[users.length - 1].id;
// Lăsăm baza de date să respire între batch-uri
await new Promise(resolve => setTimeout(resolve, 100));
}
console.log('Backfill completat cu succes!');
}Am văzut prea des scripturi de migrare care blochează baza de date în producție fix când e lumea mai dragă. Dacă ai o tabelă cu peste 5-10 milioane de rânduri în Postgres sau MySQL, un simplu ALTER TABLE cu redenumire sau adăugare de coloană ne-nullable îți poate pune business-ul în cap minute bune.
Am pățit asta acum câțiva ani la un proiect cu peste 12 milioane de utilizatori activi. Trebuia să migrăm o coloană de tip varcher (un format vechi de ID) la un UUID. Am încercat o migrare clasică pe un mediu de staging care simula producția și baza de date a înghețat complet din cauza lock-urilor pe tabelă. Atunci m-am prins că singura variantă realistă pentru sisteme cu disponibilitate ridicată este pattern-ul Expand & Contract (sau migrarea în 4 pași).
Strategia asta cere disciplină și răbdare, dar te scapă de palpitații la deploy.
Pasul 1: Expand (Add Column)
Primul pas este pur aditiv. Adaugi noua coloană în baza de date, dar o pui ca fiind nullable (sau cu o valoare default, dacă baza de date suportă asta instant, cum face Postgres de la versiunea 11 încoace).
În acest moment, codul tău din producție habar nu are de noua coloană. Rulează liniștit în continuare, scriind și citind din coloana veche. Riscul de downtime aici este zero barat.
Pasul 2: Dual Write & Backfill
Aici începe distracția. Modifici codul aplicației să facă dual write. Adică, de fiecare dată când ai un INSERT sau un UPDATE, scrii valoarea atât în coloana veche, cât și în cea nouă.
După ce ai dat deploy la codul de dual write, pornești un script de backfill pentru datele istorice. Atenție mare aici: nu rula un singur UPDATE uriaș! Îți va bloca tabela și vei consuma toate resursele serverului.
La proiectul menționat, am făcut backfill-ul în batch-uri de câte 1000 de rânduri, cu o pauză de 50-100ms între ele. A durat vreo 6 ore să migrăm tot, dar încărcarea pe procesorul bazei de date nu a trecut de 15%.
Pasul 3: Flip (Read from New)
În faza asta, ai toate datele istorice migrate și toate scrierile noi merg în ambele locuri. E timpul să schimbi sursa de adevăr pentru citiri.
Modifici aplicația să citească exclusiv din noua coloană. Totuși, lași scrierile duble active! De ce? Pentru că dacă ceva crapă în producție din cauza noului format, vrei să poți face rollback instant la codul vechi fără să pierzi datele scrise în ultimele ore.
Pasul 4: Contract (Remove Old)
După ce ai lăsat sistemul să ruleze câteva zile (sau o săptămână) și ești 100% sigur că totul este stabil, vine momentul de curățenie.
- Modifici codul să scrie doar în noua coloană (elimini dual write-ul).
- Ștergi coloana veche din baza de date (
ALTER TABLE DROP COLUMN).
Trade-off-ul sincer
Metoda asta funcționează de minune pentru uptime, dar vine cu un cost destul de mare în ceea ce privește complexitatea codului. Trebuie să scrii cod temporar de sincronizare, să rulezi scripturi de mentenanță și să dai cel puțin trei deploy-uri diferite pentru o singură modificare de structură.
Dacă ai o aplicație mică, cu 100 de utilizatori, nu te complica. Pune un ecran de mentenanță duminică la ora 3 dimineața, fă migrarea clasică în 10 secunde și mergi la culcare. Dar dacă lucrezi la un produs unde fiecare minut de downtime înseamnă mii de euro pierduți, ăsta este singurul mod corect de a lucra.
Voi cum gestionați migrările astea mari? Ați încercat să automatizați procesul de backfill direct din ORM sau folosiți tool-uri externe?