Ten wpis gościnny autorstwa architekta wydajności Intel Java, Erica Kaczmarka (pierwotnie opublikowany tutaj) opisuje, jak dostroić odśmiecanie środowiska Java (GC) dla Apache HBase, skupiając się na 100% odczytach YCSB.
Apache HBase to projekt Apache open source oferujący przechowywanie danych NoSQL. Często używany razem z HDFS, HBase jest szeroko stosowany na całym świecie. Do znanych użytkowników należą Facebook, Twitter, Yahoo i nie tylko. Z perspektywy programisty HBase to „rozproszona, wersjonowana, nierelacyjna baza danych wzorowana na Bigtable firmy Google, rozproszonym systemie przechowywania danych strukturalnych”. HBase może z łatwością obsługiwać bardzo wysoką przepustowość poprzez skalowanie w górę (tj. wdrożenie na większym serwerze) lub skalowanie w górę (tj. wdrożenie na większej liczbie serwerów).
Z punktu widzenia użytkownika opóźnienie dla każdego pojedynczego zapytania ma bardzo duże znaczenie. Współpracując z użytkownikami nad testowaniem, dostrajaniem i optymalizacją obciążeń HBase, napotykamy teraz znaczną liczbę osób, które naprawdę chcą 99. percentyla opóźnień operacji. Oznacza to podróż w obie strony, od żądania klienta do odpowiedzi z powrotem do klienta, wszystko w ciągu 100 milisekund.
Kilka czynników wpływa na zmienność opóźnień. Jednym z najbardziej niszczycielskich i nieprzewidywalnych intruzów opóźnień jest wstrzymanie „zatrzymania świata” Java Virtual Machine (JVM) w celu wyrzucania śmieci (czyszczenie pamięci).
Aby temu zaradzić, przeprowadziliśmy kilka eksperymentów przy użyciu kolektora Oracle jdk7u21 i jdk7u60 G1 (Garbage 1st). Zastosowany przez nas system serwerowy oparty był na procesorach Intel Xeon Ivy-bridge EP z technologią Hyper-threading (40 procesorów logicznych). Miał 256 GB pamięci RAM DDR3-1600 i trzy dyski SSD o pojemności 400 GB jako pamięć lokalna. Ta niewielka konfiguracja zawierała jeden master i jeden slave, skonfigurowane na jednym węźle z odpowiednio skalowanym obciążeniem. Użyliśmy HBase w wersji 0.98.1 i lokalnego systemu plików do przechowywania HFile. Tabela testowa HBase została skonfigurowana jako 400 milionów wierszy i miała rozmiar 580 GB. Użyliśmy domyślnej strategii sterty HBase:40% dla blockcache, 40% dla memstore. YCSB był używany do kierowania 600 wątków roboczych wysyłających żądania do serwera HBase.
Poniższe wykresy pokazują, że jdk7u21 działa w trybie 100% odczytu przez godzinę przy użyciu -XX:+UseG1GC -Xms100g -Xmx100g -XX:MaxGCPauseMillis=100
. Określiliśmy garbage collector, który ma być używany, rozmiar sterty i żądany czas pauzy „zatrzymaj świat” w zbieraniu elementów bezużytecznych (GC).
Rysunek 1:Dzikie wahania w czasie pauzy GC
W tym przypadku mamy szalenie kołyszące się pauzy GC. Przerwa GC miała zakres od 7 milisekund do 5 pełnych sekund po początkowym skoku, który osiągnął nawet 17,5 sekundy.
Poniższy wykres pokazuje więcej szczegółów w stanie ustalonym:
Rysunek 2:Szczegóły pauzy GC podczas stanu ustalonego
Rysunek 2 mówi nam, że pauzy GC faktycznie dzielą się na trzy różne grupy:(1) od 1 do 1,5 sekundy; (2) od 0,007 sekundy do 0,5 sekundy; (3) skoki od 1,5 sekundy do 5 sekund. To było bardzo dziwne, więc przetestowaliśmy ostatnio wydaną wersję jdk7u60, aby sprawdzić, czy dane będą się różnić:
Przeprowadziliśmy te same testy odczytu 100% przy użyciu dokładnie tych samych parametrów JVM:-XX:+UseG1GC -Xms100g -Xmx100g -XX:MaxGCPauseMillis=100
.
Rysunek 3:Znacznie ulepszona obsługa skoków czasu pauzy
Jdk7u60 znacznie poprawił zdolność G1 do obsługi skoków czasu pauzy po początkowym skoku podczas fazy stabilizacji. Jdk7u60 zdobył 1029 Young i mieszanych GC podczas godzinnego biegu. GC zdarzało się co około 3,5 sekundy. Jdk7u21 wykonał 286 GC, a każdy GC miał miejsce co 12,6 sekundy. Jdk7u60 był w stanie zarządzać czasem pauzy od 0,302 do 1 sekundy bez większych skoków.
Rysunek 4 poniżej daje nam bliższe spojrzenie na 150 pauz GC w stanie ustalonym:
Rysunek 4:Lepszy, ale niewystarczająco dobry
W stanie ustalonym jdk7u60 był w stanie utrzymać średni czas pauzy około 369 milisekund. Był znacznie lepszy niż jdk7u21, ale nadal nie spełniał naszych wymagań 100 milisekund podanych przez –Xx:MaxGCPauseMillis=100
.
Aby określić, co jeszcze możemy zrobić, aby uzyskać 100 milionów sekund czasu pauzy, musieliśmy dowiedzieć się więcej na temat zachowania zarządzania pamięcią JVM i modułu odśmiecania pamięci G1 (Garbage First). Poniższe rysunki pokazują, jak G1 działa w kolekcji Young Gen.
Ilustracja 5:Slajd z prezentacji JavaOne 2012 autorstwa Charliego Hunta i Moniki Beckwith:„Dostrajanie wydajności G1 Garbage Collector”
Po uruchomieniu JVM, na podstawie parametrów uruchamiania JVM, prosi system operacyjny o przydzielenie dużej ciągłej porcji pamięci do hostowania sterty JVM. Ten fragment pamięci jest podzielony przez JVM na regiony.
Ilustracja 6:Slajd z prezentacji JavaOne 2012 autorstwa Charliego Hunta i Moniki Beckwith:„Dostrajanie wydajności G1 Garbage Collector”
Jak pokazuje rysunek 6, każdy obiekt, który program Java przydziela za pomocą Java API, najpierw trafia do przestrzeni Eden w pokoleniu Younga po lewej stronie. Po pewnym czasie Eden zapełnia się i uruchamia się GC Młodej generacji. Obiekty, do których wciąż się odwołują (tj. „żywe”), są kopiowane do przestrzeni Ocalałego. Kiedy obiekty przetrwają kilka GC w młodym pokoleniu, awansują do przestrzeni Starej generacji.
Kiedy dzieje się Young GC, wątki aplikacji Java są zatrzymywane w celu bezpiecznego oznaczenia i skopiowania aktywnych obiektów. Te zatrzymania są znanymi przerwami GC typu „stop-the-world”, które sprawiają, że aplikacje nie odpowiadają, dopóki przerwy się nie skończą.
Ilustracja 7:Slajd z prezentacji JavaOne 2012 autorstwa Charliego Hunta i Moniki Beckwith:„G1 Garbage Collector Performance Tuning”
Stare pokolenie też może stać się zatłoczone. Na pewnym poziomie — kontrolowany przez -XX:InitiatingHeapOccupancyPercent=?
gdzie wartość domyślna to 45% całkowitej sterty — wyzwalany jest mieszany GC. Gromadzi zarówno gen Młodego, jak i Starego gen. Mieszane pauzy GC są kontrolowane przez to, ile czasu zajmuje młodemu genowi czyszczenie, gdy ma miejsce mieszana GC.
Widzimy więc w G1, pauzy GC „zatrzymaj świat” są zdominowane przez szybkość, z jaką G1 może oznaczać i kopiować żywe obiekty z przestrzeni Edenu. Mając to na uwadze, przeanalizujemy, w jaki sposób wzorzec alokacji pamięci HBase pomoże nam dostroić G1 GC, aby uzyskać pożądane 100 milisekund przerwy.
W HBase istnieją dwie struktury w pamięci, które zużywają większość jego sterty:BlockCache
, buforowanie bloków plików HBase do operacji odczytu oraz buforowanie najnowszych aktualizacji przez Memstore.
Rysunek 8:W HBase dwie struktury w pamięci zajmują większość sterty.
Domyślna implementacja BlockCache
HBase jest LruBlockCache
, który po prostu używa dużej tablicy bajtów do hostowania wszystkich bloków HBase. Kiedy bloki są „wyrzucane”, odniesienie do obiektu Java tego bloku jest usuwane, co pozwala GC na przemieszczenie pamięci.
Nowe obiekty tworzące LruBlockCache
i Memstore
idź najpierw do przestrzeni Edenu Młodego Pokolenia. Jeśli żyją wystarczająco długo (tzn. jeśli nie są eksmitowani z LruBlockCache
lub wypłukane z Memstore), a następnie po kilku młodych generacjach GC trafiają do Starej generacji sterty Java. Kiedy wolna przestrzeń Starego pokolenia jest mniejsza niż określona threshOld
(InitiatingHeapOccupancyPercent
na początek), mieszany GC uruchamia i usuwa niektóre martwe obiekty w starej generacji, kopiuje żywe obiekty z pokolenia Young i ponownie oblicza Eden z pokolenia Young i HeapOccupancyPercent
dla starszej generacji . Ostatecznie, gdy HeapOccupancyPercent
osiąga pewien poziom, FULL GC
tak się dzieje, co powoduje, że ogromne „zatrzymaj świat” GC zatrzymuje się, aby oczyścić wszystkie martwe obiekty wewnątrz starej generacji.
Po przestudiowaniu dziennika GC utworzonego przez „-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintAdaptiveSizePolicy
„, zauważyliśmy HeapOccupancyPercent
nigdy nie urósł wystarczająco duży, aby wywołać pełne GC podczas odczytu HBase 100%. Pauzy GC, które widzieliśmy, były zdominowane przez pauzy młodego pokolenia „zatrzymaj świat” i rosnące z czasem przetwarzanie referencyjne.
Po zakończeniu tej analizy wprowadziliśmy trzy grupy zmian w domyślnym ustawieniu G1 GC:
- Użyj
-XX:+ParallelRefProcEnabled
Gdy ta flaga jest włączona, GC używa wielu wątków do przetwarzania rosnących odwołań podczas Young i mieszanego GC. Z tą flagą dla HBase, czas notowania GC jest skrócony o 75%, a całkowity czas pauzy GC zostaje skrócony o 30%. Set -XX:-ResizePLAB and -XX:ParallelGCThreads=8+(logical processors-8)(5/8)
Podczas zbiórki Young wykorzystywane są promocyjne bufory alokacji lokalnej (PLAB). Używanych jest wiele wątków. Każdy wątek może wymagać przydzielenia miejsca na kopiowane obiekty w przestrzeni Survivor lub Old. PLABy są wymagane, aby uniknąć konkurencji wątków o współdzielone struktury danych, które zarządzają wolną pamięcią. Każdy wątek GC ma jeden PLAB dla przestrzeni Survival i jeden dla przestrzeni Old. Chcielibyśmy przestać zmieniać rozmiar PLABów, aby uniknąć dużych kosztów komunikacji między wątkami GC, a także zmian podczas każdego GC. Chcielibyśmy, aby liczba wątków GC była wielkością obliczaną przez 8+ (procesory logiczne-8)( 5/8). Ta formuła została niedawno zarekomendowana przez Oracle. Przy obu ustawieniach jesteśmy w stanie zobaczyć płynniejsze przerwy GC podczas przebiegu.- Zmień
-XX:G1NewSizePercent
domyślnie od 5 do 1 dla 100 GB stertyNa podstawie danych wyjściowych z-XX:+PrintGCDetails and -XX:+PrintAdaptiveSizePolicy
, zauważyliśmy, że przyczyną niepowodzenia G1 w osiągnięciu pożądanego czasu pauzy 100GC był czas potrzebny na przetworzenie Edenu. Innymi słowy, podczas naszych testów G1 potrzebował średnio 369 milisekund, aby opróżnić 5 GB Edenu. Następnie zmieniliśmy rozmiar Eden za pomocą-XX:G1NewSizePercent=
flaga z 5 do 1. Dzięki tej zmianie zauważyliśmy, że czas pauzy GC został skrócony do 100 milisekund.
Z tego eksperymentu dowiedzieliśmy się, że prędkość G1 do czyszczenia Edenu wynosi około 1 GB na 100 milisekund lub 10 GB na sekundę w przypadku używanej konfiguracji HBase.
Na podstawie tej prędkości możemy ustawić -XX:G1NewSizePercent=
więc rozmiar Eden można utrzymać na około 1 GB. Na przykład:
- 32 GB sterty,
-XX:G1NewSizePercent=3
- 64 GB sterty, –
XX:G1NewSizePercent=2
- 100 GB i więcej sterty,
-XX:G1NewSizePercent=1
- Więc nasze ostatnie opcje wiersza poleceń dla HRegionserver to:
-XX:+UseG1GC
-Xms100g -Xmx100g
(Rozmiar stosu używany w naszych testach)-XX:MaxGCPauseMillis=100
(Pożądany czas przerwy GC w testach)- –
XX:+ParallelRefProcEnabled
-XX:-ResizePLAB
-XX:ParallelGCThreads= 8+(40-8)(5/8)=28
-XX:G1NewSizePercent=1
Oto wykres czasu pauzy GC dla uruchomienia operacji odczytu 100% przez 1 godzinę:
Rysunek 9:Najwyższe początkowe skoki osiadania zostały zredukowane o ponad połowę.
Na tym wykresie nawet najwyższe początkowe skoki osiadania zostały zmniejszone z 3,792 sekundy do 1,684 sekundy. Najbardziej początkowe skoki wynosiły mniej niż 1 sekundę. Po rozliczeniu GC był w stanie utrzymać czas przerwy około 100 milisekund.
Poniższy wykres porównuje przebiegi jdk7u60 z i bez strojenia w stanie ustalonym:
Rysunek 10:jdk7u60 działa z dostrajaniem i bez niego w stanie ustalonym.
Proste dostrajanie GC, które opisaliśmy powyżej, daje idealne czasy przerwy GC, około 100 milisekund, ze średnimi 106 milisekundami i 7 milisekundowym odchyleniem standardowym.
Podsumowanie
HBase to aplikacja o krytycznym znaczeniu dla czasu odpowiedzi, która wymaga przewidywalności i zarządzania czasem pauzy GC. Z Oracle jdk7u60, na podstawie informacji GC zgłoszonych przez -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintAdaptiveSizePolicy
, jesteśmy w stanie dostroić czas pauzy GC do pożądanych 100 milisekund.
Eric Kaczmarek jest architektem wydajności Java w Intel Software Solution Group. Kieruje pracami w firmie Intel w zakresie włączania i optymalizacji frameworków Big Data (Hadoop, HBase, Spark, Cassandra) dla platform Intel.
Oprogramowanie i obciążenia używane w testach wydajności mogły zostać zoptymalizowane pod kątem wydajności tylko na mikroprocesorach firmy Intel. Testy wydajności, takie jak SYSmark i MobileMark, są mierzone przy użyciu określonych systemów komputerowych, komponentów, oprogramowania, operacji i funkcji. Wszelkie zmiany któregokolwiek z tych czynników mogą spowodować różnice w wynikach. Należy zapoznać się z innymi informacjami i testami wydajności, aby pomóc w pełnej ocenie planowanych zakupów, w tym wydajności tego produktu w połączeniu z innymi produktami.
Numery procesorów firmy Intel nie są miarą wydajności. Numery procesorów różnicują funkcje w ramach każdej rodziny procesorów. Nie w różnych rodzinach procesorów. Przejdź do:http://www.intel.com/products/processor_number.
Copyright 2014 Intel Corp. Intel, logo Intel i Xeon są znakami towarowymi firmy Intel Corporation w Stanach Zjednoczonych i/lub innych krajach.