Back-End

26 abr, 2017

Utilização de RAM pelo Java em containers: as 5 principais dicas para não perder sua memória

Publicidade

Neste artigo, gostaríamos de compartilhar detalhes de gerenciamento de memória e flexibilidade no Java dentro dos containers específicos que não são evidentes à primeira vista. Abaixo, você vai encontrar uma lista de problemas dos quais devemos estar cientes e das atualizações importantes nas próximas edições do JDK, assim como soluções existentes para alguns pontos do núcleo. Nós juntamos as 5 dicas mais interessantes e úteis para melhorar a eficiência na utilização de recursos para as aplicações Java.

Limite da pilha de memória do Java no Docker

Atualmente, a comunidade está discutindo questões sobre a determinação incorreta de limites de memória quando aplicações Java são executadas nos containers Docker.

O problema é que, caso a opção Xmx não seja definida explicitamente, o JVM utiliza ¼ de toda a memória disponível para hospedar o sistema operacional, devido a um algoritmo ergonômico de coleta interna de lixo (GC). Isso pode levar à finalização do processo Java pelo kernel se a utilização de memória pela JVM crescer acima dos limites definidos nos cgroups para os containers Docker.

Para resolver esse problema, uma melhora que foi recentemente implementada no OpenJDK 9:

“Uma primeira mudança experimental que foi adicionada ao OpenJDK 9 foi que o JVM pode entender que está sendo executado dentro de um container e ajustar os limites de memória de acordo”, do artigo: O Java 9 vai ajustar os limites de memória se estiver sendo executado com o Docker.

 Uma nova opção da JVM (-XX:+UserCGroupMemoryLimitForHeap) automaticamente configura o processo Java de acordo com o limite de memória definido no cgroup.

Como uma boa opção para resolver esse problema antes do lançamento do Java 9, o limite Xmx pode ser especificado explicitamente nas opções de inicialização para o JVM. Existe um chamado aberto para “um script para configurar melhor os valores padrão Xmx de acordo com os limites de memória do Docker” no repositório oficial OpenJDK.

O Jelastic conseguiu omitir a determinação incorreta de limites de memória utilizando um sistema melhorado de camadas de virtualização de contêineres em combinação com as imagens do Docker. Previamente, explicamos isso como funciona no artigo: O Java e os limites de memória nos containers: LXC, Docker e OpenVZ.

Rastreando o uso de memória nativa não empilhada

Enquanto aplicações Java são executadas na nuvem, também é importante prestar atenção ao uso de memória nativa pelos processos do Java, chamados de memória fora da pilha. Ela pode ser consumida para diferentes propósitos:

  • Coletores de lixo e otimizações JIT que estejam rastreando e armazenando dados dos objetos gráficos na memória nativa. Além disso, desde o JDK 8, nomes e campos das classes, bytecode dos métodos, conjuntos de constantes etc. são armazenados no Metaspace, que também fica armazenado fora da pilha da JVM.
  • Também, para ganhar alta performance, uma grande quantidade de aplicações Java alocam a memória na área nativa. Utilizando o java.nio.ByteBuffer ou bibliotecas JNI de terceiros, essas aplicações armazenam buffers grandes, de vida longa, que são gerenciados pelas operações de entrada e saída de sistemas adjacentes nativos.

Por padrão, a alocação do Metaspace é limitada somente pela quantidade de memória nativa do sistema operacional. E junto com a utilização errada da determinação dos limites de memória nos containers do Docker, isso aumenta o risco de instabilidade da aplicação. Limitar o tamanho dos metadados é importante, especialmente se você estiver enfrentando problemas de OOM. Faça isso com a opção especial -XX:MaxMetaspaceSize.

Com todos os objetos armazenados fora da pilha normal de coleta de lixo, não é óbvio que impacto eles possam causar sobre o consumo de memória de uma aplicação Java. Existe um bom artigo que explica o problema em detalhes e fornece algumas instruções de como analisar a utilização da memória nativa:

“Algumas semanas atrás, eu enfrentei um problema interessante tentando analisar o consumo de memória em minha aplicação Java (Sprint Boot + Infinispan) sendo executada no Docker. O parâmetro Xmx estava configurado em 256m, mas a ferramenta de monitoramento do Docker apresentava quase o dobro de memória utilizada” – Analisando a utilização de memória pelo Java em um contêiner Docker.

E conclusões interessantes do autor:

“O que eu posso dizer como conclusão? Bem… nunca coloque as palavras “Java” e “micro” na mesma frase :). Estou brincando – somente lembre-se que lidar com a memória no caso de Java, Linux e Docker é um pouco mais complicado do que parece no começo”.

Para rastrear a alocação da memória nativa, uma opção específica da JVM pode ser utilizada (-XX:NativeMemoryTracking=summmary). Por favor, note que você terá uma redução de performance de 5% a 10% se habilitar essa opção.

Redimensionando a utilização de memória pelo JVM no tempo de execução

Outra solução útil para reduzir o consumo de memória por uma aplicação Java é ajustar as opções gerenciáveis da JVM enquanto o processo Java está sendo executado. Desde JDK7u60 e JDK8u20, as opções MinHeapFreeRatio e MaxHeapFreeRatio se tornaram gerenciáveis, o que significa que podemos mudar seus valores em tempo de execução sem a necessidade de reiniciar o processo Java.

No artigo Redimensionamento de pilha realizado em tempo de execução, o autor descreve como reduzir a utilização de memória ajustando essas opções gerenciáveis.

“… O redirecionamento funcionou mais uma vez e a capacidade da pilha aumentou de 159MB para 444MB. Nós descrevemos que o mínimo de 85% da capacidade da nossa pilha deveriam ficar livres, e isso levou a JVM a redimensionar a pilha novamente para ganhar quase 15% de utilização”.

Tal abordagem pode trazer uma otimização significativa da utilização dos recursos para cargas de trabalho variáveis. E o próximo passo para melhorar o redimensionamento de memória da JVM pode ser permitir a mudança do Xmx em tempo de execução sem a necessidade de reiniciar o processo Java.

Melhorando a compactação de memória

Em muitos casos, os clientes querem minimizar a quantidade de memória utilizada dentro das aplicações Java induzindo a GCs mais frequentes. Por exemplo, pode ajudar você a economizar dinheiro utilizando recursos mais eficientes no desenvolvimento, teste e ambientes de construção, assim como em produção, após os picos de carregamento. No entanto, de acordo com este ticket oficial de melhoria, os algoritmos atuais de GC requerem múltiplos ciclos completos de coleta de lixo para liberar toda a memória não utilizada.

Como resultado, a nova opção da JVM (-XX:ShrinkDeapInSteps) foi lançada para regular um algoritmo de comportamento GC regulado no JDK9. Essa configuração deveria ser alterada para XX:-ShrinkHeapInSteps para desabilitar os 4 ciclos completos de CG. Isso vai liberar os recursos da memória RAM mais rápido e minimizar o tamanho da utilização da pilha Java em aplicações com carga variável.

Reduzindo o uso de memória para acelerar a migração em tempo real

A migração em tempo real de aplicações Java com um consumo pesado de memória leva uma quantidade significativa de tempo. Para reduzir o tempo total da migração e a sobrecarga dos recursos, o motor de migração deve minimizar a transferência de dados entre os hosts. Isso pode ser feito compactando a RAM com a ajuda de um ciclo GC completo antes do processo de migração. Tal abordagem pode ser mais efetiva em relação a custo para uma variedade de aplicações superarem a degradação da performance durante o ciclo GC do que migrar uma RAM sem compactação.

Nós encontramos um ótimo trabalho de pesquisa relacionado a esse tema: Migração de JVM em tempo real assistida pelo GC para aplicações de servidor Java. Os autores integraram a JVM com o CRIU(Checkpoint/Restore in Userspace) e apresentaram uma nova lógica para o GC para a redução de tempo que a aplicação Java leva para migrar de um hospedeiro para outro. O método oferecido permite a possibilidade de coleta de lixo consciente da mudança antes de tirar o snapshot do estado do processo Java, então, congelar um container em execução, criando um ponto de verificação dele no disco e mais tarde, recuperando ele do ponto onde foi congelado.

A comunidade Docker também está integrando o CRIU ao mainstream. Na época da escrita deste texto, essa funcionalidade ainda estava em estágio experimental.

Uma combinação dos dois (Java e CRIU) poderia desencadear oportunidades de otimização de desempenho e utilização para as aplicações Java hospedadas na nuvem. Você pode encontrar mais detalhes de como a migração de containers em tempo real funciona na nuvem no artigo “Migração de containers em tempo real: por trás das cenas”.

O Java é ótimo e já funciona bem na nuvem, especialmente em containers, mas acreditamos que ele pode ficar ainda melhor. Então, neste artigo nós cobrimos um conjunto de problemas que já podem ser melhorados para executar as aplicações Java suavemente e eficientemente.

Na Jelastic, estamos executando milhares de containers Java em centenas de datacenters ao redor do mundo. Um bom gerenciamento de memória é crítico para nós. É por isso que constantemente incorporamos novas descobertas sobre a memória do Java em nossas plataformas, para que os desenvolvedores não tenham que lidar explicitamente com esses problemas. Tente executar seus containers Java na plataforma aprimorada Jelastic.

***

Tetiana Fydorenchyk faz parte do time de colunistas internacionais do iMasters. A tradução do artigo é feita pela redação iMasters, com autorização do autor, e você pode acompanhar o artigo em inglês no link: http://blog.jelastic.com/2017/04/13/java-ram-usage-in-containers-top-5-tips-not-to-lose-your-memory/.