Publicaciones
Tipos de recolección de basura en .Net Core
2 de septiembre de 2019 • 5 min de lectura
La gestión de memoria en lenguajes modernos a menudo es una ocurrencia tardía. Para todos los efectos prácticos, escribimos software sin pensar en la memoria. Esto nos sirve bien pero siempre hay excepciones…
En California, existen amplios requisitos de informes financieros para las Agencias de Educación Local (LEA), una LEA puede ser un condado, un distrito, una escuela autónoma o una sola escuela. La mayoría de las LEA crean sus propios informes financieros que generalmente se centran en Excel, no es sorpresa que cada informe sea diferente. Para resolver este problema, la Junta de Educación de California encargó software para generar informes financieros.
Fui parte del equipo de desarrollo.
Mi primer paso fue revisar los registros de prueba, los registros de Ed-Pro señalaban un alto uso de memoria, ¿quizás había una fuga de memoria? Un ingeniero observó que los cálculos de Ed-Pro utilizaban una gran cantidad de memoria de corta duración. Si la memoria no se limpiaba rápidamente, podría parecer una fuga de memoria.
Ed-Pro se construye sobre .Net Core, el marco multiplataforma de Microsoft. En .Net Core, la memoria se divide en tres etiquetas: corta duración (Gen0), duración media (Gen1) y larga duración (Gen2). Gen0 es para datos de corta duración que rápidamente salen del alcance, Gen1 es para memoria de duración media que persiste un poco más, también eventualmente sale del alcance y Gen2 es memoria de larga duración que puede vivir durante toda la vida de la aplicación. La memoria Gen0 se reclama constantemente, Gen1 se reclama con menos frecuencia que Gen0, y Gen2 se reclama aún con menos frecuencia que Gen1.
La única forma segura de entender el uso de memoria de Ed-Pro era perfilarlo, a continuación se muestra una captura de pantalla usando dotMemory de JetBrains.
Como se sospechaba, encontramos grandes cantidades de memoria Gen0 (el azul), tanto que parecía que la recolección de basura no podía seguir el ritmo. Una estrategia para compensar una gran cantidad de memoria hizo que la recolección de basura oscilara entre aumentar el espacio de memoria (agregando más memoria para el uso de la aplicación) y limpiarla. Durante los ciclos de limpieza, la aplicación no responde.
Al principio, estábamos desconcertados, ¿no es el propósito del GC mantener la memoria ordenada? Dos artículos fueron fundamentales en nuestra comprensión de cómo funciona la recolección de basura en .Net: el artículo de Mark Vincze Troubleshooting high memory usage with ASP.Net Core on Kubernetes y Fundamentals of Garbage Collection de Microsoft. Ambos son excelentes lecturas y aclararon el uso de memoria en Ed-Pro.
Aquí hay un resumen de lo que aprendimos, hay dos tipos de recolección de basura en .Net: recolección de basura de servidor y recolección de basura de estación de trabajo.
La recolección de basura de servidor hace un par de suposiciones: primero, hay memoria abundante disponible y segundo, los procesadores son multinúcleo y son rápidos. Ambos pueden ser ciertos, pero vivimos en un mundo de máquinas virtuales y Docker donde es más probable que ambas suposiciones sean falsas.
La recolección de basura de servidor permite que la memoria se acumule, en algún momento, hace una de dos cosas: aumenta el espacio de memoria permitiendo que la memoria crezca o libera memoria huérfana. Cuando elige liberar memoria, la recolección de basura inicia el proceso en un hilo de alta prioridad. El hilo de alta prioridad tiene una prioridad más alta que la aplicación; si la máquina es rápida, la limpieza no debería notarse. Sin embargo, si no lo es, hará que la aplicación se detenga hasta que se complete la limpieza.
La recolección de basura de estación de trabajo funciona de manera diferente. Se ejecuta continuamente reclamando memoria en un hilo con la misma prioridad que la aplicación. Esto significa que también está compitiendo por recursos con la aplicación, lo que puede causar lentitud en la aplicación. La ventaja es que el uso de memoria de la aplicación puede mantenerse bastante bajo, principalmente cuando utiliza grandes cantidades de Gen0.
De forma predeterminada, si .Net Core detecta un servidor, ejecuta el tipo de recolección de basura de servidor, que fue el caso con nuestra aplicación. Para ejecutar el tipo de recolección de basura de estación de trabajo, agregue el siguiente fragmento a su archivo de proyecto:
<PropertyGroup>
<ServerGarbageCollection>false</ServerGarbageCollection>
</PropertyGroup>
Realizamos este cambio de configuración en Ed-Pro, usando dotMemory, perfilamos la memoria de Ed-Pro con la recolección de basura de estación de trabajo habilitada y cargamos las mismas pantallas que en la prueba anterior. Aquí están los resultados:
El uso de memoria se reduce significativamente. Las asignaciones de Gen0 son prácticamente inexistentes. Más allá de las diferencias en el gráfico, el uso de memoria de recolección de basura de servidor alcanzó 1 gig mientras que la recolección de basura de estación de trabajo alcanzó aproximadamente 200 megs.
Cada aplicación es diferente. Nuestra aplicación utilizaba una tonelada de datos temporales y, por lo tanto, utiliza una tonelada de memoria Gen0. Su aplicación puede aprovechar memoria de mayor duración como Gen1 o Gen2, en cuyo caso la recolección de basura de servidor tiene mucho sentido. Mi consejo es perfilar su memoria bajo diferentes condiciones para tener una idea de cómo se usa la memoria y luego decidir qué modo es mejor para su aplicación.
Autor: Chuck Conway es un Ingeniero de IA con casi 30 años de experiencia en ingeniería de software. Construye sistemas de IA prácticos—canalizaciones de contenido, agentes de infraestructura y herramientas que resuelven problemas reales—y comparte lo que está aprendiendo en el camino. Conéctate con él en redes sociales: X (@chuckconway) o visítalo en YouTube y en SubStack.