Lastgenerator in der getesteten JVM verschleiert GC-Tail-Latenz
Ein Benchmark, dessen Lastgenerator in derselben JVM läuft wie das gemessene System, meldet Tail-Latenzen, die zwei- bis dreimal niedriger ausfallen als bei derselben Messung aus einer getrennten JVM. Das zeigt eine Untersuchung von Jonas Norlinder, veröffentlicht am 25. September und auf inside.java vorgestellt. Verwendet wurden SPECjbb2015 v1.04 und OpenJDK 27; Norlinder betont, dass es sich um eine Forschungsauswertung der p99-Antwortzeiten handelt, nicht um eine regelkonforme Benchmark-Einreichung.
Der Mechanismus heißt Coordinated Omission. Ein Lastgenerator, der vor jeder neuen Anfrage auf die vorige Antwort wartet, hört auf, Last zu erzeugen, sobald der Server hängt. SPECjbb2015 korrigiert das auf die übliche Weise: Gemessen wird ab dem geplanten Sendezeitpunkt, nicht ab dem tatsächlichen. Diese Korrektur setzt aber voraus, dass der Generator überhaupt noch Anfragen planen kann. Lebt er in derselben JVM und hält eine Garbage-Collection-Pause den ganzen Prozess an, kann er das nicht - und die Anfragen, die echte Nutzer in dieser Zeit gestellt hätten, entstehen gar nicht erst.
Norlinder verglich den Modus Composite-Net, alles in einer JVM, mit dem Modus Distributed auf derselben Maschine. Die verteilte Variante bekam mehr Speicher, die CPU-Kerne wurden aufgeteilt, damit keine Konfiguration benachteiligt war. Die Injektionsrate lag bei 6.000 Anfragen pro Sekunde, mit zehn getrennten JVM-Läufen je Konfiguration. Bei Kollektoren mit spürbaren Pausen betrug der Abstand etwa das Zwei- bis Dreifache; bei ZGC mit Pausen unter einer Millisekunde gab es keinen. Die absoluten Werte sind deutlich: 34 ms in einer JVM gegen 100 ms getrennt gemessen in einem Fall, mit Serial GC und 6 GB Heap 278 gegen 1.750 ms. Der Abgleich der Zeitstempel mit den GC-Logs zeigte, dass im Ein-JVM-Modus während einer Pause keine Anfragen geplant wurden.

Was das bedeutet
Die Lehre reicht weit über diesen Benchmark hinaus. Jeder Latenztest, bei dem das Getestete den Lastgenerator anhalten kann - ein Harness im selben Prozess, ein Lastwerkzeug im selben CPU-Kontingent eines Containers, ein Testclient auf einem überlasteten Knoten -, meldet genau die Stillstände zu niedrig, die er aufdecken soll. Das Ergebnis wirkt präzise und ist systematisch zu optimistisch.
Daraus folgen zwei Regeln. Den Lastgenerator in einen eigenen Prozess legen, möglichst auf eigene Kerne, wann immer es um Tail-Latenz geht; SPECjbb2015 bietet dafür die Modi MultiJVM und Distributed, und der Autor empfiehlt für Latenzfragen nur diese. Und jedem Vergleich von Garbage Collectors misstrauen, der im selben Prozess gemessen wurde: Der Kollektor mit den längsten Pausen profitiert am meisten davon, dass auch der Generator pausiert.