Pola Empat Langkah
Cacat ini terletak pada empat langkah berurutan yang non-atomik:
// DEFECT
Value value = cache.get(key);
if (value == null) {
value = expensiveCompute(key);
cache.put(key, value);
}
return value;
Langkah 1: periksa cache. Langkah 2: miss. Langkah 3: hitung. Langkah 4: simpan. Keempat langkah tersebut tidak atomik. Di antara langkah 1 dan langkah 4, sejumlah thread dapat menjalankan langkah 1 dan semuanya melihat null.
The Idempotency Trap
Alasan yang melindungi pola ini: 'tidak masalah jika dua thread menghitung & menyimpan nilai yang sama. Hasilnya idempoten. Tidak terjadi korupsi data.'
Alasan ini: benar tentang kebenaran. Fatal tentang biaya.
Pada 1.000 thread saat cache miss: 1.000 thread masing-masing menjalankan expensiveCompute(key). Jika expensiveCompute melakukan query ke database, maka 1.000 query database akan dieksekusi secara bersamaan. Jika memanggil layanan eksternal, maka 1.000 HTTP request akan dikirim secara bersamaan. Sistem yang menghasilkan hasil yang benar akan runtuh karena biaya untuk menghasilkannya.
Tiga Pemicu
Thundering Herd terjadi ketika sebuah cache key berubah dari warm ke cold secara bersamaan di banyak thread:
Cold start: layanan restart dengan cache kosong. Gelombang permintaan pertama: setiap key miss. Semua komputasi dilakukan secara bersamaan.
Service restart: rolling restart mereset cache di seluruh instance. Traffic didistribusikan ulang ke instance yang cold.
TTL expiry: sebuah key ber-traffic tinggi kadaluarsa. N thread semuanya mengecek, semuanya miss, semuanya menghitung sebelum thread pertama menyimpan hasilnya.
Ketiga trigger: berkorelasi dengan lonjakan traffic. Herd menyala ketika traffic mencapai puncak & cache dalam kondisi dingin. Waktu terburuk yang mungkin terjadi.
Contoh Elasticsearch EnrichCache
Elasticsearch EnrichCache: komentar dokumentasi menyatakan 'intentionally non-locking for simplicity...OK if we re-put the same key/value in a race condition.' Pada 10.000 dokumen per detik dengan enrich cache dingin: semua 10.000 permintaan mengenai enrich index secara bersamaan. Enrich index, yang dirancang untuk pencarian sesekali, menghadapi 10.000 kueri bersamaan. Cluster menjadi tidak stabil.
Alasan idempotensi: benar dalam komentar kode. Bencana pada 10.000 dokumen per detik.
Keterkaitan dengan MOAD-0001
MOAD-0001 (Sedimentary Defect) menciptakan bottleneck O(N²) pada sistem throughput tinggi. Memperbaiki MOAD-0001 (O(N²) ke O(N)) membuka workstation tersebut. Throughput yang lebih cepat mengirim lebih banyak permintaan ke downstream. Cache downstream, yang sebelumnya dilindungi bottleneck MOAD-0001, kini menerima lonjakan traffic yang berkorelasi. MOAD-0005 menyala pada cache yang sebelumnya tidak pernah terpicu. Perbaiki satu MOAD; persiapkan yang lain.
Apa yang Salah dari Idempotency Trap
Komentar Elasticsearch mewakili pemikiran rekayasa yang cermat diterapkan pada pertanyaan yang salah. Idempotency: properti nyata yang layak dianalisis. Jebakannya: menghentikan analisis pada tingkat kebenaran tanpa melanjutkan ke biaya.
computeIfAbsent & singleflight
Perbaikannya: buat operasi pengecekan-dan-komputasi menjadi atomic. Satu thread melakukan komputasi. Semua thread lain menunggu hasil tersebut.
Java: computeIfAbsent
// DEFECT: empat langkah non-atomik
Value value = cache.get(key);
if (value == null) {
value = expensiveCompute(key);
cache.put(key, value);
}
return value;
// PERBAIKAN: atomic check-and-compute
return cache.computeIfAbsent(key, k -> expensiveCompute(k));
computeIfAbsent: jika kunci tidak ada, komputasi dilakukan tepat sekali, disimpan, lalu dikembalikan. Semua thread lain yang memanggil computeIfAbsent untuk kunci yang sama akan menunggu komputasi pertama selesai. Tidak ada komputasi N-kali. Tidak ada stampede.
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: jika komputasi untuk suatu key sedang berjalan, semua pemanggil untuk key yang sama akan menunggu & berbagi hasil tunggal. Satu komputasi, N penunggu, satu hasil dibagikan. Abstraksi 'flight': deduplikasi permintaan yang sedang berjalan.
Lock vs singleflight
Lock per-key yang naif melakukan serialisasi: thread 1 menghitung, thread 2 menunggu, thread 3 menunggu. Setelah thread 1 selesai, thread 2 masuk & memeriksa cache (kena). Thread 3 masuk & memeriksa cache (kena). N-1 akuisisi lock & pembacaan cache.
singleflight melakukan deduplikasi: thread 1 menghitung, thread 2 sampai N menunggu hasil thread 1. Tidak ada akuisisi lock terpisah. Tidak ada pembacaan cache terpisah. Satu komputasi, satu hasil, didistribusikan ke N penunggu. Lebih sedikit operasi dibandingkan lock per-key.
Keduanya mencegah stampede. singleflight mencegah pekerjaan redundan secara lebih lengkap.
Rewrite the Pattern
Terapkan perbaikan pada skenario konkret.
// Cache profil pengguna di layanan Java dengan lalu lintas tinggi
public UserProfile getProfile(String userId) {
UserProfile profile = profileCache.get(userId);
if (profile == null) {
profile = database.loadProfile(userId); // mahal: kueri DB 50ms
profileCache.put(userId, profile);
}
return profile;
}
Layanan restart setiap pagi pukul 02.00. Pukul 08.00, 10.000 pengguna meminta profil mereka secara bersamaan.