Duży HEAP, duży CPU, duży GC
Napisał: Patryk Krawaczyński
08/10/2019 w Administracja, Debug Brak komentarzy. (artykuł nr 705, ilość słów: 1466)
Nasze maszyny posiadają następujące parametry: 64 GB RAM oraz 32 wątków CPU (z HT). Jest na nich uruchomiony ElasticSearch v5 z złotą zasadą 50% na heap oraz 50% na cache systemu plików. Oczywiście heap jest ustawiony tak, aby nie przekraczał trybu “zero-based”, czyli w tym przypadku 30720 MB:
java -Xms30720m -Xmx30720m -XX:+UseConcMarkSweepGC \ -XX:+UnlockDiagnosticVMOptions -XX:+PrintCompressedOopsMode -version
heap address: 0x0000000080000000, size: 30720 MB, Compressed Oops mode: Zero based, Oop shift amount: 3 Narrow klass base: 0x0000000800000000, Narrow klass shift: 0 Compressed class space size: 1073741824 Address: 0x0000000800000000 Req Addr: 0x0000000800000000 openjdk version "1.8.0_212" OpenJDK Runtime Environment (AdoptOpenJDK)(build 1.8.0_212-b03) OpenJDK 64-Bit Server VM (AdoptOpenJDK)(build 25.212-b03, mixed mode)
Tak duży heap został ustawiony wcześniej ze względu na dużą ilość indeksów dziennych, które miały dłuższy okres retencji. Niestety problem w tym, że przy tak dużym heap następuje ponad trzy sekundowy full GC, gdzie aplikacja czytająca dane może pozwolić sobie maksymalnie na dwu sekundowe opóźnienie odpowiedzi z backendu. W przypadku Concurrent Mark Sweep oznacza to, że przez 3 sekundy Elasticsearch nie odpowiada na żadne zapytania, a aplikacja nie może dostarczyć użytkownikowi żadnych danych. W celu zmniejszenia wielkości pamięci heap oraz czasów GC wymagane jest przeindeksowanie danych z indeksów dziennych na tygodniowe, co pozwoli na zmniejszenie liczby szardów w klastrze, a tym samym ciśnienie dla pamięci. Planując zejście do 16GB przy okazji możemy jeszcze spojrzeć jak JVM z CMS podchodzi do podziału heap na maszynach o większych parametrach:
java -Xms30720m -Xmx30720m -XX:+UseConcMarkSweepGC -XX:+UnlockDiagnosticVMOptions \ -XX:+PrintFlagsFinal -version | egrep -i \ "( NewSize | OldSize | NewRatio | ParallelGCThreads )"
uintx NewRatio = 2 {product} uintx NewSize := 2006515712 {product} uintx OldSize := 30205739008 {product} uintx ParallelGCThreads = 23 {product}
java -Xms16g -Xmx16g -XX:+UseConcMarkSweepGC -XX:+UnlockDiagnosticVMOptions \ -XX:+PrintFlagsFinal -version | egrep -i \ "( NewSize | OldSize | NewRatio | ParallelGCThreads )"
uintx NewRatio = 2 {product} uintx NewSize := 2006515712 {product} uintx OldSize := 15173353472 {product} uintx ParallelGCThreads = 23 {product}
Dlaczego dla 16GB oraz 31GB NewSize jest taki sam, a skaluje się tylko OldSize? Przecież NewRatio tu i tu ma wartość 2:
Domyślna wartość NewRatio dla JVM serwera to 2: stara generacja zajmuje 2/3 sterty, podczas gdy dla nowej przewidziana jest 1/3. Większa przestrzeń dla nowej generacji może pomieścić o wiele więcej krótkotrwałych obiektów, zmniejszając potrzebę powolnych, dużych kolekcji. Rozmiar starej generacji jest wciąż wystarczający duży, aby pomieścić wiele dłużej żyjących obiektów.
Poza tym wartości te nie są w proporcji new 1/3 : old 2/3. Otóż okazuje się, że ignorowanie parametru NewRatio powyżej 4GB nie jest błędem tylko funkcjonalnością, która ma na celu utrzymanie akceptowalnego czasu dla pauz nowej generacji. Dlatego, jeśli chcemy utrzymać rzeczywisty podział dla heap większego niż 4GB musimy podać jawnie flagę -XX:NewRatio=2 przy uruchamianiu JVM:
java -Xms16g -Xmx16g -XX:+UseConcMarkSweepGC -XX:+UnlockDiagnosticVMOptions \ -XX:+PrintFlagsFinal -XX:NewRatio=2 -version | egrep -i \ "( NewSize | OldSize | NewRatio | ParallelGCThreads )"
uintx NewRatio := 2 {product} uintx NewSize := 5726601216 {product} uintx OldSize := 11453267968 {product} uintx ParallelGCThreads = 23 {product}
Istnieje jeszcze inny sposób na zmianę rozmiaru NewSize (oprócz jego zdefiniowania w linii poleceń). Otóż skoro nasze maszyny obsługują tylko jeden serwer JVM – możemy do obsługi GC przeznaczyć wszystkie wątki procesora. Aktualnie są to tylko 23 ponieważ powyżej 8 wątków ich ilość do obsługi GC jest obliczana według wzoru: 3+5*N/8, czyli 3+5*32/8 = 23:
java -Xms16g -Xmx16g -XX:+UseConcMarkSweepGC -XX:+UnlockDiagnosticVMOptions \ -XX:+PrintFlagsFinal -XX:ParallelGCThreads=32 -version | egrep -i \ "( NewSize | OldSize | NewRatio | ParallelGCThreads )"
uintx NewRatio = 2 {product} uintx NewSize := 2791702528 {product} uintx OldSize := 14388166656 {product} uintx ParallelGCThreads := 32 {product}
Jest to mniejsza proporcja niż w przypadku jawnego podania NewRatio, ale większa niż w standardzie. Dodatkowo z większą mocą na sprzątanie. W proporcji podziału New/Old zawsze chodzi o zachowanie równowagi pomiędzy czasami i częstotliwością występowania niewielkich i dużych kolekcji. Dzięki powyższej konfiguracji udało się zejść z 3 sekund do ~600ms dla old GC i tym samym pozbyć się problemu aplikacji.