Acelerando websites dinámicos a través de un proxy Nginx

2010-07-26 por Angel Abad, etiquetado como debian, servidor

Artículo traducido de: Debian Administration

La mayoría de nosotros estamos familiarizados con el uso de Apache para albergar sitios web. Puede que no sea el servidor web más rápido, pero es extremadamente popular, extremadamente flexible y una buena elección para la mayoría de la gente. Aún así hay ocasiones en las que puede ponernos en un apuro, y configurar un proxy delante nos puede venir muy bien.

nginx es un servidor HTTP muy ligero, rápido y eficiente y muy bien pensado para trabajar como proxy inverso, y no solo para HTTP, sino que también soporta SMTP.

Recientemente he actualizado este sitio - ya que estaba sufriendo mucha carga - esta pequeña introducción son los cambios que hice.

Este sitio, como la mayoría, está construido a partir de una mezcla de recursos estáticos y contenido generado dinámicamente. En nuestro caso el contenido dinámico es generado por una colección de scripts CGI Perl. Esta breve explicación prar usar nginx se podría aplicar a cualquier sitio con una mezcla de recursos estáticos y dinámicos, de la misma forma sería igual de aplicable para un sitio Ruby on Rails o basado en PHP.

Durante los dos últimos años he venido observando que Apache 2.x ofrece un buen desempeño en un día normal, pero en cuanto sube la carga ya no se comporta todo lo bien que cabría esperar. A parte de añadir mas memória a este servidor también me planteé cambiarle la configuración para apliarle el número de conexiones y hits que puede soportar.

Mi plan era:

  • Tocar la configuración de Apache lo mínimo posible.
    • En caso de que algo fallase quería tener la seguridad de poder revertir los cambios facilmente.
  • Dejar que Apache siga sirviendo todo el contenido dinámico como lo estaba haciendo hasta ahora.
  • Colocar un servidor HTTP dedicado más pequeño, rápido y simple para servir los contenidos estáticos.
    • Con la expectativa de que esto dejase a Apache más liberado para servir el contenido dinámico.

Hay varias formas de llevar este plan a cabo, pero las dos más obvias eran:

Cambiar los recursos de sitio

Podríamos dividir el manejo de los recursos estáticos moviendolos. Por ejemplo en ver de servir y hospedar http://www.debian-administration.org/images/logo.png podríamos moverlo a un dominio diferente, como por ejemplo http://images.debian-administration.org/logo.png.

Podríamos tener creado otro subdominio "static." para albergar otros contenidos, como los archivos CSS o Javascript.

Esto nos permitiría configurar un segundo servidor web para manejar el contenido estático de forma muy fácil (tal vez en un servidor diferente, pero muy probablemente en la misma). El inconveniente de esta solución es que requiere que actualicemos el código de nuestro sitio, plantillas y posiblemente otros ficheros.

Introduciendo un Proxy

Para evitar tener que andar moviendo los recursos y el trabajo extra que esto supone, la solución más simple sería colocar un proxy delante de Apache. Este examinará las peticiones entrantes HTTP y las enviará, ya sea a:

  • Apache si es una petición para /cgi-bin/
  • Otro servidor dedicado para todo el contenido estático (p.e. *.gif, *.png)

La decisión de por que usar nginx fué muy simple, existen varios proxies en el mercado bien considerados. Pero nginx era el mejor candidato por que se centra en ser a la vez un servidor HTTP y un proxy muy rápido.

Al trabajar como proxy y servidor HTTP, reduce la cantidad de software que tenemos que utilizar. Si hubieramos dedicado un proxy dedicado, habriamos tenido que tener tres servidores funcionando:

  • El proxy para recibir las peticiones.
    • Apache2 para servir el contenido dinámico.
    • Un servidor HTTP para el contenido estático.

Con nginx en escena tenemos una configurución muy simple con sólo dos servidores corriendo:

  • nginx acepta las peticiones e inmediatamente sirve el contenido estático.
    • Apache recive las peticiones dinámicas que nginx no atendió.

Instalar nginx

La instalación de nginx fué tan simple como esperabamos en servidor Debian GNU/Linux:

# aptitude install nginx

Una vez instalado, todos los ficheros de configuración se encuentran bajo el directorio /etc/nginx. Como en el caso de los paquetes de apache2 en Debian se colocan lass configuraciones de los sitios habilitados bajo un directorio sites-enables.

Bien, tampoco se pare mucho con los ficheros de configuración de nginx - el más importante es /etc/nginx/nginx.conf y está muy bien explicado y se lee muy fácil. El único cambio que tenemos que hacer es borrar el fichero /etc/nginx/sites-enabled/default.

Configurar nginx y apache2

Configurar nginx es muy simple, y nuestra configuración actual consistirá de dos partes:

  • Configurar nginx para que escuche en el puerto 80, y redirija ciertas peticiones a Apache.
  • Cambiar la configuración en Apache para que no siga escuchando bajo *:80, cambiandolo por el puerto que vayamos a utilizar.

Nuestro sitio se compone de dos hosts virtuales, nuestra página principal y nuestro planet. Este último es con mucho el más simple, ya que no tiene contenido dinámico, unicamente ficheros estáticos.

La configuración del sitio estático consiste en crear un fichero de configuración para ello en /etc/nginx/sites-available/planet.conf con el siguiente contenido:

#
# planet-debian-administration.org is 100% static, so nginx can
# serve it all directly.
#
server {
       listen :80;

       server_name  planet.debian-administration.org;

       access_log   /home/www/planet.debian-administration.org/logs/access.log;

       root         /home/www/planet.debian-administration.org/htdocs/;
}

Esto es suficiente para que nginx sirva el host virtual planet.debian-administration.org desde el directorio /home/www/planet.debian-administration.org/htdocs - y logee las peticiones entrantes en el fichero adecuado.

El manejo dinámico de nuestro sitio principal es un poco mas complicado. El contenido del fichero de configuración /etc/nginx/sites-enabled/d-a.conf sería algo así:

#
#  This configuration file handles our main site - it attempts to
# serve content directly when it is static, and otherwise pass to
# an instance of Apache running upon 127.0.0.1:8080.
#
server {
       listen :80;

       server_name  www.debian-administration.org debian-administration.org;
       access_log  /var/log/nginx/d-a.proxied.log;

       #
       # Serve directly:  /images/ + /css/ + /js/
       #
       location ^~ /(images|css|js) {
               root   /home/www/www.debian-administration.org/htdocs/;
               access_log  /var/log/nginx/d-a.direct.log ;
       }

       #
       # Serve directly: *.js, *.css, *.rdf,, *.xml, *.ico, & etc
       #
       location ~* \.(js|css|rdf|xml|ico|txt|gif|jpg|png|jpeg)$ {
               root   /home/www/www.debian-administration.org/htdocs/;
               access_log  /var/log/nginx/d-a.direct.log ;
       }

       #
       # Proxy all remaining content to Apache
       #
       location / {

           proxy_pass         http://127.0.0.1:8080/;
           proxy_redirect     off;

           proxy_set_header   Host             $host;
           proxy_set_header   X-Real-IP        $remote_addr;
           proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;

           client_max_body_size       10m;
           client_body_buffer_size    128k;

           proxy_connect_timeout      90;
           proxy_send_timeout         90;
           proxy_read_timeout         90;

           proxy_buffer_size          4k;
           proxy_buffers              4 32k;
           proxy_busy_buffers_size    64k;
           proxy_temp_file_write_size 64k;
       }
}

Este fichero de configuración tiene varios puntos interesantes, pero para los detalles completos necesitará consultar la documentación de nginx. Obviamente las secciones de más interes son las reglas que determinan que contenido es gestionado directamente por nginx.

Verá que tenemos dos reglas diferentes:

  • Una regla que dice que todo lo que esté por debajo de /images/, css o js debería ser gestionado directamente.
  • Otra regla que dice que independientemente de la ubicación los ficheros *.js, *.css, *.rdf, *.xml, etc... siempre serán gestionados directamente.

Estas reglas podrían parecer redundantes pero es mejor ser explicito acerca de nuestras intenciones. El resto del fichero contiene configuraciones para el reenvio de todas las demás peticiones a la instancia local de Apache - aquí no hicé cambios en la configuración de ejemplo.

El otro punto a tener en cuenta es que logeo las peticiones entrantes en dos ficheros, dependiendo de si son reenviadas a nuestra instancia de Apache o gestionadas directamente. Esto no es obligatorio pero da una idea de que peticiones van a cada servidor.

Con estos dos ficheros de configuración ya tenemos casi todo, sólo tenemos que asegurarnos de que Apache nunca más bloqueará el puerto 80 para sí. Esto lo hacemos modificando el fichero /etc/apache2/ports.conf parar que quede de la siguiente manera:

NameVirtualHost *:8080
Listen 8080

<IfModule mod_ssl.c>
    # SSL name based virtual hosts are not yet supported, therefore no
    # NameVirtualHost statement here
    Listen 443
</IfModule>

Esto nos asegura que Apache escuchará en puerto 8080 y no en el 80. Ahora deberemos hacer cambios en las configuraciones de nuestros virtual hosts. Por ejemplo /etc/apache2/sites-enabled/debian-administration.org:

#  Debian Administration domain.
#
<VirtualHost *:8080>
        ServerAdmin [email protected]
        ServerName www.debian-administration.org
        DirectoryIndex index.cgi index.html

        DocumentRoot /home/www/www.debian-administration.org/htdocs/
        ...
        ...

Con estos cambios realizados ya podemos empezar a usar nuestro proxy:

/etc/init.d/apache2 stop
/etc/init.d/nginx start
/etc/init.d/apache2 start

(Paramos apache2 para que el puerto 80 quede libre, entonces arrancamos nginx que usará ese puerto, y finalmente reiniciamos apache2 que a partir de ahora estará disponible en el puerto 8080 a través del cual nginx puede hablar con él.)

Nota: En este ejemplo apache está escuchando en el puerto 8080 en todas las IPs en vez de solo en 127.0.0.1:8080 - Estó lo cambié después.

Problemas experimentados

A la hora de hacer el despliegue se plantean dos problemas con los que antes no se había contado:

  • Falta de soporte IPv6.
  • Se logean direcciones IP incorrectas

Desafortunadamente la versión de nginx disponible en la versión Lenny de Debian no tiene soporte IPv6 - esto es una verdadera lástima ya que llevamos corriendo sobre IPv6 ya bastante tiempo (Sobre el 3% de nuestros visitantes usan IPv6 nativo, incluyendome a mi mismo, y no me gustaría perderlos.)

La solución al problema con IPv6 fue hacer un backport del paquete disponible en la distribución Debian inestable (un proceso dolorosa). Después de hecho esto el archivo de configuración de nginx debería actualizarse de la siguiente manera:

# Listen on both IPv6 & IPv4.
listen [::]:80;

El segundo problema estaba relacionado con como Apache recibía todas las conexiones desde el mundo exterior a través de nuestro servidor local nginx. Esto significaba que Apache identificaba cada petición entrante la hacía la IP 127.0.0.1.

Afortunadamente había una solución muy simple a este problema, el módulo para Apache 2.x libapache2-mod-rpaf permite hacer visible la IP del exterior y poder logearla.

El módulo RPAF coge la dirección IP de donde se inició la conexión original, y que nginx coloca en una cabecera X-Forwarded-For, además se asegura de que esta IP está disponible para nuestros scripts dinámicos y los logs de apache.

Aplicar esta solución fué tan simple como:

aptitude install libapache2-mod-rpaf
a2enmod rpaf
/etc/init.d/apache2 force-reload

Después de hacer esto nuestras conexiones entrantes eran logeadas correctamente y nuestro código veia la IP real de cada conexión en lugar de la de loopback del servidor proxy.

Cambios potenciales

Como habrás podido ver en los ficheros de configuración todas las peticiones entrantes al puerto 80 serán gestionadas directamente o reenviadas - pero no he hecho cambios en la gestión del puerto 443, o las peticiones SSL.

Hemos ofrecido SSL durante un largo periodo de tiempo pero pocos visitantes los utilizaban, asi que opté por dejar esto como está.

Si la situación cambia entonces se actualizará nginx para que haga de proxy para las peticiones SSL también - tiene soporte para ello, como indica la documentación.

Todavía es demasiado pronto para decir si esta solución ha incrementado nuestra escalabilidad, pero soy muy optimista. El uso de recursos ha caido y la combinación de nginx y apache es muy buena y no demasiado complicada.