Connected cubes

Errori OOMKilled nella memoria diretta e in un container 

Share with your network!

“Prospettive di ingegneria” è una serie di articoli del blog che fornisce uno sguardo dietro le quinte sulle problematiche tecniche, le lezioni apprese e i miglioramenti che aiutano i nostri clienti a proteggere i loro collaboratori e i loro dati ogni giorno. Negli articoli che scrivono, i nostri ingegneri spiegano il processo che ha portato a un’innovazione Proofpoint.  

NOTA: questo articolo verte su una soluzione che l’autore ritiene interessante da un punto di vista tecnico. Tuttavia, i test di unità descritti nell’articolo non hanno alcun rapporto con le soluzioni Proofpoint né alcun impatto sui clienti. 

Di recente, due dei nostri microservizi hanno riscontrato degli errori di compilazione CI (Continuous Integration, ovvero integrazione continua) causati da test di unità Java. 

Anche se il limite di dimensione dello spazio di memoria assegnato a Java (heap o mucchio) era adeguato alle risorse del pod Docker, ogni test ha generato un errore OOMKilled di Kubernetes. Non sono stati riscontrati errori nei registri e non sono stati generati file core dump (dump di memoria) o heap dump (dump di heap). 

I messaggi di errore OOMKilled menzionavano la libreria C Netty. In entrambi i casi, il problema derivava da API di Google Cloud Platform (GCP) e di OpenTelemetry, che utilizzano entrambe delle chiamate gRPC che si basano su Netty. gRPC è un framwork di chiamate di procedura remota (RPC) recente, open source e altamente performante. Durante i test, le chiamate gRPC fallivano perché non erano destinate a essere eseguite e quindi non erano configurate correttamente, La disattivazione delle connessioni gRPC durante i test ha risolto il problema. 

Anche se le connessioni gRPC non avrebbero dovuto essere attivate durante i test, nulla permetteva di spiegare perché questa situazione causava degli errori OOMKilled. Il limite di dimensione dell’heap Java specificato era corrette rispetto al container Docker. 

È stato allora che ho scoperto la seguente informazione nel blog di questo sviluppatore: “Netty utilizza ByteBuffers e la memoria diretta per allocare e disallocare la memoria”. Ho quindi realizzato che un aumento inatteso dell’utilizzo della memoria fuori dall’heap poteva essere la causa degli errori OOMKilled. 

Il problema: un aumento dell’utilizzo della memoria fuori dall’heap 

Prendiamo un esempio astratto che illustra questo tipo di problema. In questo esempio, esamineremo l’utilizzo della memoria Java da parte della nostra applicazione prima e dopo le chiamate gRPC. 

Dimensione dell’heap Java 

  -Xms128m (dimensione iniziale dell’heap) 

  -Xmx256m (dimensione massima dell’heap)  

Limiti e richieste di risorse del pod Kubernetes 

  risorse: 

    richieste: 

      memoria: 200M 

    limiti: 

      memoria: 360M 

Figure 1

Figura 1. Allocazione della memoria Java prima delle chiamate gRPC - 200M in totale 

 Figure 2

 

Figura 2. Allocazione della memoria Java dopo le chiamate gRPC - 360M in totale 

Come si evince da questo esempio, l’utilizzo della memoria nell’heap rimane simile in termini di dimensione, ma l’utilizzo della memoria fuori dall’heap aumenta in modo significativo dopo le chiamate gRPC. In particolare, la dimensione massima dell’heap Java non viene superata, ma l’utilizzo della memoria fuori dall’heap supera il limite di memoria del pod. Ciò è dovuto al fatto che la memoria fuori dall’heap non è gestita dal Garbage Collector (processo di pulizia della memoria) della macchina virtuale Java (JVM). Di conseguenza, la memoria fuori dall’heap può aumentare oltre i limiti delle risorse del pod, generando così un errore OOMKilled di Kubernetes. 

La soluzione: configurare limiti di memoria aggiuntivi 

gRPC viene spesso utilizzato nelle librerie client Google Cloud, OpenTelemetry, ecc. Quando un’applicazione utilizza gRPC o un framework simile che usa molta memoria nativa, è importante conoscere l’utilizzo della memoria fuori dall’heap e sapere come può influenzare il limite della memoria del pod. Poiché il pacchetto io.grpc utilizza Netty, questa situazione può ripetersi in un microservizio. Per evitare gli errori, sono necessari degli adattamenti. 

Comunemente, la dimensione dell’heap Java è configurata su un valore lievemente inferiore al limite di memoria del pod. Puoi configurarla utilizzando le opzioni -Xms e -Xmx della JVM o l’opzione MaxRAMPercentage della JVM. Tuttavia, in situazioni simili all’esempio precedente, la memoria diretta fuori dall’heap può superare il limite di memoria del pod. Ciò genera un errore OOMKilled. 

NOTA: il Garbage Collector ha bisogno di memoria aggiuntiva quando esegue la pulizia della memoria. Nel peggiore dei casi, ciò corrisponde al doppio della dimensione massima dell’heap, ma in genere è molto inferiore. Data la memoria aggiuntiva necessaria, è possibile che la dimensione del set residente (RSS, Resident Set Size) aumenti temporaneamente durante la pulizia. Ciò può causare problemi in qualsiasi ambiente con limitazioni di memoria, per esempio un container. 

Un approccio più controllato consiste nel configurare il limite della memoria diretta e nel definire il limite della memoria del pod come la somma dei valori dell’heap massimo e della memoria fuori dall’heap (memoria diretta). Devi anche tenere conto di altri tipi di memoria, come Metaspace, le classi, ecc. Per controllare l’utilizzo della memoria nativa, puoi configurare l’opzione MaxDirectMemorySize della JVM. 

NOTA: Native Image può anche allocare memoria separata dall’heap Java. Un caso d’uso comune è un java.nio.DirectByteBuffer che fa riferimento diretto al valore -XX:MaxDirectMemorySize della memoria nativa. Questo valore rappresenta la dimensione massima delle allocazioni di memoria buffer diretta. 

Questi limiti ulteriori, combinati con una configurazione corrispondente alle risorse del pod, ti offrono un approccio più controllato. Qui di seguito illustriamo un esempio completo. 

File Docker  

FROM openjdk... 

... 

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

Limiti e richieste di risorse del pod Kubernetes 

  resources: 

    requests: 

      memory: "2Gi" # minimum memory request 

    limits: 

      memory: "3Gi" # maximum memory limit 

 
Unisciti al team Proofpoint 

I nostri collaboratori, e la diversità delle loro esperienze e percorsi, sono l’elemento trainante del nostro successo. La nostra missione è proteggere le persone, i dati , e i marchi contro le minacce avanzate attuali e i rischi di non conformità.   

Assumiamo i migliori talenti per:   

  • Sviluppare e migliorare la nostra piattaforma di sicurezza comprovata   
  • Combinare innovazione e velocità in un’architettura cloud in costante evoluzione   
  • Analizzare le nuove minacce e offrire informazioni dettagliata grazie a una threat intelligence basata sulle minacce   
  • Collaborare con i nostri clienti per risolvere le loro sfide di sicurezza informatica più complesse   

Se sei interessato a saperne di più sulle opportunità di lavoro in Proofpoint, visita la nostra pagina dedicata

Informazioni sull’autore 

Figure 1

Liran Mendelovich è Senior Software Developer. È interessato a tutti gli aspetti dello sviluppo - progettazione e implementazione, risoluzione dei problemi, ottimizzazione delle prestazioni, programmazione concorrente, sistemi distribuiti e microservizi - oltre alle librerie open source.