Paweł Łukasiewicz
2024-06-15
Paweł Łukasiewicz
2024-06-15
Udostępnij Udostępnij Kontakt
Wprowadzenie

Udało nam się dobrnąć do ostatniego wpisu. Tym razem przechodzimy szybko do konkretów, skupimy się na najlepszy praktykach związanych z DynamoDB. Skupimy się na tabelach, elementach, zapytaniach i skanach oraz na lokalnych i globalnych indeksach. Przedstawione praktyki mają na celu optymalizację kodu, zapobieganie błędom oraz minimalizację kosztów przepustowości podczas pracy z różnymi tabelami i ich elementami.

Tabele

Ogólne zasady projektowania tabel DynamoDB zalecają ograniczenie ich liczby do minimum. Amazon w większości wypadków zaleca używanie jednej tabeli. Jeżeli jednak takie rozwiązanie jest dla nas niewystarczające lub mało opłacalne spójrzmy na poniższe wytyczne pochodzące z oficjalnej dokumentacji: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/bp-table-design.html

Przy projektowaniu tabel należy dążyć do jednolitego dostępu do danych na elementach tabeli. Optymalne wykorzystanie przepustowości opiera się na wyborze klucza głównego i wzorcach obciążenia dostępu do elementów. Obciążenie powinniśmy rozłożyć równomiernie na wartości klucza partycji. Powinniśmy unikać takich rzeczy jak niewielka ilość intensywnie używanych wartości klucza partycji. Przy projektowaniu tabel powinniśmy skupić się na dużej liczbie odrębnych kluczy partycji.

Pamiętajmy o tym, że DynamoDB oferuje mechanizm przygotowany na nagłe skoki zużycia przepustowości przy odczycie jak i zapisie danych. Powinniśmy jednak starać się unikać takich "pików" ponieważ są odpowiedzialne za duże wzrosty zużycia przepustowości – ponadto nie jest to rozwiązanie gwarantujące 100% skuteczność i niezawodności. Powinniśmy dokonywać monitorowania naszch tabel celem zdefiniowania optymalnych (dla naszych rozwiązań) wartości przepustowości odczytu jak i zapisu.

W przypadku często używanych elementów warto skupić się na ich cachowaniu w celu odciążenia aktywności odczytu z bazy danych a sięganiu po informacje do pamięci podręcznej.

Elementy

Największe wątpliwości dotyczące elementów tabeli to wspomniany wielokrotnie throttling, wydajność związana z dostępem do elementów, ich rozmiar oraz same koszty dostępu. W tym punkcie musimy się zatrzymać i skupić na "wzorcach dostępu". Pojęcie to odnosi się do sposobu w jaki aplikacja odczytuje i zapisuje dane. Jego składową jest również informacja dotycząca rodzaju pobieranych danych, jak często i w jakiej ilości. Znajomość tych wzorców pozwoli nam na zaprojektowanie tabeli w sposób optymalizujący wydajność i minimalizujący koszty dostępu do danych.

W przypadku dużych wartości danych powinniśmy wykorzystać standardowe narzędzia do kompresji przez ich zapisaniem. Nic nie stoi na przeszkodzie przed wykorzystaniem alternatywnego magazynu danych, tj. S3. Obiekty możemy przechowywać w odpowiednich do tego "kubełkach" a identyfikator w naszym elemencie w DynamoDB.

Zapytania i skany

Ogólnie rzecz biorąc, operacje skanowania są mniej wydajne niż inne operacje w DynamoDB. Operacja Scan zawsze skanuje całą tabelę lub indeks pomocniczy. Następnie odfiltrowuje wartości, aby zapewnić pożądany wynik, zasadniczo dodając dodatkowy krok usuwania danych z zestawu wyników.

Jeżeli to możliwe, powinniśmy unikać operacji skanowania na dużej tabeli lub indeksie z filtrem, który usuwa wiele wyników. Ponadto, wraz ze wzrostem tabeli lub indeksu operacja Scan zwalnia. Operacja skanowania sprawdza każdy element pod kątem żądanych wartości i może wykorzystać zarezerwowaną przepustowość dla dużej tabeli lub indeksu w jednej operacji. Aby uzyskać szybsze czasy odpowiedzi należy zaprojektować tabele i indeksy tak, aby aplikacje mogły używać operacji Query zamiast Scan.

Alternatywnym rozwiązaniem jest zaprojektowanie aplikacji tak, aby korzystała z operacji Scan w sposób minimalizujący wpływ na szybkość żądań. Może to obejmować modelowanie, kiedy bardziej efektywne może być użycie globalnego indeksu pomocniczego zamiast operacji Scan. Więcej na ten temat: https://youtu.be/LM84N-E_b_M

Kolejny niezwykle istotny punkt dotyczy unikania nagłych skoków aktywności odczytu. Wspominałem już o tym w poprzednich wpisach ale musimy pamiętać jaki wpływ na odczyt danych ma silnie spójny odczyt, tj. strongly consistent read. W przypadku silnie spójnych odczytów jednostka pojemności odczytu to dwa żądania odczytu 4KB na sekundę (w przypadku odczytu danych o rozmiarze 4KB na sekundę). Operacja skanowania domyślnie wykonuje ostatecznie spójny odczyt i może zwrócić do 1MB (na stronę) danych. W związku z tym pojedyncze żądanie może pochłonąć 128 operacji odczytu (rozmiar strony: 1MB/rozmiar elementu: 4KB) / 2 (ostatecznie spójne odczyty). Jeżeli jednak zażądamy silnie spójnych odczytów to operacja skanowania zużyje dwa razy więcej udostępnionej przepustowości, tj. 256 operacji odczytu. Tak nagły wzrost zużycia w porównaniu do skonfigurowanej pojemności odczytu dla tabeli może uniemożliwić innym (potencjalnie ważniejszym) żądaniom na dostęp do jednostek przepustowości. Takie żądanie prawdopodobnie skończy się wyjątkiem ProvisionedThroughputExceeded.

Zanim przejdziemy dalej spójrzmy jeszcze na możliwość wykorzystania skanowania równoległego. Doskonałym przykładem może być analiza danych historycznych, gdzie skanowanie równoległe może zostać wykonane znacznie szybciej niż sekwencyjnie. Dodatkowo, takie równoległe skanowanie, może odbywać się z niskim priorytetem bez wpływu na ruch produkcyjny. Takie podejście nie jest oczywiście idealne, ponieważ z uwagi na liczbę równoległego przetwarzania może szybko dojść do zużycia całej zarezerwowanej przepustowości. Sięgając po skanowanie równoległe warto upewnić się, że spełnione są poniższe warunki: rozmiar tabeli to 20GB lub więcej, udostępniona przepustowość odczytu tabeli nie jest w pełni wykorzystywana oraz tam, gdzie operacje skanowania sekwencyjnego są zbyt wolne.

Ogólne wytyczne dotyczące indeksów pomocniczych

Jak już doskonale wiecie Amazon obsługuje dwa typy indeksów pomocniczych:

  • GSI (globalny indeks pomocniczy) to indeks z kluczem partycji i kluczem sortowania, które mogą się różnić od tych w tabeli bazowej. Globalny indeks pomocniczy jest uważany za "globalny" ponieważ zapytania w indeksie mogą obejmować wszystkie dane w tabeli bazowej, we wszystkich partycjach. Globalny indeks pomocniczy nie ma ograniczeń rozmiaru i ma własne ustawienia przepustowości dla odczytu i zapisu, które są oddzielne od ustawień tabeli;
  • LSI (lokalny indeks pomocniczy) to indeks, który ma ten sam klucz partycji co tabela bazowa ale inny klucz sortowania. Lokalny indeks pomocniczy jest "lokalny" w tym sensie, że każda partycja lokalnego indeksu pomocniczego jest ograniczona do partycji tabeli bazowej, która ma tę samą wartość klucza partycji. W rezultacie całkowity rozmiar indeksowanych elementów dla dowolnej wartości klucza partycji nie może przekroczyć 10 GB. Ponadto, lokalny indeks pomocniczy współdzieli ustawienia przepustowości dla aktywności odczytu i zapisu z tabelą, którą indeksuje.

Każda tabela w DynamoDB może mieć do 20 globalnych indeksów pomocniczych (domyślny limit) oraz 5 lokalnych indeksów pomocniczych.

Globalne indeksy pomocnicze są często bardziej przydatne niż lokalne indeksy pomocnicze. Określenie typu indeksu do użycia będzie również zależeć od wymagań aplikacji. Szczegółowe porównanie pomiędzy tymi indeksami oraz więcej informacji dotyczących wyboru między nimi możecie znaleźć w oficjalnej dokumentacji: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/SecondaryIndexes.html

My w tym podsumowaniu skupiamy się jedynie na kilku ogólnych zasadach i wzorcach projektowych o których należy pamiętać podczas tworzenia indeksów w DynamoDB, tj. omówimy efektywne korzystanie z indeksów, porozmawiamy o projekcjach oraz optymalizacji częstych zapytań w celu ograniczenia liczby odpytań indeksu oraz zwrócimy uwagę na limity rozmiaru kolekcji podczas tworzenia nowych lokalnych indeksów pomocniczych.

Pierwszy punkt jest niezwykle krótki i możemy go skrócić do jednego zdania: "Ogranicz liczbę indeksów do minimum." Nie powinniśmy tworzyć indeksów pomocniczych na atrybutach, które nie są często odpytywane. Indeksy, które są rzadko używane, przyczyniają się do zwiększenia kosztów przechowywania i operacji wejścia/wyjścia bez poprawy wydajności aplikacji.

Drugie zagadnienie to ostrożne wybieranie projekcji, tj. rzutowania atrybutów. Indeksy pomocnicze zużywają pamięć masową i przepustowość a co za tym idzie ich rozmiar powinien być jak najmniejszy. Ponadto, im mniejszy indeks, tym większa przewaga wydajności w porównaniu do zapytań w pełnej tabeli. Jeżeli zapytania zwykle zwracają tylko niewielki podzbiór atrybutów, a całkowity rozmiar tych atrybutów jest znacznie mniejszy niż cały element, powinniśmy rzutować tylko te atrybuty, o które regularnie prosimy w zapytaniach.

Jeżeli jednak spodziewamy się dużej aktywności zapisu w tabeli w porównaniu do ilości odczytów powinniśmy postępować zgodnie z najlepszymi praktykami:

  • należy rozważyć rzutowanie mniejszej liczby atrybutów w celu zminimalizowania rozmiaru elementów zapisywanych w indeksie. Ma to jednak zastosowanie tylko wtedy, gdy rozmiar rzutowanych atrybutów byłby większy niż pojedyncza jednostka pojemności zapisu (1 KB), np. jeśli rozmiar wpisu indeksu wynosi tylko 200 bajtów, DynamoDB zaokrągla go do 1 KB. Innymi słowy: dopóki elementy indeksu są małe można rzutować więcej atrybutów bez dodatkowych kosztów;
  • unikajmy rzutowania atrybutów, o których wiemy, że rzadko będą pojawiać się w zapytaniach. Za każdym razem, gdy aktualizujemy atrybut, który jest rzutowany w indeksie, ponosimy dodatkowe koszty aktualizowania indeksu. Nadal możemy pobierać atrybuty nieprojektowane w zapytaniu przy wyższym koszcie przepustowości, ale koszt zapytania może być znacznie niższy niż koszt częstej aktualizacji indeksu;
  • określamy parametr ALL tylko wtedy, gdy chcemy aby zapytania zwracały cały element tabeli posortowany według innego klucza sortowania. Projekcja wszystkich atrybutów eliminuje potrzebę pobierania tabeli ale w większości przypadków podwaja koszty przechowywania i aktywności zapisu.

Podsumowując powyższy akapit: powinniśmy zrównoważyć potrzebę utrzymywania jak najmniejszych indeksów z potrzebą ograniczenia pobrań/zapytań do minimum – o tym w poniższej sekcji.

W celu uzyskania najszybszych zapytań z najniższym możliwym opóźnieniem należy rzutować wszystkich atrybuty, które mają zostać zwrócone przez te zapytania. Zasada ta w szczególności dotyczy zapytań o atrybuty lokalnego indeksu pomocniczego, które nie są rzutowane – w takim wypadku DynamoDB automatycznie pobierze te atrybuty z tabeli co wymaga odczytania całego elementu z tabeli. Zachowanie takie wprowadza opóźnienia i dodatkowe operacje wejścia/wyjścia, których można uniknąć.

Musimy też pamiętać, że "sporadyczne" zapytania mogą często przekształcić się z zapytania "zbędne". Jeżeli istnieją atrybuty, który nie zamierzamy rzutować, ponieważ wydaje się nam, że będziemy korzystać z nich sporadycznie...powinniśmy się zastanowić czy nie dojdzie do zmiany wymagań i takie podejście może okazać się błędem.

Ostatni punkt, który poruszymy w tym wpisie, dotyczy limitu kolekcji elementów podczas tworzenia lokalnych indeksów pomocniczych.

Kolekcja elementów to wszystkie elementy w tabeli i jej lokalnych indeksach pomocniczych, które mają ten sam klucz partycji. Żadna kolekcja elementów nie może przekroczyć 10 GB – możliwe jest zatem, że zabraknie nam miejsca dla określonej wartości klucza partycji.

Po dodaniu lub aktualizacji elementu DynamoDB aktualizuje wszystkie lokalne indeksy pomocnicze, których dotyczy ta operacja. Jeżeli indeksowane atrybuty są zdefiniowane w tabeli, lokalne indeksy pomocnicze również rosną.

Podczas tworzenia lokalnego indeksu pomocniczego należy zastanowić się, ile danych zostanie do niego zapisanych i ile z tych elementów danych będzie miało tę samą wartość klucza partycji. Jeżeli spodziewamy się, że suma elementów tabeli i indeksu dla określonej wartości klucza partycji przekroczy 10 GB, powinniśmy zastanowić się nad słusznością tworzenia takiego klucza – lub w innych słowach, powinniśmy unikać tworzenia takiego indeksu.

Jeżeli nie możemy uniknąć tworzenia lokalnego indeksu pomocniczego, należy przewidzieć limit rozmiaru kolekcji elementów i podjąć działania przed jego przekroczeniem. Jakie działania możemy podjąć? Tutaj odsyłam do oficjalnej dokumentacji: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/LSI.html#LSI.ItemCollections.SizeLimit

Podsumowanie

I to by było na tyle - w tej serii wpisów zapoznaliśmy się lepiej z DynamoDB rozszerzając jednocześnie swoją wiedzę dotyczącą usług AWS.

Co będzie dalej? Jeszcze nie wiem ponieważ mamy sporo tematów do pokrycia. Jeżeli coś dla Was jest szczególnie interesujące, może chcecie wejść głębiej w istniejące tematy...dajcie proszę znać przez przygotowany formularz kontaktowy.

Doceniasz moją pracę? Wesprzyj bloga 'kupując mi kawę'.

Jeżeli seria wpisów dotycząca DynamoDB była dla Ciebie pomocna, pozwoliła Ci rozwinąć obecne umiejętności lub dzięki niej nauczyłeś się czegoś nowego... będę wdzięczny za wsparcie w każdej postaci wraz z wiadomością, którą będę miał przyjemność przeczytać.

Z góry dziekuje za każde wsparcie - i pamiętajcie, wpisy były, są i będą za darmo!