Ir al contenido

Cuando la caché hace más lenta una aplicación Laravel

La caché es lo primero a lo que se recurre y lo último que se mide. Cuatro patrones que añaden latencia en vez de quitarla, y la pregunta que conviene hacerse antes de ninguno.

5 min de lectura

Toda aplicación lenta acaba teniendo una caché. Es la intervención con la mejor historia: el trabajo es caro, así que hágalo una vez y guarde la respuesta. A menudo funciona, y la página que tardaba dos segundos tarda ahora doscientos milisegundos.

Lo bastante a menudo como para escribir sobre ello, no funciona. Estas son las formas que toma cuando va al revés.

Una llamada a caché por fila

La versión más común, y la más difícil de ver porque cada pieza suelta es correcta.

foreach ($orders as $order) {
    $rate = Cache::remember("fx.{$order->currency}", 3600, fn () =>
        $this->rates->for($order->currency)
    );
}

Cada llamada es un milisegundo. Hay cuatrocientos pedidos, así que la página se pasa cuatrocientos milisegundos preguntándole a un sistema rápido el mismo puñado de cosas una y otra vez. La consulta original que sustituyó se ejecutaba una vez y tardaba doce.

La caché no falló aquí. La metieron dentro de un bucle, lo que convirtió una operación cara en cientos de baratas y luego las sumó. Cachee la colección en lugar del elemento, o lea el conjunto distinto una vez antes de que empiece el bucle.

Cachear algo más barato que la consulta

Un select por clave primaria indexada sobre una tabla caliente está muy por debajo del milisegundo. Un viaje a Redis por la red es comparable, a veces más lento, y ahora es dueño de dos sistemas donde uno bastaba, más la invalidación.

Merece la pena decirlo sin rodeos porque es donde más esfuerzo se gasta para menos retorno. Antes de cachear nada, cronometre lo que está a punto de cachear. Si la respuesta es un número de milisegundos de una sola cifra, la caché casi con seguridad no es la mejora que busca.

La estampida

Una clave con un informe que tarda cuatro segundos en construirse, caducando a medianoche, en un sitio con tráfico a medianoche. Cada petición que llega en los siguientes cuatro segundos encuentra la clave vacía y empieza a construirla. Nadie sirve desde caché, la base de datos ejecuta la misma consulta pesada cien veces en paralelo, y la caída la causa la caché en lugar de evitarla.

Laravel trae la respuesta:

$report = Cache::lock('report:monthly', 10)->block(5, function () {
    return Cache::remember('report:monthly', 3600, fn () => $this->build());
});

Una petición reconstruye; las demás esperan y luego leen lo que escribió. La alternativa - y la mejor cuando se acepta algo de desfase - es no dejar que la clave caduque bajo tráfico en absoluto: refrésquela desde un comando programado y deje que los lectores encuentren siempre algo ahí.

Invalidación que cuesta más que las lecturas

Una caché vale lo que ahorra menos lo que cuesta mantenerla correcta. Ate una clave a las actualizaciones de un modelo y una importación masiva que toque cincuenta mil filas dispara cincuenta mil invalidaciones, cada una una escritura al almacén de caché. Las lecturas de la página ahorraban ocho milisegundos cada una. La importación tarda ahora cuatro minutos más.

La misma trampa viene con etiquetas demasiado amplias. Una etiqueta que cubre todas las claves de un inquilino es cómoda hasta que vaciarla tiene que recorrer un espacio de claves grande, momento en el que la invalidación es el camino lento y se ejecuta en cada escritura.

La pregunta que va primero

No qué deberíamos cachear sino por qué esto es lento.

Una caché por delante de un N+1 hace que el N+1 salga más barato de ejecutar, y el N+1 seguirá ahí, y seguirá ejecutándose, ahora con un segundo sistema implicado y un problema de corrección enganchado. Una caché por delante de un índice que falta es una manera de no añadir el índice. Ninguna de las dos cosas es ilegítima como parche, pero las dos deberían anotarse como tales, porque un parche que nadie anota es arquitectura al año siguiente.

Cuando la respuesta es de verdad que el trabajo es caro y correcto y tiene que ocurrir igualmente - una llamada a un tercero con límite de peticiones, un agregado sobre millones de filas, un documento renderizado - cachear es exactamente lo correcto. Ese es un conjunto de casos más estrecho de lo que sugiere el reflejo.

Qué medir

Tres números, los tres baratos de obtener y rara vez mirados:

Tasa de aciertos por prefijo de clave. Por debajo del ochenta por ciento y el coste de los fallos se está comiendo las ganancias. Por debajo del cincuenta está pagando por una caché que en la práctica no tiene.

Llamadas a caché por petición. Una es una caché. Cuarenta es un bucle del que aún no se ha dado cuenta.

La página con la caché desactivada. Si la diferencia es pequeña, la caché es complejidad sin retorno, y borrarla es una mejora de rendimiento en el único sentido que importa: el número de cosas que pueden salir mal.

A menudo la consulta de debajo era el problema desde el principio, y el N+1 es la forma habitual que adopta. Si es toda la ruta de la petición y no una consulta, el trabajo de rendimiento empieza por medir. La caché llega después, o no llega.

Preguntas relacionadas

¿No es Redis lo bastante rápido como para que esto dé igual?
Una lectura en Redis es un viaje de ida y vuelta por red: rápido, pero no gratis, y el coste es por llamada y no por página. Sustituya una consulta de 8 ms por cuarenta lecturas de caché de 0,4 ms y habrá hecho la página más lenta mientras todas las gráficas que mira dicen que la caché está sana.
¿Cómo sé si una caché se está ganando el sitio?
Mida la página con ella y sin ella, con datos de forma parecida a producción, y mire la tasa de aciertos. Una caché por debajo de aproximadamente el ochenta por ciento suele pagar el coste del fallo lo bastante a menudo como para anular las ganancias, y una por debajo del cincuenta le está costando dinero.
¿Qué es una estampida de caché?
Muchas peticiones encontrando la misma clave caducada en el mismo instante y recalculándola todas a la vez. La consulta cara que cacheó para no ejecutarla a menudo se ejecuta ahora cien veces en un segundo, bajo la carga que la hacía cara.
¿Merece la pena usar etiquetas de caché?
Son la forma más limpia de invalidar un grupo de claves y necesitan un almacén que las soporte - Redis o Memcached, no fichero ni base de datos. El coste es que vaciar una etiqueta recorre claves, así que una etiqueta que cubre muchísimas convierte la invalidación en la operación lenta en lugar de la lectura.

← Volver a todos los artículos

Llamar+1 848 272 7583WhatsApp+90 850 308 5436Correoinfo@codefacture.comPágina de contacto