El patrón de cuatro pasos
El defecto reside en cuatro pasos secuenciales, no atómicos:
// DEFECTO
Value value = cache.get(key);
if (value == null) {
value = expensiveCompute(key);
cache.put(key, value);
}
return value;
Paso 1: verificar caché. Paso 2: fallo. Paso 3: calcular. Paso 4: almacenar. Los cuatro pasos: no atómicos. Entre el paso 1 y el paso 4, cualquier número de hilos puede ejecutar el paso 1 y todos verán null.
La Trampa de Idempotencia
El razonamiento que protege este patrón: 'está bien si dos hilos calculan y almacenan el mismo valor. El resultado es idempotente. No ocurre corrupción de datos.'
Este razonamiento: correcto en cuanto a corrección. Fatal en cuanto al costo.
Con 1,000 hilos en un fallo de caché: 1,000 hilos ejecutan expensiveCompute(key). Si expensiveCompute consulta una base de datos, se disparan 1,000 consultas simultáneas. Si llama a un servicio externo, se disparan 1,000 solicitudes HTTP simultáneas. El sistema que produce resultados correctos colapsa bajo el costo de producirlos.
Tres Desencadenantes
Una Manada de Tormenta se dispara cuando una clave de caché pasa de caliente a fría simultáneamente en muchos hilos:
Inicio en frío: el servicio se reinicia con una caché vacía. Primera ola de solicitudes: todas las claves fallan. Todo se calcula simultáneamente.
Reinicio del servicio: un reinicio gradual reinicia la caché en todas las instancias. El tráfico se redistribuye a instancias frías.
Expiración del TTL: una clave de alto tráfico expira. N hilos verifican, todos fallan, todos calculan antes de que el primer hilo almacene el resultado.
Los tres triggers: correlacionados con picos de tráfico. El rebaño se dispara cuando el tráfico alcanza su máximo y la caché está fría. El peor momento posible.
El ejemplo de Elasticsearch EnrichCache
Elasticsearch EnrichCache: el comentario documentado dice 'intencionalmente sin bloqueo por simplicidad...OK si volvemos a poner la misma clave/valor en una condición de carrera'. A 10.000 documentos por segundo con una caché de enriquecimiento fría: las 10.000 solicitudes golpean el índice de enriquecimiento simultáneamente. El índice de enriquecimiento, diseñado para búsquedas ocasionales, enfrenta 10.000 consultas concurrentes. El clúster se desestabiliza.
El razonamiento de idempotencia: correcto en el comentario del código. Catastrófico a 10.000 documentos por segundo.
Acoplamiento a MOAD-0001
MOAD-0001 (Defecto Sedimentario) crea un cuello de botella O(N²) en sistemas de alto rendimiento. Corregir MOAD-0001 (de O(N²) a O(N)) desbloquea esa estación de trabajo. El mayor rendimiento envía más solicitudes aguas abajo. Las cachés aguas abajo, previamente protegidas por el cuello de botella MOAD-0001, ahora reciben picos de tráfico correlacionados. MOAD-0005 se dispara en cachés que nunca lo habían activado antes. Arregla un MOAD; prepara el otro.
Lo que la Trampa de Idempotencia Entiende Mal
El comentario de Elasticsearch representa un razonamiento de ingeniería cuidadoso aplicado a la pregunta equivocada. Idempotencia: una propiedad real que vale la pena analizar. La trampa: detener el análisis en la corrección sin continuar hasta el costo.
computeIfAbsent & singleflight
La solución: hacer atómica la comprobación y el cálculo. Un hilo calcula. Todos los demás hilos esperan ese resultado.
Java: computeIfAbsent
// DEFECTO: cuatro pasos no atómicos
Value value = cache.get(key);
if (value == null) {
value = expensiveCompute(key);
cache.put(key, value);
}
return value;
// FIX: verificación atómica y cálculo
return cache.computeIfAbsent(key, k -> expensiveCompute(k));
computeIfAbsent: si la clave no existe, calcula exactamente una vez, almacena y devuelve. Todos los demás hilos que llamen a computeIfAbsent para la misma clave esperan el primer cálculo. Sin cómputo N-veces. Sin estampida.
Go: singleflight.Group
var g singleflight.Group
func getOrCompute(key string) (Value, error) {
v, err, _ := g.Do(key, func() (interface{}, error) {
return expensiveCompute(key)
})
return v.(Value), err
}
singleflight: si ya hay un cálculo en curso para una clave, todos los llamadores de esa misma clave esperan y comparten el único resultado. Un cálculo, N esperas, un resultado compartido. La abstracción 'flight': deduplica las solicitudes en vuelo.
Lock vs singleflight
Un candado ingenuo por clave serializa: el hilo 1 calcula, el hilo 2 espera, el hilo 3 espera. Tras finalizar el hilo 1, el hilo 2 entra y consulta la caché (acierta). El hilo 3 entra y consulta la caché (acierta). N-1 adquisiciones de candado y lecturas de caché.
singleflight deduplica: el hilo 1 calcula, los hilos 2 a N esperan el resultado del hilo 1. Sin adquisiciones de candado separadas. Sin lecturas de caché separadas. Un cálculo, un resultado, distribuido a N esperas. Menos operaciones que un candado por clave.
Ambos evitan la estampida. singleflight evita el trabajo redundante de forma más completa.
Reescribe el Patrón
Aplica la corrección a un escenario concreto.
// Una caché de perfiles de usuario en un servicio Java de alto tráfico
public UserProfile getProfile(String userId) {
UserProfile profile = profileCache.get(userId);
if (profile == null) {
profile = database.loadProfile(userId); // costoso: consulta DB de 50 ms
profileCache.put(userId, profile);
}
return profile;
}
El servicio se reinicia cada mañana a las 2 AM. A las 8 AM, 10,000 usuarios solicitan sus perfiles simultáneamente.