Zmiany schematu bez przestoju dzięki wzorcowi expand/contract
Planuj i wdrażaj zmiany schematu bez przestoju dzięki wzorcowi expand/contract, bezpiecznym backfillom, zgodnym wydaniom, weryfikacji i wycofaniu.

Dlaczego zmiany schematu powodują awarie
Zmiany schematu powodują awarie, gdy wersje aplikacji, workery działające w tle i baza danych przestają zgadzać się co do tego, które struktury i wartości są poprawne. Skutek może być oczywisty, na przykład każdy request zwraca błąd, albo narastać stopniowo, na przykład rosną opóźnienia zapytań, zapisy kończą się niepowodzeniem, repliki mają lag, a joby trzeba odtworzyć.
Wdrożenie na produkcji rzadko zmienia wszystkie procesy naraz. Wdrożenia kroczące zostawiają równocześnie uruchomione stare i nowe instancje aplikacji. Długo działające workery mogą korzystać ze starego buildu przez wiele godzin, klienci mobilni mogą pozostawać aktywni przez miesiące, a joby raportujące lub integracyjne mogą używać tabel bez przechodzenia przez główną aplikację. Wszystkie korzystają z jednej bazy danych.
Typowe tryby awarii to:
- Nowy kod zapisuje do kolumny, zanim zakończy się migracja, która ją tworzy.
- Stary kod odczytuje tabelę lub kolumnę, którą późniejsze wydanie przemianowało albo usunęło.
- Przepisanie tabeli, backfill lub budowanie indeksu zużywa tyle I/O i CPU, że spowalnia normalny ruch.
- Polecenie zmiany schematu czeka na blokadę, a requesty ustawiają się za nim w kolejce.
- Nowe ograniczenie odrzuca zapisy z procesu, który nie został jeszcze zaktualizowany.
Niebezpieczne bywa często zdobycie blokady, a nie nominalny czas wykonania. Szybkie ALTER TABLE może czekać za długą transakcją. W tym czasie kolejne zapytania mogą ustawiać się za oczekującą blokadą schematu, zamieniając małą migrację w zastój całej aplikacji.
Brak przestoju wymaga, aby każdy pośredni stan bazy pozostawał użyteczny dla każdej wersji aplikacji, która może jeszcze działać. Najpierw dodaj zgodne struktury, potem przenoś ruch i dane kontrolowanymi krokami, a starą ścieżkę usuń dopiero, gdy zniknie jej ostatni odbiorca.
Taki wysiłek ma sens w systemach z żywym ruchem, wdrożeniami kroczącymi, rygorystycznymi celami dostępności lub kosztownymi procedurami odzyskiwania. Małe wewnętrzne narzędzie z mało aktywną bazą może lepiej obsłużyć przetestowane okno serwisowe. Decyzja powinna uwzględniać koszt awarii i złożoność operacyjną migracji.
Expand/contract prostymi słowami
Wzorzec expand/contract zamienia jedną niezgodną zmianę w serię zgodnych wydań. Baza danych przez pewien czas obsługuje dwie reprezentacje, podczas gdy kod i dane przechodzą ze starej do nowej.
Sekwencja ma trzy części:
- Rozszerzenie przez dodanie kolumn, tabel, indeksów lub ograniczeń bez usuwania czegokolwiek, czego potrzebuje bieżący kod.
- Przejście przez wdrożenie zgodnego kodu, przeniesienie danych historycznych i skierowanie odczytów oraz zapisów do nowej reprezentacji.
- Usunięcie starego kodu i obiektów bazy po weryfikacji, która potwierdzi, że nie są używane.
Załóżmy, że tabela PostgreSQL przechowuje imię i nazwisko osoby w full_name, a aplikacja potrzebuje osobnych pól first_name i last_name. Rozszerzenie dodaje kolumny dopuszczające NULL, zachowując full_name. Zgodne wydanie zapisuje reprezentacje potrzebne w okresie przejściowym. Backfill rozdziela istniejące wartości według jasno określonej zasady dla imion i nazwisk, których nie da się wiarygodnie podzielić. Odczyty przechodzą na nowe pola dopiero, gdy są one wystarczająco kompletne. W fazie usuwania znika full_name.
Ta kolejność pasuje do wdrożeń kroczących, ponieważ stary build nadal znajduje full_name, a nowy wszystkie trzy kolumny. Zachowuje też ścieżkę wycofania aplikacji. Jeśli nowe wydanie działa nieprawidłowo, można uruchomić poprzedni build, ponieważ jego zależności od schematu nie zostały usunięte.
Wycofanie bazy danych różni się od wycofania aplikacji. Odwrócenie migracji po przekształceniu danych może utracić informacje albo przywrócić nieaktualną wartość. W okresie przejściowym lepiej skierować ruch aplikacji z powrotem na znaną reprezentację i pozostawić dodane obiekty bazy. Gdy incydent się uspokoi, popraw migrację do przodu.
Wzorzec nie oznacza, że każda zmiana wymaga zapisu do dwóch miejsc. Dodanie opcjonalnej kolumny, z której korzysta tylko nowy kod, może wymagać jednej migracji rozszerzającej i jednego wdrożenia. Zmiany nazw, zmiany reprezentacji, podział tabel i zmiany wymaganych pól zwykle potrzebują większej liczby faz, ponieważ dwie wersje aplikacji inaczej nie mogą bezpiecznie współdzielić schematu.
Sklasyfikuj zmianę przed wyborem kroków
Plan migracji powinien odpowiadać rzeczywistemu ryzyku blokad, przepisywania danych, zgodności i konwersji danych. Traktowanie każdego ALTER TABLE jako równoważnego prowadzi albo do zbędnej ceremonii, albo do niebezpiecznego wydania.
Zmiany addytywne są zwykle najprostsze. Kolumnę dopuszczającą NULL, osobną tabelę lub indeks utworzony metodą online można często wprowadzić, zanim kod aplikacji zacznie z nich korzystać. Polecenie nadal potrzebuje blokady, dlatego przetestuj jego zachowanie na tabeli i obciążeniu transakcyjnym zbliżonym do produkcji.
Zmiany destrukcyjne obejmują usuwanie lub zmianę nazw kolumn, zawężanie typów, zastępowanie tabel i dodawanie ostrzejszych ograniczeń. Unieważniają założenie istniejącego kodu. Umieść je w fazie contract, po usunięciu odwołań w kodzie i odbiorców zewnętrznych.
Operacje zmieniające dane zasługują na osobną ocenę. Konwersja znaczników czasu, normalizacja numerów telefonów, łączenie rekordów lub dzielenie swobodnego tekstu może prowadzić do utraty informacji. Zanim rozpocznie się backfill, określ, jak obsłużyć nieprawidłowe i niejednoznaczne wartości. Jeśli transformacji nie da się odwrócić, zachowaj źródło, aż wynik przejdzie kontrolę na poziomie biznesowym.
Przydatny przegląd przed startem obejmuje pięć pytań:
- Jakiej blokady wymaga każde polecenie i jak długo może na nią czekać lub ją utrzymywać?
- Czy operacja przepisze tabelę, wygeneruje dużo WAL albo zwiększy lag replik?
- Które aplikacje, joby, raporty i odbiorcy change data capture używają zmienianych obiektów?
- Czy bieżące i planowane wydanie może działać z każdym stanem przejściowym?
- Jaki sygnał wstrzymuje operację i jaki dokładnie stan pozostaje po zatrzymaniu?
Uruchom dokładną migrację na danych o realistycznym wolumenie i rozkładzie. Tabela testowa z tysiącem uporządkowanych wierszy niewiele mówi o tabeli produkcyjnej zawierającej setki milionów wierszy, szerokie krotki, martwe wiersze, skośne wartości i długo działające transakcje.
Bezpieczne rozszerzanie w PostgreSQL
Bezpieczne rozszerzanie PostgreSQL korzysta z krótkich zmian metadanych, ograniczonego oczekiwania na blokady oraz osobnych operacji online, gdy baza ich wymaga. Dodaj nową strukturę przed wdrożeniem kodu, który od niej zależy.
Dodanie kolumny dopuszczającej NULL bez wartości domyślnej jest zwykle krótką operacją na metadanych:
BEGIN;
SET LOCAL lock_timeout = '2s';
SET LOCAL statement_timeout = '30s';
ALTER TABLE customers
ADD COLUMN phone_e164 text;
COMMIT;
Limit czasu zapobiega niekończącemu się oczekiwaniu wydania za otwartą transakcją. Jeśli nie można szybko zdobyć blokady, pozwól migracji zakończyć się błędem, sprawdź, co blokuje operację, i ponów próbę w bezpieczniejszym momencie. Nie ponawiaj automatycznie w ciasnej pętli, ponieważ kolejne żądania blokady mogą nadal zakłócać ruch produkcyjny.
Współczesne wydania PostgreSQL potrafią dodać kolumnę ze stałą wartością domyślną bez natychmiastowego zapisania jej w każdym istniejącym wierszu. Ta optymalizacja nie sprawia, że każda wartość domyślna jest nieszkodliwa. Wyrażenie zmienne może wymagać przepisania tabeli, a ALTER TABLE nadal potrzebuje krótkiej blokady ACCESS EXCLUSIVE. Potwierdź zachowanie dla wdrożonej wersji PostgreSQL i konkretnego wyrażenia, zamiast polegać na ogólnej zasadzie.
Zwykłe CREATE INDEX może blokować zapisy. Gdy tabela musi pozostać zapisywalna, użyj tworzenia współbieżnego:
CREATE INDEX CONCURRENTLY idx_customers_phone_e164
ON customers (phone_e164);
CREATE INDEX CONCURRENTLY nie może działać wewnątrz bloku transakcji. Trwa dłużej, wykonuje dodatkową pracę i może czekać na starsze transakcje, ale normalne inserty, aktualizacje i usunięcia mogą być kontynuowane. Nadal zużywa CPU, I/O i WAL, dlatego podczas działania monitoruj opóźnienia bazy i repliki.
Nieudane budowanie współbieżne może pozostawić nieprawidłowy indeks. Przed ponowną próbą sprawdź stan indeksu, a następnie świadomie usuń lub przebuduj nieprawidłowy obiekt. Narzędzia migracyjne, które opakowują każdy plik w transakcję, potrzebują obsługiwanego trybu bez transakcji dla współbieżnych operacji indeksowych.
Nowe tabele często łatwiej wprowadzić niż przekształcać dane w miejscu. Dla relacji jeden do wielu lub wiele do wielu dodaj tabelę docelową i jej indeksy, zachowując kolumnę źródłową. Usunięcie źródła odłóż do czasu, gdy nowe zapisy, dane historyczne, odczyty i odbiorcy downstream zostaną przeniesieni.
Zmiany typów wymagają szczególnej ostrożności. Część to wyłącznie zmiany metadanych, inne przepisują każdy wiersz albo zbyt długo utrzymują restrykcyjną blokadę. Przy ryzykownej konwersji dodaj kolumnę o docelowym typie, wypełniaj ją partiami, przełącz dostęp aplikacji, a oryginał usuń później. Zespół zyska też miejsce do rejestrowania błędów konwersji, zamiast wymuszać powodzenie lub porażkę jednego dużego ALTER COLUMN TYPE.
Wdrażaj kod, który pozostaje zgodny
Zgodny kod aplikacji toleruje brakujące wartości przejściowe i nigdy nie wymaga destrukcyjnej migracji w trakcie tego samego rollout. Rozszerzenie bazy powinno zakończyć się, zanim pierwsza instancja aplikacji zacznie używać nowego obiektu.
Zapis do dwóch miejsc przydaje się, gdy obie reprezentacje muszą pozostać aktualne. Gdy to możliwe, wykonuj oba zapisy w tej samej transakcji bazy danych. Asynchroniczny drugi zapis może się nie udać po powodzeniu pierwszego, tworząc rozbieżność widoczną przy późniejszych odczytach.
Logika dual write potrzebuje też jednego źródła prawdy. Jeśli phone_e164 jest wyprowadzane z phone, określ, które dane wejściowe wygrywają, gdy przekazano oba pola, i stosuj tę samą normalizację w handlerach API, workerach, importach oraz narzędziach administracyjnych. W przeciwnym razie dwie pozornie poprawne ścieżki kodu mogą zapisywać różne wyniki.
Odczyty przełączaj później niż zapisy. Pozostaw odczyty na sprawdzonym polu, podczas gdy nowe zapisy wypełniają obie formy, a backfill obsługuje wiersze historyczne. Po weryfikacji wdróż ścieżkę odczytu, która preferuje nowe pole i używa starej wartości tylko zgodnie z określoną regułą awaryjną. Mierz użycie fallbacku. Niewidoczny fallback może na zawsze ukryć niekompletne dane.
Typowa sekwencja wydań wygląda tak:
- Wydanie 1 dodaje nowe obiekty bazy bez zmiany zachowania aplikacji.
- Wydanie 2 zapisuje reprezentacje przejściowe, nadal korzystając ze sprawdzonych odczytów.
- Wydanie 3 przełącza odczyty po zaliczeniu backfillu i kontroli spójności.
- Wydanie 4 przestaje utrzymywać starą reprezentację po upływie kryteriów wycofania.
- Wydanie 5 usuwa stare odwołania kodu, a później następuje czyszczenie bazy.
Oddziel publiczne kontrakty API od fizycznych zmian schematu. Zmiana nazwy kolumny w bazie nie wymaga natychmiastowej zmiany nazwy pola w odpowiedziach webowych, mobilnych lub integracyjnych. Takie kontrakty zmieniaj według własnej polityki zgodności, zwłaszcza gdy klientów nie da się aktualizować razem z serwerem.
Zidentyfikuj każdego zapisującego. Handlery HTTP są tylko jednym źródłem zmian. Konsumenci kolejek, zadania zaplanowane, skrypty importu, narzędzia naprawy danych, triggery bazy i bezpośrednie operacje administracyjne mogą nadal tworzyć wiersze w starym formacie. Tam, gdzie to praktyczne, oznaczaj połączenia z bazą nazwą aplikacji i loguj użycie ścieżek przejściowych, aby pominięty proces stał się widoczny.
Długo działające procesy mogą zachowywać nieaktualne założenia przez przygotowane zapytania, buforowane metadane lub warstwę mapowania obiektowo-relacyjnego. Przetestuj kroczące restarty i zachowanie puli połączeń przed fazą contract. Proces, który ostatnio nie obsługiwał ruchu, nadal może zawieść, gdy po raz pierwszy uruchomi się rzadki job.
Uzupełniaj dane bez przeciążania bazy
Bezpieczny backfill aktualizuje małe, wznawialne partie i zwalnia, gdy pogarsza się kondycja produkcji. Rozpoczyna się dopiero wtedy, gdy aktywni zapisujący potrafią utrzymywać nową reprezentację.
Dobieraj partie według czasu trwania i wpływu na bazę, a nie według uniwersalnej liczby wierszy. Tysiąc wąskich wierszy może zakończyć się w milisekundach, podczas gdy tysiąc wierszy z dużymi wartościami lub kosztownymi transformacjami może wygenerować znaczne I/O. Zacznij ostrożnie i celuj w transakcje kończące się w kilka sekund. Zatwierdzaj między partiami, aby blokady i stare wersje wierszy nie gromadziły się w jednej transakcji.
PostgreSQL nie obsługuje ORDER BY i LIMIT bezpośrednio w zwykłym UPDATE. Wybierz partię we wspólnym wyrażeniu tabelowym, a potem zaktualizuj te wiersze:
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;
Aplikacja zapisuje największe ukończone id jako kursor. Warunkowa aktualizacja sprawia, że ponowne uruchomienie jest idempotentne, więc awaria po zatwierdzeniu nie uszkodzi już przetworzonych wierszy. Zapisuj postęp na tyle starannie, aby kursor nie przesunął się poza niezatwierdzoną partię.
Rosnący kursor id zapobiega wielokrotnemu skanowaniu początku tabeli, ale nie obejmuje późnych poprawek ani wierszy wstawionych poniżej kursora. Zakończ pracę przebiegiem wyrównującym po wszystkich pozostałych wartościach NULL. Jeśli identyfikatory nie są uporządkowane albo wiersze mogą zmieniać stan kwalifikacji, użyj tabeli roboczej lub innego jawnego punktu kontrolnego, zamiast zakładać, że jeden przebieg do przodu wystarczy.
Wielu workerów może przejmować wiersze przez FOR UPDATE SKIP LOCKED, ale równoległość zwiększa presję na zapisy i komplikuje śledzenie postępu. Nie łącz pominiętych wierszy z kursorem, który na stałe przesuwa się za nimi. Dla równoległych workerów bezpieczniejsza jest kolejka przejętych identyfikatorów albo powtarzany skan kwalifikacji.
Ograniczaj tempo na podstawie pomiarów produkcyjnych, takich jak opóźnienie zapytań, aktywne połączenia, oczekiwanie na blokady, generowanie WAL, opóźnienie odtwarzania repliki i wzrost martwych wierszy. Wstrzymaj pracę po przekroczeniu progu, a potem wznów ją od punktu kontrolnego. Stałe przerwy są proste, ale sygnały z bazy lepiej reagują na zmianę ruchu.
Unikaj zmieniania każdego wiersza, gdy pracy wymaga tylko część. Filtruj po nowym polu, stanie źródłowym lub znaczniku migracji. Jeśli transformacja jest kosztowna, oblicz ją poza transakcją aktualizacji tam, gdzie spójność na to pozwala, a potem wykonaj krótki warunkowy zapis. Zachowuj liczbę i próbkę odrzuconych wartości, zamiast po cichu wymyślać dane.
Autovacuum i repliki muszą poradzić sobie z pracą po każdej aktualizacji. Backfill może zakończyć się pomyślnie na primary, gdy repliki mocno zostają w tyle albo bloat tabeli pogarsza późniejsze zapytania. Limity tempa powinny uwzględniać ten odroczony koszt, a nie tylko natychmiastowy czas wykonania partii.
Weryfikuj dane i ruch produkcyjny
Migracja jest gotowa do fazy contract dopiero wtedy, gdy kontrole danych, telemetria aplikacji i dowody dotyczące zależności zgodnie pokazują, że nowa ścieżka jest autorytatywna. Sam licznik ukończonych jobów nie dowodzi poprawności.
Zacznij od kompletności i spójności. PostgreSQL IS DISTINCT FROM porównuje wartości z jawną obsługą NULL, w przeciwieństwie do <>, które zwraca nieznany wynik, gdy jedna z wartości to NULL:
SELECT count(*)
FROM customers
WHERE normalize_phone(phone) IS DISTINCT FROM phone_e164;
Nie uruchamiaj wielokrotnie nieindeksowanego liczenia całej tabeli na zajętej, bardzo dużej tabeli. Użyj jednorazowej kontrolowanej walidacji, ograniczonych zakresów identyfikatorów, próbek lub tymczasowego procesu weryfikacji, który przechodzi przez tabelę. Właściwa metoda zależy od kosztu błędu i dostępnego zapasu mocy bazy.
Weryfikacja powinna obejmować:
- Brak nieoczekiwanych brakujących wartości w wierszach wymagających nowego pola.
- Zgodność nowej wartości z ustaloną transformacją, także dla błędnych i pustych danych wejściowych.
- Spójność nowych wierszy i aktualizacji po zakończeniu przebiegu historycznego.
- Użycie odczytów awaryjnych na zaplanowanym poziomie, zwykle zerowym dla ruchu kontrolowanego przez serwer.
- Błędy, opóźnienia zapytań, blokady i lag replik mieszczące się w limitach wydania.
Porównuj wyniki biznesowe, a nie tylko kolumny. Jeśli migracja zmienia ceny, uprawnienia, stan konta lub identyfikatory, waliduj sumy i niezmienniki, od których zależą użytkownicy. Dwie kolumny mogą mechanicznie się zgadzać, choć obie kodują niewłaściwą regułę biznesową.
Obserwuj pełny cykl działania przed czyszczeniem. Właściwy okres wynika z faktycznego zachowania systemu, a nie ze sztywnej zasady jednego tygodnia. Może obejmować przetwarzanie na koniec miesiąca, rzadki job rozliczeniowy, opóźnione ponowienia kolejki albo maksymalny czas życia starszego klienta mobilnego. Zapisuj dowody, że każdy odbiorca został przeniesiony.
Jeśli architektura aplikacji na to pozwala, wykonaj canary przełączenia odczytu. Skieruj niewielką część ruchu na nową ścieżkę odczytu, porównuj wyniki i stopniowo zwiększaj zakres. Utrzymuj proste wycofanie: skieruj odczyty z powrotem do sprawdzonej reprezentacji bez odwracania backfillu.
Dodawaj ograniczenia, gdy dane są gotowe
Ograniczenia powinny stać się ścisłe dopiero po tym, gdy wszyscy zapisujący stosują regułę, a istniejące dane przeszły walidację. Wymuszenie NOT NULL, CHECK lub klucza obcego w fazie rozszerzenia może blokować ruch albo odrzucać zapisy starszego procesu.
PostgreSQL pozwala dodać ograniczenie CHECK jako NOT VALID, które egzekwuje regułę dla nowych lub zmienionych wierszy bez natychmiastowego skanowania wszystkich danych historycznych. Zweryfikuj je osobno po backfillu:
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;
Po pomyślnej walidacji obsługiwane wydania PostgreSQL mogą wykorzystać ten dowód przy ustawianiu kolumny na NOT NULL, unikając kolejnego pełnego skanowania tabeli. Końcowa zmiana nadal wymaga silnej blokady tabeli, dlatego użyj ograniczonego limitu oczekiwania na blokadę i planu ponowienia:
ALTER TABLE customers
ALTER COLUMN phone_e164 SET NOT NULL;
ALTER TABLE customers
DROP CONSTRAINT customers_phone_e164_present;
Tymczasowe CHECK może pozostać, jeśli ma wartość, lecz utrzymywanie równoważnych ograniczeń zaśmieca katalog bez zmiany reguły.
Klucze obce mogą przejść podobną sekwencję z NOT VALID i VALIDATE CONSTRAINT. Nowe zapisy są sprawdzane po utworzeniu ograniczenia, a walidacja danych historycznych następuje później. Celowo dodaj indeks wspierający, gdy zachowanie usuwania lub aktualizacji relacji referencjonowanej inaczej powodowałoby kosztowne skany.
Walidacja aplikacji powinna poprzedzać egzekwowanie przez bazę, ale go nie zastępuje. Kod daje użytkownikowi czytelniejsze błędy, a baza chroni dane zapisane każdą ścieżką. Podczas rollout obserwuj naruszenia ograniczeń, aby wykryć zapisujący proces pominięty w audycie zależności.
Bezpiecznie usuń starą ścieżkę
Faza contract powinna usuwać zależności aplikacji przed usunięciem obiektów bazy. Gdy telemetria i weryfikacja potwierdzą, że nowa ścieżka jest autorytatywna, czyszczenie może postępować przez osobne wydania.
Najpierw przestań odczytywać stare pole i usuń logikę fallbacku. Potem wyłącz jego zapisy i obserwuj produkcję dostatecznie długo, aby wykryć rzadkie ścieżki. Usuń flagi funkcji, triggery, widoki zgodności, skrypty naprawcze i zaplanowane joby wspominające starą reprezentację. Przeszukaj wyeksportowany kod źródłowy i kod migracji, ale sprawdź też raporty, zapytania integracyjne i konfiguracje change data capture poza głównym repozytorium.
Bezpieczna kolejność czyszczenia:
- Usuń odczyty awaryjne i potwierdź, że nie pojawiają się już w telemetrii.
- Zatrzymaj stare zapisy i usuń kod synchronizacji.
- Usuń odwołania aplikacji ze wszystkich wersji możliwych do wdrożenia.
- Usuń przestarzałe indeksy i ograniczenia odpowiednią metodą online.
- Usuń starą kolumnę lub tabelę w późniejszym wydaniu bazy.
Usunięcie kolumny PostgreSQL jest przede wszystkim zmianą katalogu, ale nadal wymaga blokady ACCESS EXCLUSIVE. Krótkie polecenie może więc czekać za długą transakcją i blokować późniejszą pracę. Ustaw limit oczekiwania na blokadę, wcześniej sprawdź długo działające transakcje i zaplanuj próbę w okresie mniejszego ryzyka.
Dla przestarzałego indeksu użyj DROP INDEX CONCURRENTLY, gdy blokowanie zapisów byłoby nieakceptowalne. Tak jak tworzenie współbieżne, nie działa ono wewnątrz bloku transakcji i ma ograniczenia, które narzędzie migracyjne musi obsłużyć.
Nie łącz czyszczenia kodu i fizycznego usunięcia w jednym wydaniu. Rozdzielenie pozwala oczyszczonej aplikacji działać z bazą, która nadal zawiera nieużywany obiekt. Jeśli pojawi się problem aplikacji, wycofanie pozostaje możliwe bez odtwarzania schematu lub danych.
Przed usunięciem tabeli sprawdź własność sekwencji, widoków, funkcji, uprawnień, triggerów, publikacji replikacji i zapytań zewnętrznych. Nie używaj CASCADE jako skrótu w migracji produkcyjnej, ponieważ może usunąć zależności, które nie należały do planowanej zmiany.
Obsługuj wycofanie i nieudane kroki
Plan wycofania powinien określać bezpieczne działanie dla każdej fazy, zamiast polegać na jednej ogólnej migracji w dół. Obiekty addytywne, przenoszenie danych, przełączenia odczytów i usuwanie mają różne właściwości odzyskiwania.
Jeśli rozszerzenie nie zdobędzie blokady, pozostaw aplikację bez zmian i ponów próbę po usunięciu blokującej transakcji. Jeśli współbieżne budowanie indeksu się nie uda, sprawdź, czy pozostawiło nieprawidłowy indeks, i usuń ten konkretny obiekt przed kolejną próbą.
Jeśli backfill generuje obciążenie, wstrzymaj go. Zatwierdzone, idempotentne partie mogą pozostać na miejscu. Zmniejsz wielkość partii lub tempo, usuń przyczynę kosztownej transformacji i wznów pracę od punktu kontrolnego. Cofanie milionów poprawnych aktualizacji zwykle zwiększa ryzyko i nie pomaga produkcji wrócić do zdrowia.
Jeśli nowa ścieżka odczytu zwraca niepoprawne wyniki, skieruj odczyty z powrotem do starej reprezentacji, zachowując nowe dane do diagnozy. Kontynuuj dual write tylko wtedy, gdy wiadomo, że jest poprawny. Gdy wadliwy jest sam zapisujący proces, wyłącz go albo wycofaj aplikację przed naprawą dotkniętych wierszy.
Po fazie contract odzyskanie może wymagać przywrócenia danych, a nie tylko wdrożenia starszego buildu. Jasno określ punkt bez powrotu. Wykonaj backup lub snapshot wymagany przez politykę odzyskiwania systemu, przetestuj przywracanie przed wydaniem i przechowuj stary obiekt przez uzgodniony okres retencji, jeśli pozwala na to koszt storage.
Polecenia schematu mogą być transakcyjne, lecz skutki zewnętrzne nie zawsze są nimi objęte. Współbieżne operacje indeksowe, wiadomości kolejek, zmiany cache i wdrożenia aplikacji nie współdzielą jednej atomowej transakcji. Runbook powinien opisywać obserwowalny stan po każdej częściowej awarii oraz polecenie, które bezpiecznie z niego kontynuuje.
Unikaj typowych pułapek migracji
Większość nieudanych migracji bez przestoju albo wymusza nowy stan zbyt wcześnie, albo pomija odbiorcę starego stanu. Poniższe pułapki wymagają jawnego przeglądu przed zatwierdzeniem.
- Dodanie
NOT NULL, gdy stara instancja aplikacji nadal może pominąć to pole. - Uruchomienie dużego backfillu w jednej transakcji, która zbyt długo utrzymuje blokady i wersje wierszy.
- Zmiana nazwy kolumny tak, jakby była zmianą addytywną, mimo że stary kod nadal używa pierwotnej nazwy.
- Przełączenie odczytów, zanim wszystkie ścieżki zapisu i wiersze historyczne wypełnią nową reprezentację.
- Traktowanie udanego wdrożenia jako dowodu, że raporty, workery, repliki i integracje są zgodne.
Kolejna subtelna awaria wynika z synchronizacji w obu kierunkach. Trigger kopiuje old_col do new_col, a kod aplikacji kopiuje new_col z powrotem do old_col. Różnice w normalizacji lub kolejności triggerów mogą tworzyć pętle, nadpisywać celowe wartości albo zacierać własność danych. Wybierz jeden kierunek i udokumentuj, która reprezentacja jest autorytatywna w każdym wydaniu.
Wartości domyślne mogą ukryć brakujące aktualizacje zapisujących procesów. Jeśli nowa wymagana kolumna otrzymuje pustą lub ogólną wartość domyślną, stary kod wygląda na zgodny, choć zapisuje semantycznie niepoprawne dane. Użyj przejścia z NULL, gdy brak wartości dostarcza przydatnej informacji diagnostycznej, a rzeczywistą regułę wymuś dopiero, gdy każdy zapisujący proces podaje znaczącą wartość.
Flaga funkcji sama w sobie nie czyni niezgodnego polecenia schematu bezpiecznym. Wyłączona ścieżka kodu może nadal być załadowana, przygotowana lub uruchomiona przez starszy proces. Obiekt bazy musi pozostać, dopóki żadna aktywna ani możliwa do wdrożenia wersja się do niego nie odwołuje.
Ważna jest także odpowiedzialność za migrację. Przypisz jedną osobę lub zespół do przejścia aż do fazy contract, w tym do weryfikacji i terminów usunięcia. W przeciwnym razie tymczasowe kolumny, flagi i joby synchronizujące mogą zostać na miesiące, zwiększając koszt każdej kolejnej zmiany.
Zastąp kolumnę telefonu bez przestoju
Zastąpienie customers.phone znormalizowanym customers.phone_e164 wymaga kolumny addytywnej, zdefiniowanej polityki konwersji, zgodnego kodu, ograniczonego backfillu, przełączenia odczytów i opóźnionego czyszczenia. Polityka konwersji musi powstać przed SQL, ponieważ nie każdą zapisaną wartość da się automatycznie znormalizować.
Zacznij od klasyfikacji istniejących wartości. Poprawne numery można przekonwertować, gdy znany jest wymagany kontekst kraju. Puste wartości mogą stać się NULL. Niejednoznaczne lub błędne numery powinny trafić do raportu wyjątków, a nie być zgadywane. Ustal, czy produkt wymaga numeru telefonu dla każdego klienta, ponieważ od tego zależy, czy później odpowiednie będzie NOT NULL.
Dodaj kolumnę z krótkim limitem oczekiwania na blokadę:
BEGIN;
SET LOCAL lock_timeout = '2s';
ALTER TABLE customers
ADD COLUMN phone_e164 text;
COMMIT;
Wdróż kod, który normalizuje nowe dane wejściowe i zapisuje phone oraz phone_e164 w jednej transakcji. Początkowo pozostaw odczyty na phone. Zaktualizuj każdego zapisującego, w tym importy kont, narzędzia wsparcia, joby workerów i testy tworzące dane klientów.
Uzupełniaj kwalifikujące się wiersze krótkimi transakcjami. Zapisuj ostatni przetworzony identyfikator, liczbę przekonwertowanych i pominiętych wierszy oraz powód każdej kategorii błędów. Ograniczaj tempo joba według opóźnienia produkcji i lag replik. Po zakończeniu przebiegu do przodu przeskanuj ponownie kwalifikujące się wartości NULL, aby wychwycić współbieżne inserty lub wiersze pominięte po restarcie.
Przeprowadź kontrolę spójności według tych samych zasad normalizacji co aplikacja, a następnie ręcznie sprawdź próbkę prefiksów międzynarodowych, rozszerzeń, pustych wartości, zduplikowanych rekordów kontaktowych i starych danych importowanych. Liczba wierszy potwierdza pokrycie, lecz nie poprawność numeru telefonu.
Wdróż ścieżkę odczytu, która zwraca phone_e164, gdy pole jest obecne, a phone używa tylko dla logowanego wyjątku. Monitoruj użycie fallbacku i błędy normalizacji. Rozwiązuj pozostałe wyjątki, zamiast pozwalać, aby fallback stał się stałym zachowaniem.
Gdy nowe pole stanie się autorytatywne, usuń fallback i przestań zapisywać phone. Obserwuj rzadkie joby i ruch integracyjny przez odpowiedni cykl działania. Dodaj zwalidowane ograniczenie tylko wtedy, gdy wymaga tego reguła produktu.
Na końcu usuń odwołania kodu do phone. Osobno usuń jego indeksy lub ograniczenia, a potem usuń kolumnę w późniejszej migracji z ograniczonym oczekiwaniem na blokadę. Jeśli przełączenie odczytu zawiedzie przed tym usunięciem, wycofaj zachowanie aplikacji, gdy obie kolumny są nadal dostępne.
Ten przykład pokazuje też problem domenowy, którego mechanika schematu nie rozwiąże: dzielenie lub normalizacja danych wpisanych przez człowieka nie zawsze jest bezstratna. Plan migracji musi zachować wyjątki i dać właścicielowi sposób ich rozwiązania.
Sprawdź każde wydanie przed wysyłką
Lista kontrolna wydania powinna potwierdzać zgodność, ograniczać wpływ na produkcję i nazywać działanie odzyskiwania dla bieżącej fazy. Zachowuj dowody razem ze zmianą, aby operator nie musiał odtwarzać intencji podczas incydentu.
Przed wdrożeniem potwierdź:
- Wersja aplikacji działa ze stanem bazy przed i po tym wydaniu.
- Dla poleceń schematu, które mogą czekać za ruchem, ustawiono limity czasu blokady i polecenia.
- Job backfillu lub walidacji ma kontrolki postępu, wstrzymania, wznowienia i ograniczania tempa.
- Dashboardy obejmują błędy, opóźnienia, blokady, obciążenie bazy, WAL i lag replik.
- Działanie wycofania zostało przetestowane bez zależności od już usuniętego obiektu.
Zapisz jawne warunki ukończenia. Przykłady to zero nowych błędów spójności przez pełny cykl joba, zero odczytów awaryjnych z ruchu kontrolowanego przez serwer, aktualizacja każdego znanego odbiorcy oraz udane kontrolowane zapytanie walidacyjne. Procent ukończenia pomaga podczas backfillu, ale 100 procent przetworzonych danych nie oznacza 100 procent poprawnych danych.
Przeglądaj kolejność migracji niezależnie od przeglądu kodu. Poprawny zestaw SQL i zmian aplikacji może nadal zawieść, jeśli wdrożenie wykona je w niewłaściwej kolejności. Określ, który krok może rozpocząć się dopiero po ukończeniu innego.
Warunki zatrzymania powinny być liczbowe, gdy to możliwe. Zdefiniuj akceptowalne opóźnienie zapytań, czas oczekiwania na blokadę, lag replik, poziom błędów i czas trwania partii. Po przekroczeniu progu operator powinien wiedzieć, czy wstrzymać job, anulować oczekujące polecenie czy przekierować odczyty, bez szukania nowej zgody w trakcie incydentu.
Migracja jest zakończona dopiero wtedy, gdy nowa reprezentacja obsługuje odczyty i zapisy, dane historyczne przeszły weryfikację, stary obiekt został usunięty, a tymczasowa infrastruktura operacyjna zniknęła.
Uczyń proces powtarzalnym
Wielokrotnego użytku runbook migracji zamienia expand/contract w zwykłą pracę wydawniczą z nazwanymi właścicielami i mierzalnymi bramkami. Powinien być na tyle krótki, aby dało się go stosować podczas wdrożenia na żywo, i na tyle konkretny, aby opisywał stany częściowej awarii.
Użyj pięciu sekcji w runbooku:
- Rozszerzenie: dokładne operacje schematu, oczekiwane blokady, limity czasu i wymagania transakcyjne.
- Zgodność: dotknięty kod, zapisujący, odczytujący, flagi, klienci i kolejność wdrożenia.
- Backfill: polityka transformacji, partie, punkty kontrolne, throttling i obsługa wyjątków.
- Weryfikacja: kontrole SQL, niezmienniki biznesowe, telemetria i progi ukończenia.
- Contract: usuwanie zależności, okres obserwacji, fizyczne czyszczenie i granice odzyskiwania.
Przypisz właściciela i oczekiwaną datę ukończenia do każdego obiektu przejściowego. Śledź kolumny, indeksy, flagi, triggery i joby w tym samym miejscu. Czyszczenie jest częścią migracji, a nie opcjonalnym utrzymaniem.
Zespoły tworzące w Koder.ai mogą użyć Planning Mode, aby rozpisać te fazy i punkty kontrolne przed rozpoczęciem zmian produkcyjnych. Eksport kodu źródłowego pozwala też poddać SQL migracji i logikę zgodności takiemu samemu przeglądowi jak pozostały kod aplikacji. Koder.ai obsługuje wdrażanie, hosting, snapshoty i rollback, ale nie należy zakładać, że wycofanie aplikacji odwróci zatwierdzoną transformację danych. Zachowaj zgodność schematu, aż plan odzyskiwania bazy przestanie zależeć od starej reprezentacji.
Jeśli to możliwe, planuj pracę intensywnie zapisującą dane na czas mniejszego ruchu, lecz nie traktuj terminu jako jedynej kontroli bezpieczeństwa. Ograniczone transakcje, throttling oparty na sygnałach, obserwowalny postęp i przetestowane działanie wstrzymania pozwalają utrzymać migrację online pod kontrolą, gdy ruch lub dane zachowają się inaczej niż oczekiwano.
Często zadawane pytania
Dlaczego zmiana schematu może spowodować awarię?
Zmiany schematu psują działanie produkcji, gdy stare i nowe wersje aplikacji oczekują różnych struktur bazy danych. Podczas wdrożenia kroczącego obie wersje mogą działać jednocześnie, więc zbyt wczesne usunięcie lub przemianowanie kolumny może powodować błędy odczytu albo zapisu.
Na czym polega wzorzec migracji expand/contract?
Expand/contract dzieli niezgodną zmianę na bezpieczne etapy. Najpierw dodajesz nową strukturę, potem przenosisz do niej kod i dane, a dopiero na końcu usuwasz starą strukturę, gdy żaden odbiorca już z niej nie korzysta.
Jak zmienić nazwę lub zastąpić kolumnę bazy danych bez przestoju?
Najpierw dodaj nową kolumnę i pozostaw starą. Wdróż kod obsługujący oba pola, uzupełnij istniejące rekordy małymi partiami, przełącz odczyty po walidacji, a starą kolumnę usuń w późniejszym wydaniu.
Czy mogę dodać kolumnę PostgreSQL bez blokowania ruchu?
Zwykle tak. Kolumna dopuszczająca NULL bez wartości domyślnej jest w PostgreSQL często krótką zmianą metadanych, ale nadal wymaga blokady tabeli. Ustaw krótki limit oczekiwania na blokadę, aby migracja zakończyła się błędem zamiast czekać za długą transakcją.
Jak utworzyć indeks bez blokowania zapisów?
Gdy tabela musi pozostać zapisywalna, użyj CREATE INDEX CONCURRENTLY. Operacja trwa dłużej, zwiększa obciążenie bazy i nie może działać wewnątrz bloku transakcji, dlatego monitoruj opóźnienia, WAL i lag replik podczas jej wykonywania.
Kiedy aplikacja powinna zapisywać stare i nowe pole?
Zapisuj obie wartości w tej samej transakcji bazy danych, gdy obie reprezentacje muszą być aktualne. Ustal, które pole ma pierwszeństwo w razie rozbieżności, i stosuj te same reguły normalizacji w API, workerach, importach i narzędziach wsparcia.
Jak bezpiecznie uzupełnić dużą tabelę PostgreSQL?
Przetwarzaj krótkie, wznawialne partie i zatwierdzaj transakcję po każdej z nich. Zapisuj punkt kontrolny, aktualizuj tylko rekordy wymagające pracy, a job zwalniaj lub wstrzymuj, gdy rosną opóźnienia zapytań, oczekiwanie na blokady, wolumen WAL lub lag replik.
Skąd mam wiedzieć, że backfill jest pełny i poprawny?
Nie przełączaj odczytów tylko dlatego, że backfill się zakończył. Sprawdź, czy wymagane wartości istnieją, porównaj stare i nowe reprezentacje, monitoruj odczyty awaryjne oraz potwierdź, że nowe zapisy pozostają spójne po przetworzeniu danych historycznych.
Kiedy dodać NOT NULL, ograniczenia CHECK lub klucze obce?
Dodaj ścisłe ograniczenia po walidacji istniejących danych i gdy każdy aktywny zapisujący proces dostarcza poprawne wartości. PostgreSQL pozwala dodać niektóre ograniczenia jako NOT VALID, egzekwować je dla nowych wierszy i osobno sprawdzić dane historyczne.
Kiedy można bezpiecznie usunąć starą ścieżkę schematu?
Najpierw usuń odczyty awaryjne, potem zatrzymaj stare zapisy i obserwuj system przez pełny cykl działania. Gdy żadna aplikacja, job, raport, integracja ani klient nie odwołuje się już do starego obiektu, usuń powiązany kod, a kolumnę lub tabelę bazy danych skasuj w późniejszym wydaniu.