2011-07-08 12 views
46

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.¿Cómo ganar el control de un montón de 5 GB en Haskell?

Esperemos que alguien tenga más experiencia con procesos Haskell de larga ejecución que usan mucha memoria y pueden 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.

Un pequeño resumen de mi configuración:

  • En el inicio del servidor leí unos 5 gigabytes de datos en un gran (anidada) Estructura Data.Map-por igual 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 pocas razones que se me ocurre que podría causar que el uso de la CPU:

  • La solicitud sólo se necesita 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 trozos 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 se recomiendan cuando se utilizan grandes pilas con datos generalmente muy estables?

+1

Hmmm .. interesante ... la cantidad de RAM que tiene la caja en la que está ejecutando esta aplicación de servidor – Ankur

+0

Hay un total de 8GB de RAM en mi máquina. Eso debería ser suficiente. –

+0

Sí ... parece ser suficiente para evitar fallas en la página – Ankur

Respuesta

29

uso de memoria grande y los picos de CPU ocasionales es casi seguro que el GC patadas. Usted puede ver si este es el caso, mediante el uso de las opciones de estrategia en tiempo real como -B, lo que provoca GHC para emitir un tono cada vez que hay una colección importante, -t que será decirle estadísticas después del hecho (en particular, si los tiempos de GC son muy larga) o -Dg, que se convierte en la depuración de información para las 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 una gran cantidad de tiempo crecientes del montón. Puede decirle que tome toda la memoria que necesita a la vez especificando un 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, es posible que pueda obtener los datos estables en la generación más antigua, muy raramente GC'd, mientras que tiene los montones más jóvenes y viejos más tradicionales por encima.

  • Dependiendo del uso de 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 tiene una interfaz bien definida, puede valer la pena hacer que esta memoria no sea gestionada por GHC (utilice el C FFI) para que no haya posibilidad de un super GC .

Todo esto es una especulación, por lo que debe probar con su aplicación particular.

+5

El uso del GC de compactación también puede ser útil. –

+0

No lo he probado a fondo todavía, pero aumentar el número de generaciones parece tener un efecto positivo. –

2

que tenían un problema muy similar con un montón de 1,5 GB 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 el 98% de las consultas eran de aproximadamente 500ms, 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 ...

Cuestiones relacionadas