Connected cubes

Direct Memory und OOMKilled-Container-Fehler 

Share with your network!

Engineering Insights ist eine fortlaufende Blog-Serie, die einen Blick hinter die technischen Herausforderungen sowie auf die Erkenntnisse und Fortschritte wirft, die unseren Kunden im Alltag helfen, ihre Mitarbeiter und Daten zu schützen. Die Beiträge werden von unseren technischen Experten geschrieben und erläutern den Prozess, der zu einer Proofpoint-Innovation geführt hat.  

HINWEIS: Dieser Blog-Beitrag beschreibt eine Lösung, die der Autor aus technischer Sicht interessant findet. Die im Beitrag beschriebenen Unit Tests beziehen sich jedoch nicht auf ein Proofpoint-Produkt und haben keine Auswirkungen auf Kunden. 

Vor Kurzem wir in zwei unserer Mikroservices CI-Build-Fehler (Continuous Integration) fest, die von Java Unit Tests verursacht wurden. 

Obwohl die Java-Heap-Größenbegrenzung für die Docker-Pod-Ressourcen passend war, generierte jeder Test einen Kubernetes OOMKilled-Fehler. Es gab keine Fehler in den Protokollen und es wurde auch keine Core- oder Heap-Dump-Datei generiert. 

Die OOMKilled-Fehlermeldungen weisen auf die Netty C-Bibliothek hin. In beiden Fällen wurden die Fehler durch Google Cloud Platform (GCP)-APIs und OpenTelemetry verursacht, die beide gRPC-Aufrufe mit Netty nutzen. gRPC ist ein modernes hochleistungsfähiges Open-Source-RPC-Framework (Remote Procedure Call). Während der Tests schlugen die gRPC-Aufrufe fehl, da ihre Ausführung nicht erwartet wurde und sie daher nicht ordnungsgemäß konfiguriert waren. Das Deaktivieren der gRPC-Verbindungen während der Tests löste dieses Problem. 

Obwohl die gRPC-Verbindungen während der Tests gar nicht aktiviert sein sollten, war nicht klar, warum diese Situation zu OOMKilled-Fehlern führte. Die angegebene Java-Heap-Größenbegrenzung war für den Docker-Container angemessen. 

Dann stieß ich in diesem Entwickler-Blog auf das folgende Detail: „Netty nutzt ByteBuffer und Direct Memory zum Zuweisen und Freigeben des Speichers.“ Eine unerwartete Zunahme der Off-Heap-Speichernutzung konnte also die Ursache der OOMKilled-Fehler sein. 

Das Problem: eine Zunahme der Off-Heap-Speichernutzung 

Sehen wir uns ein abstraktes Beispiel dafür an, wie es dazu kommen konnte. Dabei untersuchen wir die Java-Speichernutzung unserer Anwendung vor und nach den gRPC-Aufrufen. 

Java-Heap-Größe 

  -Xms128m (ursprüngliche Heap-Größe) 

  -Xmx256m (maximale Heap-Größe) 

Kubernetes Pod-Ressourcenanfragen und -Begrenzungen 

  resources: 

    requests: 

      memory: 200M 

    limits: 

      memory: 360M 

Abb. 1: Java-Speicherzuweisung

Abb. 1: Java-Speicherzuweisung vor den gRPC-Aufrufen – insgesamt 200M. 

 

Abb. 2: Java-Speicherzuweisung

Abb. 2: Java-Speicherzuweisung nach den gRPC-Aufrufen – insgesamt 360M. 

Wie im Beispiel gezeigt, bleibt die On-Heap-Speichernutzung gleich groß, während die Off-Heap-Speichernutzung nach den gRPC-Aufrufen erheblich steigt. Wichtig ist: Die maximale Java-Heap-Größe wurde nicht überschritten, doch die Off-Heap-Speichernutzung überstieg die Pod-Speicherbegrenzung, da der Off-Heap-Speicher nicht vom Garbage Collector (GC) der virtuellen Java-Maschine (JVM) verwaltet wird. Demzufolge kann der Off-Heap-Speicher die Begrenzung der Pod-Ressourcen überschreiten und den Kubernetes OOMKilled-Fehler auslösen. 

Die Lösung: Zusätzliche Speicherbegrenzungen konfigurieren 

gRPC wird häufig in Google Cloud-Client-Bibliotheken, für OpenTelemetry und in anderen Bereichen verwendet. Wenn eine Anwendung gRPC oder ein ähnliches Framework mit signifikantem nativem Speicher nutzt, muss die Off-Heap-Speichernutzung und ihre Auswirkung auf die Pod-Speicherbegrenzung bekannt sein. Da das io.grpc-Paket auf Netty setzt, kann etwas Ähnliches in einem Mikroservice vorkommen. Zur Vermeidung von Fehlern sind einige Anpassungen erforderlich. 

Üblicherweise wird die Java-Heap-Größe so konfiguriert, dass sie etwas kleiner als die Pod-Speicherbegrenzung ist. Dafür können Sie die JVM-Optionen -Xms und -Xmx oder die JVM-Option MaxRAMPercentage nutzen. In Situationen wie dem Beispiel oben kann der Off-Heap-Direktspeicher (Direct Memory) die Pod-Speicherbegrenzung überschreiten, was zum OOMKilled-Fehler führt. 

HINWEIS: Der GC benötigt für die Speicherbereinigung eine gewisse Menge zusätzlichen Speichers, im schlimmsten Fall das Doppelte der maximalen Heap-Größe, meist jedoch deutlich weniger. Aufgrund dieser zusätzlichen Speicheranforderungen kann die Resident Set Size (RSS) während der Bereinigung kurzfristig steigen, was in Umgebungen mit Speicherbegrenzungen (wie Containern) zu Problemen führen kann. 

Ein kontrollierterer Ansatz besteht darin, das Direct Memory Limit ebenfalls festzulegen und den Pod-Speicher als Summe der maximalen Heap- und Off-Heap-Werte (Direct Memory) zu konfigurieren. Außerdem sollten Sie weitere Speichertypen wie Metaspace, Klassen usw. berücksichtigen. Zum Kontrollieren der nativen Speichernutzung können Sie die JVM-Option MaxDirectMemorySize festlegen. 

HINWEIS: Native Image kann ebenfalls Speicher separat vom Java-Heap zuweisen. Ein typischer Anwendungsfall ist ein java.nio.DirectByteBuffer, der direkt den nativen Speicherwert -XX:MaxDirectMemorySize referenziert. Dieser Wert ist die maximale Größe direkter Pufferzuweisungen (Direct Buffer Allocations). 

Zusammen mit einer entsprechenden Pod-Ressourcenkonfiguration ermöglichen diese zusätzlichen Begrenzungen einen kontrollierteren Ansatz. Das folgende Beispiel veranschaulicht das. 

Dockerfile  

FROM openjdk... 

... 

ENTRYPOINT ["java", "-Xms512m", "-Xmx2g", "-XX:MaxDirectMemorySize=700m", "-jar", "/app/my-grpc-app.jar"] 

Kubernetes Pod-Ressourcenanfragen und -Begrenzungen 

  resources: 

    requests: 

      memory: "2Gi" # minimum memory request 

    limits: 

      memory: "3Gi" # maximum memory limit< 

 
Werden Sie Teil unseres Teams 

Unsere Mitarbeiter mit ihren vielfältigen Erfahrungen und Hintergründen sind für den Erfolg unseres Unternehmens maßgeblich. Wir setzen uns mit großem Engagement dafür ein, Mitarbeiter, Daten und Marken vor den hochentwickelten Bedrohungen und Compliance-Risiken von heute zu schützen.   

Wir suchen die besten Mitarbeiter der Branche für folgende Aufgabenbereiche:  

  • Entwicklung und Verbesserung unserer bewährten Sicherheitsplattform  
  • Kombination von Innovation und Schnelligkeit in einer sich ständig weiterentwickelnden Cloud-Architektur  
  • Analyse neuer Bedrohungen und Bereitstellung umfassender Einblicke durch datenbasierte Bedrohungsinformationen  
  • Zusammenarbeit mit unseren Kunden, um die größten Herausforderungen im Bereich Cybersicherheit zu bewältigen  

Weitere Informationen über die Karrieremöglichkeiten bei Proofpoint finden Sie auf der Jobs und Karriere-Seite

Über den Autor 

Figure 1

Liran Mendelovich ist Senior Software Developer. Er interessiert sich für alle Aspekte der Entwicklung – Konzeption und Implementierung, Problemlösung, Leistungsoptimierung, Concurrency, verteilte Systeme und Mikroservices – sowie Open-Source-Bibliotheken.