
Replikacja to nie kopia zapasowa. Czyż nie? Oto jak wykorzystaliśmy opóźnioną replikację do przywracania danych po przypadkowym usunięciu skrótów.
na GitLab odpowiadają za działanie — 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 (). Ale teraz przekonamy się, jak szybko możemy przywrócić przypadkowo usunięte dane za pomocą opóźnionej replikacji: na użytkownik do projektu 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 (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 ). Ponadto śledzimy zmiany stanu bazy danych, archiwizując dziennik zapisów w przód (, 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. , 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 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
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 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ę (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 . Jest wywoływane z 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, µsecs);
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 .
Kiedy dowiedzieliśmy się o problemie, 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 , 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:
- Nie trzeba wyjmować całego bazowego backupu z archiwum.
- 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 ). 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 teraz chronimy przed utratą danych tylko na poziomie systemu i nie przywracamy danych na poziomie użytkownika.
Źródło: habr.com
