Usando Esi con Varnish y Symfony

¿Que es ESI?

ESI es el acrónimo es Edge Side Includes. Consiste en un lenguaje de marcado que permite “ensamblar” trozos de contendo web. Es una especficación envíada al W3C en 2001 pero que todavía no se ha aprobado. Sin embargo es una característica disponible en múltiples Proxy como por ejemplo:

Esta especificación nos permite dividir una página en varios trozos en el lado de servidor y entregar al usuario el contenido ensablado de manera transparente. Esto lo podemos usar para por ejemplo:

  • Repartir la carga de renderizado en varias máquinas, dejando que el renderizado de los diferentes fragmentos se haga de manera distribuida
  • Definir diferentes tiempos de expiración para el contenido principal y para cada fragmento.
  • Definir cabeceras individualizadas para que el proxy cache gestione los fragmentos de una manera específica.

Por ejemplo si tenemos la home de nuestro sitio, que cambia muy poco (cada 3 meses), pero el resumen de los último posts cambia cada semana, podemos definir un TTL de 3 meses para el contenido principal y de 7 días para el fragmento que contiene el resumen de posts publicados.

Esto mismo se puede hacer para respuestas de un webservice o API, ya sea una respuesta HTML, JSON o XML.

Los proxies que implementan este estandar siguen el siguiente proceso:

¿Como configurar Symfony para usar ESI?

Symfony incorpora soporte para ESI desde la versión XXXX. De hecho podemos usar ESI con Symfony sin necesidad de proxy cache alguno. A veces puede resultar un poco confuso saber si nuestra aplicación está haciendo lo que queremos.

En primer lugar si queremos usar ESI hay que habilitarlo en el fichero config.yml

framework:
# ...
esi: { enabled: true }
fragments: { path: /_fragment }

Desde este momento Symfony esperará recibir una cabecera que le anuncie que por delante de la aplicación hay algo que es capaz de procesar fragmentos ESI. Si no recibe esa cabecera lo resolverá de manera interna mediante una sub-request.

Creamos un controlador FragmentsController y lo metemos en routes.yml.

Creamos la ruta fragment_home y hacemos que devuelva algun contenido.

En la plantilla de nuestra home ponemos:

{{ render_esi(url('fragment_home'))}}

Esta es una función “segura”. Esto quiere decir que si Symfony no encuentra todos los requisitos necesarios para que se procesen los fragmentos ESI (config.yml y cabecera Subrrogate), lo resolvera como si hubiesemos llamado a la funcion render(). Esto lo podremos comprobar por que en el debuger de Symfony verémos una petición principal con una subpetición asociada.

Puede ser particularmente confuso ya que sin tener el proxy cache configurado, veremos que se están procesando los fragmentos ESI haciendonos pensar que el proxy cache ya esta preconfigurado por defecto.

Podemos probar la configuración haciendo una petición con la cabecera que espera Symfony:

curl -XGET http://localhost:81/app_dev.php --header "Surrogate-Capability:key=ESI/1.0"

En la respuesta deberíamos ver algo como:

<esi:include src="/app_dev.php/_fragment/" />

La funcion render_esi comprueba que en config.yml la funcionalidad esi está activada y que ademas recibe la cabecera Surrogate-Capability anunciando que hay un proxy que es capaz de manejar fragmentos ESI. Si alguna de estas condiciones fallase en vez de la etitqueta ESI estariamos viendo el contenido de la ruta ‘fragment_home’.

¿Como configurar Varnish para usar ESI?

Ya tenemos Symfony listo para insertar el marcado ESI donde corresponda, tenemos el contenido repartido en diferentes rutas y podemos fijar diferentes cabeceras en cada parte del mismo.

Es el momento de configurar el proxy cache.

Para ello, primero tenemos que anunciar a nuestra aplicación que el proxy soporta ESI mediante la cabecera que ya he mencionado antes. Para ello en el fichero de configuración .vcl ponemos en la función vcl_recv:

set req.http.Surrogate-Capability = "key=ESI/1.0";

Esto hace que cuando varnish recibe una petición le añada la cabecera y la envíe a nuestra aplicación.
En la función vcl_backend_response colocaremos:

if (beresp.http.Surrogate-Control ~ "ESI/1.0") {
unset beresp.http.Surrogate-Control;
set beresp.do_esi = true;
}

Estas lineas le dicen a Varnish que si el backend soporta ESI lo procese, haciendo que varnish realize las subpeticiones necesarias al backend hasta que tenga todos los bloques necesarios para devolver la respuesta.

Conclusiones

Siempre es recomendable el uso de un proxy cache cuando queremos mejorar los tiempos de carga y la experiencia general de navegación de nuestros usuarios/visitantes. Ahora bién, a veces puede ser complicado encontrar el punto de equilibrio entre una buena velocidad de carga y entregar contenido actualizado.

La especificicación ESI nos facilita en parte esta tarea, permitiendo dividir la petición principal en subpeticiones y dándoles diferentes tiempos de vida a cada una de esas sub-peticiones. De hecho incluso se puede paralelizar el procesado de estas peticiones usando Varnish Plus (versión de pago).