Connected cubes

Errores OOMKilled en la memoria directa y en un contenedor 

Share with your network!

"Engineering Insights" es una serie de artículos de blog que ofrece una perspectiva interna sobre los retos técnicos, las lecciones aprendidas y los avances que permiten a nuestros clientes proteger a las personas y defender los datos a diario. En los artículos que escriben, nuestros ingenieros explican el proceso que condujo a una innovación de Proofpoint.  

NOTA: Este artículo analiza una solución que el autor considera interesante desde el punto de vista técnico. Sin embargo, las pruebas unitarias descritas en el artículo no tienen nada que ver con ninguna solución de Proofpoint ni repercusión alguna en los clientes. 

Recientemente, dos de nuestros microservicios experimentaron fallos de compilación CI (Continuous Integration, integración continua) causados por pruebas unitarias Java. 

Aunque el límite de tamaño de la memoria heap de Java se adaptó a los recursos del pod de Docker, cada prueba generaba un error OOMKilled de Kubernetes. No aparecieron errores en los registros y no se generaron archivos core dump ni heap dump

Los mensajes de error OOMKilled mencionaban la biblioteca C Netty. En ambos casos, el problema estaba en las API de GPC (Google Cloud Platform) y OpenTelemetry, que utilizan gRPC basado en Netty. gRPC es un marco reciente de llamadas a procedimientos remotos (RPC), de código abierto y alto rendimiento. Durante las pruebas, las llamadas gRPC fallaron porque no debían ejecutarse y, por tanto, no estaban correctamente configuradas. El problema se solucionó desactivando las conexiones gRPC durante las pruebas. 

Aunque las conexiones gRPC no deberían haberse activado durante las pruebas, no se explicaba por qué esto provocaba errores OOMKilled. El límite de tamaño del segmento de memoria Java especificado era correcto en relación con el contenedor Docker. 

Entonces, en el blog de este desarrollador, descubrí la siguiente información: "Netty utiliza ByteBuffers y memoria directa para asignar y liberar memoria". Me di cuenta de que un aumento inesperado en el uso de memoria fuera del montón (off-heap) podría ser la causa de los errores OOMKilled. 

El problema: mayor uso de memoria fuera del montón (off-heap) 

Tomemos un ejemplo abstracto para ilustrar este tipo de problema. En este ejemplo, veremos cómo nuestra aplicación utiliza la memoria Java antes y después de las llamadas gRPC. 

Tamaño del montón (heap) de Java 

  -Xms128m (tamaño inicial del montón) 

  -Xmx256m (tamaño máximo del heap) 

Límites y requisitos de los recursos del pod Kubernetes 

  resources: 

    requests: 

      memory: 200 M 

    limits: 

      memory: 360 M  

Figura 1: Asignación de memoria Java

 

Figura 1: Asignación de memoria Java antes de las llamadas gRPC - 200 M en total. 

Figura 2: Asignación de memoria Java

Figura 2: Asignación de memoria Java después de las llamadas gRPC - 360 M en total. 

Como ilustra este ejemplo, el uso de memoria en el montón (on-heap) sigue siendo similar en tamaño, pero el uso de memoria fuera del montón aumenta significativamente después de las llamadas gRPC. Lo más importante a tener en cuenta es que no se excede el tamaño de la memoria heap de Java, sino que el uso de memoria off-heap excede el límite de memoria del pod. Esto se debe a que la memoria off-heap no es gestionada por el Garbage Collector (limpiador de memoria) de la máquina virtual Java (JVM). Como resultado, la memoria fuera de segmento puede aumentar más allá de los límites de los recursos del pod, dando lugar a un error OOMKilled de Kubernetes. 

La solución: configurar límites de memoria adicionales 

gRPC se utiliza a menudo en las bibliotecas de clientes de Google Cloud, OpenTelemetry, etc. Cuando una aplicación utiliza gRPC o un marco similar que utiliza mucha memoria nativa, es importante saber cuánta memoria off-heap se utiliza y cómo puede afectar al límite de memoria del pod. Como el paquete io.grpc utiliza Netty, esta situación también puede darse en un microservicio. Para evitar errores, es necesario realizar ciertas adaptaciones. 

A menudo, el tamaño de memoria heap de Java se fija en un valor ligeramente inferior al límite de memoria del pod. Puede configurarlo utilizando las opciones -Xms y -Xmx en la JVM, o la opción MaxRAMPercentage en la JVM. Sin embargo, en situaciones similares al ejemplo anterior, la memoria directa off-heap puede exceder el límite de memoria del pod. Esto genera un error OOMKilled. 

NOTA: el Garbage Collector necesita memoria adicional cuando limpia la memoria. En el peor de los casos, corresponde al doble del tamaño máximo del segmento de memoria, pero suele ser mucho menor. Debido a que se requiere memoria adicional, el tamaño del conjunto residente (Resident Set Size, RSS) puede aumentar temporalmente durante la limpieza. Esto puede ser un problema en cualquier entorno con limitaciones de memoria, como un contenedor. 

Una forma más controlada de gestionarlo es definir explícitamente el límite de memoria directa y ajustar el límite de memoria del pod para que sea igual a la suma del tamaño máximo de la memoria heap y de la memoria off-heap (memoria directa) También hay que tener en cuenta otros tipos de memoria, como Metaspace, las clases, etc. Para controlar el uso de la memoria nativa, puede configurar la opción MaxDirectMemorySize en la JVM. 

NOTA: Native Image también puede asignar memoria separada de la memoria heap de Java. Un caso de uso bastante común es una aplicación java.nio.DirectByteBuffer, que hace referencia directa al valor -XX:MaxDirectMemorySize de la memoria nativa. Este valor representa el tamaño máximo de las asignaciones directas de búfer. 

Estos límites adicionales, combinados con la correspondiente configuración de los recursos del pod, le ofrecen un enfoque más controlado. A continuación encontrará un ejemplo completo. 

Archivo Docker  

FROM openjdk... 

... 

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

Límites y requisitos de los recursos del pod Kubernetes 

  resources: 

    requests: 

      memory: "2Gi" # minimum memory request 

    limits: 

      memory: "3Gi" #  

maximum memory limit 


Únase al equipo 

En Proofpoint, consideramos que nuestro personal, con su amplia variedad de experiencias y condicionantes vitales, son la fuerza motriz de nuestro éxito. Nos dedicamos en cuerpo y alma a proteger a las personas, los datos y las marcas contra las amenazas avanzadas actuales y los riesgos de incumplimiento normativo.   

Contamos con los mejores profesionales del sector para:   

  • Crear y ampliar nuestra plataforma de seguridad demostrada.   
  • Combinar innovación y velocidad en una arquitectura cloud que evoluciona constantemente.   
  • Analizar nuevas amenazas y ofrecer información detallada a través de inteligencia basada en datos.   
  • Colaborar con nuestros clientes para resolver los principales retos para su ciberseguridad .   

Si está interesado en obtener información sobre las oportunidades de empleo en Proofpoint, visite la página de oportunidades de empleo en Proofpoint

Acerca del autor 

Figure 1

Liran Mendelovich es desarrollador de software. Entre sus intereses se incluyen todos los aspectos del desarrollo: el diseño y la implementación, la resolución de problemas, la optimización del rendimiento, la programación concurrente, los sistemas distribuidos y los microservicios, así como las bibliotecas de código abierto.