English· Español· Deutsch· Nederlands· Français· 日本語· ქართული· 繁體中文· 简体中文· Português· Русский· العربية· हिन्दी· Italiano· 한국어· Polski· Svenska· Türkçe· Українська· Tiếng Việt· Bahasa Indonesia

un

gast
1 / ?
terug naar lessen

Het Vier-Stappenpatroon

Het defect zit in vier opeenvolgende, niet-atomaire stappen:

// DEFECT
Value value = cache.get(key);
if (value == null) {
value = expensiveCompute(key);
cache.put(key, value);
}
return value;

Stap 1: controleer cache. Stap 2: miss. Stap 3: bereken. Stap 4: opslaan. Alle vier stappen: niet-atomair. Tussen stap 1 en stap 4 kunnen meerdere threads stap 1 uitvoeren en allemaal null zien.

De Idempotentie-val

De redenering die dit patroon beschermt: 'het is OK als twee threads dezelfde waarde berekenen en opslaan. Het resultaat is idempotent. Er treedt geen datacorruptie op.'

Deze redenering: correct wat betreft juistheid. Fataal wat betreft kosten.

Bij 1.000 threads bij een cache-miss: 1.000 threads voeren elk expensiveCompute(key) uit. Als expensiveCompute een database opvraagt, worden 1.000 database-query's tegelijkertijd uitgevoerd. Als het een externe service aanroept, worden 1.000 HTTP-verzoeken tegelijkertijd uitgevoerd. Het systeem dat correcte resultaten produceert, bezwijkt onder de kosten van het produceren ervan.

Drie triggers

Een Thundering Herd treedt op wanneer een cache-sleutel gelijktijdig van warm naar koud overgaat bij veel threads:

Cold start: service herstart met een lege cache. Eerste verzoekgolf: elke sleutel mist. Alle berekeningen gebeuren tegelijkertijd.

Service herstart: rolling restart reset de cache over alle instanties. Verkeer wordt herverdeeld naar koude instanties.

TTL-verval: een sleutel met veel verkeer verloopt. N threads controleren allemaal, missen allemaal, en berekenen allemaal voordat de eerste thread het resultaat opslaat.

Alle drie de triggers: gecorreleerd met verkeerspieken. De kudde vuurt wanneer het verkeer piekt & de cache koud is. Slechtst mogelijke timing.

Het Elasticsearch EnrichCache-voorbeeld

Elasticsearch EnrichCache: de gedocumenteerde opmerking luidt 'opzettelijk niet-vergrendelend voor eenvoud... OK als we dezelfde sleutel/waarde opnieuw plaatsen bij een raceconditie.' Bij 10.000 documenten per seconde met een koude verrijkingscache: alle 10.000 verzoeken raken de verrijkingsindex tegelijkertijd. De verrijkingsindex, ontworpen voor incidentele lookups, krijgt te maken met 10.000 gelijktijdige query's. Het cluster destabiliseert.

De idempotentieredenatie: correct in de codecommentaar. Catastrofaal bij 10.000 documenten per seconde.

Koppeling aan MOAD-0001

MOAD-0001 (Sedimentary Defect) creëert een O(N²)-knelpunt in systemen met hoge doorvoer. Het oplossen van MOAD-0001 (O(N²) naar O(N)) ontgrendelt dat werkstation. De snellere doorvoer stuurt meer verzoeken stroomafwaarts. Stroomafwaartse caches, eerder beschermd door het MOAD-0001-knelpunt, ontvangen nu gecorreleerde verkeerspieken. MOAD-0005 vuurt in caches die het eerder nooit triggerden. Los één MOAD op; bereid de andere voor.

Wat de Idempotentieval Misloopt

De Elasticsearch-commentaar vertegenwoordigt zorgvuldige engineering-redenering toegepast op de verkeerde vraag. Idempotentie: een echte eigenschap die het waard is om over na te denken. De val: stoppen met de analyse bij correctheid zonder door te gaan naar kosten.

Waarom leidt de redenering 'het is OK als twee threads hetzelfde resultaat schrijven' tot het defect? Wat klopt eraan, en wat mist het?

computeIfAbsent & singleflight

De oplossing: maak de check-and-compute atomair. Eén thread voert de berekening uit. Alle andere threads wachten op dat resultaat.

Java: computeIfAbsent

// DEFECT: vier niet-atomische stappen
Value value = cache.get(key);
if (value == null) {
value = expensiveCompute(key);
cache.put(key, value);
}
return value;

// FIX: atomische check-en-compute
return cache.computeIfAbsent(key, k -> expensiveCompute(k));

computeIfAbsent: als de sleutel ontbreekt, wordt de waarde exact één keer berekend, opgeslagen en geretourneerd. Alle andere threads die computeIfAbsent voor dezelfde sleutel aanroepen, wachten op de eerste berekening. Geen N-voudige berekening. Geen stormloop.

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: als een berekening voor een sleutel al loopt, wachten alle aanroepers voor dezelfde sleutel en delen ze het enkele resultaat. Eén berekening, N wachtenden, één resultaat gedeeld. De 'flight'-abstractie: dedupliceer in-flight verzoeken.

Lock vs singleflight

Een naïeve per-sleutel lock serialiseert: thread 1 berekent, thread 2 wacht, thread 3 wacht. Nadat thread 1 klaar is, gaat thread 2 binnen & controleert de cache (hit). Thread 3 gaat binnen & controleert de cache (hit). N-1 lock-acquisities & cache-lezen.

singleflight dedupliceert: thread 1 berekent, threads 2 tot en met N wachten allemaal op het resultaat van thread 1. Geen aparte lock-acquisities. Geen aparte cache-lezen. Eén berekening, één resultaat, verdeeld over N wachtenden. Minder operaties dan een per-sleutel lock.

Beide voorkomen de stormloop. singleflight voorkomt overbodig werk nog vollediger.

Herschrijf het patroon

Pas de fix toe op een concreet scenario.

// Een gebruikersprofiel-cache in een Java-service met hoge belasting
public UserProfile getProfile(String userId) {
UserProfile profile = profileCache.get(userId);
if (profile == null) {
profile = database.loadProfile(userId);  // duur: 50ms DB-query
profileCache.put(userId, profile);
}
return profile;
}

Service herstart elke ochtend om 2 uur 's nachts. Om 8 uur 's ochtends vragen 10.000 gebruikers tegelijkertijd hun profielen op.

Identificeer het defect, noem wanneer het optreedt, en herschrijf met computeIfAbsent.