Cześć wszystkim. Z tej strony Władysław Rodin. Obecnie uczę na portalu OTUS kursów dotyczących architektury oprogramowania i architektury oprogramowania pod dużym obciążeniem. W przeddzień rozpoczęcia nowej edycji kursu postanowiłem napisać krótki autorski materiał, którym chcę się z wami podzielić.

Wprowadzenie
Z powodu ograniczeń HDD, które mogą obsługiwać jedynie około 400-700 operacji na sekundę (co jest nieporównywalne z typowymi rps dla systemu o wysokim obciążeniu), klasyczna baza danych staje się wąskim gardłem architektury. Dlatego należy poświęcić szczególną uwagę wzorom skalowania tego magazynu.
Na obecny moment są dwa wzory skalowania bazy: replikacja oraz shardowanie. Shardowanie pozwala na skalowanie operacji zapisu, co w efekcie zmniejsza rps zapisu na jeden serwer w twoim klastrze. Replikacja umożliwia to samo, ale w przypadku operacji odczytu. Właśnie temu wzorowi poświęcony jest ten artykuł.
Replikacja
Patrząc na replikację z wysokiego poziomu, to prosta sprawa: miałeś jeden serwer, na którym były dane, a następnie ten serwer przestał sobie radzić z obciążeniem podczas odczytu tych danych. Dodajesz kilka serwerów, synchronizujesz dane na wszystkich serwerach, a użytkownik może odczytywać z dowolnego serwera swojego klastra.
Pomimo pozornej prostoty, istnieje kilka klasyfikacji różnych realizacji tego schematu:
- Według ról w klastrze (master-master lub master-slave)
- Według przesyłanych obiektów (oparte na wierszach, oparte na instrukcjach lub mieszane)
- Według mechanizmu synchronizacji węzłów
Dziś zajmiemy się właśnie 3. punktem.
Jak przebiega realizacja transakcji
Temat ten nie dotyczy bezpośrednio replikacji, można o nim napisać oddzielny artykuł, jednak ponieważ zrozumienie mechanizmu realizacji transakcji jest kluczowe, pozwolę sobie przypomnieć najważniejsze kwestie. Realizacja transakcji przebiega w 3 etapach:
- Rejestracja transakcji w dzienniku bazy danych.
- Zastosowanie transakcji w silniku bazy danych.
- Zwrócenie potwierdzenia klientowi o pomyślnym zastosowaniu transakcji.
W różnych bazach w tym algorytmie mogą występować niuanse: na przykład w silniku InnoDB bazie MySQL są 2 dzienniki: jeden do replikacji (binary log), a drugi do utrzymania ACID (undo/redolog), podczas gdy w PostgreSQL jest tylko jeden dziennik, który spełnia obie funkcje (write ahead log = WAL). Jednak powyżej przedstawiona jest ogólna koncepcja, która pozwala nie brać pod uwagę tych niuansów.
Replikacja synchronizacyjna (sync)
Dodajmy do algorytmu zatwierdzania transakcji logikę replikacji uzyskanych zmian:
- Rejestracja transakcji w dzienniku bazy danych.
- Zastosowanie transakcji w silniku bazy danych.
- Wysyłanie danych do wszystkich replik.
- Otrzymywanie potwierdzenia od wszystkich replik o wykonaniu na nich transakcji.
- Zwrócenie potwierdzenia klientowi o pomyślnym zastosowaniu transakcji.
Przy tym podejściu napotykamy na szereg niedogodności:
- klient czeka na zastosowanie zmian na wszystkich replikach.
- wraz ze wzrostem liczby węzłów w klastrze zmniejszamy prawdopodobieństwo, że operacja zapisu zakończy się sukcesem.
Jeśli pierwszy punkt jest w miarę jasny, to przyczyny drugiego punktu warto wyjaśnić. Jeśli podczas replikacji synchronizacyjnej nie otrzymamy odpowiedzi od przynajmniej jednego węzła, cofnę transakcję. Zwiększając liczbę węzłów w klastrze, zwiększasz prawdopodobieństwo, że operacja zapisu się nie powiedzie.
Czy możemy czekać na potwierdzenie tylko od jakiejś części węzłów, na przykład od 51% (kworum)? Tak, możemy, jednak w klasycznej wersji wymagane jest potwierdzenie od wszystkich węzłów, ponieważ w ten sposób możemy zapewnić pełną spójność danych w klastrze, co stanowi niewątpliwą zaletę tego typu replikacji.
Replikacja asynchronizacyjna (async)
Zmieńmy poprzedni algorytm. Dane do replik będziemy wysyłać „kiedyś później”, a „kiedyś później” zmiany zostaną zastosowane na replikach:
- Rejestracja transakcji w dzienniku bazy danych.
- Zastosowanie transakcji w silniku bazy danych.
- Zwrócenie potwierdzenia klientowi o pomyślnym zastosowaniu transakcji.
- Wysyłanie danych do replik i zastosowanie zmian przez nie.
To podejście prowadzi do tego, że klaster działa szybko, ponieważ nie trzymamy klienta w oczekiwaniu, aż dane dotrą do replik i zostaną zatwierdzone.
Jednakże warunek przesyłania danych do replik „kiedyś później” może prowadzić do utraty transakcji, zwłaszcza utraty potwierdzonej użytkownikowi transakcji, ponieważ jeśli dane nie zdążą się zreplikować, a potwierdzenie o pomyślnym wykonaniu operacji zostało wysłane do klienta, a węzeł, do którego wpłynęły zmiany, ma problem z dyskiem HDD, tracimy transakcję, co może prowadzić do bardzo nieprzyjemnych konsekwencji.
Replikacja półsynchroniczna (semisync)
W końcu dotarliśmy do replikacji pół-synchronnej. Ten typ replikacji nie jest zbyt znany i nie jest zbyt powszechny, jednak stanowi znaczące zainteresowanie, ponieważ może połączyć zalety zarówno replikacji synchronnej, jak i asynchronnej.
Spróbujemy połączyć 2 poprzednie podejścia. Nie będziemy długo trzymać klienta, ale zażądamy, aby dane zostały zreplikowane:
- Rejestracja transakcji w dzienniku bazy danych.
- Zastosowanie transakcji w silniku bazy danych.
- Wysyłanie danych do replik.
- Otrzymanie potwierdzenia od repliki o otrzymaniu zmian (zastosowane będą «kiedyś później»).
- Zwrócenie potwierdzenia klientowi o pomyślnym zastosowaniu transakcji.
Zauważ, że przy takim algorytmie utrata transakcji występuje tylko w przypadku awarii zarówno węzła przyjmującego zmiany, jak i węzła-repliki. Prawdopodobieństwo takiej awarii uznaje się za małe i te ryzyka są akceptowane.
Jednak w tym podejściu istnieje ryzyko tzw. phantom reads. Wyobraźmy sobie następujący scenariusz: na kroku 4 nie otrzymaliśmy potwierdzenia od żadnej repliki. Musimy cofnąć tę transakcję, a klientowi nie zwracać potwierdzenia. Ponieważ dane zostały zastosowane na kroku 2, pomiędzy zakończeniem kroku 2 a cofaniem transakcji powstaje tymczasowy luk, w trakcie którego równoległe transakcje mogą zobaczyć te zmiany, których nie powinno być w bazie.
Replikacja lose-less semisync
Jeśli się chwilę zastanowić, można jedynie zamienić miejscami kroki algorytmu, aby rozwiązać problem phantom reads w tym scenariuszu:
- Rejestracja transakcji w dzienniku bazy danych.
- Wysyłka danych do repliki.
- Otrzymanie potwierdzenia od repliki o otrzymaniu zmian (zastosowane będą «kiedyś później»).
- Zastosowanie transakcji w silniku bazy danych.
- Zwrócenie potwierdzenia klientowi o pomyślnym zastosowaniu transakcji.
Teraz zatwierdzamy zmiany tylko wtedy, gdy zostały zreplikowane.
Wnioski
Jak zawsze, nie ma idealnych rozwiązań, istnieje zbiór rozwiązań, z których każde ma swoje zalety i wady oraz nadaje się do rozwiązania różnych klas problemów. To dotyczy również wyboru mechanizmu synchronizacji danych w replikowanej bazie danych. Zestaw zalet, jakie posiada replikacja pół-synchroniczna, jest wystarczająco solidny i interesujący, aby zasługiwał na uwagę, mimo że jest rzadko stosowany.
Na tym kończymy. Do zobaczenia na !
Źródło: habr.com
