Posts
Tipos de Coleta de Lixo no .Net Core
2 de setembro de 2019 • 5 min de leitura

O gerenciamento de memória em linguagens modernas é frequentemente uma reflexão tardia. Para todos os efeitos, escrevemos software sem pensar muito sobre memória. Isso nos serve bem, mas sempre há exceções…
Na Califórnia, existem extensos requisitos de relatórios financeiros para Agências de Educação Local (LEA), uma LEA pode ser um condado, um distrito, uma escola charter ou uma única escola. A maioria das LEAs cria seus próprios relatórios financeiros que geralmente são centrados no Excel, não é surpresa quando cada relatório é diferente. Para resolver este problema, o Conselho de Educação da Califórnia encomendou software para gerar relatórios financeiros.
Eu fazia parte da equipe de desenvolvimento.
Minha primeira parada foram os logs de teste, os logs do Ed-Pro apontavam para alto uso de memória, talvez houvesse um vazamento de memória? Um engenheiro observou que os cálculos do Ed-Pro usavam uma grande quantidade de memória de curta duração. Se a memória não fosse limpa rapidamente, poderia parecer um vazamento de memória.
O Ed-Pro é construído sobre o .Net Core, o framework multiplataforma da Microsoft. No .Net Core, a memória é dividida em três categorias: Curta duração (Gen0), média duração (Gen1) e longa duração (Gen2). Gen0 é para dados de curta duração que rapidamente saem de escopo, Gen1 é para memória de média duração que permanece por um pouco mais de tempo, ela também eventualmente sai de escopo e Gen2 é memória de longa duração que pode viver durante toda a vida da aplicação. A memória Gen0 é constantemente recuperada, Gen1 é recuperada com menos frequência que Gen0, e Gen2 é recuperada ainda menos frequentemente que Gen1.
A única maneira segura de entender o uso de memória do Ed-Pro era fazer o profiling dele, abaixo está uma captura de tela usando dotMemory da JetBrains.
Como suspeitávamos, encontramos grandes quantidades de memória Gen0 (o azul), tanto que parecia que a Coleta de Lixo não conseguia acompanhar. Uma estratégia para compensar uma grande quantidade de memória fez com que a Coleta de Lixo oscilasse entre aumentar o espaço de memória (adicionando mais memória para uso da aplicação) e limpá-la. Durante os ciclos de limpeza, a aplicação fica sem resposta.
No início, ficamos perplexos, não é o propósito do GC manter a memória organizada? Dois artigos foram fundamentais em nosso entendimento de como a Coleta de Lixo funciona no .Net: o artigo de Mark Vincze Troubleshooting high memory usage with ASP.Net Core on Kubernetes e Fundamentals of Garbage Collection da Microsoft. Ambos são ótimas leituras e trouxeram clareza ao uso de memória no Ed-Pro.
Aqui está um resumo do que aprendemos, existem dois tipos de Coleta de Lixo no .Net: Coleta de Lixo do Servidor e Coleta de Lixo da Estação de Trabalho.
Coleta de Lixo do Servidor faz algumas suposições: Primeiro, há ampla memória disponível e segundo, os processadores são multi-core e são rápidos. Ambos podem ser verdadeiros, mas vivemos em um mundo de máquinas virtuais e Docker onde é mais provável que ambas as suposições sejam falsas.
A Coleta de Lixo do Servidor permite que a memória se acumule, em algum ponto, ela faz uma de duas coisas: ou aumenta o espaço de memória permitindo que a memória cresça ou libera memória órfã. Quando escolhe liberar memória, a Coleta de Lixo inicia o processo em uma thread de alta prioridade. A thread de alta prioridade tem prioridade mais alta que a aplicação; se a máquina for rápida, a limpeza não deveria ser notada. No entanto, se não for, causará a parada da aplicação até que a limpeza seja concluída.
Coleta de Lixo da Estação de Trabalho opera de forma diferente. Ela executa continuamente recuperando memória em uma thread com a mesma prioridade da aplicação. Isso significa que também está competindo por recursos com a aplicação, o que pode causar lentidão na aplicação. O lado positivo é que o uso de memória da aplicação pode permanecer bem baixo, principalmente quando usa grandes quantidades de Gen0.
Por padrão, se o .Net Core detecta um servidor, ele executa o tipo Coleta de Lixo do Servidor, que foi o caso com nossa aplicação. Para executar o tipo Coleta de Lixo da Estação de Trabalho, adicione o seguinte trecho ao seu arquivo de projeto:
<PropertyGroup>
<ServerGarbageCollection>false</ServerGarbageCollection>
</PropertyGroup>
Fizemos essa mudança de configuração no Ed-Pro, usando dotMemory, fizemos o profiling da memória do Ed-Pro com a Coleta de Lixo da Estação de Trabalho habilitada e carregamos as mesmas telas do teste anterior. Aqui estão os resultados:
O uso de memória é significativamente reduzido. As alocações Gen0 são virtualmente inexistentes. Além das diferenças no gráfico, o uso de memória da Coleta de Lixo do Servidor chegou a 1 gig enquanto a Coleta de Lixo da Estação de Trabalho chegou a aproximadamente 200 megs.
Cada aplicação é diferente. Nossa aplicação usava uma tonelada de dados temporários e, portanto, usa uma tonelada de memória Gen0. Sua aplicação pode aproveitar memória de vida mais longa, como Gen1 ou Gen2, na qual a Coleta de Lixo do Servidor faz muito sentido. Meu conselho é fazer o profiling de sua memória sob diferentes condições para ter uma ideia de como a memória é usada e então decidir qual modo é melhor para sua aplicação.
Autor: Chuck Conway é especialista em engenharia de software e IA Generativa. Conecte-se com ele nas redes sociais: X (@chuckconway) ou visite-o no YouTube.