Aceleración de Rails, Parte 1: almacenamiento en caché integrado

Esta entrada de blog es una adaptación de una charla que pronuncié en Austin acerca de almacenamiento en caché y Rails.

Ruby on Rails es un marco web potente y fácil de utilizar que permite a los desarrolladores crear aplicaciones rápidamente. Su gran popularidad se debe principalmente al “estilo Rails,” también conocido como convención sobre configuración. Ampliar las aplicaciones de Rails solía ser una tortura (¿Alguien recuerda la ballena de error de Twitter?), pero hemos recorrido un largo camino.

Almacenar en caché es una estrategia que ayuda a aliviar los problemas asociados con la ampliación, a menudo obviados por los desarrolladores de Rails. Los comienzos al almacenar en caché pueden resultar confusos, puesto que las condiciones y la documentación resultarán especialmente complicados para las personas no expertas.

En esta entrada, la primera de una serie de dos dedicada a la aceleración de Rails, explicaré las opciones de almacenamiento en caché que incorpora Rails y algunas buenas prácticas para utilizarlas. En la segunda parte, abordaré el almacenamiento en caché dinámico en el perímetro y la integración con la plataforma de aceleración de Fastly.

Contenido estático frente a contenido dinámico

Antes de entrar en el tema, es importante conocer la diferencia entre los tipos de contenido que puede almacenarse en caché en la web.

El contenido estático hace referencia a objetos web como imágenes, scripts y fichas de estilo; un contenido que no cambia a menudo y que, cuando lo hace, normalmente permite controlar las modificaciones.

El contenido dinámico, por otro lado, suele incluir objetos como JSON o HTML que cambian con frecuencia, normalmente debido a modificaciones de los usuarios finales o a interacción con aplicaciones. En Ruby on Rails puedes gestionar contenido estático con la canalización del activo y utilizar almacenamiento en caché de fragmentos para almacenar el HTML dinámico.

Opciones integradas de almacenamiento en caché en Rails

Entre las opciones de almacenamiento en caché que incorpora Rails se incluyen:

  • SQL Query

  • Páginas/Acciones

  • Canalización del activo

  • Fragmentos/Muñeca rusa o Russian Doll

Encontrarás más detalles sobre cada una de las opciones en la Guía del almacenamiento en caché en Rails.

SQL Query

Ruby on Rails proporciona almacenamiento en caché para SQL Query, utilizado para almacenar los resultados de las consultas a bases de datos durante el tiempo de la petición. Para habilitarlo se deben configurar las opciones siguientes en el archivo config/environments/*.rb apropiado (normalmente production.rb).

config.action_controller.perform_caching = true
config.cache_store = :mem_cache_store, "cache-1.example.com"

El inconveniente del almacenamiento en caché de las consultas es que los resultados de la consulta solo se almacenan en caché durante el tiempo de esa petición exclusivamente; no persisten durante varias peticiones. Esto supone un inconveniente, puesto que el almacenamiento en caché de consultas entre peticiones podría tener más ventajas. Sin embargo, es posible implementar almacenamiento en caché de consultas para varias peticiones utilizando la interfaz estándar de Rails.cache. Así, por ejemplo, podrías crear un método de clase en un modelo que almacene en caché los resultados de las consultas:

class Product < ActiveRecord::Base
  def self.out_of_stock
    Rails.cache.fetch("out_of_stock", expires_in: 1.hour) do
      Product.where("inventory.quantity = 0")
    end
  end
end

Algo que cabe mencionar es que debes configurar una tienda en caché que funcione para ti. La tienda predeterminada es de almacenamiento en disco, cuyo acceso podría ser más lento si carga muchos objetos en su memoria caché. De todas formas el acceso a almacenamiento en disco es lento.

Páginas/Acciones

En mis charlas sobre almacenamiento en caché delante de grandes grupos de personas siempre sondeo al público para hacerme una idea de las estrategias de almacenamiento en caché que utilizan. De hecho, solo me he encontrado con una persona que utilizara páginas o acciones de Rails para almacenar en caché durante la producción. Este bajo uso probablemente explique por qué el almacenamiento en caché de páginas o acciones fue retirado del núcleo de Rails 4 y cristalizó en sus propias joyas de almacenamiento en caché. Además, la mayor parte de los trabajos recientes de almacenamiento en caché en Rails se han llevado a cabo con almacenamiento de fragmentos. Puesto que fueron retirados del núcleo de Ruby on Rails no voy a extenderme acerca de su uso. De todas formas, en la segunda parte me referiré a algunos paralelismos entre el almacenamiento en caché de acciones y la implementación del almacenamiento dinámico en el edge.

Canalización del activo

A partir de Rails 3.1, la canalización del activo ha resultado ser una herramienta útil y fácil de utilizar que simplifica el manejo del contenido estático (y el almacenamiento en caché del contenido estático). Mi recomendación es que utilices los parámetros siguientes en los archivos config/environments/*.rb apropiados.

config.serve_static_assets = false

Transferencia de activo estático servido a Nginx o Apache mediante inhabilitación en el servidor de Rails de la entrega de activo y contenido en /public. El rendimiento de Nginx o Apache es mucho mejor en el manejo de archivos estáticos que tu servidor de Rails.

config.assets.css_compressor = :yui
config.assets.js_compressor = :uglifier

Compresión, minificación y concatenación de JavaScript y fichas de estilo. En la versión anterior a Rails 4 se podía configurar config.assets.compress = true, pero Rails 4 requiere actualmente que se configuren explícitamente los compresores JS y CSS. El parámetro assets.compress maneja todas estas cuestiones, lo que puede reducir considerablemente el tamaño del código JavaScript y las fichas de estilo, en ocasiones más del 80 %. Comprimir archivos para utilizar menos bytes permite un ahorro en costes de ancho de banda, y el contenido llegará más rápidamente a los usuarios finales (porque habrá menos contenido).

config.assets.digest = true

La opción Asset Digests es una forma sencilla de evitar enfrentarse al purgado del almacenamiento en caché cuando el contenido estático cambia. Te recomiendo encarecidamente activarla.

config.action_controller.asset_host = "http://cdn.myfastsite.com"

Sirve tu activo desde una CDN. Esta afirmación se explica en gran parte por sí sola.

config.static_cache_control = "public, s-maxage=15552000, max-age=2592000"

Utiliza encabezados adecuados de control del almacenamiento en caché. Se explicará más adelante.

Aprovechar todas las ventajas de las opciones de configuración de la canalización del activo no solo mejorará los tiempos de carga del activo estático, sino también la satisfacción y la experiencia del usuario final.

Rack::Deflater

Vamos a comentar rápidamente el programa intermedio Rake::Deflater que se incluye con Rails, que está habilitado en tu archivo application.rb.

# in application.rb
module FastestAppEver
  class Application < Rails::Application
    config.middleware.use Rack::Deflater
  end
end

Este programa intermedio va a comprimir (utilizando gzip, deflate u otro valor de Accept-Encoding) cada una de las respuestas emitidas por tu aplicación. Recomiendo encarecidamente utilizarlo para acelerar la entrega y reducir el ancho de banda de tus aplicaciones. Encontrarás más información en el blog Thoughtbot Rake Deflater .

Fragmentos/Muñeca rusa o Russian Doll

El almacenamiento en caché de fragmentos es la forma de almacenar en caché HTML dinámico dentro de las aplicaciones de Ruby on Rails. Almacenar en caché fragmentos supone un método de almacenamiento en caché que se utiliza en plantillas de visualización como esta:

# products/index.html.erb
<% cache(cache_key_for_products) do %>
  <% Product.all.each do |p| %>
    <%= link_to p.name, product_url(p) %>
  <% end %>
<% end %>

Empaqueta una parte arbitraria de HTML en esta etiqueta de almacenamiento en caché para almacenarla en la tienda de caché que configuraste. También puedes obtener etiquetas de almacenamiento en caché más complejas y anidarlas arbitrariamente. Esto se conoce generalmente entre la comunidad de Ruby on Rails como almacenamiento en caché de muñecas rusas orussian dolls. DHH te ofrece más información en su entrada de blog.

# products/index.html.erb
<% cache(cache_key_for_products) do %>
  All available products:
  <% Product.all.each do |p| %>
    <% cache(p) do %>
      <%= link_to p.name, product_url(p) %>
    <% end %>
  <% end %>
<% end %>

Para mí, el almacenamiento en caché de fragmentos es el mejor complemento para las técnicas de almacenamiento en caché de Rails y cada vez son más los desarrolladores de Ruby on Rails que lo utilizan en lugar del de páginas o acciones. Mi opinión es que el almacenamiento en caché de tipo russian doll o muñeca rusa resulta confuso y en realidad he evitado implementarlo debido a lo raro de su esquema de claves de caché. Además, resulta sumamente difícil trabajar con visualización de plantillas en las aplicaciones de Rails heredadas.

En realidad, el formato de las claves de almacenamiento en caché de Rails Russian Doll es bastante inteligente y recomendable para los usuarios familiarizados con Memcached. El formato de claves de almacenamiento en caché por Id./timestamp es una forma de eludir el hecho de que Memcache no sea compatible con la depuración mediante comodín.

En lugar de pensar que las claves de caché se asignan siempre al mismo contenido y que cada clave se corresponde con un contenido único, prefiero imaginar las claves de caché asignadas al contenido más actualizado (como una clave principal de base de datos). Me resulta más fácil explicar las estrategias complejas de almacenamiento en caché de esta forma, dado que su caché admite un mecanismo de purgado lo bastante rápido. En la segunda parte me extenderé más acerca de las claves de caché.

Una última idea sobre el Fragment caching: está orientado principalmente a HTML dinámico. Al retirarle el almacenamiento en caché de páginas y acciones, Rails carece ahora de un buen mecanismo integrado para el almacenamiento en caché dinámico de API. En la segunda parte abordaré el almacenamiento en caché dinámico de API.

HTTP Headers

Rails es realmente eficaz creando extractos de encabezados HTTP. Esto es fantástico hasta que uno necesita interactuar con ellos en un nivel más básico. A continuación se explican los encabezados HTTP que habitualmente afectan al almacenamiento en caché y algunas pautas de buenas prácticas.

Cache-Control

En el ejemplo anterior de control del almacenamiento en caché de la canalización del activo, he utilizado una cadena considerablemente larga con un par de "directivas" diferentes. ¿Qué significa todo esto?

public Almacenar en caché durante el tiempo especificado. Esto también puede ser "private" (privado), es decir, no se almacena en caché.

max-ageEl tiempo durante el cual un contenido se debe almacenar en caché. Esto se aplica a todo el almacenamiento en caché, si no se indica lo contrario, y normalmente pienso en la directiva "max-age" aplicable al navegador. Elige este valor en función de lo que sea conveniente para tu aplicación. Este valor se debe basar en la frecuencia con la que se producen cambios. Así, por ejemplo, si solo actualizas mi CSS dos veces al año, probablemente sea seguro almacenarlo en caché durante al menos unas semanas.

s-maxageLa duración del tiempo de almacenamiento en caché del contenido en las memorias caché proxy, por ejemplo CDN o Memcached.

Al utilizar dos valores diferentes para "max age" (uno para el navegador y otro para la CDN), puedes asegurarte de que los usuarios dispongan siempre del contenido más actualizado y puedes maximizar el tiempo de vida del contenido en la CDN, lo que minimiza el retorno de las peticiones a tu servidor de aplicaciones. Consulta la Sección 14.9 de RFC 2616 para obtener una lista completa de las directivas de control del almacenamiento en caché.

ETag

El encabezado HTTP de ETag se ofrece para determinar si el contenido ha cambiado y debe ser actualizado. Rails agrega automáticamente encabezados ETag en respuestas con el programa intermedio Rack::ETag. En el nivel más alto, las ETags permiten a Rails servir 304 respuestas no modificadas cuando no es necesario actualizar los datos del usuario final. Lamentablemente, para poder hacer esto, Rails todavía debe presentar la respuesta cada vez y generar la ETag, lo que frena el rendimiento. Sin embargo, Rails ofrece una forma de invalidar esta forma de actuar utilizando Conditional GETs con los métodos stale y fresh_when.

Vary

Vary se utiliza para cambiar una respuesta basándose en el valor de otro encabezado HTTP. Esto se explica mejor con un ejemplo. Tomemos un encabezado Vary con Accepting-Encoding como valor Vary: Accept-Encoding

Esta respuesta puede variar dependiendo del valor del encabezado Accept-Encoding. Así, por ejemplo, el valor de Accept-Encoding puede ser gzip o deflate.

Accept-Encoding: gzip => Response A (gzip encoded)
Accept-Encoding: deflate => Response A' (deflate encoded)
Steve Souders, con quien tengo el placer de trabajar en Fastly, me enseñó una buena práctica muy sencilla para el encabezado Vary. ¿Cuál era su consejo? La directiva Vary solo se debe aplicar a Accept-Encoding/Content-Encoding.

No se debe aplicar Vary al encabezado User-Agent. Esto puede parecer práctico, especialmente cuando es necesario servir imágenes de diferente tamaño a clientes móviles. Pero existen miles de user agents. Aplicar "vary" a miles de respuestas diferentes limita las ventajas del almacenamiento en caché, concretamente porque puede ser imposible servir la misma respuesta más de una vez debido a la cantidad de variaciones del valor de User Agent.

Consulta Aceleración de Rails, parte 2, donde hablaré de integrar en tus aplicaciones el almacenamiento en caché en el edge.

Michael May
Integration Engineer
Fecha de publicación:

9 min de lectura

Comparte esta entrada
Michael May
Integration Engineer

Michael May es ingeniero de integración en Fastly, cofundador de CDN Sumo (adquirida por Fastly), y se ha trasladado recientemente a San Francisco, procedente de Austin. Antes de Fastly y CDN Sumo, Michael trabajó ampliamente con JRuby en HomeAway, trabajando intensamente para incorporar Ruby a su ecosistema altamente centrado en Java.

¿List@ para empezar?

Ponte en contacto o crea una cuenta.