Este no pretende ser un manual de configuración y optimización específico para tu aplicación web, sino más bien un caso de estudio ajustado a mis necesidades específicas. Puedes tomar en cuenta el análisis hecho para sacar tus propias conclusiones. Estará compuesto de varios episodios en donde se toman en cuenta los diversos caminos a tomar.

Reitero esto ya que de seguro «tu kilometraje puede variar».

Ver también:

La idea

Tal vez uno de los servicios de mayor uso y expansión a traves de internet sea la web. Y en este ámbito quien se lleva la torta es Apache, quien según Netcraft maneja alrededor del 67% de los sitios web disponibles en la red.

Un dato aparte es que Apache ha tenido durante el 2006 una pequeña baja en su supremacía en favor de otras soluciones web, probablemente por las mismas razones que estoy escribiendo: la busqueda de la mejor performance.

Hay bastantes otros servidores web menos usados pero que logran mayores performances para contenidos particulares. Lighhtpd y su actual relación con la moda Web 2.0 y Ruby on Rails, y servidores principalmente orientados a servir óptimamente contenido estático como Thttpd, Boa (quien sirve el contenido estático de Slashdot, algo no menor) y Tux. Y por último pero no menor, la aparición de el (de esperar) completo reemplazo de Apache, Cherokee, del español Alvaro López.

Es claro entonces el interés por lograr la mayor performance posible, reuniendo el conjunto de tecnologías que lo permita sin ya existir el apego casi religioso a la predilección por Apache.

Es por ello que a partir de la versión 2.0 de este servidor es que se implementó un modelo (o MPM, de Multi Process Module) híbrido de multiprocesos y de multihilos, conocido como Worker en donde un número reducido de procesos pueden servir contenido a varios clientes a la vez mediante diferentes hebras, logrando una mayor performance y menor uso de recursos en el servidor a causa de la reducción de los cambios de contexto en los procesos.

En el modelo anterior de Prefork, un modelo que implementa procesos padre-hijos, tan solo un cliente puede ser servido a la vez por hijo lo que incrementa el costo de recursos ya que existe un mayor cambio de contexto de cada proceso, muerte y renacimiento, lo que es costoso.

Mantener una hebra disponible preparada para recibir un nuevo cliente es menos costoso que mantener un proceso disponible, asimismo crear una nueva hebra genera mucho menor costo que engendrar un nuevo hijo, es la idea tras la implementación del modelo multihebras.

¿Por qué entonces utilizar otro módulo que no sea worker?. Dos palabras: Thread safe. No todas las extensiones en Apache utilizan un sistema robusto de seguridad de contexto de memoria entre las hebras, claro ejemplo es el de PHP que con su TSRM (Thread Safe Resource Manager) no convence a nadie. La razón mas común para seguir utilizando Prefork.

Apache entonces

En la búsqueda por el óptimo se encontraba la oportunidad de cambiar definitamente de servidor y migrar fuera de apache, pero no lo iba a desechar sin antes ver que tanto podría lograr de él. Por una parte mi decisión era continuar utilizando Prefork dada la necesidad de utilizar PHP.

Existe una solución alternativa que es utilizar MPM Worker con FastCGI corriendo PHP, pero tras unos primeros intentos demostró ser menos óptimo aún para mis necesidades. A jugar entonces con lo que tenía.

¿Dónde estamos?

Antes de conocer el incremento de rendimiento debía saber en que condiciones me encontraba, sino como comparar mis progresos. Para esto probe varios sistemas de benchmarking, ab (apache benchmarking tool) a secas, Httperf, hasta que terminé por utilizar Autobench. Su gracia es que luego de realizar pruebas de stress los resultados pueden ser utilizados por Gnuplot para generar gráficos y analizar de una forma humanamente entendible.

Una vez realizadas las primeras pruebas era momento de comenzara identificar posibles cuellos de botella que impidieran el correcto escalamiento del servidor cuando las peticiones superan el millar.

Red

Decidí ir desde el cliente hacia el servidor identificando posibles problemas, ¿qué puede ser lo primero que resulte mal? Fuera de descartar que la red física se encontrara en perfectas condiciones aún las primeras suposiciones apuntan a el comportamiento de el uso de red en el servidor. El kernel entonces.

¿Qué puede salir mal?. Cuando estamos hablando de cientos de conexiones concurrentes muchas cosas pueden salir mal a bajo nivel. El servidor puede extenuar sus recursos como las conexiones en espera o connection tracking. Sin hablar de los mal intencionados que se encuentran constantemente haciendo «benchmarks» al servidor.

Primera regla de oro, habilitar SYN Cookies. Segunda regla de oro, habilitar control de «asesinatos» TIME-WAIT según el RFC-1337 (que nombre mas ad-hoc). De la misma manera, limitar el tcp keepalive (también) de forma que los procesos no queden a la espera por tan largo rato mientras la conexión se da por terminada. Esto trae consigo la posibilidad de mantener más conexiones «huérfanas», por lo que de igual manera se debería controlar el número máximo de conexiones en dicho estado. Por último incrementar el tamaño máximo del buffer asignado al stack TCP. Todo mediante los siguientes sysctl:

`

time-wait assasination

net/ipv4/tcp_rfc1337=1

syncookies

net/ipv4/tcp_syncookies=1

reduce keepalive

net.ipv4.tcp_keepalive_time=300

controla número máximo de huerfanos

net/ipv4/tcp_max_orphans=1024

incrementa tamaño de buffer TCP; sí, es gigante

net.core.rmem_max = 16777216
net.core.wmem_max = 16777216

incrementa límites buffers TCP autoajustables

min, default, y max número de bytes a usar

net.ipv4.tcp_rmem = 4096 87380 16777216
net.ipv4.tcp_wmem = 4096 65536 16777216
`

Luego entonces el backlog, el número de paquetes de entradas no procesadas antes que el kernel comience a botarlos.

<br /> net.core.netdev_max_backlog = 2500<br />

Y por último es posible que el número conexiones provoquen que el máximo de sesiones que pueden ser manejadas por netfilter en el kernel, algo que se transformaría en una condición mucho más crítica en el caso de un gateway nat. Esto puede ser solucionado incrementando el número de ip_conntrack_max.

Con estos ajustes ya me quedo tranquilo que el comportamiento de TCP no debería ser el culpable de el bajo rendimiento de conexiones finalmente establecidas con el cliente.

Más adelante el sistema de archivos, apache y mysql.