Connected cubes

Erreurs OOMKilled dans la mémoire directe et dans un conteneur 

Share with your network!

« Perspectives d'ingénierie » est une série d'articles de blog offrant un aperçu des coulisses des défis techniques, des enseignements et des avancées qui aident nos clients à protéger leurs collaborateurs et leurs données au quotidien. Dans les articles qu'ils rédigent, nos ingénieurs expliquent le processus qui a conduit à une innovation Proofpoint.  
 

REMARQUE : Cet article porte sur une solution que l'auteur estime intéressante d'un point de vue technique. Toutefois, les tests unitaires décrits dans l'article n'ont aucun rapport avec les solutions Proofpoint ni aucune incidence pour les clients. Il y a peu, deux de nos microservices ont connu des échecs de compilation CI (Continuous Integration, intégration continue) provoqués par des tests unitaires Java. 

Même si la limite de taille de l'espace mémoire alloué à Java, ou tas, était adaptée aux ressources du pod Docker, chaque test générait une erreur OOMKilled de Kubernetes. Aucune erreur n'apparaissait dans les journaux et aucun fichier core dump (cliché) ou heap dump (vidage de tas) n'était généré. 

Les messages d'erreur OOMKilled mentionnaient la bibliothèque C Netty. Dans les deux cas, le problème venait des API de Google Cloud Platform (GCP) et d'OpenTelemetry, qui utilisent tous deux des appels gRPC reposant sur Netty. gRPC est un framework d'appels de procédure distante (RPC) récent, open source et très performant. Au cours des tests, les appels gRPC échouaient car ils n'étaient pas censés s'exécuter et n'étaient donc pas correctement configurés. La désactivation des connexions gRPC au cours des tests a permis de résoudre le problème. 

Même si les connexions gRPC n'auraient pas dû être activées au cours des tests, rien ne permettait d'expliquer pourquoi cette situation entraînait des erreurs OOMKilled. La limite de taille du tas Java spécifiée était correcte par rapport au conteneur Docker. 

C'est alors que j'ai découvert l'information suivante dans le blog de ce développeur : « Netty utilise ByteBuffers et la mémoire directe pour allouer et libérer de la mémoire ». J'ai ainsi réalisé qu'une augmentation inattendue de l'utilisation de la mémoire hors tas (off-heap) pouvait être la cause des erreurs OOMKilled. 

Le problème : une augmentation de l'utilisation de la mémoire hors tas 

Prenons un exemple abstrait illustrant ce type de problème. Dans cet exemple, nous examinerons l'utilisation de la mémoire Java par notre application avant et après les appels gRPC. 

Taille du tas (heap) Java 

  -Xms128m (taille initiale du tas) 

  -Xmx256m (taille max. du tas) 

 Limites et demandes de ressources du pod Kubernetes 

  resources: 

    requests: 

      memory: 200M 

    limits: 

      memory: 360M 

 Allocation de mémoire Java avant les appels gRPC – 200 M au total

Figure 1. Allocation de mémoire Java avant les appels gRPC – 200 M au total 

Figure 2. Allocation de mémoire Java après les appels gRPC – 360 M au total

Figure 2. Allocation de mémoire Java après les appels gRPC – 360 M au total 

Comme l'illustre cet exemple, l'utilisation de la mémoire dans le tas reste similaire en termes de taille, tandis que celle de la mémoire hors tas augmente considérablement après les appels gRPC. Ce qu'il faut surtout noter, c'est que la taille maximale du tas Java n'est pas dépassée, mais que l'utilisation de la mémoire hors tas dépasse la limite de mémoire du pod. C'est dû au fait que la mémoire hors tas n'est pas gérée par le Garbage Collector (nettoyeur de mémoire) de la machine virtuelle Java (JVM). Par conséquent, la mémoire hors tas peut augmenter au-delà des limites des ressources du pod, ce qui entraîne une erreur OOMKilled de Kubernetes. 

La solution : configurer des limites de mémoire supplémentaires 

gRPC est souvent utilisé dans les bibliothèques clientes Google Cloud, OpenTelemetry, etc. Lorsqu'une application utilise gRPC ou un framework similaire qui utilise beaucoup de mémoire native, il est important de connaître l'utilisation de la mémoire hors tas et de savoir comment elle peut affecter la limite de mémoire du pod. Comme le package io.grpc utilise Netty, cette situation peut se produire également dans un microservice. Pour éviter toute erreur, certaines adaptations sont nécessaires. 

Souvent, la taille du tas Java est configurée sur une valeur légèrement inférieure à la limite de mémoire du pod. Vous pouvez la configurer à l'aide des options -Xms et -Xmx de la JVM, ou de l'option MaxRAMPercentage de la JVM. Toutefois, dans des situations semblables à l'exemple précédent, la mémoire directe hors tas peut dépasser la limite de mémoire du pod. Cela génère une erreur OOMKilled. 

REMARQUE : Le Garbage Collector a besoin de mémoire supplémentaire lorsqu'il procède au nettoyage de la mémoire. Dans le pire cas de figure, cela correspond à deux fois la taille maximale du tas, mais c'est généralement bien inférieur. En raison de la mémoire supplémentaire nécessaire, il est possible que la taille du jeu résident (RSS, Resident Set Size) augmente temporairement durant le nettoyage. Cela peut poser problème dans tout environnement présentant des contraintes de mémoire, par exemple un conteneur. 

Une approche mieux contrôlée consiste à configurer également la limite de mémoire directe et à définir la limite de mémoire du pod comme étant la somme des valeurs du tas maximal et de la mémoire hors tas (mémoire directe). Vous devez également prendre en compte d'autres types de mémoire, comme le méta-espace (Metaspace), les classes, etc. Pour contrôler l'utilisation de la mémoire native, vous pouvez configurer l'option MaxDirectMemorySize de la JVM. 

REMARQUE : Native Image peut également allouer de la mémoire distincte du tas Java. Un cas d'utilisation assez répandu est un java.nio.DirectByteBuffer qui référence directement la valeur -XX:MaxDirectMemorySize de la mémoire native. Cette valeur représente la taille maximale des allocations de mémoire tampon directe. 

Ces limites supplémentaires, combinées à une configuration correspondante des ressources du pod, vous offrent une approche plus contrôlée. Vous trouverez un exemple complet ci-dessous. 

Fichier Docker  

FROM openjdk... 

... 

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

 

Limites et demandes de ressources du pod Kubernetes 

  resources: 

    requests: 

      memory: "2Gi" # minimum memory request 

    limits: 

      memory: "3Gi" # maximum memory limit 

 
Rejoignez l'équipe Proofpoint 

Nos collaborateurs, et la diversité de leurs expériences et parcours, sont l'élément moteur de notre réussite. Nous nous sommes donné pour mission de protéger les personnes, les données et les marques contre les menaces avancées actuelles et les risques de non-conformité.   

Nous recrutons les meilleurs talents pour :   

  • Développer et améliorer notre plate-forme de sécurité éprouvée   
  • Allier innovation et vitesse au sein d'une architecture cloud en constante évolution   
  • Analyser les nouvelles menaces et fournir des informations détaillées grâce à une threat intelligence axée sur les données   
  • Collaborer avec nos clients pour résoudre leurs défis de cybersécurité les plus complexes   

Si vous souhaitez en savoir plus sur les possibilités de carrière chez Proofpoint, consultez notre page dédiée

À propos de l'auteur 

Figure 1

Liran Mendelovich est Senior Software Developer. Il s'intéresse à tous les aspects du développement — conception et implémentation, résolution des problèmes, optimisation des performances, programmation concurrente, systèmes distribués et microservices —, ainsi qu'aux bibliothèques open source.