Posts
Tipos de Coleta de Lixo no .Net Core
2 de setembro de 2019 • 5 min de leitura
Gerenciamento de memória em linguagens modernas é frequentemente uma reflexão tardia. Para todos os efeitos práticos, escrevemos software sem pensar muito em memória. Isso nos serve bem, mas sempre há exceções…
Na Califórnia, existem extensos requisitos de relatórios financeiros para Agências Locais de Educação (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 em Excel, não é surpresa quando cada relatório é diferente. Para resolver esse problema, a Junta de Educação da Califórnia encomendou um software para gerar relatórios financeiros.
Eu era parte do time de desenvolvimento.
Minha primeira parada foi nos 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.
Ed-Pro é construído sobre .Net Core, o framework multiplataforma da Microsoft. No .Net Core, a memória é dividida em três tags: curta duração (Gen0), média duração (Gen1) e longa duração (Gen2). Gen0 é para dados de curta duração que rapidamente saem do escopo, Gen1 é para memória de média duração que permanece um pouco mais, também eventualmente sai do escopo e Gen2 é memória de longa duração que pode viver pela vida útil da aplicação. A memória Gen0 é constantemente recuperada, Gen1 é recuperada com menos frequência que Gen0, e Gen2 é recuperada ainda com menos frequência que Gen1.
A única maneira certa de entender o uso de memória do Ed-Pro era perfilá-lo, 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á-lo. 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 instrumentais em nossa compreensão 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 de Servidor e Coleta de Lixo de Estação de Trabalho.
Coleta de Lixo de Servidor faz um par de suposições: Primeiro, há memória abundante disponível e segundo, os processadores são multi-núcleo 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 de 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 uma prioridade maior que a aplicação; se a máquina for rápida, a limpeza não deve ser notada. No entanto, se não for, causará que a aplicação interrompa até que a limpeza seja concluída.
Coleta de Lixo de Estação de Trabalho funciona de forma diferente. Ela executa continuamente recuperando memória em uma thread com a mesma prioridade que a 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 bastante baixo, principalmente quando usa grandes quantidades de Gen0.
Por padrão, se .Net Core detectar um servidor, ele executa o tipo Coleta de Lixo de Servidor, que era o caso com nossa aplicação. Para executar o tipo Coleta de Lixo de 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, perfilamos a memória do Ed-Pro com Coleta de Lixo de 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 praticamente inexistentes. Além das diferenças no gráfico, o uso de memória da Coleta de Lixo de Servidor chegou a 1 gig enquanto a Coleta de Lixo de 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, em que a Coleta de Lixo de Servidor faz muito sentido. Meu conselho é perfilar 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 é um Engenheiro de IA com quase 30 anos de experiência em engenharia de software. Ele constrói sistemas de IA práticos—pipelines de conteúdo, agentes de infraestrutura e ferramentas que resolvem problemas reais—e compartilha o que está aprendendo ao longo do caminho. Conecte-se com ele nas redes sociais: X (@chuckconway) ou visite-o no YouTube e no SubStack.