Cache stampede'i önlemek için singleflight doğru bir yaklaşım mı?
Soru
Go ile yazdığım bir API'de sıcak bir Redis anahtarı expire olduğunda, yüzlerce eşzamanlı istek aynı anda Postgres'e gidip aynı değeri yeniden hesaplıyor; klasik cache stampede. İstediğim şey, o anahtar boşaldığında değeri yalnızca bir isteğin yeniden kurması, gerisinin sonucu paylaşması. `singleflight` bunun için doğru araç gibi görünüyor ama üretimde tek başına yeterli mi, yoksa gözden kaçırdığım noktalar var mı?
Cevap
Kısa cevap
Evet, singleflight in-process stampede çökertmesi için doğru araçtır — ama yalnızca tek proses içinde dedupe eder.
Bağlam
Çok sayıda pod’da hâlâ pod başına bir rebuild alırsınız; bu sizin için önemliyse yanına stale-while-revalidate ya da probabilistik erken expire ekleyin.
Neden
- singleflight tam olarak ne yapar?
golang.org/x/sync/singleflight, aynı anahtarla gelen eşzamanlı çağrıları tek bir çalıştırmada birleştirir; gerisi bekler ve sonucu paylaşır. Bu tam olarak sizin “yalnızca biri yeniden kursun” ihtiyacınızdır — tek proses içinde.var g singleflight.Group func (s *Service) Get(ctx context.Context, key string) (Value, error) { if v, ok := s.cache.Get(key); ok { return v, nil } v, err, _ := g.Do(key, func() (any, error) { val, err := s.loadFromDB(ctx, key) // yalnızca biri çalışır if err != nil { return nil, err } s.cache.Set(key, val, ttl) return val, nil }) if err != nil { return Value{}, err } return v.(Value), nil } - Pod başına uyarısı. Yük dengeleyici arkasında N replica varsa, bir değil N’e kadar eşzamanlı rebuild alırsınız. Çoğu uygulamada N küçüktür (diyelim 10) ve 10 sorgu sorun değildir. Değilse, cross-process bir lock gerekir (TTL’li Redis
SETNX): yalnızca bir pod kurar, gerisi bekler ya da bayat servis eder. - Paylaşılan hata ve panic tuzağına dikkat. singleflight sonucu OLDUĞU KADAR hatayı da tüm bekleyenlere paylaştırır — tek bir başarısızlık o partideki herkesi düşürür. Yavaş bir rebuild’in çağıranları sonsuza kadar bloklamaması için context’li
DoChankullanın; geçici bir hatanın cachelenmemesi içinForget’i düşünün.
Ne yapmalı
- Rebuild sırasında bayat servis etmeyi düşünün. En düşük latency’li desen stale-while-revalidate’tir: soft bir TTL tutun, bayat değeri anında dönün ve tek bir arka plan yenilemesini (singleflight korumalı) tetikleyin. Kullanıcı rebuild’i hiç beklemez.
- Probabilistik erken expire (XFetch) senkronize expire’ı azaltır: her istek, TTL’den biraz önce yenileme yapıp yapmayacağına zar atar; böylece sürü oluşma olasılığı düşer, garantisi yoktur. singleflight’in yerine değil, tamamlayıcısıdır.
- DB tarafını unutmayın. Tek bir rebuild sorgusu bile hızlı ve index’li olmalı. Stampede koruması yavaş bir sorguyu gizler; düzeltmez.
Sonuç: Ben olsam singleflight + stale-while-revalidate ile başlardım — bu ikili uygulamaların büyük çoğunluğunda stampede’i öldürür ve latency’yi düz tutar. Redis distributed lock’ı yalnızca pod başına rebuild’lerin Postgres’i gerçekten yorduğunu ölçtüğümde eklerdim. Kısacası: replica sayınızın gerektirdiği en basit katmana uzanın, daha fazlasına değil.
İlgili Yazılar
Yorumlar
Yorum yapmak için GitHub hesabınızla giriş yapmanız yeterli. Yorumlar GitHub Discussions üzerinde saklanır.