चार-चरण पैटर्न
दोष चार अनुक्रमिक, गैर-परमाणु चरणों में निहित है:
// DEFECT
Value value = cache.get(key);
if (value == null) {
value = expensiveCompute(key);
cache.put(key, value);
}
return value;
Step 1: cache चेक करें। Step 2: miss। Step 3: compute करें। Step 4: store करें। सभी चार स्टेप्स: non-atomic। Step 1 और Step 4 के बीच कोई भी संख्या में threads Step 1 execute कर सकते हैं और सभी को null दिखेगा।
The Idempotency Trap
इस पैटर्न की रक्षा करने वाला तर्क: 'यदि दो थ्रेड्स एक ही मान की गणना करके स्टोर कर दें तो भी ठीक है। परिणाम idempotent है। कोई डेटा भ्रष्टाचार नहीं होता।'
यह तर्क: सही होने के बारे में सही है। लागत के बारे में घातक है।
1,000 थ्रेड्स पर कैश मिस होने पर: 1,000 थ्रेड्स प्रत्येक expensiveCompute(key) चलाते हैं। यदि expensiveCompute डेटाबेस क्वेरी करता है, तो 1,000 डेटाबेस क्वेरीज़ एक साथ फायर होती हैं। यदि यह कोई बाहरी सेवा कॉल करता है, तो 1,000 HTTP अनुरोध एक साथ फायर होते हैं। सही परिणाम देने वाला सिस्टम उन्हें उत्पन्न करने की लागत के बोझ तले ढह जाता है।
तीन ट्रिगर्स
एक Thundering Herd तब फायर होता है जब एक कैश कुंजी एक साथ कई थ्रेड्स में गर्म से ठंडी हो जाती है:
Cold start: खाली कैश के साथ सेवा पुनः आरंभ होती है। पहली अनुरोध लहर: हर कुंजी मिस होती है। सभी एक साथ गणना करते हैं।
Service restart: रोलिंग रीस्टार्ट सभी इंस्टेंस पर कैश रीसेट करता है। ट्रैफ़िक ठंडे इंस्टेंस पर पुनर्वितरित होता है।
TTL expiry: एक उच्च-ट्रैफ़िक कुंजी समाप्त हो जाती है। N थ्रेड्स सभी चेक करते हैं, सभी मिस होते हैं, पहला थ्रेड परिणाम स्टोर करने से पहले सभी गणना करते हैं।
तीनों ट्रिगर्स: ट्रैफ़िक स्पाइक्स से सहसंबद्ध। हर्ड तब फायर करता है जब ट्रैफ़िक पीक पर हो और कैश ठंडा हो। सबसे खराब संभव समय।
The Elasticsearch EnrichCache Example
Elasticsearch EnrichCache: दस्तावेज़ी टिप्पणी में लिखा है 'intentionally non-locking for simplicity...OK if we re-put the same key/value in a race condition.' ठंडे enrich cache के साथ प्रति सेकंड 10,000 दस्तावेज़ों पर: सभी 10,000 अनुरोध enrich index पर एक साथ पहुँचते हैं। Enrich index, जो कभी-कभार लुकअप के लिए डिज़ाइन किया गया है, अब 10,000 समवर्ती क्वेरीज़ का सामना करता है। क्लस्टर अस्थिर हो जाता है।
Idempotency का तर्क: कोड टिप्पणी में सही। 10,000 दस्तावेज़ प्रति सेकंड पर विनाशकारी।
Coupling to MOAD-0001
MOAD-0001 (Sedimentary Defect) उच्च-थ्रूपुट सिस्टम में O(N²) बाधा उत्पन्न करता है। MOAD-0001 को ठीक करने से (O(N²) से O(N)) वह वर्कस्टेशन अनब्लॉक हो जाता है। तेज़ थ्रूपुट डाउनस्ट्रीम को अधिक अनुरोध भेजता है। डाउनस्ट्रीम कैश, जो पहले MOAD-0001 बाधा से सुरक्षित थे, अब सहसंबद्ध ट्रैफ़िक स्पाइक्स प्राप्त करते हैं। MOAD-0005 उन कैश में फायर होता है जिनमें पहले कभी ट्रिगर नहीं हुआ था। एक MOAD ठीक करें; दूसरे को स्टेज करें।
What the Idempotency Trap Gets Wrong
Elasticsearch टिप्पणी गलत प्रश्न पर लागू सावधानीपूर्वक इंजीनियरिंग तर्क को दर्शाती है। Idempotency: एक वास्तविक गुण जिसके बारे में सोचना उचित है। जाल: विश्लेषण को सहीपन पर रोक देना और लागत तक जारी न रखना।
computeIfAbsent & singleflight
समाधान: check-and-compute को atomic बनाएँ। एक थ्रेड गणना करता है। बाकी सभी थ्रेड्स उस परिणाम का इंतजार करते हैं।
Java: computeIfAbsent
// DEFECT: चार गैर-परमाणु चरण
Value value = cache.get(key);
if (value == null) {
value = expensiveCompute(key);
cache.put(key, value);
}
return value;
// FIX: atomic check-and-compute
return cache.computeIfAbsent(key, k -> expensiveCompute(k));
computeIfAbsent: यदि कुंजी अनुपस्थित है, तो ठीक एक बार गणना करता है, संग्रहीत करता है और लौटाता है। उसी कुंजी के लिए computeIfAbsent कॉल करने वाले अन्य सभी थ्रेड पहली गणना का इंतजार करते हैं। कोई N-गुना गणना नहीं। कोई स्टैम्पीड नहीं।
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: यदि किसी कुंजी (key) के लिए पहले से ही कोई computation चल रहा है, तो उसी कुंजी के सभी कॉलर प्रतीक्षा करते हैं और एक ही परिणाम साझा करते हैं। एक compute, N waiters, एक परिणाम साझा। 'flight' abstraction: in-flight अनुरोधों को deduplicate करता है।
Lock vs singleflight
एक naive per-key lock serializes: thread 1 compute करता है, thread 2 प्रतीक्षा करता है, thread 3 प्रतीक्षा करता है। thread 1 पूरा होने के बाद, thread 2 प्रवेश करता है और cache चेक करता है (hit)। thread 3 प्रवेश करता है और cache चेक करता है (hit)। N-1 lock acquisitions और cache reads।
singleflight deduplicates: thread 1 compute करता है, thread 2 से N तक सभी thread 1 के परिणाम पर प्रतीक्षा करते हैं। अलग lock acquisitions नहीं। अलग cache reads नहीं। एक computation, एक परिणाम, N waiters को वितरित। per-key lock की तुलना में कम operations।
दोनों stampede रोकते हैं। singleflight redundant work को और अधिक पूरी तरह रोकता है।
Rewrite the Pattern
Apply the fix to a concrete scenario.
// उच्च-ट्रैफिक Java सेवा में एक यूजर प्रोफाइल कैश
public UserProfile getProfile(String userId) {
UserProfile profile = profileCache.get(userId);
if (profile == null) {
profile = database.loadProfile(userId); // महंगा: 50ms DB क्वेरी
profileCache.put(userId, profile);
}
return profile;
}
हर सुबह 2 AM पर सेवा पुनः आरंभ होती है। सुबह 8 AM पर 10,000 उपयोगकर्ता एक साथ अपने प्रोफाइल का अनुरोध करते हैं।