Jak wykorzystaliśmy opóźnioną replikację do awaryjnego przywracania z PostgreSQL

Jak wykorzystaliśmy opóźnioną replikację do awaryjnego przywracania z PostgreSQL
Replikacja to nie kopia zapasowa. Czyż nie? Oto jak wykorzystaliśmy opóźnioną replikację do przywracania danych po przypadkowym usunięciu skrótów.

Specjaliści ds. infrastruktury na GitLab odpowiadają za działanie GitLab.com — największej instancji GitLab na świecie. Tutaj znajduje się 3 miliony użytkowników i prawie 7 milionów projektów, co czyni go jednym z największych otwartoźródłowych serwisów SaaS z dedykowaną architekturą. Bez systemu baz danych PostgreSQL infrastruktura GitLab.com nie przetrwałaby długo, a to, co robimy dla zapewnienia odporności na awarie i ochrony danych, jest niezwykle istotne. Choć mało prawdopodobne, aby taka katastrofa miała miejsce, jesteśmy dobrze przygotowani i dysponujemy różnymi mechanizmami kopii zapasowej i replikacji.

Replikacja to nie to samo, co kopia zapasowa baz danych (zobacz poniżej). Ale teraz przekonamy się, jak szybko możemy przywrócić przypadkowo usunięte dane za pomocą opóźnionej replikacji: na GitLab.com użytkownik usunięto skrót do projektu gitlab-ce i utracono połączenie z żądaniami scalania i zadaniami.

Dzięki opóźnionej replikacji przywróciliśmy dane w zaledwie 1,5 godziny. Zobacz, jak to wyglądało.

Przywracanie do stanu z określonego momentu w czasie z PostgreSQL

PostgreSQL ma wbudowaną funkcję, która przywraca stan bazy danych do określonego momentu. Nazywa się ona Point-in-Time Recovery (PITR) i wykorzystuje te same mechanizmy, które utrzymują aktualność repliki: zaczynając od wiarygodnego zrzutu całego klastra bazy danych (bazowego kopii zapasowej), stosujemy szereg zmian stanu do określonego momentu.

Aby skorzystać z tej funkcji dla zimnej kopii zapasowej, regularnie tworzymy bazowe kopie zapasowe bazy danych i przechowujemy je w archiwum (archiwa GitLab są przechowywane w chmurze Google). Ponadto śledzimy zmiany stanu bazy danych, archiwizując dziennik zapisów w przód (write-ahead log, WAL). Dzięki temu możemy wykonać PITR dla przywracania po awarii: zaczynamy od zrzutu wykonanego przed błędem i stosujemy zmiany z archiwum WAL aż do awarii.

Czym jest opóźniona replikacja?

Opóźniona replikacja to stosowanie zmian z WAL z opóźnieniem. To znaczy, że transakcja miała miejsce w godzinie X, ale w replicie pojawi się z opóźnieniem d o godzinę X + d.

W PostgreSQL istnieją 2 sposoby skonfigurowania fizycznej repliki bazy danych: przywracanie z archiwum i replikacja strumieniowa. Przywracanie z archiwum, w zasadzie działa jak PITR, ale w trybie ciągłym: nieustannie wyciągamy zmiany z archiwum WAL i stosujemy je do repliki. A replikacja strumieniowa bezpośrednio wyciąga strumień WAL z nadrzędnego hosta bazy danych. Preferujemy odzyskiwanie z archiwum — jest łatwiejsze w zarządzaniu i zapewnia normalną wydajność, która nie odbiega od działającego klastra.

Jak skonfigurować opóźnione przywracanie z archiwum

Opcje odzyskiwania są opisane w pliku recovery.conf. Przykład:

standby_mode = 'on'
restore_command = 'envdir /etc/wal-e.d/env /opt/wal-e/bin/wal-e wal-fetch -p 4 "%f" "%p"'
recovery_min_apply_delay = '8h'
recovery_target_timeline = 'latest'

Z tymi parametrami skonfigurowaliśmy opóźnioną replikę z odzyskiwaniem z archiwum. Używamy tutaj wal-e do wydobywania segmentów WAL (restore_command) z archiwum, a zmiany będą stosowane po ośmiu godzinach (recovery_min_apply_delay). Replica będzie śledzić zmiany w harmonogramie czasowym w archiwum, na przykład w wyniku awarii w klastrze (recovery_target_timeline).

Z recovery_min_apply_delay można skonfigurować replikację strumieniową z opóźnieniem, ale są tu pewne pułapki, które są związane z slotami replikacji, sprzężeniem zwrotnym gorącego zapasowego itd. Archiwum WAL pozwala ich uniknąć.

Parametr recovery_min_apply_delay pojawiło się dopiero w PostgreSQL 9.3. W wcześniejszych wersjach do opóźnionej replikacji trzeba było skonfigurować kombinację funkcji zarządzania przywracaniem (pg_xlog_replay_pause(), pg_xlog_replay_resume()) lub przechowywać segmenty WAL w archiwum przez czas opóźnienia.

Jak PostgreSQL to robi?

Ciekawe, jak PostgreSQL implementuje opóźnione przywracanie. Spójrzmy na recoveryApplyDelay(XlogReaderState). Jest wywoływane z głównej pętli powtórzeń dla każdego rekordu z WAL.

static bool
recoveryApplyDelay(XLogReaderState *record)
{
    uint8       xact_info;
    TimestampTz xtime;
    long        secs;
    int         microsecs;

    
    /* nic nie do zrobienia, jeśli nie skonfigurowano opóźnienia */
    if (recovery_min_apply_delay <= 0)
        return false;

    /* nie stosuje się opóźnienia na bazie danych, która jeszcze nie jest spójna */
    if (!reachedConsistency)
        return false;

    /*
     * Czy to jest rekord COMMIT?
     *
     * Świadomie decydujemy się nie opóźniać abortów, ponieważ nie mają one wpływu na
     * MVCC. Już pozwalamy na powtórzenie rekordów, które nie mają znacznika czasu,
     * więc już istnieje możliwość problemów spowodowanych wczesnymi konfliktami na
     * zapasach.
     * /
    if (XLogRecGetRmid(record) != RM_XACT_ID)
        return false;

    xact_info = XLogRecGetInfo(record) & XLOG_XACT_OPMASK;

    if (xact_info != XLOG_XACT_COMMIT &&
        xact_info != XLOG_XACT_COMMIT_PREPARED)
        return false;

    if (!getRecordTimestamp(record, &xtime))
        return false;

    recoveryDelayUntilTime =
        TimestampTzPlusMilliseconds(xtime, recovery_min_apply_delay);

    /*
     * Wyjdź bez uzbrajania zatrzasku, jeśli już minął czas na zastosowanie tego
     * rekordu
     * /
    TimestampDifference(GetCurrentTimestamp(), recoveryDelayUntilTime,
                        &secs, &microsecs);
    if (secs <= 0 && microsecs <= 0)
        return false;

    while (true)
    {
        // Skróconae:
        // Użyj WaitLatch, aż dotrzemy do recoveryDelayUntilTime
        // i wtedy
        break;
    }
    return true;
}

Chodzi o to, że opóźnienie opiera się na czasie fizycznym zapisanym w znaczniku czasu komitowania transakcji (xtime). Jak widać, opóźnienie stosowane jest tylko do komitów i nie wpływa na inne zapisy — wszystkie zmiany są stosowane bezpośrednio, a komit jest odkładany, tak że zobaczymy zmiany dopiero po ustawionym opóźnieniu.

Jak używać opóźnionej repliki do przywracania danych

Przypuśćmy, że mamy w produkcji klaster bazy danych i replikę z ośmiogodzinnym opóźnieniem. Zobaczmy, jak przywrócić dane na przykładzie losowego usunięcia etykiet.

Kiedy dowiedzieliśmy się o problemie, wstrzymaliśmy przywracanie z archiwum dla opóźnionej repliki:

SELECT pg_xlog_replay_pause();

Dzięki pauzie nie mieliśmy ryzyka, że replikacja powtórzy zapytanie USUŃ. Przydatna rzecz, jeśli potrzebujesz czasu, aby wszystko ogarnąć.

Chodzi o to, że opóźniona replika powinna dotrzeć do momentu przed zapytaniem USUŃ. Mieliśmy mniej więcej świadomość fizycznego czasu usunięcia. Usunęliśmy recovery_min_apply_delay i dodaliśmy recovery_target_time do recovery.conf. Tak replika osiąga odpowiedni moment bez opóźnień:

recovery_target_time = '2018-10-12 09:25:00+00'

Z znacznikami czasowymi lepiej odjąć coś ekstra, żeby nie chybić. Prawda, że im większa redukcja, tym więcej danych tracimy. Znowu, jeśli przegapimy zapytanie, USUŃ, wszystko znowu się usunie i będziemy musieli zaczynać od nowa (albo wziąć zimny backup do PITR).

Uruchomiliśmy ponownie odłożony egzemplarz Postgres, a segmenty WAL powtarzały się do wskazanego czasu. Postęp na tym etapie można śledzić za pomocą zapytania:

SELECT
  -- bieżąca lokalizacja w WAL
  pg_last_xlog_replay_location(),
  -- bieżący znacznik czasu transakcji (stan repliki)
  pg_last_xact_replay_timestamp(),
  -- bieżący czas fizyczny
  now(),
  -- ilość czasu, która wciąż musi być zastosowana, aż osiągnięty zostanie recovery_target_time
  '2018-10-12 09:25:00+00'::timestamptz - pg_last_xact_replay_timestamp() as delay;

Jeśli znacznik czasu przestaje się zmieniać, odzyskiwanie zostało zakończone. Można skonfigurować działanie recovery_target_action, aby zamknąć, przesunąć lub wstrzymać egzemplarz po powtórzeniu (domyślnie zostaje wstrzymany).

Baza danych powróciła do stanu sprzed feralnego zapytania. Teraz można na przykład wyeksportować dane. Wyeksportowaliśmy usunięte dane o skrótach oraz wszystkie powiązania z zadaniami i merge requestami, przenosząc je do roboczej bazy danych. Jeśli straty są znaczne, można po prostu przesunąć replikę i użyć jej jako głównej. W przeciwnym razie wszystkie zmiany dokonane po momencie, do którego się odzyskaliśmy, zostaną utracone.

Zamiast znaczników czasu lepiej używać ID transakcji. Warto zapisywać te ID, na przykład dla operatorów DDL (typu DROP TABLE), za pomocą log_statements = 'ddl'. Gdybyśmy mieli ID transakcji, wzięlibyśmy recovery_target_xid i przesunęli wszystko do transakcji przed zapytaniem USUŃ.

Powrót do działania jest bardzo prosty: usuń wszelkie zmiany z recovery.conf i uruchom ponownie Postgresa. Wkrótce w replikacji znowu pojawi się ośmiogodzinna opóźnienie i będziemy gotowi na przyszłe trudności.

Zalety odzyskiwania

Z odłożoną repliką nie trzeba godzinami przywracać całego zrzutu z archiwum. Na przykład potrzebujemy pięciu godzin, aby wydobyć cały bazowy backup o wielkości 2 TB. A potem trzeba jeszcze zastosować cały dzienny WAL, aby odzyskać się do potrzebnego stanu (w najgorszym przypadku).

Odłożona replika jest lepsza od zimnego backupu z dwóch powodów:

  1. Nie trzeba wyjmować całego bazowego backupu z archiwum.
  2. Istnieje stałe ośmiogodzinne okno segmentów WAL, które należy powtórzyć.

Śledzimy również możliwość wykonania PITR z WAL i szybko zauważylibyśmy uszkodzenia lub inne problemy z archiwum WAL, monitorując opóźnienie odłożonej repliki.

W tym przykładzie odzyskanie zajęło nam 50 minut, co oznacza, że prędkość wynosiła 110 GB danych WAL na godzinę (archiwum wówczas nadal było na AWS S3). W sumie rozwiązaliśmy problem i przywróciliśmy dane w 1,5 godziny.

Podsumowanie: gdzie przyda się odroczona replikacja (a gdzie nie)

Użyj odroczonej replikacji jako środka pierwszej pomocy, jeśli przypadkowo utraciłeś dane i zauważyłeś ten problem w granicach ustawionej opóźnienia.

Jednak pamiętaj: replikacja to nie kopia zapasowa.

Kopia zapasowa i replikacja mają różne cele. Zimna kopia zapasowa przyda się, jeśli przypadkowo zrobiłeś USUŃ lub DROP TABLE. Tworzymy kopię zapasową z zimnego magazynu i przywracamy poprzedni stan tabeli lub całej bazy danych. Ale przy tym zapytanie DROP TABLE prawie natychmiastowo powiela się w wszystkich replikach na roboczym klastrze, więc zwykła replikacja tu nie pomoże. Sama replikacja utrzymuje bazę danych dostępną, gdy poszczególne serwery są obciążone i rozkłada obciążenie.

Nawet z odroczoną replikacją czasami bardzo potrzebujemy zimnej kopii zapasowej w bezpiecznym miejscu, jeśli nagle dojdzie do awarii centrum danych, ukrytej usterki lub innych zdarzeń, które natychmiast nie zauważysz. Tu od samej replikacji nie ma pożytku.

Uwaga. Na GitLab.com teraz chronimy przed utratą danych tylko na poziomie systemu i nie przywracamy danych na poziomie użytkownika.

Źródło: habr.com

Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS 🔥 Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS | ProHoster