Aprovechando que una amiga me preguntaba sobre él, me he decidido a escribir sobre un programa muy útil que puede ayudarte a servir páginas web a la velocidad del rayo, aunque tu aplicación web sea un leviatán hecho en COBOL y Visual Basic por sucesivas generaciones inconexas de becarios explotados por una gran consultora.

Varnish


Varnish es un proxy caché inverso. Se pone delante de un servidor web y recibe las consultas de los clientes, que manda o no al servidor real (de aquí en adelante, "backend") dependiendo de si tiene los datos en caché o no. Y los datos se guardan o no en caché según cómo lo configuremos. Si tenemos un backend que genera páginas dinámicamente, podemos ahorrarle mucho trabajo si hay algo como Varnish delante, cacheando las páginas que no han cambiado. Por eso también se dice que Varnish es un "acelerador web".

Técnicamente, difiere de Squid (que es la otra opción) en muchas cosas que su autor, de forma totalmente imparcial y objetiva, explica aquí. Léetelo si quieres, pero no importa para lo que vas a leer.

Aquí es donde vendría bien un esquema, pero soy demasiado vago como para hacerlo. Por suerte, hay gente en Internet mucho más trabajadora que yo, y puedo enlazar uno que ha encontrado Google:


Este blog está hospedado en la todopoderosa y cuasi-infinita nube de Google, y (espero) sobreviviría cualquier DDoS benigno (si me hago famoso un día y todo el mundo consulta la web a la vez) o maligno (si me hago famoso un día y alguien quiere tirarlo para chantajearme). Pero mi otro blog está en un VPS pequeñajo, y ahí los recursos son muy limitados. Es la situación en que Varnish puede ayudar.

Instalación, configuración, etc.


Si tienes Debian o Ubuntu, "apt-get install varnish". Si tienes CentOS, Fedora o RH, "yum install varnish". Si tienes Suse o alguna de sus variantes, pobriño. La versión que viene con las distribuciones no es la última, y hay repositorios varios para instalar "the latest and greatest" versión de Varnish. Pero para probar, llega de sobra. Mejor si es una versión 3.x, pero con una 2.1.x también puedes probar. Aunque mola más la 3. Actualiza salvo que haya algún impedimento grave.

Por defecto, Varnish viene configurado en el puerto 6081. El backend por defecto es 127.0.0.1, puerto 80. Lo más probable, si tienes un servidor web configurado, es que puedas apuntar el navegador a http://tupaginaweb.com:6081/ y (salvo limitación de firewall o parecido) consultar tu página web a través de Varnish. ¡Tachán!

Para que sea más útil hay que cambiarlo al puerto 80, y poner el backend en otro puerto. Viene comentado en los ficheros de configuración y es muy fácil. Sabrás hacerlo. Confío en ti.

Configuración; ahora en serio


Tal como viene, Varnish cacheará casi todo durante el tiempo por defecto, 120 segundos. Hay un pero, y es lo más importante que necesitas saber sobre Varnish. Tan importante, que lo voy a poner en letras enormes:

No se cachea el contenido con cookies.

Eso significa que cualquier consulta en la que haya una cabecera "Cookie" (desde el cliente al servidor) o "Set-Cookie" (desde el servidor al cliente) será servida por Varnish como un proxy sin caché: haciendo una consulta al servidor. Esto asegura que, por ejemplo, no se guardan en caché páginas autenticadas. Imagínate que entras en la página web de tu banco y te aparece la cuenta de otra persona. No mola, y por eso Varnish no toca lo que lleve cookies.

La siguiente pregunta es: ¿qué lleva cookies? Pues, según qué uses, casi todo. Wordpress, por ejemplo, es un "cookie monster". Pone cookies a todo, incluso imágenes estáticas. Si pones Varnish delante de un Wordpress, verás que el "hit ratio" (la relación entre páginas en caché y páginas consultadas al servidor) es bajo, porque hay muchas peticiones con cookies. Drupal es algo parecido. Imagino que otros sistemas también usarán muchas cookies, pero sólo he trabajado con esos dos.

Al final, verás que la mayor parte del trabajo que haces consiste en quitar cookies para asegurarte de que Varnish guarda en caché todo lo posible.

VCL, el lenguaje de configuración de Varnish


Varnish se configura mediante un pseudo-lenguaje de programación llamado VCL (Varnish Configuration Language). Por si esto no llegase para hacerlo complicado, el procesado de peticiones del cliente y respuestas del backend pasa por varias fases que hace falta conocer y entender, porque se configuran por separado. Para cada una de ellas hay una función en la que se colocan los "statements", diciéndole a Varnish cómo transformar las peticiones/respuestas para que hagan lo que queremos. Podemos romper casi todo con nuestra configuración, pero para evitar desgracias mayores, Varnish añade implícitamente su configuración por defecto al final de cada función. Es como si al final de cada función hiciera un "cut & paste" de la configuración por defecto, asegurándose de que, hagamos lo que hagamos, se mantiene un mínimo de funcionamiento.

Pero eso no es importante. Quédate sólo con la idea de que hay varias funciones predefinidas, y que tú tienes que poner ahí la configuración para que Varnish haga cosas. Salvo que quieras hacer algo complicado, los conceptos avanzados puedes mirarlos luego.

El esquema de flujo de una consulta HTTP en Varnish es éste:


Asusta, ¿verdad?

Tampoco te preocupes. Vamos a empezar con una configuración trivial, como la que viene por defecto. Le he quitado todos los comentarios para hacerla más compacta.

        backend default {
                .host = "127.0.0.1";
                .port = "80";
        }

Esto significa lo obvio: el backend está en 127.0.0.1, puerto 80. Con esto, Varnish se conectará ahí y cacheará todo lo que pueda (o sea: lo que no tenga cookies). "Todo" significa "todo": páginas estáticas, dinámicas, adjuntos, lo que sea. Si no tiene cookies, va a la caché durante el tiempo configurado por defecto.

vcl_recv


Para cambiar cosas de la consulta HTTP que lanza el cliente tenemos que configurar la función vcl_recv. Para definirla podemos poner esto:

        sub vcl_recv {
        }

¿Recuerdas lo que decía antes de que se añade la configuración por defecto? Significa que Varnish funcionará aunque hayamos definido esta función sin nada dentro. No vale de nada, pero es instructivo.

Imagínate que tienes dos VirtualHosts definidos en el backend, y quieres usar una configuración distinta en cada uno. Como el VirtualHost se escoge en función de la cabecera "Host" de la consulta HTTP que envía el cliente, para decirle a Varnish qué hace en cada caso usaríamos un bucle "if ... else" y la variable req.http.host (o req.http.Host, es "case-insensitive"). "req" es el objeto que guarda los datos de la consulta HTTP del cliente; "req.http" es el que guarda las cabeceras HTTP.

Entonces, tendríamos algo así:

        sub vcl_recv {
                if (req.http.host == "misitioweb.com") {
                        ...
                }
                else {
                        ...
                }
        }

O, si tenemos más de dos sitios web:

        sub vlc_recv {
                if (req.http.host == "misitioweb.com") {
                        ...
                }
                elsif (req.http.host == "miotrositioweb.com") {
                        ...
                }
                else {
                        ...
                }
        }

También se pueden usar expresiones regulares, usando "~" en lugar de "==":

        sub vcl_recv {
                if (req.http.host ~ "misitioweb.(com|net|org)") {
                        ...
                }
        }

Podemos quitar y poner cabeceras con "set" y "unset". Por ejemplo, para quitar todas las cookies (lo que, en general, no es buena idea):

        sub vcl_recv {
                unset req.http.cookie;
        }

Y para añadir una cabecera personalizada para enviar al backend:

        sub vcl_recv {
                set req.http.x-micabecera = "Hola mundo";
        }

No muy útil, pero también instructivo.

vlc_fetch


La siguiente fase importante es vcl_fetch, en la que transformamos la respuesta del backend. Ahí podríamos hacer que se borraran todos los intentos de establecer cookies, con esto:

        sub vcl_fetch {
                unset beresp.http.set-cookie;
        }

(tampoco nada recomendable, por cierto)

"beresp" es el objeto en el que se guardan los datos de la respuesta del backend: "b(ack)e(nd)resp(onse)". En esta fase hacemos algo muy importante: ajustar el TTL.

El TTL ("Time to live") es el tiempo que Varnish guardará un objeto en caché. Por defecto son dos minutos, que es lo mismo que si hiciéramos:

        sub vcl_fetch {
                set beresp.ttl = 120s;
        }

(podríamos poner "2m", o "3h"; se aceptan varios formatos de tiempo)

Lo normal es distinguir por tipo de objeto. Por ejemplo: ponemos un TTL por defecto pero lo aumentamos para los JPEG:

        sub vcl_fetch {
                set beresp.ttl = 5m;
                if (req.url ~ "\.(jpg|jpeg)") {
                        set beresp.ttl = 1h;
                }
        }

"req" sigue siendo el objeto donde se guarda la consulta HTTP. Para saber qué variables están disponibles en qué fases, puedes ver una tabla aquí.

En vcl_fetch también podemos alterar la petición que hace Varnish al backend, con el objeto bereq. Por ejemplo, podemos hacer algo parecido a la reescritura de URLs de Apache para cambiar la ruta en la que se encuentra un objeto:

        sub vcl_fetch {
                if (req.url ~ "^/dir/") {
                        set bereq.url = regsub(req.url, "^/dir/", "/otrodir/");
                }
        }

Pero esto es lo mismo que hacer:

        sub vcl_fetch {
                if (req.url ~ "^/dir/") {
                        set req.url = regsub(req.url, "^/dir/", "/otrodir/");
                }
        }

Y es un poco más elegante.

Esquivando la caché


A veces lo que queremos no es meter algo en caché, sino evitar que pase por ella. Por ejemplo: tras una poda de todo lo que generaba cookies en nuestro backend, puede que nos interese excluir de esas restricciones una URL determinada (la de administración, típicamente). Para eso hay que usar la acción (terminología Varnish) return, y el argumento pass. O dicho de otra forma:

        sub vcl_recv {
                if (req.url ~ "^/(admin|login)/") {
                        return(pass);
                }
        }

Sólo podemos usar return(pass) en vcl_recv. Se llama desde la configuración por defecto de Varnish (ésa que se añadía automáticamente), con lo que podríamos conseguir el mismo efecto diciéndole a Varnish que cacheara todo salvo estas URLs. Suponiendo que el tráfico a esas URLs llevara cookies, caso en el que Varnish no las metería en caché, sería algo así:

        sub vcl_recv {
                if (req.url !~ "^/(admin|login)/" {
                        ... (lo que sea que hagamos para que Varnish meta otras URLs en caché)
                }
        }

Otras fases


Con estas dos fases se cubren muchos, muchos de los casos de uso de Varnish. Hay otras fases también importantes, de las que se habla aquí. Una de las que deberías mirar es la de vcl_error, que es donde Varnish maneja los errores. Un error no tiene por qué ser un error real, sino uno que se genere en una fase anterior y que sirva para que Varnish se comporte de manera especial.

Un ejemplo real (que siempre son los más interesantes): hace unos días fui víctima de un ataque de fuerza bruta al Wordpress de mi otro blog. En los logs aparecían varias consultas por segundo desde IPs distintas, siempre a la misma URL ("/wp-login.php"). Como ésa es una de las que están excluidas de la caché (la página de login), todas las consultas iban directamente al Apache que estaba detrás de Varnish. Ese Apache tiene configurado un máximo de 10 procesos concurrentes, con lo que el servidor no se murió; pero sufrió mucho hasta que me di cuenta.

Por suerte, todas las consultas eran POST, tenían el mismo User-Agent y atacaban a la misma URL, con lo que fue fácil meter en vcl_recv unas líneas para tratar con ellas:

        if ((req.request == "POST") &&
                (req.url ~ "/wp-login.php") &&
                (req.http.user-agent == "Mozilla/5.0 (Windows NT 6.1; rv:19.0) Gecko/20100101 Firefox/19.0")) {
                error 999 "Forbidden";
        }

Con la parte de "error 999" se genera un error 999 en cuanto se detecta una consulta de estas características, y se muestra el mensaje "Forbidden". Pero ese error no llega a verlo nadie (aunque la página web que se generará, sí). Al llegar ahí, Varnish salta a la función vcl_error, y en ella tengo esto:

    if (obj.status == 999) {
        set obj.status = 403;
        return(deliver);
    }

Es decir: si llegas con un error 999, convierte el estado a un 403 ("Forbidden") y devuelve el resultado sin más procesamiento ("return(deliver)"). Con esto, todas las peticiones de este tipo fueron atendidas por Varnish, no por Apache, y sin gastar muchos ciclos de procesador.

Y además, en vcl_error puedes generar tus propias páginas de error, como ésta. ¿A que mola?

varnishstat y varnishtop


Varnish viene con varias utilidades para monitorización. Las más interesantes son varnishstat y varnishtop.

varnishstat muestra estadísticas sobre la caché. Te puedes quedar con los tres números que se muestran arriba del todo: el "hit ratio" cada minuto, cada cinco minutos y cada cuarto de hora. Mide la proporción entre objetos servidos desde la caché y objeto servidos desde el backend (sin cachear). Cuanto más se acerque a uno (lo que representaría el 100% de éxito, todo servido desde la caché), mejor. Ésta es la herramienta que usaremos para comprobar si está funcionando nuestra configuración.

varnishtop mide las instrucciones internas de Varnish que más se están ejecutando. No vale de nada sin saber cuáles son y qué significan, pero algunas son bastante obvias. Por ejemplo: cuando sufrí el ataque que mencionaba antes, la instrucción que más se ejecutaba era "pass", lo que significaba que la mayoría de las consultas estaban saltándose la caché. Se pueden filtrar las instrucciones que queremos ver, y con ello podemos diagnosticar muchos problemas de Varnish.

Palabras finales


Se pueden hacer más cosas, pero entre nosotros: ni soy tan experto como para hablar de ellas, ni tengo ganas de seguir escribiendo. Así que terminaré el artículo con una serie de enlaces en los que encontrar más información:




Listo, artículo terminado. Dudas y comentarios, en la parte de abajo.