performance - ¿Cómo ganar el control de un montón de 5 GB en Haskell?
garbage-collection memory-management (2)
Actualmente estoy experimentando con un pequeño servidor web Haskell escrito en Snap que carga y pone a disposición del cliente una gran cantidad de datos. Y me cuesta muchísimo ganar el control del proceso del servidor. En momentos aleatorios, el proceso usa una gran cantidad de CPU por segundos o minutos y deja de responder a las solicitudes de los clientes. A veces, el uso de la memoria aumenta (y en ocasiones disminuye) cientos de megabytes en segundos.
Afortunadamente, alguien tiene más experiencia con los procesos Haskell de larga ejecución que usan mucha memoria y puede darme algunos consejos para hacer que la cosa sea más estable. Lo he estado depurando durante días y ahora estoy empezando a desesperarme un poco.
Una pequeña descripción general de mi configuración:
Al inicio del servidor, leí unos 5 gigabytes de datos en una gran estructura (anidada) Data.Map-like en la memoria. El mapa anidado tiene un valor estricto y todos los valores dentro del mapa son de tipos de datos con todo su campo hecho estricto también. He dedicado mucho tiempo para asegurarme de que no quedan nada sin evaluar. La importación (dependiendo de la carga de mi sistema) toma alrededor de 5-30 minutos. Lo extraño es que la fluctuación en carreras consecutivas es mucho más grande de lo que esperaría, pero ese es un problema diferente.
La estructura de big data vive dentro de un ''TVar'' compartido por todos los hilos del cliente generados por el servidor Snap. Los clientes pueden solicitar partes arbitrarias de los datos usando un lenguaje de consulta pequeño. La cantidad de solicitud de datos generalmente es pequeña (hasta 300 kb o menos) y solo toca una pequeña parte de la estructura de datos. Todas las solicitudes de solo lectura se realizan con un ''readTVARIO'', por lo que no requieren ninguna transacción de STM.
El servidor se inicia con los siguientes indicadores: + RTS -N -I0 -qg -qb. Esto inicia el servidor en modo de subprocesos múltiples, deshabilita el tiempo de inactividad y el GC paralelo. Esto parece acelerar mucho el proceso.
El servidor se ejecuta principalmente sin ningún problema. Sin embargo, de vez en cuando, un cliente solicita un tiempo de espera y la CPU alcanza el 100% (o incluso más del 100%) y continúa haciéndolo durante un largo tiempo. Mientras tanto, el servidor ya no responde a la solicitud.
Hay algunas razones por las que puedo pensar que pueden causar el uso de la CPU:
La solicitud solo lleva mucho tiempo porque hay mucho trabajo por hacer. Esto es algo improbable porque a veces sucede para solicitudes que han demostrado ser muy rápidas en las ejecuciones anteriores (con una velocidad de 20-80 ms o más).
Todavía hay algunos thunks no evaluados que deben computarse antes de que los datos puedan procesarse y enviarse al cliente. Esto también es poco probable, con la misma razón que el punto anterior.
De alguna manera, la recolección de basura se inicia y comienza a escanear todo mi montón de 5GB. Me puedo imaginar que esto puede tomar mucho tiempo.
El problema es que no tengo ni idea de cómo averiguar qué está pasando exactamente y qué hacer con esto. Debido a que el proceso de importación lleva tanto tiempo, los resultados de los perfiles no me muestran nada útil. Parece que no hay forma de activar y desactivar condicionalmente el generador de perfiles dentro del código.
Yo personalmente sospecho que el GC es el problema aquí. Estoy usando GHC7, que parece tener muchas opciones para modificar cómo funciona GC.
¿Qué configuraciones de GC recomiendas cuando usas montones grandes con datos generalmente muy estables?
El uso de memoria grande y picos de CPU ocasionales es casi seguro que el GC está funcionando. Puede ver si este es realmente el caso usando opciones de RTS como -B
, lo que hace que GHC emita un pitido cada vez que hay una colección importante, que le dirá estadísticas después del hecho (en particular, vea si los tiempos de GC son realmente largos) o -Dg
, que enciende la información de depuración para llamadas GC (aunque necesita compilar con -debug
).
Hay varias cosas que puede hacer para aliviar este problema:
En la importación inicial de los datos, GHC está perdiendo mucho tiempo creciendo el montón. Puede decirle que tome toda la memoria que necesita a la vez especificando una gran
-H
.Un gran montón con datos estables será promovido a una generación anterior. Si aumenta el número de generaciones con
-G
, puede lograr que los datos estables estén en la generación más antigua, muy raramente generada por GC, mientras que usted tiene los montones más jóvenes y viejos más tradicionales por encima.Dependiendo del uso de la memoria del resto de la aplicación, puede usar
-F
para ajustar cuánto GHC permitirá que la generación anterior crezca antes de volver a recolectarla. Es posible que pueda ajustar este parámetro para hacer que este no basura se recopile.Si no hay escrituras, y usted tiene una interfaz bien definida, puede valer la pena que GHC no administre esta memoria (use el C FFI) para que no haya posibilidad alguna de un super GC.
Todo esto es una especulación, por lo tanto, prueba con tu aplicación particular.
Tuve un problema muy similar con un montón de 1.5GB de Mapas anidados. Con el GC inactivo activado de manera predeterminada, obtendría 3-4 segundos de congelación en cada GC, y con el GC inactivo desactivado (+ RTS-I0), obtendría 17 segundos de congelación después de unos cientos de consultas, causando un tiempo de cliente -fuera.
Mi "solución" fue primero para aumentar el tiempo de espera del cliente y pedirle a la gente que tolere que, mientras que el 98% de las consultas eran de aproximadamente 500 ms, aproximadamente el 2% de las consultas sería lento. Sin embargo, al querer una solución mejor, terminé ejecutando dos servidores con equilibrio de carga y quitándolos del clúster para realizar CEG cada 200 consultas, y luego volviendo a la acción.
Para colmo de males, esta fue una reescritura de un programa original de Python, que nunca tuvo tales problemas. Para ser justos, obtuvimos un 40% de aumento en el rendimiento, una paralelización fácil de usar y una base de código más estable. Pero este molesto problema de GC ...