Sposoby na skalowanie bazy danych: replikacja vs. sharding

Skalowanie bazy danych kiedy nie wyrabia? Poznaj replikację, shardowanie i praktyczne przykłady konfiguracji, które pomogą Ci zwiększyć wydajność i dostępność systemu.

W czym problem?

Wyobraź sobie, że Twój system startował z kilkoma tysiącami użytkowników (lub bardziej przyziemnie, z kilkunastoma :D) – bazę wybrałeś poprawnie i radziła sobie w miarę dobrze. Ale rośnie ruch, zapytań coraz więcej, część operacji zaczyna się dusić. Zauważasz, że pojedynczy serwer już nie wystarcza, albo z przyczyn zasobów (RAM, CPU, I/O), albo z przyczyn konceptualnych (łączenie wielu danych, rosnące tabele). Wtedy naturalnym pytaniem staje się: jak żyć, co robić dalej? A no, podążać za jedną z ważniejszych sentencji IT: dziel i rządź!

Rozwiązaniem jest skalowanie bazy danych

Skalowanie może dziać się w dwóch wymiarach:

  • Pionowe (vertical scaling) – dołożenie zasobów (lepszy sprzęt, więcej RAM, szybsze dyski);
  • Poziome (horizontal scaling) – rozproszenie danych i obciążenia na wiele węzłów.

W wielu realiach samo skokowe dołożenie lepszego sprzętu wystarczy na jakiś czas. Ale kiedy baza osiąga pewne granice, zaczynasz patrzeć w kierunku replikacji i shardowania. Słowa „replikacja” i „shardowanie” często pojawiają się razem, ale są to całkowicie inne mechanizmy.

Replikacja: zwiększanie dostępności i odciążanie odczytów

Skalowanie bazy danych poprzez replikację, to mechanizm, w którym dane z jednego serwera (często zwanego primary, master, leader) są kopiowane do jednego lub więcej serwerów standby, replica lub follower.

Kilka wariantów:

  • Asynchroniczna replikacja: transakcja zatwierdzona na primary nie czeka, aż zmiany dotrą do replik, co daje niskie opóźnienie, ale w przypadku awarii może dojść do utraty części danych.
  • Synchronous / semi-synchronous: master czeka aż co najmniej jedna replika potwierdzi zapis zmian do WAL, co zwiększa bezpieczeństwo, ale kosztem wydajności zapisów.
  • Cascading replication: repliki mogą być kaskadowe, czyli replika może służyć jako źródło dla kolejnej repliki, zamiast każda łączyć się do głównego węzła.
  • Logical replication: przenoszenie danych na poziomie logicznym (np. publikacja/subskrypcja tabel), co pozwala wybrać subset tabel, transformacje itp.

Gdy stosujesz replikację, głównym celem często jest odciążenie odczytów, cała warstwa odczytów może być rozproszona na kilka węzłów, podczas gdy zapis wciąż trafia do jednej (lub kilku) instancji. To podejście skaluje operacje odczytu, ale nie rozwiązuje problemu przeciążonych zapisów.

Shardowanie (fragmentacja, partycjonowanie poziome)

Replikacja to kopiowanie całej bazy lub tabeli do innych węzłów, z kolei skalowanie bazy danych poprzez shardowanie to podział samej tabeli (lub grupy tabel) na fragmenty, z których każdy trafia na inny węzeł (lub instancję). Dzięki temu:

  • każdy węzeł operuje na mniejszym zbiorze danych (lepsze cache, mniejsze indeksy),
  • można równolegle obsługiwać zapisy do różnych shardów,
  • można dopasować sprzęt dla różnych shardów (np. więcej RAMu/Dysku/Procka tam, gdzie jest taka potrzeba).

Rodzaje shardowania

  • Sharding przez FDW / foreign tables: przykładowo, w PostgreSQL można użyć FDW (foreign data wrappers) do tego, aby fragment tabeli (wirtualna partycja) fizycznie była na innym serwerze.
  • Horizontal sharding: klasyczny podział po wierszach tabeli, część tu, część tam, np. według zakresu klucza (range shard), hashowania (hash shard) lub kombinacji (composite).
  • Vertical sharding: podział według kolumn, różne grupy kolumn mogą być przechowywane w różnych bazach.
  • Directory-based / routing: aplikacja posiada warstwę decyzyjną, która wie, do którego sharda wysłać zapytanie. Wymaga to implementacji odpowiednich mechanizmów w kodzie.

Skalowanie bazy danych: jak łączyć replikację i shardowanie?

W praktyce często stosuje się oba mechanizmy razem: shardowanie dzieli dane pod kątem zapisu i skalowalności zapisu, replikacja dba o dostępność i odciążenie odczytów w każdym shardzie. Czyli: każda partycja / shard może mieć swoje repliki. Dzięki temu na każdej części danych masz redundancję i skalowalność odczytów. Właśnie takie hybrydowe podejście pozwala budować systemy o dużej przepustowości.

Aby nie być gołosłownym… Dodajmy trochę praktyki! Podziałamy na Postgres-ie, jako obecnym no-brainerze wśród baz na rynku. Załóżmy, że masz tabelę orders:

CREATE TABLE orders (
  id BIGSERIAL PRIMARY KEY,
  customer_id BIGINT,
  created_at TIMESTAMP,
  total NUMERIC
) PARTITION BY HASH (customer_id);

Dodajesz partycje:

CREATE TABLE orders_shard_0 PARTITION OF orders FOR VALUES WITH (modulus 4, remainder 0);
CREATE TABLE orders_shard_1 PARTITION OF orders FOR VALUES WITH (modulus 4, remainder 1);
CREATE TABLE orders_shard_2 PARTITION OF orders FOR VALUES WITH (modulus 4, remainder 2);
CREATE TABLE orders_shard_3 PARTITION OF orders FOR VALUES WITH (modulus 4, remainder 3);

Następnie, każdą partycję możesz fizycznie przenieść na inny serwer, korzystając z postgres_fdw lub podobnych mechanizmów. Natomiast w swojej aplikacji musisz wiedzieć, którą partycję wybrać (np. hashowanie klucza). Przykładowo, podczas zapytania typu:

SELECT * 
FROM orders
WHERE customer_id = 12345;

planner wie, którą partycję użyć (reszta 12345 mod 4 = 1, a więc orders_shard_1). Jeżeli ta partycja zostanie oznaczona, jako remote, wykonywany będzie Foreign Scan. Jeżeli zaś zapytanie obejmuje wiele partycji, to plan może wysyłać równoległe skany do różnych węzłów i scalać wyniki lokalnie, bez Twojej wiedzy.

Skalowanie bazy danych: problemy z którymi się mierzyłem

  • Rebalansowanie / resharding, kiedy jeden shard staje się „gorący” (shard, który jest znacznie częściej używany niż pozostałe), trzeba przenieść część danych do innego, co wymaga migracji, a w efekcie spowoduje potencjalny downtime.
  • Transakcje wieloshardowe, jeśli operacja dotyczy danych z więcej niż jednego shardu, konieczne może być koordynowanie transakcji (2PC), a to znacząco komplikuje warstwę aplikacji.
  • Joiny między shardami, zapytania złączeniowe między różnymi shardami mogą być kosztowne zasobowo i czasowo, należy je ograniczać lub projektować model tak, by minimalizować potrzebę globalnych joinów.
  • Opóźnienia sieciowe, jeśli shard i repliki są geograficznie rozproszone, to prędzej czy później zacznie pojawiać się problem opóźnień, bo synchronizacja może stać się wąskim gardłem.
  • Migracje schematów, każda zmiana schematu tabeli musi być kompatybilna we wszystkich shardach / replikach, inaczej wszystko się rozjedzie.

Podsumowanie

Jeśli Twoja baza zaczyna być wąskim gardłem, najpierw rozważ replikację i optymalizacje (indeksy, zapytania, cache). Dopiero gdy to przestaje wystarczać, rozważ shardowanie (lub hybrydę). Przeprowadzając shardowanie, warto zacząć od prostych reguł, unikać zbyt wielu operacji między shardami i budować warstwę routingu w aplikacji.

Autor wpisu

blog@orbisbit.com

Komentarze

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Sprawdź również