Tres tipos de pérdidas de memoria

Hola colegas

Nuestra larga búsqueda de los libros más vendidos y atemporales sobre optimización de código solo ha dado los primeros resultados, pero estamos listos para complacerle en que la traducción del legendario libro de Ben Watson " Escritura de código .NET de alto rendimiento " se ha completado literalmente. En las tiendas, tentativamente en abril, esté atento a la publicidad.

Y hoy le ofrecemos leer un artículo puramente práctico sobre los tipos más apremiantes de pérdidas de memoria, escrito por Nelson Ilheidzhe (Strike).

Por lo tanto, tiene un programa que tarda más en completarse, cuanto más tarde. Probablemente, no será difícil para usted comprender que este es un signo seguro de una pérdida de memoria.
Sin embargo, ¿qué queremos decir exactamente con "pérdida de memoria"? En mi experiencia, las pérdidas de memoria explícitas se dividen en tres categorías principales, cada una de las cuales se caracteriza por un comportamiento especial, y para depurar cada una de las categorías se necesitan herramientas y técnicas especiales. En este artículo quiero describir las tres clases y sugerir cómo reconocer correctamente, con
con qué clase está tratando y cómo encontrar una fuga.

Tipo (1): fragmento de memoria inalcanzable asignado

Esta es una pérdida de memoria clásica en C / C ++. Alguien asignó memoria usando new o malloc , y no llamó a free o delete para liberar memoria después de terminar de trabajar con ella.

 void leak_memory() { char *leaked = malloc(4096); use_a_buffer(leaked); /* ,   free() */ } 

Cómo determinar si una fuga pertenece a esta categoría

  • Si escribe en C o C ++, especialmente en C ++ sin el uso generalizado de punteros inteligentes para controlar la vida útil de los segmentos de memoria, esta es la opción que estamos considerando primero.
  • Si el programa se ejecuta en un entorno con recolección de basura, es posible que una extensión de código nativo provoque una fuga de este tipo, sin embargo, primero se deben eliminar las fugas de los tipos (2) y (3).

Cómo encontrar una fuga

  • Utiliza ASAN . Utiliza ASAN. Utiliza ASAN.
  • Usa un detector diferente. Probé las herramientas Valgrind o tcmalloc para trabajar con un grupo, también hay otras herramientas en otros entornos.
  • Algunos asignadores de memoria permiten volcar el perfil de almacenamiento dinámico, que mostrará todas las áreas de memoria no asignadas. Si tiene una fuga, luego de un tiempo, casi todas las descargas activas fluirán de ella, por lo que encontrarla probablemente no sea difícil.
  • Si todo lo demás falla, volcar un volcado de memoria y examinarlo tan meticulosamente como sea posible . Pero definitivamente no debería comenzar con esto.

Tipo (2): asignaciones de memoria de larga duración no planificadas

Tales situaciones no son "fugas" en el sentido clásico de la palabra, ya que todavía se conserva un enlace desde algún lugar a este fragmento de memoria, por lo que al final puede liberarse (si el programa logra llegar allí sin usar toda la memoria).
Las situaciones en esta categoría pueden surgir por muchas razones específicas. Los más comunes son:

  • Acumulación involuntaria de estado en una estructura global; por ejemplo, el servidor HTTP escribe en la lista global cada objeto Request recibido.
  • Cachés sin una política de obsolescencia bien pensada. Por ejemplo, un caché ORM que almacena en caché cada objeto cargado, activo durante la migración, en el que todos los registros que están presentes en la tabla se cargan sin excepción.
  • Estado demasiado voluminoso se captura en el circuito. Este caso es especialmente común en Java Script, pero también puede ocurrir en otros entornos.
  • En un sentido más amplio, la retención involuntaria de cada elemento de una matriz o secuencia, mientras se suponía que estos elementos se procesarían en línea.

Cómo determinar si una fuga pertenece a esta categoría

  • Si el programa se ejecuta en un entorno con recolección de basura, entonces esta es la opción que estamos considerando primero.
  • Compare el tamaño de almacenamiento dinámico que se muestra en las estadísticas del recolector de basura con el tamaño de la memoria libre generada por el sistema operativo. Si una fuga entra en esta categoría, los números serán comparables y, lo más importante, se seguirán con el tiempo.

Cómo encontrar una fuga

Utilice los perfiladores o las herramientas de volcado de almacenamiento dinámico disponibles en su entorno. Sé que hay guppy en Python o memory_profiler en Ruby, y también escribí ObjectSpace directamente en Ruby.

Tipo (3): memoria libre pero no utilizada o inutilizable

Esta categoría es más difícil de caracterizar, pero es precisamente la más importante de entender y tener en cuenta.

Las fugas de este tipo ocurren en la zona gris, entre la memoria que se considera "libre" desde el punto de vista del asignador dentro de la VM o el entorno de tiempo de ejecución, y la memoria que está "libre" desde el punto de vista del sistema operativo. La razón más común (pero no la única) de este fenómeno es la fragmentación del montón . Algunos asignadores simplemente toman y no devuelven memoria al sistema operativo después de que se haya asignado.

Un caso de este tipo puede considerarse con un ejemplo de un programa corto escrito en Python:

 import sys from guppy import hpy hp = hpy() def rss(): return 4096 * int(open('/proc/self/stat').read().split(' ')[23]) def gcsize(): return hp.heap().size rss0, gc0 = (rss(), gcsize()) buf = [bytearray(1024) for i in range(200*1024)] print("start rss={} gcsize={}".format(rss()-rss0, gcsize()-gc0)) buf = buf[::2] print("end rss={} gcsize={}".format(rss()-rss0, gcsize()-gc0)) 

Asignamos 200,000 buffers de 1 kb y luego guardamos cada uno de ellos. Cada segundo, mostramos el estado de la memoria desde el punto de vista del sistema operativo y desde el punto de vista de nuestro propio recolector de basura Python.

En mi computadora portátil, obtengo algo como esto:

start rss=232222720 gcsize=11667592
end rss=232222720 gcsize=5769520


Podemos asegurarnos de que Python realmente liberó la mitad de los búferes, porque el nivel de gcsize cayó casi la mitad del valor máximo, pero no pudo devolver un byte de esta memoria al sistema operativo. La memoria liberada permanece accesible para el mismo proceso de Python, pero no para cualquier otro proceso en esta máquina.

Tales fragmentos de memoria libres pero no utilizados pueden ser problemáticos e inofensivos. Si un programa Python actúa de esta manera y luego asigna un puñado de fragmentos de 1 kb, entonces este espacio simplemente se reutiliza y todo está bien.

Pero, si hicimos esto durante la configuración inicial, y posteriormente asignamos memoria al mínimo, o si todos los fragmentos asignados posteriormente eran 1.5kb cada uno y no cabían en estos búferes dejados de antemano, entonces toda la memoria asignada de esta manera siempre estaría inactiva Sería desperdiciado.

Los problemas de este tipo son especialmente relevantes en un entorno específico, a saber, en sistemas de servidores multiproceso para trabajar con lenguajes como Ruby o Python.

Digamos que configuramos un sistema en el que:

  • En cada servidor, se utilizan N trabajadores de subproceso único para atender de manera competente las solicitudes. Tomemos N = 10 para mayor precisión.
  • Como regla general, cada empleado tiene una cantidad casi constante de memoria. Para mayor precisión, tomemos 500MB.
  • Con poca frecuencia, recibimos solicitudes que requieren mucha más memoria que la solicitud mediana. Para mayor precisión, supongamos que una vez por minuto recibimos una solicitud, cuyo tiempo de ejecución requiere además 1 GB de memoria adicional, y cuando se procesa la solicitud, esta memoria se libera.

Una vez por minuto, llega una solicitud de "cetáceo", cuyo procesamiento confiamos a uno de los 10 trabajadores, por ejemplo, al azar: ~random . Idealmente, durante el procesamiento de esta solicitud, este empleado debe asignar 1 GB de RAM y, después del final del trabajo, devolver esta memoria al sistema operativo para que pueda reutilizarse más tarde. Para procesar solicitudes de forma ilimitada por este principio, el servidor necesitará solo 10 * 500MB + 1GB = 6GB RAM.

Sin embargo, supongamos que debido a la fragmentación o por alguna otra razón, la máquina virtual nunca podrá devolver esta memoria al sistema operativo. Es decir, la cantidad de RAM que requiere del sistema operativo es igual a la mayor cantidad de memoria que tiene que asignar a la vez. En este caso, cuando un empleado en particular atiende una solicitud de uso intensivo de recursos, el área ocupada por dicho proceso en la memoria aumentará para siempre en un gigabyte completo.

Cuando inicie el servidor, verá que la cantidad de memoria utilizada es 10 * 500MB = 5GB. Tan pronto como llega la primera solicitud grande, el primer trabajador ocupa 1 GB de memoria y luego no la devuelve. La cantidad total de memoria utilizada saltará a 6GB. En ocasiones, las siguientes solicitudes entrantes pueden ser redirigidas al proceso que procesó previamente la "ballena", en cuyo caso la cantidad de memoria utilizada no cambiará. Pero a veces una solicitud tan grande se entregará a otro empleado, por lo que la memoria se inflará por otro 1 GB, y así sucesivamente hasta que cada empleado tenga la oportunidad de procesar una solicitud tan grande al menos una vez. En este caso, tomará hasta 10 * (500MB + 1GB) = 15GB de RAM con estas operaciones, ¡lo cual es mucho más que los 6GB ideales! Además, si considera cómo se utiliza la flota de servidores a lo largo del tiempo, puede ver cómo la cantidad de memoria utilizada aumenta gradualmente de 5 GB a 15 GB, lo que será muy similar a una fuga "real".

Cómo determinar si una fuga pertenece a esta categoría

  • Compare el tamaño de almacenamiento dinámico que se muestra en las estadísticas del recolector de basura con el tamaño de la memoria libre generada por el sistema operativo. Si la fuga pertenece a esta (tercera) categoría, los números divergirán con el tiempo.
  • Me gusta configurar mis servidores de aplicaciones para que ambos números se apaguen periódicamente en mi infraestructura de series temporales, por lo que es conveniente mostrar gráficos en ellos.
  • En Linux, vea el estado del sistema operativo en el campo 24 de /proc/self/stat , y vea el asignador de memoria a través de un lenguaje o API específica de máquina virtual.

Cómo encontrar una fuga

Como ya se mencionó, esta categoría es un poco más insidiosa que las anteriores, ya que el problema a menudo surge incluso cuando todos los componentes funcionan "según lo previsto". Sin embargo, hay varios trucos útiles para ayudar a mitigar o reducir el impacto de tales "fugas virtuales":

  • Reinicie sus procesos con más frecuencia. Si el problema crece lentamente, quizás no sea difícil reiniciar todos los procesos de aplicación una vez cada 15 minutos o una vez por hora.
  • Un enfoque aún más radical: puede enseñar a todos los procesos a reiniciarse de forma independiente, tan pronto como el espacio que ocupan en la memoria exceda un cierto valor umbral o crezca en un valor predeterminado. Sin embargo, intente prever que toda su flota de servidores no pueda iniciar un reinicio síncrono espontáneo.
  • Cambiar el asignador de memoria. A la larga, tcmalloc y jemalloc generalmente manejan la fragmentación mucho mejor que el asignador predeterminado, y experimentar con ellos es muy conveniente usando la variable LD_PRELOAD .
  • Averigüe si tiene consultas individuales que consumen mucha más memoria que el resto. En Stripe, nuestros servidores API miden RSS (consumo de memoria constante) antes y después de atender cada solicitud de API y registran el delta. Luego, consultamos fácilmente nuestros sistemas de agregación de registros para determinar si existen tales terminales y usuarios (y patrones) que se pueden usar para cancelar las ráfagas de consumo de memoria.
  • Ajuste el recolector de basura / asignador de memoria. Muchos de ellos tienen parámetros personalizables que le permiten especificar qué tan activamente dicho mecanismo devolverá la memoria al sistema operativo, qué tan optimizado está para eliminar la fragmentación; Hay otras opciones útiles. Todo aquí también es bastante complicado: asegúrese de comprender exactamente lo que está midiendo y optimizando, y también trate de encontrar un experto en la máquina virtual adecuada y consulte con él.

Source: https://habr.com/ru/post/es432072/


All Articles