Configuración básica de Varnish

Varnish es un proxy cache que acelera drásticamente la entrega de contenido a nuestros visitantes entre otras funciones. Además permite balancear carga entre varios backends y comprobar la salud de estos de manera que sólo se envíe peticiones aquellos que están en condiciones de procesar las peticiones. En esencia consiste en un almacenamiento clave-valor. Cada vez que recibe una petición comprueba calcula la clave asociada a esa url, comprueba si esa clave esta en memoria y es válida y entrega el contenido. En caso contrario lo pide al backend.

La información necesaria sobre el proceso de instalación y configuración inicial está disponible en la web del proyecto.

La versión que voy a usar para este post es la 4.1.

Que hace Varnish

Varnish trabaja sobre el protocolo http. Entiende host (carloscompains.com, api.carloscompains.com…), metodos (GET, PUT, POST, DELETE….), rutas (/v1/test, v1/test/123) y cabeceras (Vary: Cookie,Accept-Encoding, Expires: Sun, 25 Dec 2017 00:00:00 GMT). Es decir trabaja sobre la capa 7 del modelo OSI.

Varnish por defecto respetará las cabeceras que nuestra aplicación le entregue a no ser que especifiquemos una regla que las altere. Esto quiere decir que si programamos nuestra aplicación teniendo en cuenta los aspecto referentes a la cache, gran parte del trabajo ya estará hecho y necesitaremos poca o ninguna configuración en Varnish.

Cada vez que Varnish recive una petición, la analiza, ve si tiene la respuesta correspondiente en memoria, comprueba que esa respuesta no está caducada y la entrega si es así. Esto tienen un impacto directo en los tiempos de carga, ya que ahorra al backend tener que calcular esa respuesta reduciendo los tiempos de carga significactivamente.

Podemos cachear respuestas Html, Json, Xml… cualquiera de los formatos habituales de respuesta de una aplicación web. Además podemos cachear diferentes respuestas para una misma url usando la cabecera Vary.

Un ejemplo de uso de Varnish.
Entrega de contenidos según dispositivo

Este blog entrega diferentes versiones de la misma imagen dependiendo del disposivo que los solicita.

La ruta de una imagen puede ser: http://carloscompains.com/cache/xl/4.jpg

Cuando varnish reciba la respuesta analizará el User-agent determinando si es un móvil, tablet o equipo de escritorio el que se está conectando. Una vez conocido el tipo de disposivo fijará una cabecera (X-UA-Device) indicándolo.

El backend analizará esa cabecera y determinará en base a la misma y a la url (en concreto al segmento “xl” de la url, en este caso) el tamaño de imagen a entregar fijando una cabecera Vary que le indica a Varnish que ha de cachear distintas versiones del recurso segun el valor que tenga esa cabecera. De esta manera Varnish tendrá en memoria tres versiones del mismo recurso para una url dada y las entregará en función del tipo de dispositivo.

El fichero vcl. Configurar el comportamiento de Varnish

Varnish utiliza los ficheros vcl (Varnish configuration language) para definir el comportamiento que ha de tener. Este fichero esta dividido en varias funciones, cada una de las cuales se dispara en diferentes momentos.

Además hay un primer bloque en el cual se configura los diferentes backends a los que Varnish ha de conectarse y se defininen variables que luego podemos usar en el resto del fichero como por ejemplo Acl’s (Access control list) para decidir quien tiene permiso para determindas acciones.

En el siguiente diagrama se puede ver como se relacionan los distintos bloques del fichero, así como algunas de las variables disponibles en cada uno de ellos.

Configurar un backend

Es necesario configurar cada uno de los servidores a los que Varnish se va a conectar especificando ciertos valores como la dirección IP o el puerto entre otros. Opcionalmente además se establece una url a la que Varnish enviará peticiones para ver el estado de salud de ese backend para decidir si sigue enviandole tráfico o no.

Estos valores son un poco excesivos, pero sirven para asegurarnos de que Varnish no va a considerar el backend enfermo si el servidor tarda en responder lo cual es útil en entornos de desarrollo o si determinadas peticiones a nuestra aplicación son lentas.

Conectando con los backends

Varnish nos permite distribuir el tráfico entre diferentes backends, lo cual nos da una mejor toleracia a fallos y permite balancear carga entre varios backends. Disponer de varios backends ademas nos permite mejorar los procesos de despliegue al poder ir conectando y desconectando backends y actualizandolos de manera secuencial, eliminando tiempos de caida.

Para agrupar los backends Varnish utiliza la figura de los directors. Un director es un grupo de backends (ese grupo puede contener un solo backend) sobre los que se define una política de distribución de trafico. En la versión 4 de Varnish hay dos opciones round_robin y random. La primera ira alternando las peticiones de manera secuencial entre los diferentes backends y la segunda lo hará de manera aleatoria.

Ademas el director tendrá en cuenta los resultados de los health checks realizados contra las urls o con las peticiones definidas en la variable .probe de cada backend, de manera que si un backend está marcado como enfermo, no le enviará peticiones hasta que vuelva a estar saludable.

No es obligatorio tener mas de un backend para definir un director.

La inicialización de los directors se hace en la función vcl_init.

Otras variables a definir

Es util definir Acl’s (Access control list) para decidir quien puede por ejemplo invalidar y forzar a Varnish a pedir ese contenido al backend de nuevo. En este caso creamos una Acl con diferentes direcciones IP de manera que cuando Varnish reciba una petición del tipo PURGE o BAN comprobara si la ip desde el que se pide está en esta lista y en caso afirmativo ejectutará la invalidación.

Configurando el comportamiento de Varnish

Petición recibida vcl_recv()

Cuando Varnish reciba una petición en primer lugar ejecutará el método vcl_recv(). En esta función tendremos disponibles el objeto req, el cual representa la petición recibida por Varnish. Se puede leer y escribir (Más información).

En esta función se suelen hacer varias cosas que se consideran estandar:

  • Decidir a que backend se envía el tráfico. Si tenemos varios dominios, podemos discriminar en función del valor de req.http.host y fijar uno u otro director mediante set req.backend_hint = vdir.backend();
  • Sanear la petición. Eliminar cookies, devolver un error en caso de metodos no estandar (GET, HEAD, PUT, POST, TRACE, OPTIONS, PATCH, DELETE).
  • Establecer reglas generales de cacheado, como por ejemplo que solo se cacheen los metodos GET y HEAD (han de ser idempotentes). No se debe cachear el resto de metodos ya que si se respeta el protocolo http el resto de métodos cambiarán el estado de la aplicación. Si se cacheasen ese cambio no se llegaría a realizar ya que Varnish devolvería la respuesta cacheada sin llegar a impactar nuestra petición en el backend y por lo tanto sin generar los cambios deseados.
  • Anunciar al backend del soporte para fragmentos ESI
  • Gestionar acciones de PURGE y BAN

Esta es la función vcl_recv que tal y como se muestra en la documentación de Varnish con algunas personalizaciones para este blog.

Donde guardar el objeto cuando se cree vcl_hash()

La función vcl_hash() se encarga de crear una clave que permitirá recuperar el contenido una vez guardado en memoria. Además esa clave será la que Varnish utilize para comprobar si ese objeto (el objeto contiene la respuesta que se va a entregar al cliente, puede venir desde el backend o desde la memoria de Varnish) ya está cacheado.

Por defecto Varnish usará la url de la petición, el host (lo extrae de la petición) o la dirección ip del servidor (lo extrae del objeto server). Tal y como dice la documentación Varnish no pasa a minúsculas ni el nombre del host ni la url, es decir que carloscompains.com/Varnish y carloscompains.com/varnish serian urls distintas para Varnish y se cachearian por separado.

Es preferible evitar alterar esta función. Cuando necesitamo diferentes versiones del mismo recurso es preferible usar la cabecera Vary.

El recurso está en la cache vcl_hit()

Despues de obtener la clave asociada a la petición, Varnish comprobará si el objeto está en memoria y si lo encuentra llamará a esta función. En esta función tendremos disponible el objeto req que ya hemos visto antes, en modo lectura y escritura y el objeto obj que representa el recurso en memoria que aquí es de solo lectura.

Esta es la función que aparece en la documentación de Varnish.

El recurso no está en la cache vcl_miss()

Si el recurso no está disponible en la cache de Varnish se invoca al método vcl_miss() En esta función tendremos disponibles los objetos req y bereq en modo lectura/escritura. El objeto bereq representa la petición que Varnish va a hacer al backed. Aqui se puede personalizar si es necesario.

En general esta función no se suele alterar.

Invalidar un objeto en la cache vcl_purge()

Es bastante probable que en un momento dado queramos eliminar un objeto de la cache de Varnish para forzarlo a pedir una versión actualizada al backend. Esto se puede hacer con BAN o con PURGE.

Con BAN podemos enviar un patron e invalidar varios objetos a la vez (por ejemplo tal y como se ha hecho antes, todos aquellos que contengan un string determinado en una cabecera determinada).

Con PURGE, hay que enviar una url, y Varnish purgará el contenido asociado.

Cabe recordar que ya hemos comprobado en la función vcl_recv() si el host que está solicitando el PURGE esta en la acl definida y por lo tanto puede o no puede hacer un PURGE.

Esta función se invoca cuando en alguna otra ejecutamos el código return (purge). Varnish realiará el purgado y después llamara a vcl_purge

Uno de los parámetros que se le pasa a Varnish al iniciarlo es max_restarts, este parámetro está para evitar bucles infinitos cuando reiniciamos una petición. Si por algun motivo, lanzamos una invalidación que ejecuta un restart y el backend al recibir la petición, vuelve a generar un PURGE, Varnish como máximo reiniciará la petición max_restarts veces y despues lanzará un Guru Meditation error para evitar un bucle infinito.

Cuando Varnish da un paso atras vcp_pipe()

Cuando Varnish entra en modo pipe, se limita a recibir datos del cliente, enviarlos al backend y enviar la respuesta del mismo al cliente, sin hacer nada con ellos. Pasa a ser un proxy TCP sin mas funciones. En general se usa cuando la petición es rara o no responde al estandar HTTP o se detecta alguna cabecera o metodo extraño, o la petición es para un websocket.

Algunas urls no se han de cachear vcl_pass()

Cuando desde alguna de las subrutinas ejecutamos return (pass) le estamos diciendo a Varnish que simplemente le pregunte al backend por los datos, se los de al cliente y no guarde nada.

Normalmente no haremos nada en este método.

Preparando la petición para el backend vcl_backend_fetch()

Cuando Varnish ha determinado que necesita pedirle datos al backend, preparará la petición de la siguiente manera:

  • Fijará el método a GET a no ser que la petición venga de un pass en cuyo caso mantendrá el método de la petición original
  • Fijara la cabecera Accept_Encoding a gzip si http_gzip_support(parámetro de configuración que se pasa a Varnish al iniciarlo) está habilitada a no ser que la petición venga de un pass
  • Si hay un objeto en cache que necesita ser revalidado, se fijará bereq.http.If-Modified-Since desde su cabecera Last-Modified y/o se fija bereq.http.If-None-Match desde la cabecera Etag del objeto en memoria.
  • Se fija bereq.http.X-Varnish con el id de la transacción. Este id puede ser útil para tareas de logueado, ya que se puede usar para identificar la transacción en los diferentes backends o microservicios que pueda tener nuestra aplicación.

El backend responde vlc_backend_response()

Una vez que el backend responde a la petición, Varnish llamará a esta función y pondrá a nuestra disposición los objetos bereq y beresp que contienen respectivamente la petición y la respuesta del backend.

Conclusiones

En este artículo he repasado el fichero de configuración de Varnish con la configuración más básica y alguna personalización. En general es interesante dejar que sea la aplicación la que maneje la cache mediante el uso de las cabeceras adecuadas.

Por ejemplo en este blog, las respuestas se encapsulan en dos clases PublicResponse y PrivateResponse. Ambas clases extienden de la clase Response de Symfony. En el caso de la petición sea una respuesta cacheable (parte pública del blog) se establece la cabecera “max-age” que varnish respetará fijando el ttl correspondiente y guardando la respuesta para futuras peticiones.

En ocasiones por simplicidad o para no tocar codigo de terceros, podemos definir el comportamiento por patrones de la url. En el caso de este blog, al utilizar un bundle para gestionar el proceso de login, el código de las urls que gestionan el login no se debe editar ya que no forma parte del proyecto, por lo que no se puede fijar cabeceras direactamente en el controlador. Probablemente se podría fijar igualmente usando event listeners, pero me resulta mas sencillo fijar esas excepciones en el bloque vcl_recv usando patrones de url.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store