Autor: Dean Gaudet
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.
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.
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:
Pero incluso así, si sólo se necesitan los nombres de DNS en algunos CGIs, se puede considerar la inclusión de la llamadaHostnameLookups off <Files ~ "\.(html|cgi)$"> HostnameLookups on </Files>
gethostbyname
en los CGIs que específicamente lo necesiten.
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:
y se hace una petición para el URIDocumentRoot /www/htdocs <Directory /> Options SymLinksIfOwnerMatch </Directory>
/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:
Esto, al menos, evita las comprobaciones adicionales para la ruta de la raíz del documento (DocumentRoot /www/htdocs <Directory /> Options FollowSymLinks </Directory> <Directory /www/htdocs> Options -FollowSymLinks +SymLinksIfOwnerMatch </Directory>
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
.
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,
y se hace una petición para el URIDocumentRoot /www/htdocs <Directory /> AllowOverride all </Directory>
/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.
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:
usar una lista completa de opciones:DirectoryIndex index
en la que se listará primero la opción más corriente.DirectoryIndex index.cgi index.pl index.shtml index.html
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.
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).
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):
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 elfor (;;) { 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; }
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):
Las funcionesfor (;;) { 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; }
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
flock(2)
para
bloquear un fichero de bloqueos (localizado en la directiva LockFile
).
USE_FCNTL_SERIALIZED_ACCEPT
fcntl(2)
para
bloquear un fichero de bloqueos (localizado en la directiva LockFile
).
USE_SYSVSEM_SERIALIZED_ACCEPT
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
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
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.
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.
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:
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 definirvoid 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); }
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).
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.
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<Directory /> AllowOverride none Options FollowSymLinks </Directory>
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:
Estas dos llamadas pueden ser eliminadas definiendoflock(18, LOCK_UN) = 0 ... flock(18, LOCK_EX) = 0
SINGLE_LISTEN_UNSERIALIZED_ACCEPT
como se describió previamente.
Nótese la manipulación de SIGUSR1
:
Esto es provocado por la implementación de reinicializaciones "elegantes". Cuando un padre recibe unsigaction(SIGUSR1, {SIG_IGN}, {0x8059954, [], SA_INTERRUPT}) = 0 ... sigaction(SIGUSR1, {SIG_IGN}, {SIG_IGN}) = 0 ... sigaction(SIGUSR1, {0x8059954, [], SA_INTERRUPT}, {SIG_IGN}) = 0
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:
Es posible eliminar esta llamada en muchas situaciones (como cuando no hay anfitriones virtuales, o cuando las directivasgetsockname(3, {sin_family=AF_INET, sin_port=htons(8080), sin_addr=inet_addr("127.0.0.1")}, [16]) = 0
Listen
usadas no tienen direcciones
comodín). Pero todavía no se han hecho esfuerzos para realizar esas
optimizaciones.
Apache deshabilita el algoritmo Nagle:
por los problemas descritos en un documento de John Heidemann.setsockopt(3, IPPROTO_TCP1, [1], 4) = 0
Nótense las dos llamadas a time
:
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.time(NULL) = 873959960 ... time(NULL) = 873959960
Como se describió previamente, ExtendedStatus On
origina
dos llamadas a gettimeofday
y una llamada a times
:
Pueden eliminarse estableciendogettimeofday({873959960, 404935}, NULL) = 0 ... gettimeofday({873959960, 417742}, NULL) = 0 times({tms_utime=5, tms_stime=0, tms_cutime=0, tms_cstime=0}) = 446747
ExtendedStatus Off
( que es el valor
por defecto ).
Podría parecer raro llamar a stat
:
Esto es parte del algoritmo que calcular la información de ruta (stat("/home/dgaudet/ap/apachen/htdocs/6k", {st_mode=S_IFREG|0644, st_size=6144, ...}) = 0
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
:
En algunas arquitecturas es más lento hacermmap(0, 6144, PROT_READ, MAP_PRIVATE, 4, 0) = 0x400ee000 ... munmap(0x400ee000, 6144) = 0
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:
Mientras hace el codificado por piezas requerido por HTTP/1.1, Apache generará hasta cuatro elementoswritev(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
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:
que fueron descritas anteriormente.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
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.
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:
time(2)
.
mod_include.
Estas
llamadas se usan en pocos sitios, pero se necesitan por compatibilidad hacia
atrás.
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.