Unter der Haube enthält hh.ru eine große Anzahl von Java-Diensten, die in Docker-Containern ausgeführt werden. Während ihres Betriebs stießen wir auf viele nicht triviale Probleme. In vielen Fällen musste ich lange googeln, die OpenJDK-Quellen lesen und sogar die Services in der Produktion profilieren, um der Lösung auf den Grund zu gehen. In diesem Artikel werde ich versuchen, die Quintessenz des dabei gewonnenen Wissens zu vermitteln.
CPU-Grenzen
Früher lebten wir in virtuellen kvm-Maschinen mit CPU- und Speicherbeschränkungen und legten bei der Umstellung auf Docker ähnliche Einschränkungen in cgroups fest. Und das erste Problem, auf das wir stießen, waren genau die CPU-Grenzwerte. Ich muss sofort sagen, dass dieses Problem für neuere Versionen von Java 8 und Java ≥ 10 nicht mehr relevant ist. Wenn Sie mit der Zeit gehen, können Sie diesen Abschnitt sicher überspringen.
Wir starten also einen kleinen Dienst im Container und stellen fest, dass er eine große Anzahl von Threads erzeugt. Oder die CPU verbraucht viel mehr als erwartet, Timeout wie viel umsonst. Oder hier ist eine andere reale Situation: Auf einem Computer startet der Dienst normal und auf einem anderen stürzt er mit denselben Einstellungen ab, genagelt von einem OOM-Killer.
Die Lösung stellt sich als sehr einfach heraus - nur Java sieht die im Docker festgelegten Einschränkungen von
--cpus
nicht und glaubt, dass alle Kernel des Host-Computers für ihn zugänglich sind. Und es kann viele davon geben (in unserem Standard-Setup - 80).
Bibliotheken passen die Größe der Thread-Pools an die Anzahl der verfügbaren Prozessoren an - daher die große Anzahl der Threads.
Java selbst skaliert die Anzahl der GC-Threads auf die gleiche Weise, daher den CPU-Verbrauch und die Zeitüberschreitung. Der Dienst verwendet eine Menge Ressourcen für die Speicherbereinigung, wobei der Löwenanteil des ihm zugewiesenen Kontingents verwendet wird.
Außerdem können Bibliotheken (insbesondere Netty) in bestimmten Fällen die Größe des Off-Hip-Speichers an die Anzahl der CPUs anpassen, was zu einer hohen Wahrscheinlichkeit führt, dass die für den Container festgelegten Grenzwerte überschritten werden, wenn sie auf einer leistungsstärkeren Hardware ausgeführt werden.
Als sich dieses Problem manifestierte, haben wir zunächst versucht, die folgenden Arbeitsrunden zu verwenden:
- hat versucht, einige Dienste zu verwenden
libnumcpus - eine Bibliothek, mit der Sie Java "betrügen" können, indem Sie eine andere Anzahl verfügbarer Prozessoren
festlegen ;
- explizit die Anzahl der GC-Threads angegeben,
- Setzen Sie explizit Grenzen für die Verwendung von Direktbytepuffern.
Aber natürlich ist es nicht sehr bequem, sich mit solchen Krücken zu bewegen, und der Wechsel zu Java 10 (und dann zu Java 11), bei dem all diese Probleme
fehlen , war eine echte Lösung. Fairerweise ist es erwähnenswert, dass auch bei den Acht mit dem im Oktober 2018 veröffentlichten
Update 191 alles in Ordnung war. Zu diesem Zeitpunkt war es für uns schon irrelevant, was ich auch für Sie wünsche.
Dies ist ein Beispiel, bei dem die Aktualisierung der Java-Version nicht nur moralische Befriedigung bietet, sondern auch einen spürbaren Gewinn in Form eines vereinfachten Betriebs und einer höheren Serviceleistung.
Docker- und Serverklassenmaschine
In Java 10 wurden die
-XX:ActiveProcessorCount
und
-XX:+UseContainerSupport
(und auf Java 8
-XX:+UseContainerSupport
), wobei die Standardgrenzen für cgroups berücksichtigt wurden. Jetzt war alles wunderbar. Oder nicht?
Einige Zeit nachdem wir zu Java 10/11 gewechselt waren, bemerkten wir einige Kuriositäten. Aus irgendeinem Grund sahen die GC-Grafiken in einigen Diensten so aus, als hätten sie G1 nicht verwendet:
Dies war, gelinde gesagt, ein wenig unerwartet, da wir sicher wussten, dass G1 der Standardkollektor ist, beginnend mit Java 9. Gleichzeitig gibt es bei einigen Diensten kein solches Problem - G1 wird wie erwartet eingeschaltet.
Wir beginnen eine
interessante Sache zu verstehen und stoßen darauf. Es stellt sich heraus, dass Java, wenn es auf weniger als 3 Prozessoren und mit einem Speicherlimit von weniger als 2 GB ausgeführt wird, sich selbst als Client betrachtet und nichts anderes als SerialGC zulässt.
Dies betrifft übrigens nur die
Auswahl des GC und hat nichts mit den Kompilierungsoptionen -client / -server und JIT zu tun.
Als wir Java 8 verwendeten, wurden die Docker-Beschränkungen offensichtlich nicht berücksichtigt und es wurde angenommen, dass es viele Prozessoren und Speicher hat. Nach dem Upgrade auf Java 10 wurde SerialGC plötzlich von vielen Diensten mit niedrigeren Grenzwerten verwendet. Glücklicherweise wird dies sehr einfach behandelt - indem die
-XX:+AlwaysActAsServerClassMachine
explizit
-XX:+AlwaysActAsServerClassMachine
.
CPU-Limits (ja, wieder) und Speicherfragmentierung
Bei Betrachtung der Diagramme bei der Überwachung haben wir irgendwie festgestellt, dass die Resident-Set-Größe des Containers zu groß ist - dreimal so viel wie die maximale Hüftgröße. Könnte dies bei einem nächsten kniffligen Mechanismus der Fall sein, der entsprechend der Anzahl der Prozessoren im System skaliert und die Einschränkungen des Dockers nicht kennt?
Es stellt sich heraus, dass der Mechanismus überhaupt nicht schwierig ist - es ist das bekannte Malloc von glibc. Kurz gesagt, glibc verwendet die sogenannten Arenen, um Speicher zuzuweisen. Beim Erstellen wird jedem Thread eine der Arenen zugewiesen. Wenn ein Thread, der glibc verwendet, eine bestimmte Menge an Speicher im nativen Heap seinen Anforderungen zuweisen möchte und malloc aufruft, wird der Speicher in der ihm zugewiesenen Arena zugewiesen. Wenn die Arena mehrere Threads bedient, konkurrieren diese Threads darum. Je mehr Arenen, desto weniger Wettbewerb, aber desto mehr Fragmentierung, da jede Arena ihre eigene Liste von Freiflächen hat.
Auf 64-Bit-Systemen ist die Standardanzahl der Arenen auf 8 * die Anzahl der CPUs festgelegt. Dies ist natürlich ein enormer Aufwand für uns, da nicht alle CPUs für den Container verfügbar sind. Darüber hinaus ist für Java-basierte Anwendungen der Wettbewerb um Arenen nicht so relevant, da die meisten Zuweisungen in Java-Heap erfolgen, dessen Speicher beim Start vollständig zugewiesen werden kann.
Diese Funktion von malloc ist seit langem bekannt, ebenso wie seine Lösung - die Umgebungsvariable
MALLOC_ARENA_MAX
zu verwenden, um die Anzahl der Arenen explizit anzugeben. Es ist sehr einfach für jeden Behälter zu tun. Hier ist der Effekt der Angabe von
MALLOC_ARENA_MAX = 4
für unser Haupt-Backend:
Das RSS-Diagramm enthält zwei Instanzen: In einer (blau)
MALLOC_ARENA_MAX
wir
MALLOC_ARENA_MAX
, in der anderen (rot) starten wir gerade neu. Der Unterschied ist offensichtlich.
Danach besteht jedoch ein vernünftiger Wunsch, herauszufinden, wofür Java im Allgemeinen Speicher ausgibt. Ist es möglich, einen Microservice unter Java mit einem Speicherlimit von 300-400 Megabyte auszuführen und keine Angst zu haben, dass er von Java-OOM fällt oder nicht von einem System-OOM-Killer getötet wird?
Wir verarbeiten Java-OOM
Zunächst müssen Sie sich darauf vorbereiten, dass OOMs unvermeidlich sind, und Sie müssen sie richtig handhaben - zumindest Hüftdumps sparen. Seltsamerweise hat auch dieses einfache Unterfangen seine eigenen Nuancen. Beispielsweise werden Hip-Dumps nicht überschrieben. Wenn ein gleichnamiger Hip-Dump bereits gespeichert ist, wird einfach kein neuer erstellt.
Java kann
die Dump-Seriennummer und die Prozess-ID
automatisch zum Dateinamen
hinzufügen , dies hilft uns jedoch nicht weiter. Die Seriennummer ist nicht nützlich, da dies OOM ist und nicht der regelmäßig angeforderte Hip-Dump. Die Anwendung wird danach neu gestartet und der Zähler zurückgesetzt. Und die Prozess-ID ist nicht geeignet, da sie im Docker immer dieselbe ist (meistens 1).
Deshalb sind wir zu dieser Option gekommen:
-XX:+HeapDumpOnOutOfMemoryError
-XX:+ExitOnOutOfMemoryError
-XX:HeapDumpPath=/var/crash/java.hprof
-XX:OnOutOfMemoryError="mv /var/crash/java.hprof /var/crash/heapdump.hprof"
Es ist ganz einfach und mit einigen Verbesserungen können Sie sogar lehren, nicht nur den neuesten Hip-Dump zu speichern, sondern für unsere Bedürfnisse ist dies mehr als genug.
Java OOM ist nicht das einzige, was wir uns stellen müssen. Jeder Container hat eine Begrenzung des von ihm belegten Speichers und kann überschritten werden. In diesem Fall wird der Container vom System-OOM-Killer getötet und neu
restart_policy: always
(wir verwenden
restart_policy: always
). Dies ist natürlich unerwünscht, und wir möchten lernen, wie Sie die von der JVM verwendeten Ressourcen korrekt einschränken.
Speicherverbrauch optimieren
Bevor Sie jedoch Grenzwerte festlegen, müssen Sie sicherstellen, dass die JVM keine Ressourcen verschwendet. Wir haben es bereits geschafft, den Speicherverbrauch zu reduzieren, indem wir die Anzahl der CPUs und die Variable
MALLOC_ARENA_MAX
. Gibt es andere "fast freie" Möglichkeiten, dies zu tun?
Es stellt sich heraus, dass es noch ein paar Tricks gibt, die ein wenig Speicherplatz sparen.
Die erste ist die Verwendung der
-XX:ThreadStackSize
-Xss
(oder
-XX:ThreadStackSize
), mit der die
-Xss
für Threads
-XX:ThreadStackSize
. Der Standardwert für eine 64-Bit-JVM beträgt 1 MB. Wir haben herausgefunden, dass 512 KB für uns ausreichen. Aus diesem Grund wurde eine StackOverflowException noch nie abgefangen, aber ich gebe zu, dass dies nicht für jeden geeignet ist. Und der Gewinn daraus ist sehr gering.
Das zweite ist das
-XX:+UseStringDeduplication
(mit aktiviertem G1 GC). Sie können Speicherplatz sparen, indem Sie doppelte Zeilen aufgrund zusätzlicher Prozessorlast reduzieren. Der Kompromiss zwischen Speicher und CPU hängt nur von der spezifischen Anwendung und den Einstellungen des Deduplizierungsmechanismus selbst ab. Lesen Sie das
Dock und testen Sie in Ihren Diensten, wir haben diese Option hat ihre Anwendung noch nicht gefunden.
Und schließlich ist eine Methode, die nicht für jeden geeignet ist (aber zu uns passt),
Jemalloc anstelle des nativen
Malloc zu verwenden. Diese Implementierung zielt darauf ab, die Speicherfragmentierung und eine bessere Multithreading-Unterstützung im Vergleich zu malloc von glibc zu reduzieren. Für unsere Dienste hat jemalloc mit
MALLOC_ARENA_MAX=4
etwas mehr Speichergewinn
MALLOC_ARENA_MAX=4
als malloc, ohne die Leistung wesentlich zu beeinträchtigen.
Andere Optionen, einschließlich der von Alexei Shipilev in
JVM Anatomy Quark Nr. 12: Native Memory Tracking beschriebenen , schienen ziemlich gefährlich oder führten zu einer spürbaren Verschlechterung der Leistung. Aus pädagogischen Gründen empfehle ich jedoch, diesen Artikel zu lesen.
Fahren Sie in der Zwischenzeit mit dem nächsten Thema fort und versuchen Sie schließlich zu lernen, wie Sie den Speicherverbrauch begrenzen und die richtigen Grenzwerte auswählen.
Begrenzung des Speicherverbrauchs: Heap, Nicht-Heap, direkter Speicher
Um alles richtig zu machen, müssen Sie sich daran erinnern, woraus Speicher in Java im Allgemeinen besteht. Schauen wir uns zunächst die Pools an, deren Status über JMX überwacht werden kann.
Das erste ist natürlich
hip . Es ist ganz einfach: Wir
-Xmx
, aber wie geht das richtig? Leider gibt es hier kein universelles Rezept, alles hängt von der Anwendung und dem Lastprofil ab. Für neue Dienste beginnen wir mit einer relativ vernünftigen Heap-Größe (128 MB) und erhöhen oder verringern sie gegebenenfalls. Um vorhandene zu unterstützen, gibt es eine Überwachung mit Diagrammen des Speicherverbrauchs und der GC-Metriken.
Gleichzeitig mit
-Xmx
wir
-Xms == -Xmx
. Wir haben kein Überverkaufen des Speichers, daher liegt es in unserem Interesse, dass der Service die Ressourcen, die wir ihm zur Verfügung gestellt haben, maximal nutzt. Darüber hinaus enthalten wir in normalen Diensten
-XX:+AlwaysPreTouch
und den Mechanismus für transparente große Seiten:
-XX:+UseTransparentHugePages -XX:+UseLargePagesInMetaspace
. Lesen Sie jedoch vor dem Aktivieren von THP die
Dokumentation sorgfältig durch und testen Sie, wie sich Dienste mit dieser Option lange Zeit verhalten. Überraschungen sind auf Maschinen mit unzureichendem RAM nicht ausgeschlossen (zum Beispiel mussten wir THP auf Prüfständen ausschalten).
Als nächstes kommt
kein Haufen . Nicht-Heap-Speicher umfasst:
- Metaspace und komprimierter Klassenraum,
- Code-Cache.
Betrachten Sie diese Pools der Reihe nach.
Natürlich hat jeder von
Metaspace gehört , ich werde nicht im Detail darüber sprechen. Es speichert Klassenmetadaten, Methodenbytecode usw. Tatsächlich hängt die Verwendung von Metaspace direkt von der Anzahl und Größe der geladenen Klassen ab, und Sie können sie wie hip nur durch Starten der Anwendung und Entfernen der Metriken über JMX bestimmen. Standardmäßig ist Metaspace durch nichts eingeschränkt, aber mit der
-XX:MaxMetaspaceSize
ist dies recht einfach.
Der komprimierte Klassenraum ist Teil von Metaspace und wird angezeigt, wenn die Option
-XX:+UseCompressedClassPointers
aktiviert ist (standardmäßig für Heaps mit weniger als 32 GB aktiviert,
-XX:+UseCompressedClassPointers
wenn dadurch ein echter Speichergewinn erzielt werden kann). Die Größe dieses Pools kann durch die Option
-XX:CompressedClassSpaceSize
begrenzt werden.
-XX:CompressedClassSpaceSize
ist jedoch wenig sinnvoll, da der komprimierte Klassenraum in Metaspace enthalten ist und die Gesamtmenge des gesperrten Speichers für Metaspace und den komprimierten Klassenraum letztendlich auf eine
-XX:MaxMetaspaceSize
.
Übrigens, wenn Sie sich die JMX-Messwerte ansehen, wird die Größe des Nicht-Heap-Speichers immer als die
Summe aus Metaspace, komprimiertem Klassenraum und Code-Cache berechnet. Tatsächlich müssen Sie nur Metaspace und CodeCache zusammenfassen.
Im Nicht-Heap blieb also nur der
Code-Cache übrig - das vom JIT-Compiler kompilierte Code-Repository. Standardmäßig ist die maximale Größe auf 240 MB festgelegt und für kleine Dienste um ein Vielfaches größer als erforderlich. Die Größe des Code-Cache kann mit der Option
-XX:ReservedCodeCacheSize
. Die richtige Größe kann nur ermittelt werden, indem die Anwendung ausgeführt und unter einem typischen Lastprofil verfolgt wird.
Es ist wichtig, hier keinen Fehler zu machen, da unzureichender Code-Cache kalten und alten Code aus dem Cache löscht (die
-XX:+UseCodeCacheFlushing
standardmäßig aktiviert), was wiederum zu einem höheren CPU-Verbrauch und Leistungseinbußen führen kann . Es wäre großartig, wenn Sie OOM auslösen könnten, wenn der Code-Cache überläuft. Dazu gibt es sogar das
-XX:+ExitOnFullCodeCache
, das jedoch leider nur in der
Entwicklungsversion der JVM verfügbar ist.
Der letzte Pool, über den Informationen in JMX vorhanden sind, ist der
direkte Speicher . Standardmäßig ist seine Größe nicht begrenzt, daher ist es wichtig, eine bestimmte Grenze festzulegen - zumindest Bibliotheken wie Netty, die aktiv direkte Bytepuffer verwenden, werden davon geleitet. Es ist nicht schwierig, mit dem
-XX:MaxDirectMemorySize
ein Limit
-XX:MaxDirectMemorySize
, und auch hier hilft uns nur die Überwachung, den richtigen Wert zu ermitteln.
Was bekommen wir also so weit?
Java-Prozessspeicher =
Heap + Metaspace + Code Cache + Direkter Speicher =
-Xmx +
-XX: MaxMetaspaceSize +
-XX: ReservedCodeCacheSize +
-XX: MaxDirectMemorySize
Versuchen wir, alles in das Diagramm zu zeichnen und es mit dem RSS-Docker-Container zu vergleichen.
Die obige Zeile ist das RSS des Containers und eineinhalb Mal höher als der Speicherverbrauch der JVM, den wir über JMX überwachen können.
Weiter graben!
Begrenzung des Speicherverbrauchs: Native Memory Tracking
Zusätzlich zu Heap-, Nicht-Heap- und Direktspeicher verwendet die JVM natürlich eine ganze Reihe anderer Speicherpools. Das Flag
-XX:NativeMemoryTracking=summary
hilft uns dabei
-XX:NativeMemoryTracking=summary
. Durch Aktivieren dieser Option können wir Informationen zu Pools abrufen, die der JVM bekannt sind, aber in JMX nicht verfügbar sind. Weitere Informationen zur Verwendung dieser Option finden Sie in der
Dokumentation .
Beginnen wir mit dem offensichtlichsten - dem Speicher, den die
Fadenstapel einnehmen . NMT produziert für unseren Service etwa Folgendes:
Thread (reserviert = 32166 KB, festgeschrieben = 5358 KB)
(Thread # 52)
(Stapel: reserviert = 31920 KB, festgeschrieben = 5112 KB)
(malloc = 185 KB # 270)
(Arena = 61 KB # 102)
Übrigens kann seine Größe auch ohne Native Memory Tracking gefunden werden, indem jstack verwendet und ein wenig in
/proc/<pid>/smaps
. Andrey Pangin hat hierfür ein
besonderes Dienstprogramm entwickelt .
Die Größe des
gemeinsam genutzten Klassenraums ist noch einfacher zu bewerten:
Gemeinsamer Klassenraum (reserviert = 17084 KB, festgeschrieben = 17084 KB)
(mmap: reserviert = 17084 KB, festgeschrieben = 17084 KB)
Dies ist der Mechanismus
-Xshare
,
-Xshare
-XX:+UseAppCDS
-Xshare
und
-XX:+UseAppCDS
. In Java 11 ist die Option
-Xshare
standardmäßig auf auto eingestellt. Wenn Sie also das
$JAVA_HOME/lib/server/classes.jsa
(es befindet sich im offiziellen OpenJDK-Docker-Image), wird die Speicherzuordnung geladen. Ohm beim Start der JVM, wodurch die Startzeit beschleunigt wird. Dementsprechend ist die Größe des gemeinsam genutzten Klassenraums leicht zu bestimmen, wenn Sie die Größe von jsa-Archiven kennen.
Im Folgenden sind die nativen
Garbage Collector- Strukturen aufgeführt:
GC (reserviert = 42137 KB, festgeschrieben = 41801 KB)
(malloc = 5705 KB # 9460)
(mmap: reserviert = 36432 KB, festgeschrieben = 36096 KB)
Alexey Shipilev im bereits erwähnten Handbuch zu Native Memory Tracking
sagt, dass sie ungefähr 4-5% der Größe des Heaps einnehmen, aber in unserem Setup für kleine Heaps (bis zu mehreren hundert Megabyte) erreichte der Overhead 50% der Größe des Heaps.
Symboltabellen können viel Platz
einnehmen :
Symbol (reserviert = 16421 KB, festgeschrieben = 16421 KB)
(malloc = 15261 KB # 203089)
(Arena = 1159 KB # 1)
Sie speichern die Namen von Methoden, Signaturen sowie Links zu internierten Zeichenfolgen. Leider scheint es möglich zu sein, die Größe der Symboltabelle nur post factum mithilfe von Native Memory Tracking zu schätzen.
Was bleibt übrig? Laut Native Memory Tracking gibt es viele Dinge:
Compiler (reserviert = 509 KB, festgeschrieben = 509 KB)
Intern (reserviert = 1647 KB, festgeschrieben = 1647 KB)
Andere (reserviert = 2110 KB, festgeschrieben = 2110 KB)
Arena Chunk (reserviert = 1712 KB, festgeschrieben = 1712 KB)
Protokollierung (reserviert = 6 KB, festgeschrieben = 6 KB)
Argumente (reserviert = 19 KB, festgeschrieben = 19 KB)
Modul (reserviert = 227 KB, festgeschrieben = 227 KB)
Unbekannt (reserviert = 32 KB, festgeschrieben = 32 KB)
Aber das alles nimmt ziemlich viel Platz ein.
Leider können viele der genannten Speicherbereiche weder eingeschränkt noch gesteuert werden, und wenn dies der Fall sein könnte, würde die Konfiguration zur Hölle werden. Selbst die Überwachung ihres Status ist keine triviale Aufgabe, da die Einbeziehung von Native Memory Tracking die Leistung der Anwendung geringfügig beeinträchtigt und es keine gute Idee ist, sie für die Produktion in einem kritischen Dienst zu aktivieren.
Lassen Sie uns dennoch aus Gründen des Interesses versuchen, alles, was Native Memory Tracking meldet, in der Grafik zu reflektieren:
Nicht schlecht! Der verbleibende Unterschied ist ein Overhead für die Fragmentierung / Zuweisung von Speicher (er ist sehr gering, da wir jemalloc verwenden) oder der von nativen Bibliotheken zugewiesene Speicher. Wir verwenden nur eine davon zur effizienten Speicherung des Präfixbaums.
Für unsere Anforderungen reicht es also aus, das einzuschränken, was wir können: Heap, Metaspace, Code-Cache, direkter Speicher. Für alles andere hinterlassen wir einige vernünftige Grundlagen, die sich aus den Ergebnissen praktischer Messungen ergeben.
Nachdem wir uns mit der CPU und dem Speicher befasst haben, gehen wir zur nächsten Ressource über, um die Anwendungen konkurrieren können - zu den Festplatten.
Java und Laufwerke
Und bei ihnen ist alles sehr schlecht: Sie sind langsam und können zu einer spürbaren Mattheit der Anwendung führen. Daher lösen wir Java so weit wie möglich von Festplatten:
- Wir schreiben alle Anwendungsprotokolle über UDP in das lokale Syslog. Dies lässt eine gewisse Chance, dass die erforderlichen Protokolle irgendwo auf dem Weg verloren gehen, aber wie die Praxis gezeigt hat, sind solche Fälle sehr selten.
- Wir werden JVM-Protokolle in tmpfs schreiben. Dazu müssen wir nur den Docker mit dem
/dev/shm
an der gewünschten Stelle /dev/shm
.
Wenn wir Protokolle in syslog oder in tmpfs schreiben und die Anwendung selbst nur Hip-Dumps auf die Festplatte schreibt, stellt sich heraus, dass der Verlauf der Festplatten als geschlossen angesehen werden kann.
Natürlich nicht.
Wir achten auf die grafische Darstellung der Dauer von Stop-the-World-Pausen und sehen ein trauriges Bild - Stop-The-World-Pausen auf Hosts betragen Hunderte von Millisekunden, und auf einem Host können sie bis zu einer Sekunde erreichen:
Unnötig zu erwähnen, dass sich dies negativ auf die Anwendung auswirkt? Hier ist zum Beispiel ein Diagramm, das die Antwortzeit des Dienstes nach Kunden widerspiegelt:
Dies ist ein sehr einfacher Dienst, der größtenteils zwischengespeicherte Antworten gibt. Woher stammen also solche unerschwinglichen Zeitpunkte, beginnend mit dem 95. Perzentil? Andere Dienste haben ein ähnliches Bild. Darüber hinaus regnen Zeitüberschreitungen mit beneidenswerter Konstanz, wenn eine Verbindung vom Verbindungspool zur Datenbank hergestellt, Anforderungen ausgeführt werden usw.
Was hat das Laufwerk damit zu tun? - Du fragst. Es hat sehr viel damit zu tun.
Eine detaillierte Analyse des Problems ergab, dass lange STW-Pausen entstehen, weil die Threads lange Zeit zum Sicherheitspunkt gehen. Nach dem Lesen des JVM-Codes haben wir festgestellt, dass die JVM während der Synchronisierung von Threads auf dem Sicherheitspunkt die Datei
/tmp/hsperfdata*
über die Speicherzuordnung schreiben kann, in die sie einige Statistiken exportiert. Dienstprogramme wie
jstat
und
jps
verwenden
jstat
jps
.
Deaktivieren Sie es auf demselben Computer mit der Option
-XX:+PerfDisableSharedMem
und ...
Jetty Treadpool-Metriken stabilisieren sich:
(, ):
, , , .
?
Java- , , , .
Nuts and Bolts , . , . , , JMX.
, . .
statsd JVM, (heap, non-heap ):
, , .
— , , , , ? . () -, , RPS .
: , . . ammo-
. . . :
.
, . , , - , , .
Abschließend
, Java Docker — , . .