[DOCUMENTACIÓN DE APACHE]

Servidor Apache de HTTP, Versión 1.3

Notas sobre el rendimiento de Apache

Autor: Dean Gaudet


Introducción

Apache es un servidor general de web, que está diseñado para ser primero correcto y después rápido. Aún así, su rendimiento es bastante satisfactorio. Muchos sitios tienen menos de 10 Mbits de ancho de banda de salida, que Apache puede ocupar usando tan sólo un servidor basado en un Pentium de gama baja. En la práctica, los sitios con más ancho de banda requieren más de una máquina para ocuparlo debido a otras restricciones (como sobrecarga por CGI o transacciones de bases de datos). Por esos motivos, el enfoque del desarrollo ha sido en mayor medida para la correctitud y configurabilidad.

Desafortunadamente, mucha gente pasa por alto esos hechos y cita números brutos de rendimiento como si fueran indicativos de la calidad de un servidor web. Hay un mínimo rendimiento básico que es aceptable, por encima del cual la velocidad adicional sólo abastece a un segmento mucho más reducido del mercado. Pero para evitar este obstáculo en la aceptación de Apache dentro de algunos mercados, en Apache 1.3 se puso el esfuerzo en aumentar el rendimiento hasta un punto en el que la diferencia con otros servidores web de gama alta fuese mínima.

Finalmente, están las personas que simplemente quieren ver cómo de rápido puede ir algo. El autor entra en esta categoría. El resto de este documento está dedicado a esas personas que quieren exprimir hasta la última gota de rendimiento del modelo actual de Apache, y que quieren entender por qué éste hace algunas cosas que lo ralentizan.

Nótese que este documento está orientado hacia Apache 1.3 en Unix. Algo de lo aquí indicado se aplica a Apache en NT. Apache en NT no ha sido afinado todavía para rendimiento; de hecho, es probable que se comporte pobremente, dado que el rendimiento en NT requiere un modelo de programación distinto.


Aspectos relativos al hardware y a los sistema operativos

El mayor problema aislado que afecta al rendimiento de los servidores web es la RAM. Un servidor web nunca jamás debería tener que hacer intercambio (swapping). El intercambio incrementa la latencia de cada petición mas allá del punto que los usuarios consideran como "suficientemente rápido". Esto provoca que los usuarios pulsen parada y recarga, incrementando adicionalmente la carga. Se puede, y debe, controlar los parámetros MaxClients para que el servidor no produzca tantos hijos que empiece a intercambiar.

Más allá de esto, el resto es mundano: conseguir una CPU suficientemente rápida, una tarjeta de red suficientemente rápida, y discos suficientemente rápidos, donde "suficientemente rápido" es algo que necesita determinarse mediante experimentación.

La elección del sistema operativo es principalmente un asunto de consideraciones locales. Pero una regla general es usar siempre los últimos parches TCP/IP del vendedor. El servicio de HTTP rompe completamente muchas de las asunciones integradas en los núcleos Unix en 1994 en incluso 1995. Buenas elecciones incluyen el reciente FreeBSD y Linux.


Aspectos relativos a la configuración en tiempo de ejecución

Búsquedas del nombre del anfitrión (HostnameLookups)

Antes de Apache 1.3, las búsquedas del nombre del anfitrión estaban activadas por defecto (On). Esto añade latencia a cada petición ya que requiere completar una búsqueda DNS antes de que la petición termine. En Apache 1.3, este valor está deshabilitado por defecto (Off). De cualquier modo (para 1.3 o versiones posteriores), si se usa cualquier directiva Allow from domain o Deny from domain (Permitido desde dominio o Denegado desde dominio), entonces se pagará una búsqueda DNS doble inversa (una inversa, seguida de una directa para asegurar que la inversa no está siendo enmascarada). Por tanto, para el mayor rendimiento, se debe evitar el uso de esas directivas (es permisible usar direcciones IP en lugar de nombres de dominio).

Nótese que es posible constreñir las directivas, por ejemplo dentro de una sección <Location /server-status>. En este caso, las búsquedas DNS son sólo realizadas para peticiones que respondan al criterio. Aquí hay un ejemplo que deshabilita las búsquedas excepto para ficheros .html y .cgi:

HostnameLookups off
<Files ~ "\.(html|cgi)$">
    HostnameLookups on
</Files>
Pero incluso así, si sólo se necesitan los nombres de DNS en algunos CGIs, se puede considerar la inclusión de la llamada gethostbyname en los CGIs que específicamente lo necesiten.

Seguir enlaces simbólicos y enlaces simbólicos si el propietario coincide (FollowSymLinks y SymLinksIfOwnerMatch)

En cualquier lugar dentro del espacio de URL que no tenga una opción para seguir enlaces simbólicos (Options FollowSymLinks) o que tenga una opción para enlaces simbólicos si el propietario coincide (Options SymLinksIfOwnerMatch), Apache tendrá que realizar llamadas adicionales al sistema para comprobar los enlaces simbólicos. Una llamada adicional por componente del nombre del fichero. Por ejemplo, si se tiene:

DocumentRoot /www/htdocs
<Directory />
    Options SymLinksIfOwnerMatch
</Directory>
y se hace una petición para el URI /index.html, entonces Apache realizará lstat(2) en /www, /www/htdocs, y /www/htdocs/index.html. Los resultados de estos lstats nunca son almacenados, por lo que ocurrirán en cada petición. Si realmente se desea la comprobación de seguridad de los enlaces simbólicos, se puede hacer algo como esto:
DocumentRoot /www/htdocs
<Directory />
    Options FollowSymLinks
</Directory>
<Directory /www/htdocs>
    Options -FollowSymLinks +SymLinksIfOwnerMatch
</Directory>
Esto, al menos, evita las comprobaciones adicionales para la ruta de la raíz del documento (DocumentRoot). Nótese la necesidad de añadir secciones similares si se tiene cualquier sinónimo (Alias) o rutas de reescritura de regla (RewriteRule) fuera de la raíz del documento. Para el mayor rendimiento y ninguna protección de enlaces simbólicos, se deben establece FollowSymLinks en todas partes, y nunca activar SymLinksIfOwnerMatch.

Permitir invalidaciones (AllowOverride)

Dondequiera que en el espacio de URL se permitan invalidaciones (típicamente en ficheros .htaccess), Apache tratará de abrir .htaccess para cada componente del nombre del fichero. Por ejemplo,

DocumentRoot /www/htdocs
<Directory />
    AllowOverride all
</Directory>
y se hace una petición para el URI /index.html, entonces Apache intentará abrir /.htaccess, /www/.htaccess, y /www/htdocs/.htaccess. Las soluciones son similares a las del caso previo con las opciones de seguimiento de enlaces simbólicos (Options FollowSymLinks). Para el mayor rendimiento, usar AllowOverride None en todas partes dentro del sistema de ficheros.

Negociación

Si es completamente posible, se debe evitar la negociación de contenido si realmente se está interesado en obtener hasta la última pizca de rendimiento. En la práctica, los beneficios de la negociación sobrepasan las penalizaciones en el rendimiento. Existe un caso en el que se puede acelerar el servidor. En lugar de usar un comodín como:

DirectoryIndex index
usar una lista completa de opciones:
DirectoryIndex index.cgi index.pl index.shtml index.html
en la que se listará primero la opción más corriente.

Creación de procesos

Antes de Apache 1.3, los valores de MinSpareServers, MaxSpareServers, y StartServers (mínimo número de servidores disponibles, máximo número de servidores disponibles y servidores iniciales, respectivamente) tenían efectos drásticos sobre los resultados en los bancos de pruebas. En concreto, Apache necesitaba un periodo de "ascenso" para alcanzar un número de hijos suficiente para servir la carga que estaba siendo aplicada. Después de la creación inicial de StartServers hijos, solamente podía crearse un hijo por segundo para safisfacer los valores de MinSpareServers. Así, un servidor al que accedían simultáneamente 100 clientes, usando por defecto el valor 5 para StartServers, necesitaría del orden de 95 segundos para crear suficientes hijos para manejar la carga. Esto funciona bien en la práctica en lo servidores reales, ya que no son reiniciados frecuentemente. Pero se comporta realmente mal en bancos de prueba que puede que sólo se ejecuten durante diez minutos.

La regla de uno-por-segundo se implementó en un esfuerzo para evitar inundar la máquina con la inicialización de nuevos hijos. Si la máquina está ocupada creando nuevos hijos, no puede atender peticiones. Pero ésta tiene un efecto tan drástico en la percepción del rendimiento de Apache que tuvo que ser reemplazada. Para Apache 1.3, el código relaja la regla de uno-por-segundo. Crea uno, espera un segundo, creará entonces dos, esperará un segundo, creará entonces cuatro, y continuará exponencialmente hasta que cree 32 hijos por segundo. Se detendrá cuando satisfaga el valor de MinSpareServers.

Esto parece responder suficientemente para que sea casi innecesario entretenerse con los valores de MinSpareServers, MaxSpareServers y StartServers. Cuando se creen más de 4 hijos por segundo, se emitirá un mensaje hacia el registro de errores (Errorlog). Si se observan muchos de esos errores, entonces se deberían refinar esos valores. Puede usarle la salida de mod_status ocomo guía.

Relacionada con la creación de procesos está la destrucción de procesos inducida por el valor de MaxRequestsPerChild (número máximo de peticiones por hijo). Por defecto éste es 0, lo que significa que no hay límite en el número de peticiones manejadas por hijo. Si la configuración contiene actualmente este valor establecido a un número bastante bajo, como 30, puede que se desee aumentarlo de un modo significativo. Si se está utilizando SunOS o una versión antigua de Solaris, se debería limitar a 10000 o así debido a las filtraciones de memoria.

Cuando esté activada la opción de mantener activos (keep-alives), los hijos serán mantenidos ocupados sin hacer nada mientras esperan más peticiones en la conexión ya establecida. El valor por defecto de 15 para KeepAliveTimeout (fin del plazo para mantener activos) trata de minimizar este efecto. Aquí el conflicto está entre el ancho de banda de la red y los recursos del servidor. Por ningún motivo se debería aumentar este valor por encima de 60 segundos, ya que se pierden la mayoría de los beneficios.


Aspectos relativos a la configuración en tiempo de compilación

mod_status y ExtendedStatus On

Si se incluye mod_status y también se establece ExtendedStatus On (estado extendido) cuando se construya y ejecute Apache, entonces para cada petición Apache realizará dos llamadas a gettimeofday(2) (o times(2), dependiendo del sistema operativo), y (antes de 1.3) diversas llamadas adicionales a time(2). Todo esto se hace para que el informe de estado incluya indicaciones de temporización. Para el mayor rendimiento, establecer ExtendedStatus off (que es el valor por defecto).

Aceptar serialización - sockets múltiples

Esto discute un defecto en la API de sockets de Unix. Supongamos que el servidor web utiliza varias instrucciones Listen para escuchar ya sea en varios puertos o en varias direcciones. Para comprobar cada socket para ver si hay alguna conexión lista, Apache utiliza select(2). select(2) indica que un socket tiene cero  o al menos una  conexion esperando en él. El modelo de Apache incluye multiples hijos, y todos los que están ociosos examinan al mismo tiempo si tienen nuevas conexiones. Una implementacion sencilla se parece algo a la siguiente (estos ejemplos no corresponden al codigo, han sido ideados con propósitos pedagógicos):

    for (;;) {
	for (;;) {
	    fd_set accept_fds;

	    FD_ZERO (&accept_fds);
	    for (i = primer_socket; i <= ultimo_socket; ++i) {
		FD_SET (i, &accept_fds);
	    }
	    rc = select (ultimo_socket+1, &accept_fds, NULL, NULL, NULL);
	    if (rc < 1) continue;
	    nueva_conexion = -1;
	    for (i = primer_socket; i <= ultimo_socket; ++i) {
		if (FD_ISSET (i, &accept_fds)) {
		    nueva_conexion = accept (i, NULL, NULL);
		    if (nueva_conexion != -1) break;
		}
	    }
	    if (nueva_conexion != -1) break;
	}
	procesar la nueva_conexion;
    }
Pero esta ingenua implementación tiene un serio problema de inanición. Recordemos que varios hijos realizan este bucle al mismo tiempo, y por tanto varios hijos se verán bloqueados en el select cuando estén entre peticiones. Todos esos hijos despertarán y volverán desde el select cuando una petición aparezca en cualquier socket (el número de hijos despertados varía dependiendo del sistema operativo y de las características de temporización). Entonces, todos caerán dentro del bucle e intentarán aceptar (accept) la conexión. Pero sólo uno lo conseguirá (asumiendo que sólo haya una conexión preparada), el resto se vera bloqueado en el accept. Esto efectivamente obliga a esos hijos a servir peticiones desde ese socket y no desde otro, y permanecerán bloqueados allí hasta que haya suficientes peticiones en ese socket como para despertarlos a todos. Este problema de inanición se documentó inicialmente en PR#467. Existen, al menos, dos soluciones.

Una solucion es hacer el socket no bloqueante. En este caso la llamada a accept no bloqueará a los hijos, y se les permitirá continuar inmediatamente. Pero esto desperdicia tiempo de CPU. Supongamos que se tienen diez hijos ociosos en el select, y llega una conexión. Entonces, nueve de esos hijos serán despertados, intentarán aceptar (accept) la conexión, fallarán, y retornarán al select, sin haber conseguido nada. Mientras, ninguno de esos hijos está sirviendo las peticiones que ocurran en otros sockets hasta que vuelvan de nuevo al select. En general, esta solución no parece demasiado gratificante a menos que se disponga de tantas CPUs desocupadas (en un sistema multiprocesador) como hijos ociosos, una situación no muy común,

Otra solución, la que Apache utiliza, es serializar la entrada al bucle interno. El bucle aparece así (diferencias resaltadas):

    for (;;) {
	accept_mutex_on ();
	for (;;) {
	    fd_set accept_fds;

	    FD_ZERO (&accept_fds);
	    for (i = primer_socket; i <= ultimo_socket; ++i) {
		FD_SET (i, &accept_fds);
	    }
	    rc = select (ultimo_socket+1, &accept_fds, NULL, NULL, NULL);
	    if (rc < 1) continue;
	    nueva_conexion = -1;
	    for (i = primer_socket; i <= ultimo_socket; ++i) {
		if (FD_ISSET (i, &accept_fds)) {
		    nueva_conexion = accept (i, NULL, NULL);
		    if (nueva_conexion != -1) break;
		}
	    }
	    if (nueva_conexion != -1) break;
	}
	accept_mutex_off ();
	procesar la nueva_conexion;
    }
Las funciones accept_mutex_on y accept_mutex_off implementan un semáforo de exclusión mutua. Solamente un hijo puede poseer el semáforo en un momento dado. Hay diversas opciones para la implementación de esos semáforos. La elección esta definida en src/conf.h (antes de 1.3) o src/include/ap_config.h (1.3 o posteriores). Algunas arquitecturas no tienen ninguna elección hecha para los cerrojos, en esas arquitecturas es inseguro usar varias directivas Listen.
USE_FLOCK_SERIALIZED_ACCEPT
Este método usa la llamada al sistema flock(2) para bloquear un fichero de bloqueos (localizado en la directiva LockFile).
USE_FCNTL_SERIALIZED_ACCEPT
Este método usa la llamada al sistema fcntl(2) para bloquear un fichero de bloqueos (localizado en la directiva LockFile).
USE_SYSVSEM_SERIALIZED_ACCEPT
(1.3 o posteriores) Este método utiliza semáforos estilo SysV para implementar la exclusión mutua. Desafortunadamente, los semáforos estilo SysV tienen algunos efectos colaterales indeseados. Uno es que puede que Apache termine sin eliminar el semáforo (véase la página del manual para ipcs(8)). La otra es que la API del semáforo permite ataques de denegación de servicio desde cualquier CGIs que se ejecute con el mismo identificador de usuario (uid) que el servidor web (esto es, todos los CGIs, a menos que se use algo como suexec o cgiwrapper). Por estos motivos este método no se utiliza en ninguna arquitectura salvo IRIX (las dos opciones anteriores son prohivitivamente caras en la mayoría de los sistemas IRIX).
USE_USLOCK_SERIALIZED_ACCEPT
(1.3 o posteriores). Este método sólo está disponible en IRIX, y usa usconfig(2) para crear un semáforo de exclusión mutua. Mientras que este método evita los riesgos de los semáforos estilo SysV, no es el utilizado por defecto en IRIX. Esto es así porque en máquinas IRIX con un único procesador (5.3 ó 6.2), el código uslock es dos veces mas lento que el código de los semáforos estilo SysV. En máquinas multiprocesador IRIX, el código uslock es un orden de magnitud más rápido que el código de los semáforos estilo SysV. Una situación un tanto liosa. Así que si se está usando una máquina IRIX multiprocesador, se debería reconstruir el servidor web con -DUSE_USLOCK_SERIALIZED_ACCEPT el los EXTRA_CFLAGS.
USE_PTHREAD_SERIALIZED_ACCEPT
(1.3 o posteriores) Este método usa semáforos de exclusión mutua POSIX y debería funcionar en cualquier arquitectura que implemente la especificación completa de las hebras POSIX, aunque sólo parece funcionar en Solaris (2.5 o posteriores, e incluso entonces sólo en algunas configuraciones). Si se experimenta con esta opción se deberías vigilar si el servidor se cuelga y no responde. Los servidores que sólo sirven contenido estáticos podrían funcionar simplemente bien.

Si su sistema tiene otro método de serializacion que no se encuentra en la lista de arriba, entonces podría valer la pena añadir código para él (y enviar un parche a Apache).

Otra solución que ha sido considerada pero nunca implementada es serializar parcialmente el bucle - esto es, permitir la entrada a cierto número de procesos. Esto sólo podría ser interesante en máquinas multiprocesador donde es posible que varios hijos puedan ejecutarse simultáneamente y la serialización realmente no se beneficia del ancho de banda completo. Ésta es un área de posible investigación futura, pero su prioridad permanece baja ya que los servidores web altamente paralelizables no son la norma.

Idealmente, debería ejecutar servidores sin múltiples instrucciones Listen si desea obtener el máximo rendimiento. Pero siga leyendo.

Serialización del accept - socket único

Lo arriba indicado está bien para servidores con múltiples sockets, pero, ¿ qué ocurre con los servidores con un único socket? En teoría, no deberían experimentar ninguno de esos mismos problemas ya que todos los hijos pueden simplemente bloquearse en la llamada a accept(2) hasta que llegue una conexión, y no se presenta inanición. En la práctica, esto oculta casi el mismo comportamiento "enredado" discutido arriba en la solución no bloqueante. En el modo en que la mayoría de las pilas TCP están implementadas, el núcleo realmente despierta a todos los procesos bloqueados en el accept cuando llega una única conexión. Uno de esos procesos retiene la conexión y regresa al espacio de usuario y los demás se enredan en el núcleo y vuelven a dormir cuando descubren que no hay conexión para ellos. Este enredo esta oculto para el mundo del código del usuario, pero pese a todo allí está. Esto puede resultar en el mismo comportamiento derrochador de picos de carga que el que puede derivarse de una solución no bloqueante de múltiples sockets.

Por este motivo hemos encontrado que muchas arquitecturas se comportan mas "dulcemente" si serializamos hasta en el caso de socket único. Así que ésta es la configuración por defecto en la mayoría de los casos. Experimentos sencillos bajo Linux (2.0.30 en un Pentium Pro dual 166 w/128 Mb RAM) han demostrado que la serializacion en el caso de socket único causa un descenso de menos del 3% en las peticiones por segundo frente a un socket único no serializado. Pero los socket únicos no serializados muestra unos 100ms extra de latencia en cada petición. Esta latencia es probablemente una pincelada en líneas de largo alcance, y sólo un problema en redes de área local (LANs). Si se quiere pasar por alto la serializacion para socket único se puede definir SINGLE_LISTEN_UNSERIALIZED_ACCEPT y entonces los servidores de socket único no serializarán en absoluto.

Cierre Lento (Lingering Close)

Como se discute en la sección 8 del borrador-ietf-http-conexion-00.txt, para que un servidor HTTP implemente confiablemente el protocolo, necesita terminar cada dirección de la comunicación independientemente (recalcar que una conexión TCP es bidireccional, cada mitad independiente de la otra). Este hecho es pasado por alto por otros servidores, pero está correctamente implementado en Apache desde 1.2.

Cuando se añadió esta característica a Apache, causó muchos problemas en diversas versiones de Unix por una falta de vista. La especificación TCP no indica que el estado FIN_WAIT_2 tiene un plazo de expiración, pero no lo prohíbe. En sistemas sin este plazo, Apache 1.2 induce a que muchos sockets permanezcan en el estado FIN_WAIT_2 para siempre. En muchos casos esto puede evitarse simplemente actualizando con los últimos parches TCP/IP proporcionados por el vendedor. En los casos para los que el vendedor nunca proporciona parches (esto es, SunOS4 - aunque ciertas personas con una licencia para el código pueden parchearlo ellos mismos) hemos decidido deshabilitar esta característica.

Hay dos maneras de conseguir esto. Una es la opción del socket SO_LINGER. Pero desgraciadamente, no ha sido implementada correctamente en la mayoría de las pilas TCP/IP. Incluso en aquellas pilas con una implementación correcta (Linux 2.0.31) este método resulta ser mas caro (en tiempo de CPU) que la siguiente solución.

En la mayor parte, Apache implementa esto en una función llamada lingering_close (en http_main.c). La función es, grosso modo, como ésta:

    void lingering_close (int s)
    {
	char junk_buffer[2048];
	/*  Terminar el lado que envia */
	shutdown (s, 1);

	signal (SIGALRM, lingering_death);
	alarm (30);

	for (;;) {
	    select (s para leer, 2 segundos de expiración de plazo);
	    if (error) break;
	    if (s esta lista para lectura) {
		if (read (s, junk_buffer, sizeof (junk_buffer)) <= 0) {
		    break;
		}
		/* simplemente arroja lo que haya sido leido */
	    }
	}

	close (s);
    }
Esto naturalmente añade cierto gasto al final de una conexión, pero es necesario para una implementación confiable. Mientras HTTP/1.1 se hace más común, y todas las conexiones se hacen persistentes, este gasto será amortizado por más peticiones. Si se quiere jugar con fuego y deshabilitar esta capacidad, se puede definir NO_LINGCLOSE, pero no se recomienda en absoluto. En concreto, al usarse las conexiones persistentes entubadas (pipelined) de HTTP/1.1, lingering_close es un necesidad absoluta (y las conexiones entubadas son más rápidas, así que se querrá darles soporte).

Fichero marcador

Los padres e hijos de Apache se comunican entre ellos a través de algo llamado el marcador. Idealmente, esto debería implementarse en memoria compartida. Para aquellos sistemas operativos para los que se tiene acceso o para los que han sido portados detalladamente, esto se implementa típicamente usando memoria compartida. El resto por defecto utiliza un fichero en disco. El fichero en disco no resulta tan lento, pero no es confiable (y tiene menos capacidades). Lea atentamente el fichero src/main/conf.h para su arquitectura y busque USE_MMAP_SCOREBOARD o USE_SHMGET_SCOREBOARD. Definir una de las dos (como sus acompañantes HAVE_MMAP y HAVE_SHMGET respectivamente) habilita el código proporcionado para memoria compartida. Si su sistema tiene otro tipo de memoria compartida, edite el fichero src/main/http_main.c y añada el código necesario para usarla con Apache. (Envíenos también un parche, por favor).

Nota histórica: La implementación de Apache para Linux no comenzó a usar memoria compartida hasta la versión 1.2 de Apache. Este descuido resultó en un comportamiento realmente pobre y nada confiable de las versiones iniciales de Apache para Linux.

DYNAMIC_MODULE_LIMIT

Si no tiene intención de usar módulos cargados dinámicamente (probablemente no tendrá si está leyendo esto y afinando su servidor para obtener hasta la última gota de rendimiento), entonces debería añadir -DDYNAMIC_MODULE_LIMIT=0 al construir su servidor. Esto ahorrará RAM que es asignada únicamente para el soporte de módulos cargados dinámicamente.


Apéndice: Análisis detallado de una traza

Aquí presentamos la traza de llamadas al sistema de Apache 1.3 ejecutándose bajo Linux. El fichero de configuración en tiempo de ejecución es esencialmente el que se tiene por defecto más:
<Directory />
    AllowOverride none
    Options FollowSymLinks
</Directory>
El fichero que se está solicitando en un fichero estático de 6K sin ningún contenido particular. Las trazas de las peticiones no estáticas o peticiones con negociación de contenido son totalmente diferentes (y bastante horribles en algunos casos). Primero, la traza completa, después examinaremos los detalles. (Esto ha sido generado por el programa strace, otros programas similares incluyen truss, ktrace, y par).
accept(15, {sin_family=AF_INET, sin_port=htons(22283), sin_addr=inet_addr("127.0.0.1")}, [16]) = 3
flock(18, LOCK_UN)                      = 0
sigaction(SIGUSR1, {SIG_IGN}, {0x8059954, [], SA_INTERRUPT}) = 0
getsockname(3, {sin_family=AF_INET, sin_port=htons(8080), sin_addr=inet_addr("127.0.0.1")}, [16]) = 0
setsockopt(3, IPPROTO_TCP1, [1], 4)     = 0
read(3, "GET /6k HTTP/1.0\r\nUser-Agent: "..., 4096) = 60
sigaction(SIGUSR1, {SIG_IGN}, {SIG_IGN}) = 0
time(NULL)                              = 873959960
gettimeofday({873959960, 404935}, NULL) = 0
stat("/home/dgaudet/ap/apachen/htdocs/6k", {st_mode=S_IFREG|0644, st_size=6144, ...}) = 0
open("/home/dgaudet/ap/apachen/htdocs/6k", O_RDONLY) = 4
mmap(0, 6144, PROT_READ, MAP_PRIVATE, 4, 0) = 0x400ee000
writev(3, [{"HTTP/1.1 200 OK\r\nDate: Thu, 11"..., 245}, {"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 6144}], 2) = 6389
close(4)                                = 0
time(NULL)                              = 873959960
write(17, "127.0.0.1 - - [10/Sep/1997:23:39"..., 71) = 71
gettimeofday({873959960, 417742}, NULL) = 0
times({tms_utime=5, tms_stime=0, tms_cutime=0, tms_cstime=0}) = 446747
shutdown(3, 1 /* send */)               = 0
oldselect(4, [3], NULL, [3], {2, 0})    = 1 (in [3], left {2, 0})
read(3, "", 2048)                       = 0
close(3)                                = 0
sigaction(SIGUSR1, {0x8059954, [], SA_INTERRUPT}, {SIG_IGN}) = 0
munmap(0x400ee000, 6144)                = 0
flock(18, LOCK_EX)                      = 0

Nótese la serialización del accept:

flock(18, LOCK_UN)                      = 0
...
flock(18, LOCK_EX)                      = 0
Estas dos llamadas pueden ser eliminadas definiendo SINGLE_LISTEN_UNSERIALIZED_ACCEPT como se describió previamente.

Nótese la manipulación de SIGUSR1:

sigaction(SIGUSR1, {SIG_IGN}, {0x8059954, [], SA_INTERRUPT}) = 0
...
sigaction(SIGUSR1, {SIG_IGN}, {SIG_IGN}) = 0
...
sigaction(SIGUSR1, {0x8059954, [], SA_INTERRUPT}, {SIG_IGN}) = 0
Esto es provocado por la implementación de reinicializaciones "elegantes". Cuando un padre recibe un SIGUSR1, envía un SIGUSR1 a todos sus hijos (y esto también incrementa un "contador de generaciones" en memoria compartida). Cualquier hijo que estuviera ocioso (entre conexiones) sería destruido tras recibir la señal. Pero cualquier hijo que tuviese una conexión y estuviera todavía esperando la primera conexión no sería destruido inmediatamente.

Para ver por qué esto es necesario, considérese cómo un navegador reacciona frente a una conexión cerrada. Si la conexión era del tipo mantener-activo (keep-alive) y la petición que estaba siendo servida no era la primera petición, entonces el navegador tranquilamente realizará la petición sobre una nueva conexión. Tiene que hacer esto ya que el servidor siempre siempre es libre de terminar una conexión mantener-activo entre peticiones (esto es, debido a una expiración de plazo (timeout) o al número máximo de peticiones). Pero si la conexión se cierra antes de que la primera respuesta haya sido recibida, el navegador típico mostrará un mensaje de "El documento no contiene datos" ("document contains no data") o un icono de una imagen rota. Esto se hace bajo la suposición de que el servidor está de algún modo roto (o quizá demasiado sobrecargado para responder). Así que Apache trata de evitar cerrar deliberadamente una conexión antes de que ésta haya enviado una respuesta. Ésta es la razó de esas manipulaciones de SIGUSR1.

Nótese que es teóricamente posible eliminar las tres llamadas. Pero en pruebas toscas la ganancia era casi imperceptible.

Para implementar los anfitriones virtuales, Apache necesita conocer la dirección del socket local usada para aceptar la conexión:

getsockname(3, {sin_family=AF_INET, sin_port=htons(8080), sin_addr=inet_addr("127.0.0.1")}, [16]) = 0
Es posible eliminar esta llamada en muchas situaciones (como cuando no hay anfitriones virtuales, o cuando las directivas Listen usadas no tienen direcciones comodín). Pero todavía no se han hecho esfuerzos para realizar esas optimizaciones.

Apache deshabilita el algoritmo Nagle:

setsockopt(3, IPPROTO_TCP1, [1], 4)     = 0
por los problemas descritos en un documento de John Heidemann.

Nótense las dos llamadas a time:

time(NULL)                              = 873959960
...
time(NULL)                              = 873959960
Una de ellas sucede al comienzo de la petición, y la otra ocurre como resultado de la escritura del registro (log). Al menos una de ellas se necesita para implementar correctamente el protocolo HTTP. La segunda ocurre porque el formato común para registros (Common Log Format) dicta que la entrada en el registro incluya una señal del tiempo del final de la petición. Un módulo a medida para registros podría eliminar una de esas llamadas. O podría usarse un método para trasladar el tiempo a memoria compartida, véase la sección de parches más adelante.

Como se describió previamente, ExtendedStatus On origina dos llamadas a gettimeofday y una llamada a times:

gettimeofday({873959960, 404935}, NULL) = 0
...
gettimeofday({873959960, 417742}, NULL) = 0
times({tms_utime=5, tms_stime=0, tms_cutime=0, tms_cstime=0}) = 446747
Pueden eliminarse estableciendo ExtendedStatus Off ( que es el valor por defecto ).

Podría parecer raro llamar a stat:

stat("/home/dgaudet/ap/apachen/htdocs/6k", {st_mode=S_IFREG|0644, st_size=6144, ...}) = 0
Esto es parte del algoritmo que calcular la información de ruta (PATH_INFO) que será usada por los CGIs. De hecho, si la petición hubiese sido para el URI /cgi-bin/printenv/foobar, entonces se hubiésen tenido dos llamadas a stat. La primera para /home/dgaudet/ap/apachen/cgi-bin/printenv/foobar, que no existe, y la segunda para /home/dgaudet/ap/apachen/cgi-bin/printenv, que existe. Pese a todo, al menos se necesita una llamada a stat cuando se están sirviendo ficheros estáticos debido a que el tamaño de fichero y los tiempos de modificación se usan para generar cabeceras HTTP (como Content-Length, Last-Modified) e implementar características del protocolo (como If-Modified-Since). Un servidor algo más inteligente hubiese podido evitar los stat al servir ficheros no estáticos, aunque hacer esto en Apache es muy difícil dada la estructura modular

Todos los ficheros estáticos son servidor usando mmap:

mmap(0, 6144, PROT_READ, MAP_PRIVATE, 4, 0) = 0x400ee000
...
munmap(0x400ee000, 6144)                = 0
En algunas arquitecturas es más lento hacer mmap sobre ficheros pequeños que simplemente leerlos con read. La definición de MMAP_THRESHOLD puede establecerse al valor mínimo requerido antes de usar mmap. Por defecto, está establecido a 0 (excepto en SunOS4, donde la experimentación ha mostrado que 8192 es un valor más adecuado). Usando una herramienta como lmbench puede determinarse el valor óptimo para cada entorno.

También podría querer experimentarse con MMAP_SEGMENT_SIZE (valor por defecto 32768) que determina el mayor número de octetos que serán escritos de una vez para ficheros mmap()eados. Apache sólo resetea los plazos de expiración (Timeout) del cliente entre escrituras (write()). Así que establecer un valor grande podría bloquear a los clientes con bajo ancho de banda a menos que también se incremente el valor de Timeout.

Puede incluso darse el caso de que mmap no sea usado para su arquitectura; si es así, entonces el definir USE_MMAP_FILES y HAVE_MMAP podría funcionar (si funciona, infórmenos de ello).

Apache hace todo lo que puede para evitar recopiar octetos dentro de memoria. La primera escritura de cualquier petición típicamente se transforma en una writev que combina las cabeceras y el primer trozo de datos:

writev(3, [{"HTTP/1.1 200 OK\r\nDate: Thu, 11"..., 245}, {"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 6144}], 2) = 6389
Mientras hace el codificado por piezas requerido por HTTP/1.1, Apache generará hasta cuatro elementos writev. El objetivo es trasladar la copia de octetos al núcleo, donde típicamente tiene que ocurrir de cualquier modo (para ensamblar paquetes de red). El las pruebas, varios sistemas Unix (BSDI 2.x, Solaris 2.5, Linux 2.0.31+) combinan adecuadamente los elementos en paquetes de red. Las versiones de Linux anteriores a 2.0.31 no los combinan, y crearán un paquete para cada elemento, así que actualizar es una buena idea. Definir NO_WRITEV deshabilitará este combinado, pero resultará en un rendimiento de codificación por piezas muy pobre.

La escritura en el registro:

write(17, "127.0.0.1 - - [10/Sep/1997:23:39"..., 71) = 71

puede ser retardada definiendo BUFFERED_LOGS. En este caso, hasta PIPE_BUF octetos (una constante definida por POSIX) de entradas del registro son almacenados antes de la escritura. Nunca separará una entrada del registro entre límites de PIPE_BUF, ya que estas escrituras podrían no ser atómicas. (esto es, entradas de varios hijos podrían llegar a estar entremezcladas). El código hace todo lo que puede para vaciar este almacén cuando un hijo muere.

El código de cierre lento origina cuatro llamadas al sistema:

shutdown(3, 1 /* send */)               = 0
oldselect(4, [3], NULL, [3], {2, 0})    = 1 (in [3], left {2, 0})
read(3, "", 2048)                       = 0
close(3)                                = 0
que fueron descritas anteriormente.

Apliquemos algunas de estas optimizaciones: -DSINGLE_LISTEN_UNSERIALIZED_ACCEPT -DBUFFERED_LOGS y ExtendedStatus Off. Aquí está la traza final:

accept(15, {sin_family=AF_INET, sin_port=htons(22286), sin_addr=inet_addr("127.0.0.1")}, [16]) = 3
sigaction(SIGUSR1, {SIG_IGN}, {0x8058c98, [], SA_INTERRUPT}) = 0
getsockname(3, {sin_family=AF_INET, sin_port=htons(8080), sin_addr=inet_addr("127.0.0.1")}, [16]) = 0
setsockopt(3, IPPROTO_TCP1, [1], 4)     = 0
read(3, "GET /6k HTTP/1.0\r\nUser-Agent: "..., 4096) = 60
sigaction(SIGUSR1, {SIG_IGN}, {SIG_IGN}) = 0
time(NULL)                              = 873961916
stat("/home/dgaudet/ap/apachen/htdocs/6k", {st_mode=S_IFREG|0644, st_size=6144, ...}) = 0
open("/home/dgaudet/ap/apachen/htdocs/6k", O_RDONLY) = 4
mmap(0, 6144, PROT_READ, MAP_PRIVATE, 4, 0) = 0x400e3000
writev(3, [{"HTTP/1.1 200 OK\r\nDate: Thu, 11"..., 245}, {"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 6144}], 2) = 6389
close(4)                                = 0
time(NULL)                              = 873961916
shutdown(3, 1 /* send */)               = 0
oldselect(4, [3], NULL, [3], {2, 0})    = 1 (in [3], left {2, 0})
read(3, "", 2048)                       = 0
close(3)                                = 0
sigaction(SIGUSR1, {0x8058c98, [], SA_INTERRUPT}, {SIG_IGN}) = 0
munmap(0x400e3000, 6144)                = 0

Esto es, 19 llamadas al sistema, de las cuales cuatros son relativamente fáciles de eliminar, pero no merecen el esfuerzo.

Apéndice: Parches disponibles

  Existen bastantes parches de rendimiento disponibles para 1.3. Aunque puede que no sean limpiamente aplicables a la versión actual, no debería ser difícil actualizarlos para alguien con un poco conocimiento de C. En concreto:

Apéndice: El modelo de prerramificado (Pre-Forking)

Apache (en Unix), es un modelo de servidor de prerramificado. El proceso padre  es responsable únicamente de la ramificación de procesos hijo , no sirve ninguna petición o sockets de red. Los procesos hijo realmente procesan las conexiones, sirven múltiples conexiones (una cada vez) antes de morir. El padre crea nuevos hijos o destruye antiguos en respuesta a los cambios en la carga del servidor (lo hace monitorizando un marcador que los hijos mantienen actualizado).

Este modelo para servidores ofrece una robustez que otros modelos no. Concretamente, el código del padre es muy simple, y con un alto grado de confianza el padre continuará haciendo su trabajo sin errores. Los hijos son complejos, y cuando se añade código de terceros mediante módulos, se arriesga a fallos de segmentación y a otras formas de corrupción. Incluso ocurriendo una cosa así, sólo afectaría una conexión y el servidor continuaría sirviendo peticiones.El padre replaza rápidamente al hijo muerto.

El prerramificado es también muy portable dentro de los dialectos de Unix. Históricamente, ésta ha sido una importante meta para Apache, y continúa siéndolo.

El modelo de prerramificado se critica por varios aspectos de rendimiento. De particular importancia son la sobrecarga de ramificar un proceso, la sobrecarga de cambios de contexto entre procesos, y la sobrecarga de memoria por tener múltiples procesos. Adicionalmente, no ofrece tantas oportunidades para almacenarmiento de datos entre peticiones (como un fondo de ficheros mmapeados). Existen otros varios modelos y un análisis extensivo puede encontrarse en los papeles del proyecto JAWS. En la práctica todos esos costes varían drásticamente dependiendo del sistema operativo.

El código central de Apache ya utiliza multihebra, y la versión 1.3 de Apache es multihebra en NT. Ha habido al menos otras dos implementaciones experimentales de Apache con hebras, una usando el código base 1.3 en DCE y otra usando un paquete de hebras a medida en el nivel de usuario y el código base de 1.0; ninguna es públicamente disponible. También se ha portado experimentalmente Apache 1.3 para el tiempo de ejecución portable de Netscape, que está disponible (pero se recomienda que se una a la lista de correo nuevo-httpd si se pretende usarlo). Parte de nuestro rediseño para la versión 2.0 de Apache incluirá abstracciones del modelo del servidor para que podamos seguir dando soporte al modelo de prerramificado y también soporte para varios modelos de hebras.


Servidor Apache de HTTP, Versión 1.3

Índice Casa