Tunen oder nicht stimmen? G1GC gegen Giant Allocation

Tunen oder nicht stimmen? G1GC gegen Giant Allocation

Blog post image

Dieser Artikel beschreibt ausgewählte JVM-Tuning-Probleme mit ausgewählten Flags. Verwenden Sie keine JVM-Flags, wenn Sie nicht wissen, welche Auswirkungen sie haben können.

Was bist du Objekte einfrieren?

G1 unterteilt den gesamten Haufen in Regionen. The goal is following 2048 Regions, with a 2-GB-Heap, is G1 a division in 2048 regions with each 1 MB. Die Größe der Region ist leicht abzuschätzen. Die einfachste Art der Ausführung (Java 11):

java -Xms2G -Xmx2G -Xlog:gc+heap -version

Java gibt die Größe der Region an:

[0.002s][info][gc,heap] Heap region size: 1M

Wenn Sie die Größe der Region nach dem Ausführen der JVM wissen möchten, können Sie jcmd und viele andere Tools:

jcmd <pid> GC.heap_info

Beispielergebnis:

dateFrom garbage-first heap   total 514048K, used 2048K [0x000000060a000000, 0x0000000800000000)
 region size 2048K, 2 young (4096K), 0 survivors (0K)
Metaspace       used 5925K, capacity 6034K, committed 6272K, reserved 1056768K
 class space    used 500K, capacity 540K, committed 640K, reserved 1048576K

Die zweite Zeile enthält die Größe der Region.

Gefährliche Gegenstände Sind Objekte, die Größe überschreiten die Hälfte der Region. Das sind normalerweise große Arrays.

Einzelheiten zur Bewerbung

  • JDK 11u5 von Oracle
  • Stapel size 5 GB
  • Nach dem Tuning wurde die Regionsgröße geändert 8 MB, wir haben auch 640 Regioni
  • Das oben genannte Tuning wurde durchgeführt, um die Full-GC-Phase nach einem starken Anstieg der Giant-Allokation zu vermeiden. Has hold, but the problems not completely solved

Eine Probe für die Impfung dieser Fehler

Wir schauen uns die Größe des Haufens nach der GC-Sammlung an:

Alles ist normal, bis zu einem starken Anstieg im Inneren. Schauen wir uns die GC-Statistiken in den Tabellen an und:

Wir haben einen Stopp Voller Stehender GC, Zyklusnummer 1059. Dieser Zyklus durchläuft zwei Zyklen mit früheren Situationen In allem geschaffen (kein Platz für GC, um Objekte zu kopieren). Schauen wir uns Rohtextprotokolle, züklen von 1056 bis zu 1058.

GC(1056) Eden regions: 210->0(319)
GC(1056) Survivor regions: 14->13(28)
GC(1056) Old regions: 233->231
GC(1056) Humongous regions: 0->0
GC(1057) To-space exhausted
GC(1057) Eden regions: 319->0(219)
GC(1057) Survivor regions: 13->5(42)
GC(1057) Old regions: 231->534
GC(1057) Humongous regions: 72->4
GC(1058) To-space exhausted
GC(1058) Eden regions: 97->0(224)
GC(1058) Survivor regions: 5->0(28)
GC(1058) Old regions: 534->636
GC(1058) Humongous regions: 4->4

Hier ist ein kleines Tutorial zur Interpretation dieses Denkens:

Gedicht „<type>Regionen: <from>-> <to>(<max>)“, zum Beispiel:“„Eden regions: 210->0(319)” bedeutet:

  • vor dem GC-Zyklus die Anzahl der Regionen <from>210 im Obiegen Beispiel
  • nach dem GC-Zyklus die Anzahl der Regionen <to>im Obiegen Beispiel
  • maximale Anzahl von Regionen im nächsten Zyklus <type> Liste <max>319 Eden im Obiegen Beispiel

Im Zyklus 1056 alles war normal und dann sauber 1057 wir hatten 72 neue riesige Objekte. GC hat den Evakuierungsprozess nicht bestanden (das Set ist markiert als To-space exhausted) und er übertrug fast alle Objekte der jüngeren Generation auf die alte. Dies zeigt sich in der Linie mit den alten Regionen 231->534, so schien es 303 neue alte Regionen. Dann konnte der G1 die große alte Generation einfach nicht in einer „ausreichend“ Zeit bereinigen und ging in den abgesicherten Modus Full GC. Wenn wir alle Regionen in einem Zyklus zählen 1057, wir erhalten 319+13+231+72=635 Regionen gefüllt (mit 640), also blieb G1 nur übrig Regionen für diese Aufgabe. Kein Wunder, dass der G1 es nicht geschafft hat.

Wenn wir uns die Anzahl der Regionen vor dem GC ansehen, stellen wir fest, dass es nur einen starken Anstieg gab. riesige Objekte den ganzen Tag über (Protokolle decken einen Zeitraum von 24 Stunden ab).


Mir wurde gesagt, dass die Situation in jedem Knoten dieser App ein- oder zweimal täglich auftritt.

Was können wir tun, um das Problem zu beheben?

Wenn wir die Antwort googeln, finden wir zunächst Vorschläge zur Optimierung des GC, indem wir die Region vergrößern. Aber welche Auswirkungen wird dieses Tuning auf die übrigen Anwendungen haben? Niemand weiß das. In manchen Situationen kann das versucht werden, aber was ist, wenn riesiges Objekt wird haben 20 MB? Es ist notwendig, die Größe der Region zu erhöhen 64 MBzu riesiges Objekt sind zu normalen Objekten der jüngeren Generation geworden. Leider ist das nicht möglich, die maximale Regionsgröße ist 32 MB (JDK 11u9). Ein weiterer Punkt, über den man nachdenken sollte, ist die Lebensdauer solcher riesige Objekte in der App. Wenn es nicht kurz ist, werden sie vom Sammler der jüngeren Generation kopiert, was sehr teuer werden kann. Sie können auch den Großteil der jüngeren Generation beschäftigen, was die Häufigkeit von GC-Sammlungen erhöhen kann.

Die oben erwähnte Anwendung hat ein solches Tuning zweimal durchgeführt. Zunächst erhöhte das Entwicklungsteam die Größe der Region auf 4 MBund dann zu 8 MB. Das Problem ist immer noch da.

Können wir statt GC-Tuning noch etwas anderes tun?

Wir können einfach einen Ort finden, um diese zu erstellen riesige Objekte in der App und ändere den Code. Anstatt also den GC so einzustellen, dass er besser mit der Anwendung funktioniert, können wir die Anwendung so ändern, dass sie besser mit dem GC funktioniert.

Wie geht das?

Was das Objekt im Stapel betrifft, gibt es zwei Gesichtspunkte:

  • wir können die Position des Objekts im Stapel mit Heap Dump überprüfen
  • oder wir finden im Thread-Stack, der diese Objekte mit dem Allocation Profiler erstellt
  • oder wir können im Thread-Stack finden, der diese Objekte erstellt, indem wir die internen Aufrufe der JVM verfolgen

Weil wir einen Ort zum Schaffen finden wollen riesige Objekte, die zweite und dritte Option sind besser geeignet, aber es gibt Situationen, in denen der Dump des Haufens „gut genug“ ist.

Zuweisungsprofiler

Es gibt zwei Arten von Allokationsprofilern:

  • Profiler, die in Produktionssystemen ausgeführt werden können
  • Profiler, die die Leistung so stark beeinträchtigen, dass Sie es nicht können

Ich werde nur den ersten dieser Typen besprechen. Zwei Allokationsprofiler, die meines Wissens für solche Zwecke in der Produktion verwendet werden können, sind:

  • Async-profilerunter Verwendung von -e alloc (Version >= 2.0) - Open Source, zum Zeitpunkt des Schreibens von Version 2.0 befand sich Version 2.0 bereits in der Phase des „Early Access“
  • Java Flight Recorder- Open Source ab JDK 11

Sie verwenden das gleiche Funktionsprinzip. Laut der readme Async-profiler:

The profiler features TLAB-driven sampling. It relies on HotSpot-specific callbacks to receive two kinds of notifications:

-- when an object is allocated in a newly created TLAB (aqua frames in a Flame Graph);

-- when an object is allocated on a slow path outside TLAB (brown frames).

This means not each allocation is counted, but only allocations every N kB, where N is the average size of TLAB. This makes heap sampling very cheap and suitable for production. On the other hand, the collected data may be incomplete, though in practice it will often reflect the top allocation sources.

Wenn Sie nicht wissen, was TLAB ist, empfehle ich Ihnen, dies zu lesen Articles. Kurzum: ein TLAB ist ein kleiner Teil von Eden, der einem einzelnen Thread zugeordnet ist und in dem die Zuordnung durch nur einen Thread möglich ist.

Beide Profiler bieten Informationen über:

  • Art Objekt in der Zuordnung
  • Thread-Stapelin dem das Objekt erstellt wurde
  • Zeitstempel,
  • Threaddas erzeugt dieses Objekt

Nach dem Datenabzug ist es notwendig, die Ausgabedatei zu verarbeiten und zu finden riesige Objekte.

Async-Profiler-Beispiel

Meiner Meinung nach ist Async-Profiler der beste Profiler der Welt, daher werde ich ihn im folgenden Beispiel verwenden.

Die Beispielanwendung, die ich geschrieben habe, erstellt riesige Zuweisungen. Versuchen wir, einen Ort zu finden, an dem die Zuweisung durchgeführt werden kann. Wir führen (aus dem Verzeichnis async-profiler) den Befehl profiler.sh mit den folgenden Parametern aus:

  • Dauer: zehn Sekunden: -Tag 10
  • Ausgabedatei: -f /tmp/humongous.jfr
  • Wir interessieren uns auch für die Verwaltungsabteilung: -e Alloc
  • Application das ist unsere Meisterklasse
./profiler.sh -d 10 -f /tmp/humongous.jfr -e alloc Application

Aktuell ist es sinnvoll, an der Ausgabe zu arbeiten und interessante Stacks zu finden. Wir schauen uns zunächst die GC-Logs an, um die Größen zu ermitteln:

...
Dead humongous region 182 object size 16777232 start 0x000000074b600000 with remset 0 code roots 0 is marked 0 reclaim candidate 1 type array 1
Dead humongous region 199 object size 33554448 start 0x000000074c700000 with remset 0 code roots 0 is marked 0 reclaim candidate 1 type array 1
Dead humongous region 232 object size 67108880 start 0x000000074e800000 with remset 0 code roots 0 is marked 0 reclaim candidate 1 type array 1
...

Dann suchen wir nach Objekten mit den Größen: 16777232, 33554448, 67108880. Bitte beachten Sie, dass wir die Protokolle aus dem Zeitraum analysieren müssen, da sie in dem Profiler ausgeführt wurden. Ab Version 2.0 der Async Profiler-JFR-Serviceprogramme handelt es sich um die Release-Version 2.0. We can use the JFR-command zeilentool, that was delivered with JDK 11, to analysis the output file.

Lassen Sie uns zunächst den Inhalt der Ausgabedatei analysieren:

jfr summary humongous.jfr
Version: 2.0
Chunks: 1
Start: 2020-11-10 07:02:10 (UTC)itit
Duration: 10 s
Event Type                          Count  Size (bytes)
=========================================================
jdk.ObjectAllocationInNewTLAB       65632       1107332
jdk.ObjectAllocationOutsideTLAB       134          2304
jdk.CPULoad                            10           200
jdk.Metadata                            1          4191
jdk.CheckPoint                          1          4449
jdk.ActiveRecording                     1            73
jdk.ExecutionSample                     0             0
jdk.JavaMonitorEnter                    0             0
jdk.ThreadPark                          0             0

Wir haben uns registriert:

  • 134 Outsourcing von TLAB
  • 65632 Anwendungen im neuen TLAB

Gefährliche Gegenstände werden normalerweise außerhalb des TLAB zugewiesen, da sie sehr groß sind. Lass' uns diese Anweisungen analysieren:

jfr print --events jdk.ObjectAllocationOutsideTLAB humongous.jfr
...
jdk.ObjectAllocationOutsideTLAB {
 startTime = 2020-11-10T07:02:17.329539461Z
 objectClass = byte[] (classLoader = null)
 allocationSize = 16777232
 eventThread = "badguy" (javaThreadId = 37)
 stackTrace = [
   pl.britenet.profiling.demo.nexttry.Application.lambda$main$2() line: 56
   pl.britenet.profiling.demo.nexttry.Application$$Lambda$17.1778535015.run() line: 0
   java.lang.Thread.run() line: 833
 ]
}

jdk.ObjectAllocationOutsideTLAB {
 startTime = 2020-11-10T07:02:17.331986883Z
 objectClass = byte[] (classLoader = null)
 allocationSize = 33554448
 eventThread = "badguy" (javaThreadId = 37)
 stackTrace = [
   pl.britenet.profiling.demo.nexttry.Applic./profiler.sh -d 10 -e "G1CollectedHeap::humongous_obj_allocate" Applicationation.lambda$main$2() line: 56
   pl.britenet.profiling.demo.nexttry.Application$$Lambda$17.1778535015.run() line: 0
   java.lang.Thread.run() line: 833
 ]
}

jdk.ObjectAllocationOutsideTLAB {
 startTime = 2020-11-10T07:02:17.337044969Z
 objectClass = byte[] (classLoader = null)
 allocationSize = 67108880
 eventThread = "badguy" (javaThreadId = 37)
 stackTrace = [
   pl.britenet.profiling.demo.nexttry.Application.lambda$main$2() line: 56
   pl.britenet.profiling.demo.nexttry.Application$$Lambda$17.1778535015.run() line: 0
   java.lang.Thread.run() line: 833
 ]
}
...

Die verfügbaren Formate lauten wie folgt:

  • Einfacher, für Menschen lesbarer Text — wie im obigen Beispiel
  • JSON
  • XML

Standardmäßig wird der Stack-Dump auf die letzten 5 Frames gekürzt. Dies kann mit der Option --stack-depth geändert werden.

Aus der obigen Ausgabe können wir lesen, dass wir 3 Objekte haben, nach denen wir gesucht haben. Wir können daraus lesen, dass die riesige Zuweisung vom „Badguy“ -Thread durchgeführt wird und in Lambda in der Application-Klasse erfolgt.

Verfolgung interner JVM-Mechanismen

Riesige Zuteilung findet in g1CollectedHeap.cpp statt, als Teil von:

G1CollectedHeap: :humongous_obj_allocate (size_t word_size)

Um seine Anrufe zu verfolgen, gibt es:

  • Tracking-Tools innerhalb des Betriebssystems, z. eBPF
  • Async-Profiler, der das Ereignis G1CollectedHeap: :humongous_obj_allocate verwendet. Das funktioniert auch in der Version 1.8 (und früher)

Async-Profiler-Beispiel

Das Verfolgen solcher Aufrufe mit dem Async-Profiler-Tool ist wirklich einfach. Wir müssen sie mit den folgenden Optionen ausführen:

  • Dauer: zehn Sekunden: -Tag 10
  • Ausgabedatei: -f /tmp/humongous.svg (für Version 1.8. *, in dem Fall 2. * verwenden Sie die HTML-Erweiterung () für die Ausgabe im FlameGraph-Format
  • Die Tracking-Methode wird übergeben -e G1 hat Heap gesammelt: :humongous_obj_allocate
  • Aplikasi das ist unsere Meisterklasse
./profiler.sh -d 10 -f /tmp/humongous.svg -e G1CollectedHeap::humongous_obj_allocate Application

Wir können sehen riesige Zuweisungen:

Wenn wir einen Threadnamen benötigen, können wir einen Schalter hinzufügen -t beim Ausführen des Async-Profiler-Dienstprogramms.

Schwieriger Teil  Änderung der Anwendung

Bis zu diesem Zeitpunkt war alles einfach. Jetzt müssen wir die Anwendung ändern. Natürlich kann ich nicht jede riesige Zuteilung mit einem Artikel korrigieren. Ich kann nur Produktionsbeispiele schreiben und die darin ausgeführten Aktionen beschreiben.

Hazelcast hat handgemachte Erinnerungsstücke verteilt

Riesige Zuteilung: Große Byte-Arrays des verteilten Hazelcast-Cache während der Streuung zwischen Knoten.

Reparieren: Änderung des Cache-Anbieters für Karten, bei denen Giant Allocation angezeigt wurde. Riesige Anordnungen von Bytes wurden über das Netzwerk übertragen, was völlig ineffizient war.

Netzwerkantrag — Einreichungsformular an DMS

Riesige Zuteilung: Die Anwendung enthielt ein HTML-Formular, mit dem Dateien mit einer Größe von bis zu 40 MB an das DMS (Document Management System) gesendet werden konnten. Der Inhalt dieser Datei auf Java-Codeebene ist einfach ein Array von Bytes. Natürlich war eine solche Matrix ein riesiges Objekt.

Reparieren: Das Backend dieses Einreichungsformulars wurde in eine separate Anwendung verschoben.

Überwintern

Riesige Zuteilung:
 Die Hibernate-Engine erstellte sehr große Arrays von Objekten. Aus dem Dump des Heaps konnte ermittelt werden, um welche Arten von Objekten es sich handelte und welchen Inhalt sie enthielten. Der Profiler ermöglichte es, den Thread ihrer Zuordnung und die Protokolle der Anwendung, die sie als Geschäftsvorgang bezeichnete, zu ermitteln. Die Klassendefinition wurde im Anwendungscode gefunden:

@Entity
public class SomeEntity {
   ...
   @OneToMany(fetch = FetchType.EAGER ...)
   private Set<Mapping1> map1;
   @OneToMany(fetch = FetchType.EAGER ...)
   private Set<Mapping2> map2;
   @OneToMany(fetch = FetchType.EAGER ...)
   private Set<Mapping3> map3;
   @OneToMany(fetch = FetchType.EAGER ...)
   private Set<Mapping4> map4;
   @OneToMany(fetch = FetchType.EAGER ...)
   private Set<Mapping5> map5;
   ...
}

Eine solche Zuordnung erzeugte mehrere Joins auf SQL-Ebene und doppelte Ergebnisse, die eine Verarbeitung durch Hibernate erforderten.

Reparieren: In diesem Objekt war es möglich, fetchType.SELECT hinzuzufügen und genau das wurde getan. Denken Sie daran, dass dies in einigen Fällen zu Problemen mit der Datenintegrität führen kann. Ich wiederhole: Diese Änderung hat dieser Anwendung geholfen. aber es kann einem anderen schaden.

Riesige Reaktionen nach WebService

Riesige Zuteilung: Eine Anwendung hat über WebService einen „Produktkatalog“ aus einer anderen Anwendung heruntergeladen. Dies führte zur Erstellung eines riesigen Byte-Arrays (ca. 100 MB) zum Abrufen von Daten aus dem Netzwerk.

Reparieren: Beide Apps wurden geändert, sodass Sie den „Produktkatalog“ in Stücken herunterladen können.

Tunen oder nicht stimmen?

Demnach können wir manchmal, anstatt den GC so einzustellen, dass er besser mit der Anwendung funktioniert, die Anwendung so ändern, dass sie besser mit dem GC funktioniert. Was sind die Vor- und Nachteile?

Anwendungsänderung — Vorteile

  • Die volle Kontrolle über den Code Ihrer eigenen Anwendung ermöglicht es Ihnen, Änderungen vorzunehmen
  • Ein tiefes Verständnis der internen Mechanismen der JVM ist nicht erforderlich. Möglichkeit der Ausführung durch das eigene Team nach der Analyse.

Änderung der Anwendung — Nachteile

  • Einsatz
  • Verwaltung von Veröffentlichungen
  • Andere Unternehmensfragen

GC-Tuning — Vorteile

  • Es erfordert nur das Ändern der JVM-Flags. Manchmal in der Produktion ohne Implementierung durchführbar.

GC-Tuning — Nachteile

  • Erfordert ein tiefes Verständnis der internen Mechanismen der JVM
  • Interne Mechanismen können sich im Laufe der Zeit ändern, sodass nach der Aktualisierung des JDK möglicherweise andere Probleme mit dem Betrieb der Anwendung auftreten.

Als Faustregel schlage ich vor, zuerst die App zu ändern, wenn dadurch GC-Probleme behoben werden können.

Related posts

All posts
Blog post image

Technologien

Ein verteiltes Team leiten — wie geht das richtig?

Der Stil des Teammanagements wird oft von vielen Faktoren beeinflusst. Dazu geh...

Read more
Blog post image

Technologien

Was ist Docker und was sind die Vorteile seiner Verwendung?

Bevor Docker-Plattformen entwickelt wurden, mussten zum Starten von Anwendungen...

Read more
Blog post image

Technologien

Wie hat Blockchain unsere Welt verändert?

Jeden Tag wächst die Popularität von Blockchain. Fast seit ihrer Geburt im Ja...

Read more