ruby on rails - cache - ¿Cómo el caché de muñecas rusas Rails 4 previene las estampidas?
rails cache clear (6)
Estoy buscando información sobre cómo el mecanismo de almacenamiento en caché en Rails 4 evita que varios usuarios intenten regenerar las claves de caché a la vez, también conocido como una estampida de caché: http://en.wikipedia.org/wiki/Cache_stampede
No he podido encontrar mucha información a través de Google. Si observo otros sistemas (como Drupal), la prevención de estampida de caché se implementa a través de una tabla de semaphores
en la base de datos.
Gran pregunta Una respuesta parcial que se aplica a los servidores Rails de subprocesos múltiples múltiples pero no a los entornos de multiproceso (o) (gracias a Nick Urban por dibujar esta distinción) es que el código de compilación de la plantilla ActionView se bloquea en una exclusión mutua que es por plantilla. Vea la línea 230 en template.rb aquí . Observe que hay una verificación de la compilación completada antes de agarrar la cerradura y después.
El efecto es serializar los intentos de compilar la misma plantilla, donde solo el primero realmente realizará la compilación y el resto obtendrá el resultado ya completado.
La configuración :race_condition_ttl
en ActiveSupport::Cache::Store#fetch
debería ayudar a evitar este problema. Como dice la documentation :
Configuración: race_condition_ttl es muy útil en situaciones donde una entrada de caché se usa con mucha frecuencia y está bajo una gran carga. Si un caché caduca y, debido a una gran carga, siete procesos diferentes intentarán leer los datos de forma nativa y luego todos intentarán escribir en el caché. Para evitar ese caso, el primer proceso para encontrar una entrada de caché caducada superará el tiempo de caducidad del caché por el valor establecido en: race_condition_ttl. Sí, este proceso está extendiendo el tiempo para un valor obsoleto por unos pocos segundos más. Debido a la vida útil prolongada de la memoria caché anterior, otros procesos continuarán usando datos un poco más antiguos por un poco más de tiempo. Mientras tanto, el primer proceso continuará y escribirá en el caché el nuevo valor. Después de eso, todos los procesos comenzarán a obtener un nuevo valor. La clave es mantener: race_condition_ttl small.
No hay protección contra las estampidas de memcache. Este es un problema real cuando hay varias máquinas involucradas y múltiples procesos en esas máquinas múltiples. -Ay-.
El problema se agrava cuando uno de los procesos clave ha "muerto" y se ha "bloqueado" ... bloqueado.
Para evitar estampidas, debe volver a calcular los datos antes de que caduquen. Por lo tanto, si sus datos son válidos durante 10 minutos, debe regenerarse nuevamente en el quinto minuto y restablecer los datos con una nueva caducidad durante 10 minutos más. Por lo tanto, no espere hasta que los datos caduquen para volver a establecerlos.
Tampoco debe permitir que sus datos caduquen en la marca de 10 minutos, sino que debe volver a calcularlos cada 5 minutos y nunca debe caducar. :)
Puede usar wget & cron para llamar periódicamente al código.
Recomiendo usar redis, que le permitirá guardar los datos y volver a cargarlos en caso de que se produzca un bloqueo.
-daniel
Pregunta muy interesante. Busqué en google (obtienes más resultados si buscas "dog stack" en lugar de "stampede") pero al igual que tú, no obtuve ninguna respuesta, excepto esta única entrada de blog: protección contra dogpile utilizando memcache .
Básicamente, almacena el fragmento en dos claves: key:timestamp
(donde timestamp sería updated_at
para los objetos de registro activos) y key:last
.
def custom_write_dogpile(key, timestamp, fragment, options)
Rails.cache.write(key + '':'' + timestamp.to_s, fragment)
Rails.cache.write(key + '':last'', fragment)
Rails.cache.delete(key + '':refresh-thread'')
fragment
end
Ahora, al leer de la memoria caché y al intentar obtener una memoria caché no existente, intentará en su lugar encontrar la key:last
fragmento en su lugar:
def custom_read_dogpile(key, timestamp, options)
result = Rails.cache.read(timestamp_key(name, timestamp))
if result.blank?
Rails.cache.write(name + '':refresh-thread'', 0, raw: true, unless_exist: true, expires_in: 5.seconds)
if Rails.cache.increment(name + '':refresh-thread'') == 1
# The cache didn''t exists
result = nil
else
# Fetch the last cache, as the new one has not been created yet
result = Rails.cache.read(name + '':last'')
end
end
result
end
Este es un resumen simplificado de Moshe Bergman al que he vinculado anteriormente, o puede encontrarlo aquí .
Rails no tiene un mecanismo incorporado para evitar estampidas de caché.
Según README para atomic_mem_cache_store
(un reemplazo para ActiveSupport::Cache::MemCacheStore
que mitiga las estampidas de caché):
Rails (y cualquier marco que se base en el almacén de caché de soporte activo) no ofrece ninguna solución integrada para este problema
Desafortunadamente, supongo que esta gema tampoco resolverá tu problema. Admite el almacenamiento en caché de fragmentos, pero solo funciona con una caducidad basada en el tiempo.
Lea más sobre esto aquí: https://github.com/nel/atomic_mem_cache_store
Actualización y posible solución:
Pensé un poco más en esto y se me ocurrió lo que me parece una solución plausible. No he verificado que esto funcione, y probablemente haya mejores maneras de hacerlo, pero estaba tratando de pensar en el cambio más pequeño que mitigaría la mayor parte del problema.
Supongo que está haciendo algo como cache model do
en sus plantillas como lo describe DHH ( http://37signals.com/svn/posts/3113-how-key-based-cache-expiration-works ). El problema es que cuando la columna updated_at
del modelo cambia, la cache_key
también cambia, y todos los servidores intentan volver a crear la plantilla al mismo tiempo. Para evitar que los servidores se impriman, deberá conservar la antigua cache_key por un breve tiempo.
Es posible que pueda hacer esto (dum da dum) almacenando en caché la clave de caché del objeto con una caducidad corta (por ejemplo, 1 segundo) y un race_condition_ttl
.
Puedes crear un módulo como este e incluirlo en tus modelos:
module StampedeAvoider
def cache_key
orig_cache_key = super
Rails.cache.fetch("/cache-keys/#{self.class.table_name}/#{self.id}", expires_in: 1, race_condition_ttl: 2) { orig_cache_key }
end
end
Repasemos lo que pasaría. Hay un montón de servidores que llaman cache model
. Si su modelo incluye StampedeAvoider
, entonces su cache_key
ahora buscará /cache-keys/models/1
, y devolverá algo como /models/1-111
(donde 111 es la marca de tiempo), que cache
usará para recuperar el fragmento de la plantilla compilada. .
Cuando actualice el modelo, model.cache_key
comenzará a devolver /models/1-222
(asumiendo que 222 es la nueva marca de tiempo), pero después del primer segundo, la cache
seguirá viendo /models/1-111
, ya que eso es lo que es devuelto por cache_key
. Una vez que pasa 1 segundo, todos los servidores obtendrán un error de /cache-keys/models/1
en /cache-keys/models/1
e intentarán regenerarlo. Si todos lo recrearan de inmediato, anularían el punto de anular cache_key
. Pero debido a que establecimos race_condition_ttl
en 2, todos los servidores, excepto el primero, se retrasarán durante 2 segundos, tiempo durante el cual continuarán obteniendo la plantilla almacenada en caché basada en la clave de caché anterior. Una vez que hayan transcurrido los 2 segundos, fetch
comenzará a devolver la nueva clave de caché (que habrá sido actualizada por el primer hilo que intentó leer / update /cache-keys/models/1
) y obtendrán un acierto de caché, devolviendo el Plantilla compilada por ese primer hilo.
¡Ta-da! Estampida evitada.
Tenga en cuenta que si hiciera esto, estaría haciendo el doble de lecturas de caché, pero dependiendo de qué tan comunes sean las estampidas, podría valer la pena.
No he probado esto. Si lo intentas, por favor hazme saber cómo va :)
Una estrategia razonable sería:
- use a
:race_condition_ttl
con al menos el tiempo esperado para actualizar el recurso. No es recomendable configurarlo en menos tiempo del esperado para realizar una actualización, ya que la mafia enojada terminará intentando actualizarla, lo que resultará en una estampida. - use an
:expires_in
time calculado como el tiempo de vencimiento máximo aceptable menos el:race_condition_ttl
para permitir que un solo trabajador:race_condition_ttl
el recurso y evite una estampida.
El uso de la estrategia anterior garantizará que no exceda el plazo de caducidad / caducidad y también evite una estampida. Funciona porque solo un trabajador se actualiza, mientras que la mafia enojada se race_condition_ttl
usando el valor de caché con el tiempo de extensión race_condition_ttl
hasta el tiempo de vencimiento originalmente previsto .