Przy pracy z dużymi zbiorami danych problem braku miejsca na dyskach może stać się naglący. Jednym ze sposobów rozwiązania tego problemu jest kompresja, która pozwala na zwiększenie pojemności przechowywania na tym samym sprzęcie. W tym artykule omówimy, jak działa kompresja danych w Apache Ignite. Skupimy się wyłącznie na metodach kompresji zaimplementowanych w produkcie. Inne metody kompresji danych (w sieci, w pamięci), zarówno te zaimplementowane, jak i nie, zostaną pominięte.
Zatem, w trybie z włączonym persistence, w wyniku zmian danych w cache'ach, Ignite zaczyna zapisywać na dysku:
- Zawartość cache'ów
- Dziennik wcześniejszego zapisu (Write Ahead Log, dalej WAL)
Od dłuższego czasu istnieje mechanizm kompresji WAL, zwany kompresją WAL. W niedawno wydanej wersji Apache Ignite 2.8 pojawiły się dwa nowe mechanizmy umożliwiające kompresję danych na dysku: kompresja stron dyskowych oraz kompresja zrzutów stron WAL dla niektórych wpisów WAL. Więcej o tych trzech mechanizmach poniżej.
Kompresja stron dyskowych
Jak to działa
Na początek krótko opiszmy, jak Ignite przechowuje dane. Do przechowywania wykorzystywana jest pamięć stronicowa. Rozmiar strony jest określany przy uruchamianiu węzła i nie może być zmieniany na późniejszych etapach, dodatkowo rozmiar strony musi być potęgą liczby 2 i wielokrotnością rozmiaru bloku systemu plików. Strony są ładowane do RAM z dysku w miarę potrzeb, a rozmiar danych na dysku może przekraczać przydzieloną pamięć RAM. W przypadku braku miejsca w RAM na załadowanie strony z dysku, stare, już nieużywane strony będą wypychane z RAM.
Na dysku dane są przechowywane w następujący sposób: dla każdej partycji każdej grupy cache'ów tworzony jest osobny plik, w którym strony idą jedna po drugiej w porządku rosnącym indeksu. Pełny identyfikator strony zawiera identyfikator grupy cache, numer partycji oraz indeks strony w pliku. Dzięki pełnemu identyfikatorowi strony możemy jednoznacznie określić plik i offset w pliku dla każdej strony. Więcej informacji na temat struktury pamięci stronicowej można przeczytać w artykule na wikipedii Apache Ignite: .
Mechanizm kompresji stron dyskowych, jak można się domyślić z nazwy, działa na poziomie stron. Przy włączonym tym mechanizmie operacje na danych w RAM są realizowane bez żadnej kompresji, ale w momencie zapisywania stron z RAM na dysk przeprowadza się ich kompresję.
Jednak skompresowanie każdej strony osobno to jeszcze nie rozwiązanie problemu, trzeba jakoś zmniejszyć rozmiar końcowych plików z danymi. Jeśli rozmiar strony przestaje być stały, nie możemy już zapisywać stron do pliku jedna po drugiej, ponieważ może to prowadzić do szeregu problemów:
- Nie będziemy w stanie obliczyć offsetu strony przy pomocy indeksu, na którym się ona znajduje w pliku.
- Nie wiadomo, co zrobić ze stronami, które znajdują się nie na końcu pliku i zmieniają swój rozmiar. Jeśli rozmiar strony się zmniejsza, miejsce, które zostało zwolnione, znika. Jeśli rozmiar strony się zwiększa, trzeba będzie szukać dla niej nowego miejsca w pliku.
- Jeśli strona przesunie się o liczbę bajtów, która nie jest wielokrotnością rozmiaru bloku systemu plików, do jej odczytu lub zapisu zajmie się o jeden blok systemu plików więcej, co może prowadzić do spadku wydajności.
Aby nie rozwiązywać tych problemów na swoim poziomie, kompresja stron dyskowych w Apache Ignite wykorzystuje mechanizm systemu plików zwany plikami sparsenymi. Plik sparseny to plik, w którym niektóre obszary wypełnione zerami mogą być oznaczone jako "dziury". Bloki systemu plików nie są przydzielane do przechowywania tych dziur, co prowadzi do oszczędności miejsca na dysku.
Logiczne jest, że aby zwolnić blok systemu plików, rozmiar dziury musi być większy lub równy rozmiarowi bloku systemu plików, co nakłada dodatkowe ograniczenie na rozmiar strony w Apache Ignite: aby kompresja przyniosła jakikolwiek efekt, rozmiar strony musi być ściśle większy niż rozmiar bloku systemu plików. Jeśli rozmiar strony będzie równy rozmiarowi bloku, nigdy nie będziemy mogli zwolnić żadnego bloku, ponieważ aby zwolnić pojedynczy blok, skompresowana strona musi zajmować 0 bajtów. Jeśli natomiast rozmiar strony będzie równy rozmiarowi 2 lub 4 bloków, będziemy mogli zwolnić przynajmniej jeden blok, jeśli nasza strona skompresuje się przynajmniej do 50% lub do 75% odpowiednio.
W ten sposób, końcowy opis działania mechanizmu: podczas zapisu strony na dysk, podejmowana jest próba kompresji strony. Jeśli rozmiar skompresowanej strony pozwala na zwolnienie jednego lub więcej bloków systemu plików, strona jest zapisywana w skompresowanej formie, a w miejscu zwolnionych bloków powstaje 'dziura' (wykonywane jest wywołanie systemowe z flagą „punch hole”). fallocate() Jeśli rozmiar skompresowanej strony nie pozwala na zwolnienie bloków, strona jest przechowywana bez zmian, w formie nieskompresowanej. Wszystkie offsety stron są liczone tak samo jak bez kompresji, mnożąc indeks strony przez rozmiar strony. Żadna relokacja stron nie jest wymagana. Offsety stron, jak przy braku kompresji, trafiają na granice bloków systemu plików.

W bieżącej implementacji Ignite może pracować z plikami sparse tylko na systemie Linux, odpowiednio kompresja stron dyskowych może być włączona tylko podczas korzystania z Ignite na tym systemie operacyjnym.
Algorytmy kompresji, które mogą być używane do kompresji stron dyskowych: ZSTD, LZ4, Snappy. Ponadto istnieje tryb pracy (SKIP_GARBAGE), w którym tylko wyrzucane jest nieużywane miejsce na stronie bez stosowania kompresji na pozostałych danych, co pozwala zmniejszyć obciążenie CPU w porównaniu do wcześniej wymienionych algorytmów.
Wpływ na wydajność
Niestety, nie przeprowadzałem rzeczywistych pomiarów wydajności na rzeczywistych stanowiskach, ponieważ nie planujemy używać tego mechanizmu w produkcji, ale można rozważyć teoretycznie, gdzie stracimy, a gdzie zyskamy.
W tym celu musimy przypomnieć sobie, jak odbywa się odczyt i zapis stron podczas ich próby:
- Podczas wykonywania operacji odczytu najpierw wykonuje się jej wyszukiwanie w RAM, jeśli wyszukiwanie zakończyło się niepowodzeniem, strona jest ładowana do RAM z dysku tym samym wątkiem, który wykonuje odczyt.
- Podczas wykonywania operacji zapisu, strona w RAM jest oznaczana jako brudna, przy czym fizyczne zapisanie strony na dysk nie następuje natychmiast w wątku, który wykonuje zapis. Wszystkie brudne strony zapisują się na dysk później w procesie checkpointa przez oddzielne wątki.
W ten sposób wpływ na operacje odczytu:
- Pozytywny (disk IO), dzięki zmniejszeniu liczby odczytanych bloków systemu plików.
- Negatywny (CPU) z powodu dodatkowego obciążenia, jakie system operacyjny potrzebuje do pracy z plikami sparse. Może również wystąpić dodatkowa operacja IO związana z zachowaniem bardziej złożonej struktury pliku sparse (niestety nie znam się na szczegółach pracy z plikami sparse).
- Negatywny (CPU) z powodu konieczności dekompresji stron.
- Nie ma wpływu na operacje zapisu.
- Wpływ na proces checkpointingu (wszystko tu jest analogiczne do operacji odczytu):
- Pozytywny (disk IO) z powodu zmniejszenia liczby zapisanych bloków systemu plików.
- Negatywny (CPU, możliwe disk IO) z powodu pracy z plikami sparse.
- Negatywny (CPU) z powodu konieczności kompresji stron.
Która strona szali przeważy? Wszystko bardzo zależy od środowiska, ale skłaniam się ku temu, że kompresja stron dyskowych raczej doprowadzi do degradacji wydajności w większości systemów. Tym bardziej, że testy na innych DBMS, które stosują podobne podejście z plikami sparse, wykazują spadek wydajności przy włączonej kompresji.
Jak włączyć i skonfigurować
Jak już wspomniano, minimalna wersja Apache Ignite wspierająca kompresję stron dyskowych to 2.8, a wsparcie dostępne jest tylko na systemie operacyjnym Linux. Włączenie i konfiguracja przebiega następująco:
- W class-path musi znajdować się moduł ignite-compression. Domyślnie znajduje się on w dystrybucji Apache Ignite w katalogu libs/optional i nie jest włączony do class-path. Można po prostu przenieść katalog na jeden poziom wyżej do libs, a wtedy przy uruchamianiu przez ignite.sh zostanie automatycznie włączony.
- Trwałość musi być włączona (Włącza się przez
DataRegionConfiguration.setPersistenceEnabled(true)). - Rozmiar strony musi być większy niż rozmiar bloku systemu plików (można go ustawić za pomocą
DataStorageConfiguration.setPageSize()). - Dla każdego cache'a, którego dane wymagają kompresji, konieczne jest w konfiguracji ustawienie metody kompresji oraz (opcjonalnie) poziomu kompresji (metody
CacheConfiguration.setDiskPageCompression(), CacheConfiguration.setDiskPageCompressionLevel()).
Kompresja WAL
Jak to działa
Czym jest WAL i po co jest potrzebny? Krótko mówiąc, to dziennik, w który trafiają wszystkie zdarzenia zmieniające ostatecznie magazyn stron. Jest on przede wszystkim potrzebny do możliwości przywracania w przypadku awarii. Każda operacja, zanim odda kontrolę użytkownikowi, musi najpierw zapisać zdarzenie w WAL, aby w razie awarii móc odtworzyć wszystko z dziennika i przywrócić wszystkie operacje, na które użytkownik otrzymał pozytywną odpowiedź, nawet jeśli te operacje nie zdążyły jeszcze zostać odzwierciedlone w magazynie stron na dysku (wcześniej opisano, że faktyczne zapisywanie w magazynie stron wykonywane jest w procesie zwanym „checkpointem” z pewnym opóźnieniem przez oddzielne wątki).
Zapisy w WAL dzielą się na logiczne i fizyczne. Zapisami logicznymi są same klucze i wartości. Fizyczne — odzwierciedlają zmiany stron w magazynie stron. Jeśli zapisy logiczne mogą być przydatne w jeszcze innych przypadkach, to zapisy fizyczne potrzebne są tylko do przywracania w razie awarii i potrzebne są zapisy tylko od momentu ostatniego udanego checkpointu. Nie będziemy tutaj wnikać w szczegóły ani wyjaśniać, dlaczego tak to działa, ale zainteresowani mogą odwołać się do już wspomnianego artykułu na Wiki Apache Ignite: .
Na jeden zapis logiczny często przypada kilka zapisów fizycznych. Na przykład jedna operacja put w pamięci podręcznej dotyka kilku stron w pamięci stron (strona z danymi, strony z indeksami, strony z listami wolnych miejsc). Na niektórych testach syntetycznych udawało mi się, że zapisy fizyczne zajmowały do 90% objętości pliku WAL. Przy tym potrzebne są one tylko przez bardzo krótki czas (domyślny interwał między checkpointami wynosi 3 minuty). Logicznym byłoby pozbyć się tych danych po utracie ich aktualności. Właśnie tym zajmuje się mechanizm kompresji WAL, pozbywa się zapisów fizycznych i kompresuje pozostałe zapisy logiczne za pomocą zip, przy tym rozmiar pliku znacznie się zmniejsza (czasami nawet dziesięciokrotnie).
Fizyczny WAL składa się z kilku segmentów (domyślnie 10) o stałym rozmiarze (domyślnie 64MB), które są nadpisywane w cyklu. Gdy bieżący segment się zapełni, przypisany zostaje mu następny segment, a zapełniony segment jest kopiowany do archiwum odrębnym wątkiem. Kompresja WAL już działa z segmentami archiwalnymi. Oprócz tego, osobny wątek śledzi wykonanie punktów kontrolnych i rozpoczyna kompresję dla segmentów archiwalnych, fizyczne zapisy których nie są już potrzebne.

Wpływ na wydajność
Ponieważ kompresja WAL działa w osobnym wątku, nie powinna mieć bezpośredniego wpływu na wykonywane operacje. Niemniej jednak generuje dodatkowe obciążenie w tle dla CPU (kompresja) i dysku (odczyt każdego segmentu WAL z archiwum i zapis skompresowanych segmentów), dlatego jeśli system działa na granicy możliwości, może to również prowadzić do degradacji wydajności.
Jak włączyć i skonfigurować
Włączyć kompresję WAL można za pomocą właściwości WalCompactionEnabled do DataStorageConfiguration (DataStorageConfiguration.setWalCompactionEnabled(true)). Można również ustawić poziom kompresji za pomocą metody DataStorageConfiguration.setWalCompactionLevel(), jeśli domyślna wartość (BEST_SPEED) nie jest zadowalająca.
Kompresja migawki strony WAL
Jak to działa
Już wcześniej ustaliliśmy, że zapisy WAL dzielą się na logiczne i fizyczne. Dla każdej zmiany każdej strony w pamięci roboczej tworzony jest fizyczny zapis WAL. Fizyczne zapisy z kolei dzielą się na dwa podtypy: zapis migawki strony (page snapshot record) oraz zapis delta (delta record). Za każdym razem, gdy zmieniamy coś na stronie i przechodzimy z stanu czystego do brudnego, w WAL zapisywana jest pełna kopia tej strony (page snapshot record — migawka strony). Nawet jeśli zmieniamy tylko jeden bajt, w WAL zapisuje się zapis o rozmiarze nieco większym niż rozmiar strony. Jeśli jednak zmieniamy coś na już brudnej stronie, w WAL tworzy się zapis delta, który odzwierciedla jedynie zmiany w porównaniu z poprzednim stanem strony, a nie całą stronę jako taką. Ponieważ resetowanie stanu stron z brudnego na czysty wykonywane jest w trakcie punktu kontrolnego, tuż po rozpoczęciu punktu kontrolnego prawie wszystkie fizyczne zapisy będą składać się tylko z migawek stron (ponieważ wszystkie strony bezpośrednio po rozpoczęciu punktu kontrolnego są czyste), a następnie w miarę zbliżania się do następnego punktu kontrolnego udział zapisów delta zaczyna rosnąć, by znów zostać zresetowany na początku następnego punktu kontrolnego. Pomiar na kilku syntetycznych testach wykazał, że udział migawek stron w ogólnej objętości zapisów fizycznych osiąga 90%.
Ideą kompresji migawki strony WAL jest kompresowanie migawek stron przy użyciu istniejącego narzędzia do kompresji stron (por. kompresja stron na dysku). W tym przypadku zapisy w WAL są przechowywane sekwencyjnie w trybie append-only i nie ma potrzeby wiązania zapisów z granicami bloków systemu plików, dlatego tutaj, w przeciwieństwie do mechanizmu kompresji stron na dysku, nie potrzebujemy plików sparse, w związku z czym ten mechanizm zadziała nie tylko na systemie operacyjnym Linux. Ponadto nie jest już dla nas istotne, jak bardzo udało nam się skompresować stronę. Nawet jeśli uwolniliśmy 1 bajt, to już pozytywny wynik, a my możemy przechowywać w WAL skompresowane dane, w przeciwieństwie do kompresji stron na dysku, gdzie przechowujemy skompresowaną stronę tylko wtedy, gdy uwolniliśmy więcej niż 1 blok systemu plików.
Strony to dane, które dobrze się kompresują, ich udział w całkowitej objętości WAL jest bardzo wysoki, w ten sposób, nie zmieniając formatu pliku WAL, możemy uzyskać znaczące zmniejszenie jego rozmiaru. Kompresja logów logicznych wymagałaby natomiast zmiany formatu i utraty zgodności, na przykład dla zewnętrznych użytkowników, którzy mogą być zainteresowani zapisami logicznymi, a jednocześnie nie przyniosłaby istotnego zmniejszenia objętości pliku.
Podobnie jak w przypadku kompresji stron dyskowych, dla kompresji zrzutów stron WAL mogą być używane algorytmy kompresji ZSTD, LZ4, Snappy, a także tryb SKIP_GARBAGE.
Wpływ na wydajność
Jak łatwo zauważyć, bezpośrednie włączenie kompresji zrzutów stron WAL wpływa tylko na wątki, które zapisują dane w pamięci stron, tzn. na te wątki, które zmieniają dane w pamięciach podręcznych. Odczyt z fizycznych zapisów WAL odbywa się tylko raz, w momencie uruchamiania węzła po awarii (i tylko w przypadku awarii podczas checkpointu).
Wpływ na wątki zmieniające dane jest następujący: uzyskujemy negatywny efekt (CPU) z powodu konieczności każdorazowego kompresowania strony przed zapisem na dysk i pozytywny efekt (disk IO) z powodu zmniejszenia liczby zapisanych danych. Odpowiednio, tu wszystko jest proste, jeśli wydajność systemu ogranicza procesor CPU, otrzymujemy niewielką degradację, jeśli chodzi o dyskowy wejście/wyjście - otrzymujemy przyrost.
Pośrednio zmniejszenie rozmiaru WAL również wpływa (pozytywnie) na wątki, które przenoszą segmenty WAL do archiwum oraz na wątki kompresji WAL.
Rzeczywiste testy wydajności w naszym środowisku na danych syntetycznych pokazały niewielki wzrost (przepustowość wzrosła o 10%-15%, opóźnienie zmniejszyło się o 10%-15%).
Jak włączyć i skonfigurować
Minimalna wersja Apache Ignite: 2.8. Włączenie i konfiguracja odbywa się w następujący sposób:
- W class-path musi znajdować się moduł ignite-compression. Domyślnie znajduje się on w dystrybucji Apache Ignite w katalogu libs/optional i nie jest włączony do class-path. Można po prostu przenieść katalog na jeden poziom wyżej do libs, a wtedy przy uruchamianiu przez ignite.sh zostanie automatycznie włączony.
- Trwałość musi być włączona (Włącza się przez
DataRegionConfiguration.setPersistenceEnabled(true)). - Tryb kompresji należy ustawić za pomocą metody
DataStorageConfiguration.setWalPageCompression(), domyślnie kompresja jest wyłączona (tryb DISABLED). - Opcjonalnie można określić stopień kompresji za pomocą metody
DataStorageConfiguration.setWalPageCompression(), dopuszczalne wartości dla każdego z trybów znajdziesz w javadoc metody.
Podsumowanie
Rozważane mechanizmy kompresji danych w Apache Ignite mogą być używane niezależnie od siebie, ale również dopuszczalne są wszelkie ich kombinacje. Zrozumienie zasad ich działania pozwoli określić, jak dobrze pasują do Twoich zadań w Twoim środowisku oraz co trzeba będzie poświęcić przy ich używaniu. Kompresja stron dysku jest przeznaczona do kompresji głównego magazynu i może zapewnić średni poziom kompresji. Kompresja zrzutów stron WAL zapewni średni poziom kompresji plików WAL, przy czym prawdopodobnie nawet zwiększy wydajność. Kompaktowanie WAL nie wpłynie pozytywnie na wydajność, ale maksymalnie zmniejszy rozmiar plików WAL poprzez usunięcie fizycznych zapisów.
Źródło: habr.com
