Java and Docker


Dockerfile mit JAVA_OPTS

Es ist sehr praktisch die JVM-Parameter nicht fix in der Dockerfile zu setzen, sondern dazu eine Umgebungsvariable zu nutzen.

FROM openjdk:11-jre-slim

EXPOSE 8080

RUN mkdir -p /run/
COPY build/libs/java-service-*.jar /run/java-service.jar

ENV JAVA_OPTS="-XX:MaxRAMPercentage=50 -XshowSettings:vm -XX:+UseSerialGC -XX:MinHeapFreeRatio=20 -XX:MaxHeapFreeRatio=40"

ENTRYPOINT java $JAVA_OPTS -jar /run/java-service.jar

Falls ihr dann zum Test oder für ein Experiment diese Einstellungen ändern möchtet, könnt ihr das ohne ein neues Image zu erstellen innerhalb Openshift als Umgebungsvariable überlagern. JVM-Settings könnten somit in ConfigMaps ausgelagert werden.

Der Parameter -XshowSettings:vm empfiehlt sich weil er zu Beginn der Startphase ausgibt wie hoch MaxHeap für die JVM ist. Somit könnt ihr in den Logs eurer Service immer kontrollieren wieviel Ram sich Java nimmt.

VM settings:
    Max. Heap Size (Estimated): 29.97G
    Using VM: OpenJDK 64-Bit Server VM

Java Memory Limits

Damit die Container in der OTC die gesetzten Limits korrekt respektieren sind je nach Java-Version bestimmte Einstellungen vorzunehmen.

Java 11

Grundlegende Empfehlung ist es ab sofort auf Java 11 zu wechseln. Es gibt keinen weiteren Support für ältere Java Versionen.

Für Java 11 reicht dann die Prozentangabe für MaxRam

-XX:MaxRAMPercentage=50

Java 10

Java 10 respektiert normalerweise die cgroups der Speichereinstellungen bereits. Damit der Speicher besser ausgenutzt werden kann, verwendet bitte folgende Parameter für den java-Befehl in eurer Dockerfile.

-XX:MaxRAMPercentage=50

Damit ist sichergestellt, dass der Heap für die JVM die Hälfte des verfügbaren Speichers verwendet statt im Default 14.

Java 8

Für Java 8 muss neben der Einstellung für Java 10 erst einaml dafür gesorgt werden, dass Java die Docker Ram Limits respektiert. Daneben verwendet Java 8 noch einen mittlerweile veralteten Parameter für die Ram-Nutzung. Dazu sind folgende Parameter für den Java-Befehl zu verwenden.

-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap -XX:MaxRAMFraction=2

Garbage-Collector Einstellungen

Es wird dringend empfohlen serielles, non-multithreaded, Garbage Collecting zu nutzen sofern euer Java-Service in Openhshift/Kubernetes nur 1 CPU oder weniger zugewiesen bekommt. Der dazugehörige Parameter ist -XX:+UseSerialGC.

Weiterhin ist empfohlen die JVM-Settings zur Rückgabe von Memory-Ressourcen an das Betriebssystem aggressiver zu konfigurieren. Die Settings dazu sind -XX:MinHeapFreeRatio=20 -XX:MaxHeapFreeRatio=40.

Kubernetes Resource Requests und Limits

Wundert ihr euch, dass mit Java 10 eure Services 2GB Ram Limit benötigen um überhaupt zu starten? Dann mag das an den obigen Problemen liegen. Sind die Settings richtig gesetzt, ist es kein Problem einen Java 10 Service mit Limit=512MB zu starten.

Es kommt trotzdem zu OOMKilled-Container restarts

Ja, unter bestimmten Umständen kann es trotz korrekter Settings zu Container-Restarts kommen. Und das ist auch wahrscheinlich richtig so.

Beispiel

Ihr habt einen Java-Service mit folgender Kubernetes-Definition und einer MaxRAMPercentage von 50%

resources:
  requests:
    memory: 256Mi
  limits:
    memory: 1Gi

Möglicherweise kann der Pod gestartet werden. Glück gehabt, denn nur 256MB Ram sind dem Pod zugesichert und scheinbar war auf der Node noch weiterer Ram verfügbar.

Aber was wenn der Pod nicht gestartet werden kann? Der MaxHeap berechnet sich für die JVM immer anhand des Limits, in diesem Fall also 512MB. Wenn der Pod jetzt auf einem Knoten gestartet wird, auf dem gerade nur noch etwas mehr als 256MB Ram frei sind, kann es je nach Speicherbedarf des Services zu einem Kill des Containers kommen.

Um auf Nummer sicher zu gehen, könnt ihr natürlich Memory Request und Limit gleich setzen (Kubernetes QoS class - Guaranteed). Ist das nicht notwendig, sollte Memory-Request nicht kleiner sein als MaxRAMPercentage von Memory-Limit um unnötige Restarts zu vermeiden.

Quellen