event Una variante del MPM worker con el objetivo de consumir hilos sólo para conexiones con procesamiento activo MPM event.c mpm_event_module

El Módulo de Multi-Proceso (MPM en inglés) event es, como su nombre indica, una implementación asíncrona basada en eventos, diseñada para permitir que se sirvan más peticiones simultáneamente mediante la concesión de algo de trabajo de procesamiento a los hilos "listeners" (de escucha), liberando a los hilos worker (trabajadores) para servir más peticiones.

Para usar el MPM event, añada --with-mpm=event a los parámetros del script configure cuando esté compilando httpd.

El MPM worker
Relación con el MPM Worker

event está basado en el MPM worker, que implmementa un servidor híbrido de multi-proceso multi-hilo. Un solo proceso (el padre) es responsable de lanzar procesos child (hijos). Cada proceso child crea un número fijo de hilos de servidor tal y como se especifica en la directiva ThreadsPerChild, así como un hilo listener que está en escucha para recibir conexiones y las pasa al hilo worker para procesamiento según van llegando.

Las directivas de configuración en tiempo real son idénticas a aquellas facilitadas por worker, con la única diferencia de que event además tiene la directiva AsyncRequestWorkerFactor.

Como Trabaja

El objetivo original de este MPM era arreglar el 'problema del keep alive' en HTTP. Después de que un cliente completa su primera petición, puede mantener la conexión abierta, enviando más peticiones utilizando el mismo socket y ahorrando una cantidad significativa de tiempo en abrir nuevas conexiones TCP. Sin embargo, el Servidor Apache HTTP tradicionalmente mantiene un proceso/hilo child esperando a que le lleguen datos del cliente, lo cual tiene sus propias desventajas. Para resolver este problema, este MPM usa un hilo dedicado de tipo listener en cada proceso junto con un grupo de hilos worker, compartiendo colas específicas para esas peticiones en modo keep-alive (o, más sencillamente, "readable"), aquellos en modo terminando-escritura, y aquellos en proceso de cerrarse ("closing"). Un bucle de eventos, activado por el estado de disponibilidad del socket, ajusta estas colas y manda el trabajo al grupo de workers.

Esta nueva arquitectura, haciendo uso de sockets no bloqueantes y características de kernel modernos expuestos por APR (como epoll de Linux), ya no necesita mpm-accept Mutex configurado para evitar el problema de thundering herd (manada estruendosa).

La cantidad total de conexiones que un solo bloque de proceso/hilo puede gestionar se regula con la directiva AsyncRequestWorkerFactor.

Conexiones Async

Las conexiones Async necesitarían un hilo worker fijo dedicado con los MPMs previos, pero no con event. La página de estado de mod_status muestra columnas nuevas bajo la sección de conexiones Async:

Writing
Mientras se envía la respuesta al cliente, puede ocurrir que el buffer de escritura TCP se llene porque la conexión es muy lenta. Generalmente en este caso un write() al socket devuelve EWOULDBLOCK o EAGAIN, para volver a estar disponible para escritura de nuevo después de un tiempo de inactividad. El worker que tiene en uso el socket podría ser capaz de liberar la tarea de espera al hilo listener, que a cambio la reasinará al primer hilo worker inactivo disponible una cuando se eleve un evento para el socket (por ejemplo, "el socket está ahora disponible para escritura"). Por favor compruebe la sección de Limitaciones para más información.
Keep-alive
La gestión de Keep Alive es la mejora más básica con el MPM worker. Cuando un hilo worker termina de vaciar la respuesta al cliente, puede descargar la carga de la gestion del socket al hilo listener, que a cambio esperará a cualquier evento del SO, como "el socket es legible". Si viene cualquier petición nueva del cliente, entonces el listener la enviará al primer hilo worker disponible. En cambio, si ocurre el KeepAliveTimeout entonces el el listener cerrará el socket. En este método los hilos worker no son responsables de los socket inactivos y pueden reutilizarse para atender otras peticiones.
Closing
A veces el MPM necesita realizar un cierre prolongado, concretamente enviar de vuelta un error al cliente mientras éste está todavía transmitiendo datos a httpd. Enviar la respuesta y entonces cerrar la conexión inmediatamente no es la forma correcta de proceder puesto que el cliente (que todavía está intentando enviar el resto de la petición) obtendría un connection reset y no podría leer la respuesta de httpd. Así que en estos casos, httpd intenta leer el resto de la petición para permitir al cliente consumir la respuesta. El cierre prolongado tiene tiempo limitado pero puede llevar relativamente cierto tiempo, así que un hilo worker puede descargar este trabajo al listener.

Estas mejoras son válidas para ambas conexiones HTTP/HTTPS.

Los estados de conexión indicados más arriba se gestionan por el hilo listener a través de colas dedicadas, que hasta la versión 2.4.27 se comprobaban cada 100ms para encontrar llegaban a configuración de timeout como Timeout y KeepAliveTimeout. Esto era una solución sencilla y eficiente, pero presentaba un problema, el pollset forzaba un wake-up del hilo listener incluso si no había necesidad (por ejemplo aunque estuviera completamente inactivo), malgastando recursos. A partir de la versión 2.4.28 estas colas se gestionarán completamente a través de la lógica basada en eventos, no dependiendo ya de un polling activo. Los entornos con pocos recursos, como servidores embebidos, se beneficiarán de esta mejora.

Cierre de procesos graceful y uso de Scoreboard

Este mpm mostró algunos cuellos de botella de escalabilidad en el pasado llevando al siguiente error: "scoreboard is full, not at MaxRequestWorkers". MaxRequestWorkers limita el número de peticiones simultáneas que van a ser atendidas en un momento dado y también el número de procesos permitidos (MaxRequestWorkers / ThreadsPerChild), mientras tanto el Scoreboard es una representación de todos los procesos que se están ejecutando y el estado de sus hilos worker. Si el scoreboard está lleno (de manera que todos los hilos tienen un estado que no es inactivo) pero el número de peticiones activas servidas no es MaxRequestWorkers, significa que algunos de ellos están bloqueando nuevas peticiones que podrían servirse pero que se están encolando en su lugar (hasta el límite impuesto por ListenBacklog). La mayor parte de las veces los hilos están atascados en estado Graceful, concretamente están esperando a finalizar su trabajo con una conexión TCP para cerrar y liberar limpiamente un hueco en el scoreboard (por ejemplo gestionando peticiones que duran mucho, clientes lentos con conexiones con keep-alive activado). Dos escenarios son muy comunes:

  • Durante un reinicio graceful. El proceso padre manda una señal a los procesos hijo para completar su trabajo y terminar, mientras que éste recarga la configuración y abre nuevos procesos. Si los hijos que estaban activos previamente siguen ejecutándose durante un tiempo antes de parar, el scoreboard estaría parcialmente ocupado hasta que esos huecos se liberaran.
  • Cuando la carga del servidor baja de manera que causa que httpd pare algunos procesos (por ejemplo debido a MaxSpareThreads), esto es particularmente problemático porque cuando la carga se incrementa de nuevo, httpd intentará arrancar nuevos procesos. Si el patrón se repite, el número de procesos puede incrementarse bastante, y se puede acabar con una mezcla de procesos antiguos intentando parar y nuevos intentando hacer algún trabajo.

Desde la versión 2.4.24 en adelante, mpm-event es más inteligente y es capaz de gestionar los reinicios graceful mucho mejor. Algunas de las mejoras que trae son:

  • Permitir el uso de todos los slots del scoreboard hasta ServerLimit. MaxRequestWorkers y ThreadsPerChild se usa para limitar la cantidad de procesos activos, mientras tanto ServerLimit también tiene en cuenta los que están haciendo un cierre graceful para permitir slots adicionales cuando sea necesario. La idea es usar ServerLimit para informar a httpd sobre cuántos procesos en total se toleran antes de impactar los recursos del sistema.
  • Forzar cierre graceful de procesos para cerrar sus conexiones en estado keep-alive.
  • Durante una parada graceful, si hay más hilos worker ejecutándose que conexiones abiertas para un proceso determinado, cerrar estos hilos para recuperar recursos más rápido (que podrían ser necesaios para nuevos procesos).
  • Si el scoreboard está lleno, previene que más procesos se cierren de manera graceful debido a una redirección de carga hasta que los antiguos procesos hayan terminado (si no la situación sería peor una vez que la carga subiera de nuevo).

El comportamiento descrito en el último punto se puede observar completamente a través de mod_status en la tabla de resumen de conexiones en dos nuevas columnas: "Slot" y "Stopping". La primera indica el PID y la última si el proceso está parando o no; el estado extra "Yes (old gen)" indica un proceso que todavía se está ejecutando después de un reinicio graceful.

Limitaciones

La gestión de conexión mejorada podría no funcionar para ciertos filtros de conexión que se han declarado incompatibles con event. En estos casos, este MPM retornará al comportamiento del MPM worker y reservará un hilo worker por conexión. Todos los módulos incluidos con el servidor son compatibles con el MPM event.

Una restricción similar está actualmente presente para peticiones involucradas en un filtro de salida que necesita leer y/o modificar el cuerpo completo de la respuesta. Si la conexión al cliente se bloquea mientras el filtro está procesando los datos, y la cantidad de datos producidos por el filtro es demasiado grande para meterse en buffer de memoria, el hilo usado para esta petición no se libera mientras httpd espera hasta que los datos pendientes se envían al cliente.
Para ilustrar este punto podemos sopesar las dos situaciones siguientes: servir un elemento estático (como por ejemplo un fichero CSS) en contraposición con servir contenido extraido de un servidor FCGI/CGI o un servidor al que se accede con servidor proxy. El primero es predecible, a saber, el MPM event tiene completa visibilidad en el final del contenido y puede usar eventos: el hilo worker sirviendo la respuesta puede hacer un desalojo de los primeros bytes hasta que se devuelve EWOULDBLOCK o EAGAIN, delegando el resto al listener. Este a cambio espera a un evento en el socket, y delega el trabajo para hacer una desalojo del resto del contenido al primero hilo worker inactivo. Mientras tanto, en el último ejemplo (FCGI/CGI/proxied content) el MPM no puede predecir el final de la respuesta y un hilo worker tiene que terminar su trabajo antes de devolver el control al listener. La única alternativa es almacenar la respuesta en un buffer de memoria, pero no sería la opción más segura en pos de la estabilidad del servidor y uso de memoria.

Trasfondo

El modelo event fue posible por la introducción de APIs en los sistemas operativos soportados:

  • epoll (Linux)
  • kqueue (BSD)
  • event ports (Solaris)

Antes de que estas APIs nuevas estuvieran disponibles, se tenían que usar las APIs tradicionales select y poll. Esas APIs se volvían lentas si se usaban para gestionar muchas conexiones o la posibilidad de un grupo de muchas conexiones repentinas era alta. Las nuevas APIs permiten controlar muchas más conexiones y trabajan mucho mejor cuando el grupo de conexiones a controlar cambia frecuentemente. Así que estas APIs hicieron posible que se desarrollara el MPM event, que escala mucho mejor con el patrón típico HTTP de muchas conexiones inactivas.

El MPM asume que la implementación subyacente de apr_pollset es razonablemente segura trabajando con hilos (threadsafe). Esto permite que el MPM evite un alto nivel de bloqueos, o tener que despertar el hilo listener para enviarle un socket keep-alive. Esto actualmente es sólo compatible con KQueue and EPoll.

Requerimientos

Este MPM depende de operaciones atómicas de comparar-y-cambiar de APR para sincronización de hilos. Si está compilando para una máquina x86 y no necesita soportar 386, o está compilando para SPARC y no necesita funcionar en chips pre-UltraSPARC, añada --enable-nonportable-atomics=yes a los parámetros del script configure. Esto hará que APR implemente operaciones atómicas usando los opcode eficientes no disponibles en CPU's más antiguas.

Este MPM no rinde bien en plataformas más antiguas que no tienen un buen sistema multihilo, pero el requerimiento de EPoll o KQueue hace esto irrelevante.

CoreDumpDirectory EnableExceptionHook Group Listen ListenBacklog SendBufferSize MaxRequestWorkers MaxMemFree MaxConnectionsPerChild MaxSpareThreads MinSpareThreads PidFile ScoreBoardFile ServerLimit StartServers ThreadLimit ThreadsPerChild ThreadStackSize User AsyncRequestWorkerFactor Limita el número de conexiones concurrentes por proceso AsyncRequestWorkerFactor factor 2 server config Disponible en versión 2.3.13 y posterior

El MPM event gestiona algunas conexiónes de manera asíncrona, donde hilos worker de petición están solo alojados por cortos periodos de tiempos según es necesario, y otras conexiones con un hilo worker de petición reservado por conexión. Esto puede llevar a situaciones donde todos los workers están trabajando y no hay ningun hilo worker disponible para gestionar nuevo trabajo en las conexiones asíncronas establecidas.

Para mitigar este problema, el MPM event hace dos cosas:

  • limita el número de conexiones aceptadas por proceso, dependiendo del número de hilos worker inactivos;
  • si todos los workers están ocupados, cerrará conexiones en estado keep-alive incluso si el timeout no ha expirado. Esto permite que los respectivos clientes reconecten a diferentes procesos que pueden tener todavía hilos worker disponibles.

Esta directiva puede usarse para afinar el límite de conexiones por-proceso. Un proceso solo aceptará conexiones nuevas si el número actual de conexiones (sin contar las que están en estado "closing") es menor que:

ThreadsPerChild + (AsyncRequestWorkerFactor * número de workers inactivos)

Una estimación del máximo de conexiones concurrentes entre todos los procesos dado un valor medio de hilos worker inactivos puede ser calculado con:

(ThreadsPerChild + (AsyncRequestWorkerFactor * número de workers inactivos)) * ServerLimit

Example ThreadsPerChild = 10 ServerLimit = 4 AsyncRequestWorkerFactor = 2 MaxRequestWorkers = 40 workers_inactivos = 4 (media de todos los procesos para mantenerlo sencillo) max_conexiones = (ThreadsPerChild + (AsyncRequestWorkerFactor * idle_workers)) * ServerLimit = (10 + (2 * 4)) * 4 = 72

Cuando todos los hilos worker están inactivos, entonces el máximo absoluto de conexiones concurrentes puede calcularse de una forma más sencilla::

(AsyncRequestWorkerFactor + 1) * MaxRequestWorkers

Example ThreadsPerChild = 10 ServerLimit = 4 MaxRequestWorkers = 40 AsyncRequestWorkerFactor = 2

Si todoso los procesos tienen hilos inactivos entonces:

idle_workers = 10

Podemos calcular el máximo absoluto de conexiones concurrentes de dos formas:

max_connections = (ThreadsPerChild + (AsyncRequestWorkerFactor * idle_workers)) * ServerLimit = (10 + (2 * 10)) * 4 = 120 max_connections = (AsyncRequestWorkerFactor + 1) * MaxRequestWorkers = (2 + 1) * 40 = 120

Configurar AsyncRequestWorkerFactor requiere conocimiento sobre el tráfico que se recibe por httpd y cada caso de uso específico, así que cambiar el valor por defecto requiere comprobaciones y extracción de datos intensivas desde mod_status.

MaxRequestWorkers se llamaba MaxClients antes de la versión 2.3.13. El valor de más arriba muestra que el nombre antiguo no describía de una manera certera su significado para el MPM event.

AsyncRequestWorkerFactor puede tomar valores numéricos no integrales, p. ej. "1.5".