Polaa Langkah Empat
Keterbelakangan hidup dalam empat langkah berurutan, non-atomik:
// KETERBELAKANGAN
Value value = cache.get(key);
if (value == null) {
value = expensiveCompute(key);
cache.put(key, value);
}
return value;
Langkah 1: periksa cache. Langkah 2: ketinggalan. Langkah 3: menghitung. Langkah 4: menyimpan. Empat langkah: non-atomik. Antara langkah 1 & langkah 4, jumlah thread apa pun dapat menjalankan langkah 1 & semua melihat null.
Jebakan Idempotensi
Pemikiran yang melindungi pola ini: 'baik jika dua thread menghitung & menyimpan nilai yang sama. Hasilnya idempoten. Tidak terjadi kerusakan data.'
Pemikiran ini: benar tentang kebenaran. Fatal tentang biaya.
Pada 1.000 thread pada kegagalan cache: 1.000 thread masing-masing menjalankan expensiveCompute(key). Jika expensiveCompute mengakses basis data, 1.000 kueri basis data yang dikeluarkan secara bersamaan. Jika itu menggilan layanan eksternal, 1.000 permintaan HTTP yang dikeluarkan secara bersamaan. Sistem yang menghasilkan hasil yang benar runtuh di bawah biaya produksi mereka.
Tiga Pemicu
Kafil Bala Tentara terjadi ketika kunci cache beralih dari hangat ke dingin secara bersamaan di antara banyak thread:
Mulai dingin: layanan restart dengan cache yang kosong. Gelombang permintaan pertama: setiap kunci ketinggalan. Semua menghitung secara bersamaan.
Restart layanan: restart bergulir mengatur ulang cache di antara instance. Lalu lintas didistribusikan ke instance yang dingin.
Kadaluarsa TTL: kunci sibuk yang luar biasa melewati masa kadaluarsa. N thread semuanya memeriksa, semua ketinggalan, semua menghitung sebelum thread pertama menyetor hasilnya.
Ketiga pemicu: terkait dengan lonjakan lalu lintas. Kafil Bala Tentara terjadi ketika lalu lintas mencapai puncak & cache dingin. Waktu terburuk.
Contoh Elasticsearch EnrichCache
Elasticsearch EnrichCache: komentar yang dokumentasi dibaca 'sengaja tidak mengunci untuk kesederhanaan ... baik jika kita me-replace kunci / nilai dalam kondisi balapan.' Pada 10.000 dokumen per detik dengan cache enrich yang dingin: semua 10.000 permintaan menghubungi indeks enrich secara bersamaan. Indeks enrich, dirancang untuk pencarian yang jarang, menghadapi 10.000 kueri koncurrent. Klaster menjadi tidak stabil.
Pemikiran idempotensi: benar dalam komentar kode. Kacau pada 10.000 dokumen per detik.
Keterkaitan dengan MOAD-0001
MOAD-0001 (Defek Sedimen) menciptakan botolan O(N²) pada sistem high-throughput. Memperbaiki MOAD-0001 (O(N²) ke O(N)) melepaskan kerja stasiun tersebut. Aliran yang lebih cepat mengirimkan lebih banyak permintaan ke arah hilir. Penyimpanan hilir, sebelumnya dilindungi oleh botolan MOAD-0001, sekarang menerima lonjakan lalu lintas yang korelasional. MOAD-0005 meledak di penyimpanan yang tidak pernah memicu sebelumnya. Perbaiki satu MOAD; tingkatkan yang lain.
Apa yang Salah dalam Jebakan Idempotensi
Komentar Elasticsearch mewakili pemikiran insinyur yang hati-hati terhadap pertanyaan yang salah. Idempotensi: properti yang benar-benar layak dipikirkan. Jebakan: berhenti menganalisis pada kebenaran tanpa melanjutkan ke biaya.
computeIfAbsent & singleflight
Perbaikan: buat pengecekan dan komputasi atom. Satu thread menghitung. Thread lain menunggu hasil tersebut.
Java: computeIfAbsent
// DEFECT: empat langkah non-atom
Value value = cache.get(key);
if (value == null) {
value = expensiveCompute(key);
cache.put(key, value);
}
return value;
// FIX: pengecekan dan komputasi atom
return cache.computeIfAbsent(key, k -> expensiveCompute(k));
computeIfAbsent: jika kunci tidak ada, menghitung sekali, menyimpan, mengembalikan. Semua thread lain yang menggilah computeIfAbsent untuk kunci yang sama menunggu komputasi pertama. Tidak ada komputasi N-guna. Tidak ada kerumunan.
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 kunci sudah berjalan, semua penggilan untuk kunci yang sama menunggu & membagi hasil yang sama. Satu komputasi, N penganggung jawab, satu hasil yang dibagikan. Abstraksi 'flight': mengurangi duplikat permintaan yang sedang terbang.
Lock vs singleflight
Pemilik kunci yang naif serializes: thread 1 menghitung, thread 2 menunggu, thread 3 menunggu. Setelah thread 1 selesai, thread 2 memasuki & memeriksa cache (menabrak). Thread 3 memasuki & memeriksa cache (menabrak). N-1 pengambilan kunci kunci & baca cache.
singleflight deduplicates: thread 1 menghitung, thread 2 hingga N semua menunggu pada hasil thread 1. Tidak ada pengambilan kunci terpisah. Tidak ada baca cache terpisah. Satu komputasi, satu hasil, didistribusikan ke N penganggung jawab. Operasi yang lebih sedikit daripada pemilik kunci per-kunci.
Keduanya mencegah kerumunan. singleflight mencegah kerja yang berulang lebih lengkap.
Menulis Kembali Pola
Terapkan perbaikan pada skenario konkrit.
// Cache profil pengguna di layanan Java yang sangat padat
public UserProfile getProfile(String userId) {
UserProfile profile = profileCache.get(userId);
if (profile == null) {
profile = database.loadProfile(userId); // mahal: 50ms query DB
profileCache.put(userId, profile);
}
return profile;
}
Layanan dimulai kembali setiap pagi pukul 2 AM. Pada pukul 8 AM, 10.000 pengguna meminta profil mereka secara bersamaan.