Volver al blog

Síguenos y suscríbete

Aceleración de Rails, Parte 2: HTTP caching dinámico

Michael May

Integration Engineer

La Parte 1 de esta serie aporta perspectivas clave sobre los diversos tipos de almacenamiento en caché que hay integrados en Rails y sobre cómo utilizarlos con eficacia. Esta entrada trata enteramente sobre el HTTP caching, proceso que ocurre en su mayoría fuera del ámbito del stack de Rails.

En la segunda parte de nuestra serie sobre la aceleración de Rails, voy a tratar la configuración de algunas características de Fastly, Varnish y Varnish Configuration Language (VCL), y estrategias de almacenamiento en caché de contenido dinámico dirigidas a los desarrolladores de Rails. Conviene recordar que el almacenamiento en caché de contenido dinámico no es una solución aplicable a todas las situaciones. Las aplicaciones de Rails difieren en modelos de dominio, detalles de implementación y requisitos, de modo que no siempre es posible adoptar una implementación generalizada y conseguir que funcione al instante para todo el mundo.

Espero que esta entrada te enseñe principalmente a reflexionar con mayor profundidad sobre el caching de contenido dinámico y a considerar que algunas de las estrategias que aquí describimos posiblemente sean extensibles a tu propio caso.

Configuración de Fastly

La configuración del servicio Fastly es sencilla: solo tienes que facilitar un nombre de dominio y una dirección de servidor de origen (es decir, la del servidor de Rails); Fastly estará a tu disposición de inmediato para implementar tus instrucciones de almacenamiento en caché. La plataforma de aceleración de Fastly es completamente configurable, con lo que se posibilita la puesta a punto de tu servicio según los requisitos concretos que hayas especificado. En esta sección, veremos las configuraciones habituales que funcionan con aquellas aplicaciones de Rails altamente dinámicas. El primer paso de configuración, y el más importante, es definir el DNS de modo que el tráfico sea enrutado a través de la red global de Fastly.

¿Qué incluyen un CNAME?

Todo CNAME logra que el comportamiento de tu dominio equivalga al de otro dominio de modo que haya transparencia para el cliente. Imagínate que tienes una aplicación de Rails que se está ejecutando en test.herokuapp.com y cuyo dominio es ejemplo.com. Lo más probable es que prefieras que clientes y usuarios accedan a tu aplicación a través del dominio habitual ejemplo.com en lugar del dominio que le asigna tu proveedor de alojamiento. Esto se logra creando un registro CNAME en los ajustes de tu proveedor de DNS.

Te proponemos utilizar el comando dig, que se incluye en la mayoría de distribuidores de Linux, para comprobar el aspecto de una consulta real de DNS y hacerte una idea de cómo funciona esto.

$ dig test.herokuapp.com

;; QUESTION SECTION:
;test.herokuapp.com.            IN      A

;; ANSWER SECTION:
test.herokuapp.com.     299     IN      CNAME   us-east-1-a.route.herokuapp.com.us-east-1-a.route.herokuapp.com. 59 IN  A       23.21.43.109

El resultado proporciona algunos datos útiles sobre el modo de enrutar las solicitudes destinadas a este dominio; es decir, que el dominio herokuapp.com realmente está dirigido a something.route.herokuapp.com, que es un registro A correspondiente a la dirección IPv4 23.21.43.109. Cada vez que los clientes accedan a tu aplicación en este dominio, la dirección IP que en realidad van a ver es esta.

La creación de un CNAME específico para Fastly es el modo de definir que el tráfico circule a través de nuestra red. Encontrarás instrucciones de configuración de CNAME para la mayoría de proveedores de DNS en este documento. Toda configuración adecuada de Fastly suele parecerse a la siguiente:

$ dig www.fastly.com

;; QUESTION SECTION:
;www.fastly.com.                        IN      A

;; ANSWER SECTION:
www.fastly.com.         567     IN      CNAME   global-ssl.fastly.net.global-ssl.fastly.net.  8       IN      CNAME   fallback.global-ssl.fastly.net.fallback.global-ssl.fastly.net. 8 IN    A       199.27.79.184
fallback.global-ssl.fastly.net. 8 IN    A       23.235.47.184

Recuerda que la respuesta es devuelta con un CNAME dirigido a "global-ssl.fastly.net". Recuerda también que la sección answer contiene múltiples registros A. Estos son direcciones IP de los POP de Fastly más próximos al origen de la solicitud. ¿Por qué dos direcciones IP? Por motivos de redundancia en caso de que una de estas no logre determinar la dirección. La que realmente se selecciona para su uso depende en gran medida de la implementación del solucionador de DNS utilizado. En el futuro, las mejoras en los solucionadores de DNS posiblemente permitan seleccionar la IP cuya latencia sea menor.

Cada vez que un cliente formula una solicitud a ejemplo.com, la red la enruta al POP de Fastly ubicado en una de las direcciones IP recogidas en la consulta de DNS. Cuando la solicitud llega al POP, o bien se recupera y se retorna directamente al cliente desde la caché, o bien se traslada al origen configurado con tu servicio de Fastly.

Si se configura el tráfico de modo que circule a través de Fastly, se obtienen ventajas de rendimiento a través de optimizaciones de sesiones TCP en nuestra red, lo cual supone reducir los recorridos de ida y vuelta y, en consecuencia, se rebaja la latencia.

Nota sobre la canalización de activos de Rails y los CNAME  

Una vez que se haya creado un registro de CNAME dirigido a Fastly, ya no tendrás que proporcionar un ajuste de configuración del tipo asset_host, como se mencionaba en la parte 1 de esta serie de entradas de blog. Puesto que todas las solicitudes fluyen a través de las cachés de Fastly, solamente tienes que definir encabezados de caducidad adecuados, como Cache-Control, de modo que la caché los recoja.

Inicio de sesión

Los registros son una herramienta vital para hacer un seguimiento de errores de programación y de tareas asíncronas en ejecución. Al proporcionar respuestas a partir de los recursos de la caché, las líneas de los registros no se muestran en los registros de tu aplicación ya que las solicitudes en ningún caso llegan al origen. Para lograr la visibilidad de estas respuestas, facilitamos el envío remoto de registros a un amplio espectro de proveedores de registros, incluidos Papertrail, S3 y Splunk. Si no recurres a terceros para el almacenamiento y el análisis de registros, puedes configurar un punto de conexión de syslog. Todo ello se logra con facilidad a través del panel de Fastly o mediante nuestra API de registros remotos.

Origin Shielding

Origin Shield de Fastly permite enrutar todas tus solicitudes que vayan destinadas a tu origen a través de un punto de presencia (POP) de Fastly designado. Se reducen así las solicitudes enviadas a tu origen, dado que el resto de POP traen contenido proveniente de Origin Shield en lugar de hacerlo directamente desde tu origen.

Escoge un POP de blindaje que minimice la latencia que proviene de tu origen con dirección al POP. La ubicación de todos nuestros POP se puede verificar en nuestra página de red. Por ejemplo, si tienes cualquier aplicación de Rails alojada en la región EE. UU. de Heroku (EC2 US East en Ashburn, Virginia), escoge el POP de Fastly Ashburn, VA a modo de blindaje. Si tienes contratado el servicio de alojamiento con Google Cloud Platform (GCP), optar por un POP de blindaje en San José o Ashburn mejora los tiempos de respuesta que ofrecen nuestras interconexiones directas con GCP.

Se puede verificar la Origin Shielding inspeccionando los encabezados de cualquier objeto solicitado. Por ejemplo, la primera solicitud de cualquier objeto suele tener este aspecto. (Recuerda que la primera caché enumerada es el nodo de blindaje y que la segunda es el nodo del edge local).

X-Served-By: cache-ash1212, cache-sjc3333
X-Cache: MISS, MISS
X-Cache-Hits: 0, 0

Estas son solo algunas de las características disponibles para su configuración. He omitido algunas, incluidos el balanceo de carga de origen, las verificaciones de estado y la reducción de peticiones. Todos estos documentos se encuentran en docs.fastly.com.

Pasemos ahora al lenguaje VCL (Varnish Configuration Language), que nos va a servir de partida para analizar algunos casos de uso interesantes.

Edge scripting con Varnish y VCL

Fastly está construido en Varnish, un caché de proxy inverso HTTP de código abierto. Esta sección se centra en VCL, lenguaje específico de dominios destinado a interactuar con solicitudes y respuestas durante el procesamiento de Varnish. Gracias a Fastly, podrás utilizar VCL para modificar solicitudes en la caché de borde, con lo que acercarás el procesamiento a tus clientes.

The Varnish Book ofrece abundante documentación fácilmente asimilable que abarca la máquina de estados de Varnish y los básicos sobre VCL. Me saltaré esas explicaciones. Hablemos de respuestas sintéticas, que nos van a ser de utilidad más adelante cuando toquemos las estrategias HTTP caching.

Respuestas sintéticas

Las respuestas sintéticas sirven para devolver una respuesta directamente desde el nodo de la caché sin tener que realizar una búsqueda en el almacenamiento en caché ni trasladar la solicitud a tu origen. En otras palabras, la respuesta se denomina "sintética" porque en ningún caso fue obtenida a partir del origen.

VCL proporciona la función synthetic con este fin, que es útil en varios aspectos. Por ejemplo, la devolución de errores directamente desde el edge o el almacenamiento en caché de botones "me gusta" y "compartir".

Imagínate que tienes un punto de conexión de API JSON /secret-endpoint que precisa de un token para estar presente en el HTTP header de autorización del tipo My-Key:somethingSuperSecret. Si el encabezado de autorización no está presente o no contiene "My-Key", la solicitud debería denegarse y debería devolverse un error del tipo 401 No autorizado.

Con Rails, sería probable que gestiones este tipo de verificación del HTTP header con un método del tipo ActionController que haga asignaciones a /secret-endpoint. Si te sientes capacitado/a para ello, podrías escribir un programa intermedio para gestionarlo a fin de reducir algo la sobrecarga en todo el stack de Rails. Eso no está nada mal, pero si existiera la capacidad de efectuar este procesamiento en el edge, más cerca de tus usuarios, ¿por qué hacer que el cliente aguarde a una respuesta que ha de hacer todo el recorrido desde el origen?

Las respuestas sintéticas permiten responder a cualquier mensaje del tipo 401 Unauthorized (no autorizado) directamente desde la caché de borde, eliminando así la penalización de latencia que supone tener que acudir al origen. Échale un vistazo a esto:

# vcl_recv is the first VCL function executed.
# full docs at https://www.varnish-software.com/static/book/VCL_Basics.html
sub vcl_recv {
  if (req.url ~ "^/secret-endpoint" && !req.http.authorization ~ "My-Key\.") {
    error 401 "Unauthorized";
  }
}

# vcl_error handles the error we returned in vcl_recv
sub vcl_error {
  if (obj.status == 401) {
    set obj.http.Content-Type = "application/json; charset=utf-8";

  # our "synthetic" json error response
    synthetic {"
      {
        "code": 401,
        "error": "Unauthorized",
        "msg": "Invalid Authorization"
      }
    "};
  }
  return(deliver);
}

Este ejemplo de VCL no sustituye en modo alguno a la implementación de una autenticación apropiada en el lado de Rails. Se trata de un mero recurso con el que transferir el procesamiento de solicitudes procedente de Rails con el fin de rebajar la latencia y la carga que gestiona el servidor de Rails.

Encontrarás lecturas complementarias sobre este tipo de scripting en nuestro blog, incluida esta otra entrada, que viene bastante al caso: Almacenamiento en caché de botones "me gusta" y "compartir". Encontrarás más información sobre la función vcl_error en The Varnish Book. A mí también me gusta tener a mano esta chuleta de regex de VCL.

¿VCL no es una opción? Sin problema.

Si VCL no te gusta o no te resulta cómodo, en Fastly no te lo vamos a echar en cara. De hecho, proporcionamos una gama exhaustiva de API de condiciones y tutoriales dedicados a su uso que posibilita la configuración de "condiciones de edge" parecidas. La API permite sacar partido del scripting de edge por medio de condiciones sin tener que profundizar en los conocimientos de VCL o Varnish.

Tras haber abordado VCL y Varnish, pasemos ahora a analizar estrategias con vistas al almacenamiento en caché de contenido dinámico.

Estrategias de almacenamiento en caché

Al configurar el almacenamiento en caché dinámico, probablemente tengas que reflexionar en profundidad sobre la forma en que páginas y API interactúan con los datos subyacentes, a fin de tomar decisiones adecuadas sobre el almacenamiento en caché. Una de las maneras de potenciar el rendimiento de las API con Fastly es el almacenamiento de las respuestas de estas en caché de borde.

Almacenamiento de API en caché en edge

Las API HTTP son parte integral de nuestro día a día: las empresas implementan API en multitud de ámbitos, desde aplicaciones móviles a satélites pasando por frigoríficos. Las formas convencionales de almacenar en caché API dinámicas de Rails incluyen las siguientes:

  • Definición de TTL cortos en Cache-Control;

  • Mezcla de almacenamiento en caché integrado de páginas/acciones/fragmentos;

  • Ejecución de un acelerador HTTP como Varnish con prioridad respecto de Rails (si consigues hacerlo, ¡te felicito!).

TTl cortos

El uso de un periodo de vida (TTL, por sus siglas en inglés) corto —p. ej., de 30 segundos— junto con optimizaciones de milla intermedia desde redes CDN como Fastly mejora los tiempos de respuesta y rebaja el volumen de solicitudes enviado al origen. Esta estrategia es relativamente sencilla de implementar, aunque no tiene mucho sentido cuando se trata de gestionar cambios impredecibles, tales como actualizaciones activadas por los usuarios.

Imaginemos que utilizas un valor de Cache-Control de public, max-age=30 para las respuestas de API. ¿Qué sucede si la respuesta cambia uno o dos segundos después de tu respuesta inicial? Si has respondido "nada", has acertado.

Esto tiene el desafortunado efecto secundario de forzar a los clientes a quedarse durante el TTL con una respuesta (obsoleta) que ha perdido validez. Se genera así frustración y confusión entre los usuarios, ya que estos esperan que los cambios se reflejen de manera instantánea. A día de hoy, los tiempos de respuesta de las API son inferiores al segundo o incluso solamente unos cientos de milisegundos, lo cual permite que los cambios se realicen con extrema rapidez. El ajuste de valores arbitrariamente pequeños para los TTL dando por supuesto que el contenido no cambia no es suficiente si tus necesidades son de alto rendimiento.

Almacenamiento en caché de Rails

En cambio, el uso de elementos integrados de Rails, como el almacenamiento en caché de acciones o fragmentos para API dinámicas, permite gestionar mejor cambios de contenido impredecibles, puesto que tendrás un mayor grado de control sobre tus mecanismo de almacenamiento en caché (incluida la capacidad de forzar la caducidad de fragmentos). Sin embargo, con algunas de las optimizaciones de red que proporciona la CDN no todo son ventajas, ya que se precisan más trayectos hacia tu capa de almacenamiento en caché u origen y se tarda más tiempo en efectuar el procesamiento en el interior del stack de Rails.

Un truco interesante que utiliza el almacenamiento en caché de fragmentos de Rails para camuflar los fragmentos que van a caducar (práctica que también se conoce como invalidación) son claves especiales de caché que cambian cada vez que se actualiza un objeto. A continuación, el algoritmo interno de la caché de uso menos reciente procede a forzar la caducidad del almacenamiento en caché para decidir qué contenido debe ser nuevo. Se trata de un truco verdaderamente inteligente, aunque tiende a malinterpretarse, que solventa la falta de un marco de invalidación de HTTP que esté bien enraizado, tal y como explica el VP of Tecnhology de Fastly, Hooman Beheshti, en la entrada "Aprovechamiento de tu CDN para almacenar en caché contenido dinámico".

Acceso al purgado

Todo sistema de purgado que tenga capacidad para mantener altos ritmos en los cambios de contenido permite HTTP caching dinámico más inteligente que los TTL cortos y gana en intuición y transparencia en comparación con el almacenamiento en caché por fragmentos. Gracias a Fastly, el almacenamiento en caché de API dinámicas es sinónimo de la potencia de Instant Purge.

A continuación pasamos a explicar cómo funciona:

Recuerda el ejemplo anterior en el que se definía un valor de TTL de 30 segundos para respuestas de API. Cada vez que se hace una actualización (y ello provoca que la respuesta almacenada en caché caduque), la solicitud se remite al origen. El origen lleva a cabo una escritura de la base de datos y cualquier otro procesamiento que venga definido en tu controlador de solicitudes de actualización. Nuestro controlador de solicitudes de actualización debe también emitir una solicitud de purgado, que se envía a las cachés. La caché reconoce el purgado suprimiendo la respuesta invalidada y sustituyéndola con una respuesta nueva procedente del origen.

En Rails, el controlador de solicitudes de actualización es la acción de controlador que gestiona esa actualización. Lo habitual es que esto fuera el método update en una clase heredada de ActionController.

Queda aún por responder una cuestión importante: ¿cómo se especifica qué objeto invalidar? Un método es efectuar la invalidación en función de la URL del recurso del objeto almacenado en caché. Se permite así el uso del método HTTP PURGE de tal forma que curl -X PURGE http://example.com/object/to/purge funcione. Pero ¿qué ocurriría si las respuestas están relacionadas y son dependientes?

En este caso, el purgado a partir de direcciones URL del recurso precisaría el envío de una solicitud de purgado por cada objeto dependiente, así como el objeto del nivel superior. Lo cual es poco práctico si la caché contiene miles de objetos dependientes.

El almacenamiento en caché de fragmentos de Rails con caducidad key-based es útil en esta situación porque permite almacenar en caché objetos anidados y dependientes sin darle muchas vueltas.

Fastly te permite especificar claves de caché mediante un encabezado HTTP Surrogate-Key (échale un vistazo a esta entrada del blog acerca de claves subrogadas). El etiquetado de respuestas con claves suplentes permite que la caducidad basada en claves (o invalidación) suceda en un objeto de respuesta única o en grupos de objetos dependientes. Al igual que el método expire_fragment en ActionController, la llamada de API de invalidación de Fastly elimina el objeto de la caché.

De este modo, el proceso para almacenar en caché contenido dinámico es el siguiente:

  1. Establece un valor adecuado para el TTL de la respuesta (cuanto más largo sea, mejor, ya que eres tú quien controla la invalidación).

  2. Etiqueta la respuesta con una o varias claves suplentes (se da en HTTP GET).

  3. Invalida por clave suplente cuando la respuesta cambie (se da en HTTP POST, PUT, DELETE).

Para ayudar a realizar este proceso con Rails, se concibió Fastly-Rails. Esta perla es un plugin de Rails que proporciona aplicaciones auxiliares y extensiones con el fin de implementar el proceso anterior en tus aplicaciones de Rails. Encontrarás explicaciones y ejemplos de uso detallados en el archivo README del repositorio de GitHub.

En Fastly, prestamos atención a los comentarios y revisamos abiertamente los parches para que todos los usuarios se sientan como en casa. ¿Tienes alguna sugerencia que hacer o quieres compartir algún comentario? Plantea un problema o una solicitud de incorporación de cambios en el repositorio o echa un vistazo a nuestro Foro comunitario.

Quizás veas que hay una sección especial del README que trata sobre las cookies. Quisiera agradecer profundamente a Jessie Young, de Thoughtbot, por sus aportaciones a esta sección y, además, por haber escrito la estupenda guía de uso de Fastly junto con Rails. Esta sección puede ser de gran ayuda al investigar por qué objetos que deberían haberse almacenado en caché no se han almacenado. Sugerencia: por defecto, las respuestas que contengan Set-Cookie no se almacenan en caché.

El seguimiento de este problema dio pie a la aportación de un programa intermedio de rack que elimina el header o encabezado HTTP Set-Cookie de las respuestas que contengan un encabezado del tipo Surrogate-Key o Surrogate-Control.

Durante la revisión de algunos fragmentos de lenguaje VCL para esta entrada, me dio por pensar, tras los hechos, que una solución alternativa sería utilizar VCL en el edge para quitar Set-Cookie. Por ejemplo, algo como esto:

sub vcl_backend_response {
  if (req.url ~ "^/thing/to/cache") {
    unset beresp.http.set-cookie;
  }
}

Échale un vistazo a este documento de VCL sobre cookies para obtener más información.

Existen en realidad múltiples situaciones en las que se podría emplear VCL en lugar de programas intermedios de racks. Un caso evidente es el de la compresión de gzip. En lugar de utilizar el programa intermedio Rack::Deflater, mencionado en la Parte 1 de esta serie de entradas de blog, podrías hacer esto mismo en el edge mediante VCL o a través de tu configuración de Fastly.

Cookies y contenidos de usuarios conectados

Una situación delicada que se pone de manifiesto al almacenar en caché HTML o JSON es la de gestionar contenido específico de usuarios o autenticado. Aunque ciertamente no pretendemos almacenar datos privados en cachés públicas compartidas, lo que sí buscamos es acelerar la entrega proporcionando tantos recursos de la caché como sea posible.

Una solución al respecto es utilizar un lenguaje de marcado Edge Side Include (ESI), cuyo concepto es algo parecido a los parciales de vista de Rails. Para utilizar el ESI, sustituye el fragmento de código HTML (o JSON) autenticado o específico de usuarios con un fragmento de XML que se parezca a este:

<html>
  ...
  <esi:include src="/user/profile" />
  ...
</html>

Una vez que se haya eliminado del resto de la página el fragmento de contenido autenticado, esta página puede ser almacenada en caché como cualquier otra. Cuando se solicita la página, la caché formula al origen una solicitud del contenido específico de usuarios y lo inserta en la página antes de enviarlo al cliente.

Échale un vistazo a esta entrada del blog acerca del uso de ESI para ver un ejemplo de cómo almacenar en caché una página que contiene el carrito de la compra de un usuario conectado "no susceptible de almacenar en caché". Esa entrada te permitirá descubrir que AJAX se puede emplear como alternativa a ESI. ESI o AJAX: la elección depende de tus circunstancias, requisitos y preferencias.

En la última parte de la entrada, se describen a grandes rasgos ESI, JSON y las respuestas sintéticas con el fin de diseñar objetos JSON específicos de usuarios, ya que AJAX no es una opción disponible para contenido JSON.

Almacenamiento en caché de HTML básico

Aunque las aplicaciones compuestas por una sola página están de moda, ello no altera un ápice las exigencias que hacen nuestros usuarios respecto de páginas de carga rápida. Resulta frustrante tener que estar delante de una página vacía o a medio cargar durante uno o varios segundos mientras las llamadas AJAX terminan de presentar la página.

El almacenamiento en caché de HTML básico puede proporcionar una mejora de rendimiento. Cuanto más rápida sea la entrega al cliente de la página de HTML básico, antes podrán activarse las llamadas AJAX y antes terminará de presentarse la página.

Una forma de conseguirlo es utilizar el almacenamiento en caché de páginas de Rails si el HTML básico se presenta a partir de la acción de un controlador (p. ej., HomepageController). Para ello se necesitan cambios en el código de tu aplicación; así que otra forma de lograrlo sin hacer modificación alguna en la aplicación es utilizar VCL o configurar una condición y un encabezado a través de la API de Fastly. Datos de sesión aparte, el lenguaje VCL destinado a este fin podría parecerse a esto:

sub vcl_fetch {
  if (req.url == '/') {
    # set client TTL after the origin responds but before it's cached
    set beresp.http.cache-control = "public, max-age=900";

    # set the time to keep in the edge cache
    set beresp.ttl = 1w;
  }
}

Aunque todo tiene una pinta estupenda, Rails te ayuda a prevenir los scripts de sitios cruzados (XSS) inyectando un único token de falsificación de solicitud de sitios cruzados (CSRF, por sus siglas en inglés) en el header de la página con la aplicación auxiliar csrf_meta_tag.

James Rosen, ingeniero de Fastly, publicó recientemente una entrada de blog sobre almacenamiento en caché con seguridad contra CSRF. Se trata de una lectura reveladora para quienes piensan que el almacenamiento en caché de HTML básico es pertinente para tus necesidades.

Conclusión

Tengo que admitir que esta entrada tiene un montón de información que asimilar.

Hemos visto la configuración de varias funciones de Fastly y, además, la explicación sobre CNAME y algunos ejemplos valiosos sobre el comando dig. Nos hemos sumergido en la sintaxis de VCL y en las respuestas sintéticas con vistas al scripting del lado del edge; también hemos expuesto estrategias de HTTP caching de API dinámicas de Rails, incluidas Instant Purge, ESI y AJAX. Por último, recuerda que el scripting del lado del edge se adecua de forma natural a algunas tareas que se suelen completar con programas intermedios de rack.

Espero que hayas disfrutado, hayas aprendido un par de cosas y que te haya picado el gusanillo de ampliar el alcance de tu caché. Si este tema te resulta interesante y te gustaría hacerlo a diario, buscamos personal. ¡Feliz HTTP caching!