ZpětBackend

Zero‑downtime databázové migrace

Prosinec 202512 min

Zero-downtime není magie, ale choreografie: změnit schéma, bezpečně doplnit data a teprve potom přepnout provoz. Tenhle tanec jsem absolvoval na e-shopových checkoutech, ERP synchronizacích i obyčejných CRUD aplikacích — kroky se nemění, mění se jen tempo.

Výpadky málokdy způsobí samotné SQL. Způsobí je předpoklad, že se kód a schéma přepnou ve stejný okamžik. Nepřepnou: během rolling deploye obsluhují provoz staré i nové pody vedle sebe, klidně celé hodiny. Přejmenujte sloupec a starý kód spadne. Přidejte NOT NULL constraint a polovina zápisů se odrazí. Spusťte dlouhý ALTER s exkluzivním zámkem a každý požadavek se zařadí do fronty za něj. Lék je pravidlo, které odmítám porušit: každý deploy musí fungovat proti schématu před změnou i po ní.

Tříkrokový migrační vzor

Každá riskantní změna schématu se dá rozložit do tří malých deployů, z nichž každý je sám o sobě nudný:

  • Expand: přidejte nové sloupce nebo tabulky bez breaking změn.
  • Backfill: doplňte data, zatímco běží stará i nová verze.
  • Contract: odstraňte starou cestu až po přepnutí čtení i zápisu.

Expand skoro nic nestojí. Nullable sloupce, nové tabulky, indexy vytvořené konkurentně. Starý kód ignoruje, o čem neví, takže se nic nerozbije. Constraintům se v této fázi bráním — validace přijde později, až data doženou realitu.

-- Expand: additive, nullable, no exclusive locks
ALTER TABLE orders ADD COLUMN customer_email text;
CREATE INDEX CONCURRENTLY idx_orders_email ON orders (customer_email);

Backfill je místo, kde se vyplácí disciplína. Nejdřív zapněte v aplikaci dual-write: každý insert i update plní staré i nové pole. Pak projděte historické řádky v malých dávkách — pár tisíc najednou, s pauzou mezi dávkami — a sledujte přitom replikační lag a čekání na zámky. Jeden obří UPDATE nad vytíženou tabulkou je nejrychlejší způsob, jak z plánu bez výpadku udělat incident report.

-- Backfill: small batches, repeat until zero rows updated
UPDATE orders SET customer_email = legacy_contact
WHERE id IN (
  SELECT id FROM orders
  WHERE customer_email IS NULL
  LIMIT 5000
);

Contract přijde jako poslední — a později, než je příjemné. Přepněte čtení na nový sloupec za feature flagem, nechte dual-write běžet ještě jeden release a teprve pak starou cestu odstraňte. Smazání sloupce jsou jednosměrné dveře; jednosměrnými dveřmi procházím pomalu.

Ověření v produkci

Migrace, kterou neumíte ověřit, je migrace, u které jen doufáte. Během backfillu si plánuji tři levné kontroly: porovnat stará a nová pole řádek po řádku, sledovat, jak poměr NULL hodnot v novém sloupci klesá k nule, a pustit nejdůležitější read-only dotazy nad novou cestou dřív, než to udělá uživatel. Jsou to až trapně jednoduché signály — a odhalí drift o dny dřív, než by ho našel zákazník.

-- Drift check: must trend to zero and stay there
SELECT count(*) AS mismatched
FROM orders
WHERE customer_email IS DISTINCT FROM legacy_contact;

Když to neměříte, nemůžete tomu věřit — zvlášť pokud chcete čistý rollback. A rollback je tady záměrně levný: protože je každý krok zpětně kompatibilní, znamená návrat nasazení předchozí verze, ne obnovu zálohy ve tři ráno. Starý sloupec pořád existuje, starý kód pořád funguje a backfill při dalším pokusu prostě naváže tam, kde skončil.

Checklist před fází contract

  • Drift mezi starými a novými poli je nulový po celý byznysový cyklus, včetně víkendových batch jobů.
  • Ve slow logu ani v pg_stat_statements se starého sloupce nedotýká žádný dotaz.
  • Dual-write běžel minimálně jeden release a rollback jste si skutečně vyzkoušeli, ne jen sepsali.

Nic z toho není efektní. Je to choreografie: malé kroky, ve správném pořadí, s cestou zpět v každém bodě. Přesně to zero downtime znamená.

Další

Zpět na přehled