ოთხნაბიჯიანი შაბლონი
დეფექტი ცხოვრობს ოთხ თანმიმდევრულ, არაატომურ ნაბიჯში:
// DEFECT
Value value = cache.get(key);
if (value == null) {
value = expensiveCompute(key);
cache.put(key, value);
}
return value;
ნაბიჯი 1: ქეშის შემოწმება. ნაბიჯი 2: მისი არარსებობა. ნაბიჯი 3: გამოთვლა. ნაბიჯი 4: შენახვა. ყველა ოთხი ნაბიჯი: არა-ატომური. ნაბიჯ 1-სა და ნაბიჯ 4-ს შორის ნებისმიერი რაოდენობის თრედს შეუძლია შეასრულოს ნაბიჯი 1 და ყველა დაინახავს null-ს.
იდემპოტენტობის ხაფანგი
ამ ნიმუშის დამცავი მსჯელობა: „კარგია, თუ ორი თრედი ერთსა და იმავე მნიშვნელობას გამოთვლის და შეინახავს. შედეგი იდემპოტენტურია. მონაცემთა დაზიანება არ ხდება.“
ეს მსჯელობა: სისწორის თვალსაზრისით სწორია. ღირებულების თვალსაზრისით კი ფატალური.
1 000 თრედის შემთხვევაში ქეშის გაცდენისას: 1 000 თრედი თითოეული ასრულებს expensiveCompute(key). თუ expensiveCompute მონაცემთა ბაზას მიმართავს, ერთდროულად 1 000 მოთხოვნა გაიგზავნება. თუ გარე სერვისს იძახებს, ერთდროულად 1 000 HTTP მოთხოვნა გაიგზავნება. სისტემა, რომელიც სწორ შედეგებს იძლევა, იშლება ამ შედეგების წარმოების ღირებულების ქვეშ.
სამი ტრიგერი
Thundering Herd მაშინ ხდება, როდესაც ქეშის გასაღები ერთდროულად ბევრ თრედში თბილიდან ცივზე გადადის:
ცივი დაწყება: სერვისი ცარიელი ქეშით იწყება. პირველი მოთხოვნების ტალღა: ყველა გასაღები გაცდენილია. ყველა ერთდროულად ითვლის.
სერვისის გადატვირთვა: როლინგ გადატვირთვა ქეშს ყველა ინსტანციაზე აღადგენს. ტრაფიკი ცივ ინსტანციებზე გადანაწილდება.
TTL-ის ვადის გასვლა: მაღალი ტრაფიკის მქონე გასაღები იწურება. N თრედი ყველა ამოწმებს, ყველა გაცდენილია, ყველა ითვლის სანამ პირველი თრედი შედეგს შეინახავს.
სამივე ტრიგერი: დაკავშირებულია ტრაფიკის პიკებთან. ჰერდი იწყებს მუშაობას, როდესაც ტრაფიკი პიკს აღწევს და ქეში ცივია. ყველაზე ცუდი შესაძლო დრო.
Elasticsearch-ის EnrichCache-ის მაგალითი
Elasticsearch EnrichCache: დოკუმენტირებული კომენტარი ამბობს „განზრახ არა-დაბლოკვადი მარტივობისთვის... კარგია, თუ იგივე გასაღები/მნიშვნელობა ხელახლა ჩავსვით რბოლის პირობებში“. 10,000 დოკუმენტის წამში ცივი EnrichCache-ით: ყველა 10,000 მოთხოვნა ერთდროულად მოხვდება Enrich ინდექსზე. Enrich ინდექსი, რომელიც შექმნილია შემთხვევითი მოძებნებისთვის, აწყდება 10,000 ერთდროულ მოთხოვნას. კლასტერი დესტაბილიზდება.
იდემპოტენტობის მსჯელობა: სწორია კოდის კომენტარში. კატასტროფულია 10,000 დოკუმენტის წამში.
დაკავშირება MOAD-0001-თან
MOAD-0001 (Sedimentary Defect) ქმნის O(N²) ბოთლის კისერს მაღალი გამტარუნარიანობის სისტემებში. MOAD-0001-ის გამოსწორება (O(N²)-დან O(N)-მდე) ათავისუფლებს ამ სამუშაო სადგურს. უფრო სწრაფი გამტარუნარიანობა უფრო მეტ მოთხოვნას უგზავნის ქვედა დონეს. ქვედა დონის ქეშები, რომლებიც ადრე დაცული იყო MOAD-0001-ის ბოთლის კისრით, ახლა იღებენ კორელირებულ ტრაფიკის პიკებს. MOAD-0005 იწყებს მუშაობას იმ ქეშებში, რომლებშიც ადრე არასოდეს გააქტიურებულა. გამოასწორეთ ერთი MOAD; მოამზადეთ მეორე.
რა არის არასწორი იდემპოტენტობის ხაფანგში
Elasticsearch-ის კომენტარი წარმოადგენს ფრთხილ ინჟინერიულ მსჯელობას, რომელიც გამოყენებულია არასწორ კითხვაზე. იდემპოტენტობა: რეალური თვისება, რომელზეც ღირს მსჯელობა. ხაფანგი: ანალიზის შეჩერება სისწორეზე, ღირებულების გათვალისწინების გარეშე.
computeIfAbsent & singleflight
გამოსწორება: შეამოწმეთ და გამოთვალეთ ატომურად. ერთი თრედი ასრულებს გამოთვლას. ყველა სხვა თრედი ელოდება ამ შედეგს.
Java: computeIfAbsent
// DEFECT: ოთხი არა-ატომური ნაბიჯი
Value value = cache.get(key);
if (value == null) {
value = expensiveCompute(key);
cache.put(key, value);
}
return value;
// FIX: ატომური შემოწმება და გამოთვლა
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: თუ გასაღებისთვის გამოთვლა უკვე მიმდინარეობს, ყველა გამომძახებელი იმავე გასაღებისთვის ელოდება და იზიარებს ერთ შედეგს. ერთი გამოთვლა, N მომლოდინე, ერთი შედეგი გაზიარებული. 'flight' აბსტრაქცია: დედუპლიკაცია მიმდინარე მოთხოვნებისთვის.
Lock vs singleflight
ნაივი per-key lock სერიალიზებს: thread 1 ანგარიშობს, thread 2 ელოდება, thread 3 ელოდება. thread 1-ის დასრულების შემდეგ, thread 2 შედის და ამოწმებს ქეშს (hits). thread 3 შედის და ამოწმებს ქეშს (hits). N-1 lock შეძენა და ქეშის წაკითხვა.
singleflight ახდენს დედუპლიკაციას: thread 1 ანგარიშობს, thread 2-დან N-მდე ყველა ელოდება thread 1-ის შედეგს. არანაირი ცალკეული lock შეძენა. არანაირი ცალკეული ქეშის წაკითხვა. ერთი გამოთვლა, ერთი შედეგი, განაწილებული N მომლოდინეზე. ნაკლები ოპერაცია ვიდრე per-key lock-ის შემთხვევაში.
ორივე ხელს უშლის stampede-ს. singleflight უფრო სრულად ხელს უშლის ზედმეტ მუშაობას.
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 მონაცემთა ბაზის მოთხოვნა
profileCache.put(userId, profile);
}
return profile;
}
სერვისი ყოველ დილით 2 საათზე იწყებს თავიდან გაშვებას. დილის 8 საათზე 10 000 მომხმარებელი ერთდროულად ითხოვს თავის პროფილებს.