...

Optimización del grupo de subprocesos para servidores web: comparación entre Apache, NGINX y LiteSpeed

Este artículo muestra cómo la servidor web de grupo de subprocesos Configuración en Apache, NGINX y LiteSpeed Controla la paralelidad, la latencia y los requisitos de memoria. Explico qué ajustes son importantes bajo carga y dónde basta con el autoajuste, con claras diferencias en las solicitudes por segundo.

Puntos centrales

  • Arquitectura: Procesos/subprocesos (Apache) frente a eventos (NGINX/LiteSpeed)
  • Autoajuste: El ajuste automático reduce la latencia y las interrupciones.
  • Recursos: Los núcleos de la CPU y la RAM determinan los tamaños de subproceso adecuados.
  • Carga de trabajo: Las tareas con gran carga de E/S necesitan más subprocesos, las tareas con gran carga de CPU necesitan menos.
  • Sintonización: Los parámetros pequeños y específicos tienen un efecto mayor que los valores generales.

Comparación de arquitecturas de grupos de subprocesos

Empiezo con el Arquitectura, ya que define los límites del espacio de ajuste. Apache se basa en procesos o subprocesos por conexión, lo que consume más RAM y aumenta la latencia en horas punta [1]. NGINX y LiteSpeed siguen un modelo basado en eventos, en el que unos pocos trabajadores multiplexan muchas conexiones, lo que ahorra cambios de contexto y reduce la sobrecarga [1]. En las pruebas, NGINX procesó 6025,3 solicitudes/s, Apache alcanzó 826,5 solicitudes/s en el mismo escenario y LiteSpeed se situó a la cabeza con 69 618,5 solicitudes/s [1]. Si desea profundizar en la comparación de arquitecturas, encontrará más datos clave en Apache frente a NGINX, que utilizo para una primera clasificación.

También es importante cómo gestiona cada motor las tareas bloqueantes. NGINX y LiteSpeed desacoplan el bucle de eventos del sistema de archivos o la E/S ascendente mediante interfaces asíncronas y subprocesos auxiliares limitados. Apache vincula un subproceso/proceso por conexión en el modelo clásico; con MPM event se puede aliviar Keep-Alive, pero la huella de memoria por conexión sigue siendo mayor. En la práctica, esto significa que cuantos más clientes lentos o grandes cargas simultáneas haya, más rentable será el modelo de eventos.

Cómo funciona realmente el autoajuste

Los servidores modernos controlan la Hilo-A menudo automáticamente. El controlador comprueba la carga en ciclos cortos, compara los valores actuales con los históricos y escala el tamaño del grupo hacia arriba o hacia abajo [2]. Si una cola se atasca, el algoritmo acorta su ciclo y añade subprocesos adicionales hasta que el procesamiento vuelve a funcionar de forma estable [2]. Esto ahorra intervenciones, evita la sobreasignación y reduce la probabilidad de bloqueos en la cabeza de la cola. Como referencia, utilizo el comportamiento documentado de un controlador de autoajuste en Open Liberty, que describe claramente el mecanismo [2].

Para ello, presto atención a tres palancas: una Histéresis contra el aleteo (sin reacción inmediata a cada pico), un límite máximo estricto contra desbordamientos de RAM y una tamaño mínimo, para que no se produzcan costes de calentamiento con cada ráfaga. También es conveniente establecer un valor objetivo específico para activo Hilos (coreThreads) frente a hilos máximos (maxThreads). De este modo, el grupo permanece activo sin consumir recursos en reposo [2]. En entornos compartidos, reduzco la tasa de expansión para que el servidor web no consuma agresivamente ranuras de CPU frente a servicios vecinos [4].

Cifras clave de los índices de referencia

Los valores reales ayudan a Decisiones. En escenarios de ráfagas, NGINX destaca por su latencia muy baja y su alta estabilidad [3]. En condiciones de paralelismo extremo, Lighttpd ofrece en las pruebas el mayor número de solicitudes por segundo, seguido de cerca por OpenLiteSpeed y LiteSpeed [3]. NGINX logra transferencias de archivos grandes de hasta 123,26 MB/s, seguido de cerca por OpenLiteSpeed, lo que subraya la eficiencia de la arquitectura controlada por eventos [3]. Utilizo estos indicadores para evaluar dónde los ajustes de subprocesos realmente aportan beneficios y dónde los límites provienen de la arquitectura.

Servidor Modelo/Hilos Tasa de ejemplo mensaje clave
Apache Proceso/hilo por conexión 826,5 solicitudes/s [1] Flexible, pero mayor necesidad de RAM
NGINX Evento + pocos trabajadores 6.025,3 solicitudes/s [1] Bajo Latencia, económico
LiteSpeed Evento + LSAPI 69 618,5 solicitudes/s [1] Muy rápido, Ajuste de la interfaz gráfica de usuario
Lighttpd Evento + Asíncrono 28 308 solicitudes/s (alta paralelización) [3] Escala en Consejos muy bueno

La tabla muestra valores relativos. Ventajas, sin compromisos fijos. Siempre las evalúo en el contexto de mis propias cargas de trabajo: respuestas dinámicas cortas, muchos archivos estáticos pequeños o flujos grandes. Las desviaciones pueden deberse a la red, el almacenamiento, la descarga de TLS o la configuración de PHP. Por eso correlaciono métricas como el robo de CPU, la longitud de la cola de ejecución y el RSS por trabajador con el número de subprocesos. Solo esta perspectiva separa los verdaderos cuellos de botella de los subprocesos de los límites de E/S o de las aplicaciones.

Para obtener cifras fiables, utilizo fases de aceleración y comparo latencias p50/p95/p99. Una curva p99 pronunciada con valores p50 constantes, apunta más a colas que a una saturación pura de la CPU. Los perfiles de carga abiertos (controlados por RPS) en lugar de cerrados (controlados solo por concurrencia) también muestran mejor dónde el sistema comienza a descartar activamente las solicitudes. Esto me permite definir el punto en el que las elevaciones de subprocesos ya no sirven de nada y es más sensato utilizar la contrapresión o los límites de velocidad.

Práctica: Dimensionar trabajadores y conexiones

Empiezo con el CPU-Núcleos: los procesos de trabajo (worker_processes) o los trabajadores LSWS no deben superar los núcleos, ya que de lo contrario aumentará el cambio de contexto. Para NGINX, ajusto las conexiones de trabajo (worker_connections) de modo que la suma de las conexiones y los descriptores de archivos se mantenga por debajo del ulimit-n. En Apache evito MaxRequestWorkers demasiado altos, porque el RSS por hijo consume rápidamente la RAM. En LiteSpeed mantengo el equilibrio entre los grupos de procesos PHP y los trabajadores HTTP para que PHP no se convierta en un cuello de botella. Quien quiera comprender las diferencias de velocidad entre los motores, se beneficiará de la comparación. LiteSpeed frente a Apache, que utilizo como fondo de tuning.

Una regla empírica sencilla: primero calculo el presupuesto FD (ulimit-n menos la reserva para registros, upstreams y archivos), lo divido por las conexiones simultáneas previstas por trabajador y compruebo si la suma es suficiente para HTTP + upstream + búfer TLS. A continuación, dimensiono la cola de espera de forma moderada, lo suficientemente grande para los picos y lo suficientemente pequeña como para no ocultar la sobrecarga. Por último, configuro los valores de Keep Alive para que se ajusten a los patrones de solicitud: las páginas cortas con muchos activos se benefician de tiempos de espera más largos, mientras que el tráfico de API con pocas solicitudes por conexión se beneficia más de valores más bajos.

Ajuste fino de LiteSpeed para cargas elevadas

Con LiteSpeed, apuesto por LSAPI, porque minimiza los cambios de contexto. En cuanto noto que los procesos CHILD están agotados, aumento LSAPI_CHILDREN gradualmente de 10 a 40, y si es necesario hasta 100, acompañándolo siempre de comprobaciones de CPU y RAM [6]. La interfaz gráfica de usuario me facilita la creación de listeners, la liberación de puertos, los reenvíos y la lectura de .htaccess, lo que acelera los cambios [1]. Bajo carga continua, pruebo el efecto de pequeños pasos en lugar de grandes saltos para detectar pronto los picos de latencia. En entornos compartidos, reduzco los coreThreads cuando otros servicios consumen CPU, para que el autoajustador no mantenga demasiados subprocesos activos [2][4].

Además, observo Keep-Alive por oyente y el uso de HTTP/2/HTTP/3: la multiplexación reduce el número de conexiones, pero aumenta los requisitos de memoria por socket. Por lo tanto, mantengo los búferes de envío de forma conservadora y solo activo la compresión cuando la ganancia neta es clara (muchas respuestas textuales, casi sin límite de CPU). Para archivos estáticos grandes, confío en mecanismos de copia cero y limito las ranuras de descarga simultáneas para que los trabajadores de PHP no se queden sin recursos cuando se producen picos de tráfico.

NGINX: uso eficiente del modelo de eventos

Para NGINX, configuro worker_processes en coche o el número central. Con epoll/kqueue, accept_mutex activo y valores de backlog ajustados, mantengo las aceptaciones de conexión uniformes. Me aseguro de configurar keepalive_requests y keepalive_timeout de manera que los sockets inactivos no obstruyan el grupo FD. Los archivos estáticos grandes los transfiero con sendfile, tcp_nopush y un output_buffers adecuado. Solo utilizo la limitación de velocidad y los límites de conexión cuando los bots o los picos de tráfico sobrecargan indirectamente el grupo de subprocesos, ya que cada limitación genera una gestión de estado adicional.

En escenarios proxy, Mantenimiento de conexión ascendente Decisivo: si es demasiado bajo, se produce latencia en el establecimiento de la conexión; si es demasiado alto, se bloquean los FD. Elijo valores que se ajustan a la capacidad del backend y mantengo los tiempos de espera para conectar/leer/enviar claramente separados, de modo que los backends defectuosos no bloqueen los bucles de eventos. Con reuseport y la afinidad de CPU opcional, distribuyo la carga de forma más uniforme entre los núcleos, siempre que la configuración IRQ/RSS de la NIC lo permita. Para HTTP/2/3, calibro cuidadosamente los límites de encabezado y control de flujo para que las transmisiones grandes individuales no dominen toda la conexión.

Apache: configurar correctamente MPM event

En Apache utilizo evento en lugar de prefork, para que las sesiones Keep-Alive no ocupen permanentemente los trabajadores. Configuro MinSpareThreads y MaxRequestWorkers de manera que la cola de ejecución por núcleo se mantenga por debajo de 1. Mantengo el ThreadStackSize pequeño para que quepan más trabajadores en la RAM disponible; no debe ser demasiado pequeño, ya que de lo contrario se corre el riesgo de que se produzcan desbordamientos de pila en los módulos. Con un tiempo de espera de KeepAlive moderado y KeepAliveRequests limitados, evito que unos pocos clientes bloqueen muchos subprocesos. Traslado PHP a PHP-FPM o LSAPI para que el servidor web siga siendo ligero.

También presto atención a la relación entre ServerLimit, ThreadsPerChild y MaxRequestWorkers: estos tres factores determinan conjuntamente cuántos subprocesos se pueden crear realmente. Para HTTP/2 utilizo MPM event con límites de flujo moderados; los valores demasiado altos aumentan el consumo de RAM y los costes del programador. Solo cargo módulos con grandes cachés globales cuando son necesarios, ya que las ventajas de Copy-on-Write desaparecen cuando los procesos se ejecutan durante mucho tiempo y modifican la memoria.

RAM y subprocesos: calcular correctamente la memoria

Cuento los RSS por trabajador/hijo por el número máximo previsto y añado el búfer del núcleo y las cachés. Si no queda búfer, reduzco los subprocesos o nunca aumento el intercambio, porque el intercambio hace que la latencia se dispare. Para PHP-FPM o LSAPI, calculo además el PHP-RSS medio para que la suma del servidor web y SAPI se mantenga estable. Tengo en cuenta los costes de terminación TLS, ya que los handshakes de certificados y los grandes buffers de salida aumentan el consumo. Solo cuando el balance de RAM es coherente, sigo ajustando los hilos.

En HTTP/2/3, tengo en cuenta los estados adicionales de encabezado/control de flujo por conexión. GZIP/Brotli almacenan en búfer datos comprimidos y sin comprimir al mismo tiempo, lo que puede suponer varios cientos de KB adicionales por solicitud. También planifico reservas para registros y archivos temporales. En Apache, los valores más pequeños de ThreadStackSize aumentan la densidad, mientras que en NGINX y LiteSpeed influyen principalmente el número de sockets paralelos y el tamaño de los búferes de envío/recepción. Sumar todos los componentes antes del ajuste evita sorpresas desagradables más adelante.

Cuándo intervengo manualmente

Confío en Autoajuste, hasta que las métricas indiquen lo contrario. Si comparto la máquina en un alojamiento compartido, reduzco coreThreads o MaxThreads para que otros procesos mantengan suficiente tiempo de CPU [2][4]. Si existe un límite estricto de subprocesos por proceso, establezco maxThreads de forma conservadora para evitar errores del sistema operativo [2]. Si se producen patrones similares a un bloqueo, solo aumento el tamaño del grupo a corto plazo, observo las colas y luego lo vuelvo a reducir. Si desea comparar patrones típicos con valores medidos, encontrará pistas en el Comparación de la velocidad de los servidores web, que me gusta utilizar como comprobación de plausibilidad.

Como señales de intervención utilizo principalmente: picos p99 persistentes a pesar de la baja carga de la CPU, colas de socket en aumento, fuerte crecimiento de TIME_WAITNúmeros o un aumento repentino de FD abiertos. En tales casos, primero reduzco las suposiciones (límites de conexión/velocidad), desacoplo los backends con tiempos de espera y solo después aumento cuidadosamente los subprocesos. De esta manera, evito trasladar la sobrecarga solo al interior y empeorar la latencia para todos.

Errores típicos y comprobaciones rápidas

Veo a menudo alto Tiempo de espera de mantenimiento que vincula subprocesos aunque no fluyan datos. También es habitual: MaxRequestWorkers muy por encima del presupuesto de RAM y ulimit-n demasiado bajo para la paralelización objetivo. En NGINX, muchos subestiman el uso de FD por las conexiones ascendentes; cada backend cuenta doble. En LiteSpeed, los pools PHP crecen más rápido que los trabajadores HTTP, lo que hace que las solicitudes se acepten, pero se atiendan demasiado tarde. Con pruebas de carga cortas, comparación de heap/RSS y un vistazo a la cola de ejecución, encuentro estos patrones en cuestión de minutos.

También es frecuente que el syn-backlog sea demasiado pequeño, por lo que las conexiones rebotan antes de llegar al servidor web; los registros de acceso sin búfer, que se escriben de forma sincronizada en un almacenamiento lento; los registros de depuración/rastreo, que permanecen activos accidentalmente y consumen CPU. Al cambiar a HTTP/2/3, los límites de flujo y los búferes de encabezado demasiado generosos aumentan el consumo de memoria por conexión, lo que se nota especialmente cuando muchos clientes transfieren pocos datos. Por lo tanto, compruebo la distribución de respuestas cortas frente a largas y ajusto los límites en consecuencia.

HTTP/2 y HTTP/3: lo que significan para los grupos de subprocesos

La multiplexación reduce enormemente el número de conexiones TCP por cliente. Esto es bueno para los FD y los costes de aceptación, pero desplaza la presión a los estados por conexión. Por lo tanto, establezco límites prudentes para las transmisiones simultáneas en HTTP/2 y calibro el control de flujo para que las descargas grandes individuales no dominen la conexión. Con HTTP/3 se eliminan los bloqueos de cabeza de línea relacionados con TCP, pero aumenta el consumo de CPU por paquete. Lo compenso con suficiente capacidad de trabajo y tamaños de búfer pequeños para que la latencia se mantenga baja. En todos los casos se aplica lo siguiente: es mejor tener menos conexiones bien utilizadas con valores de keep-alive razonables que sesiones inactivas demasiado largas que ocupan subprocesos y memoria.

Factores de la plataforma: kernel, contenedores y NUMA

En cuanto a la virtualización, presto atención a Robo de CPU y límites de cgroups: si el hipervisor roba núcleos o el contenedor solo tiene núcleos parciales, worker_processes=auto puede ser demasiado optimista. Si es necesario, asigno trabajadores a núcleos reales y ajusto el número al eficaz presupuesto disponible. En los hosts NUMA, los servidores web se benefician de la asignación de memoria local; evito accesos innecesarios entre nodos agrupando los trabajadores por socket. A menudo desactivo las páginas enormes transparentes para cargas de trabajo críticas en cuanto a latencia, con el fin de evitar picos de fallos de página.

A nivel del sistema operativo, controlo los límites de los descriptores de archivos, los retrasos en las conexiones y el rango de puertos para las conexiones salientes. Solo aumento lo que realmente necesito, compruebo el comportamiento durante la renovación y mantengo estrictos los límites de seguridad. En cuanto a la red, me aseguro de que la distribución RSS/IRQ y la configuración MTU se ajusten al perfil de tráfico; de lo contrario, el ajuste del servidor web se echa a perder porque los paquetes llegan demasiado lentos o se quedan atascados en la cola de la NIC.

Medir en lugar de adivinar: guía práctica para las pruebas

Realizo pruebas de carga en tres etapas: calentamiento (cachés, JIT, sesiones TLS), meseta (RPS/concurrencia estables) y ráfaga (picos cortos). Los perfiles separados para archivos estáticos, llamadas a API y páginas dinámicas ayudan a ver de forma aislada dónde limitan los subprocesos, las E/S o los backends. Anoto paralelamente las cifras FD, las colas de ejecución, los cambios de contexto, el RSS por proceso y las latencias p50/p95/p99. Como objetivo, elijo puntos de funcionamiento con una carga de 70-85 %, lo que proporciona suficiente margen para las fluctuaciones reales sin funcionar permanentemente en el rango de saturación.

Guía para la toma de decisiones en resumen

Yo elijo NGINX, cuando lo que importa es una latencia baja, un uso eficiente de los recursos y opciones flexibles de ajuste .conf. Yo apuesto por LiteSpeed cuando predomina la carga PHP, la GUI debe simplificar el funcionamiento y LSAPI reduce los cuellos de botella. Recurro a Apache cuando dependo de módulos y .htaccess y tengo la configuración MPM-event bajo control. Los mecanismos de autoajuste son suficientes en muchos casos; solo tengo que intervenir cuando las métricas indican bloqueos, límites estrictos o presión de RAM [2]. Con presupuestos realistas de núcleo y RAM, pequeños incrementos y observación de las curvas de latencia, el ajuste de subprocesos me lleva de forma fiable a mi objetivo.

Artículos de actualidad