← blog · 5 Kasım 2026

Önbellek Çığı: Sıcak Anahtar Düştüğünde Veritabanını Korumak, İstek Birleştirme, Eskiyi Sunmak ve Erken Yenileme

Sıcak bir anahtarın süresi dolduğunda yüzlerce istek aynı sorguyu veritabanına gönderir. TTL sapması, istek birleştirme, dağıtık kilit, eskiyi sunup arkada yenileme ve olasılıksal erken yenileme arasında nasıl seçim yapılır, hangi tuzaklara dikkat edilir.

Önbellek, arkasındaki veritabanını korumak için konur. Ama sıcak bir anahtarın süresi dolduğu anda o koruma bir saniyeliğine kalkar: o anahtarı isteyen yüzlerce istek aynı anda boş önbellek görür ve hepsi aynı pahalı sorguyu kaynağa gönderir. Kaynak yavaşlar, sorgular uzar, uzayan sorgular boyunca gelen yeni istekler de boş önbellek görür. Önbellek çığı (cache stampede, dogpile) budur. Normal günde görünmez, trafik zirvesinde ya da önbellek yeniden başladığında sistemi düşürür. Bu yazıda sorunun nereden çıktığını, çözüm yollarını ve hangisinin ne zaman seçilmesi gerektiğini anlatıyoruz.

Sorunun üç biçimi

Çığ tek bir şekilde gelmez, ayırt etmek çözümü seçmeyi kolaylaştırır.

Tek sıcak anahtar. Ana sayfa verisi, popüler bir ürün, herkesin okuduğu bir yapılandırma. Süresi dolduğunda eşzamanlı istek sayısı kadar yeniden hesaplama başlar.

Eşzamanlı süre dolumu. Bir toplu iş binlerce anahtarı aynı anda, aynı TTL ile yazar. Bir saat sonra hepsi aynı saniyede düşer. Tek tek bakınca hiçbiri sıcak değildir, toplamı kaynağı ezer.

Soğuk başlangıç. Önbellek sunucusu yeniden başlar, bellek boşaltılır ya da bir dağıtımda anahtar biçimi değişir. Bütün anahtarlar aynı anda boştur. Bu en ağır biçimdir çünkü yukarıdaki iki çözümün hiçbiri tek başına yetmez.

Yaklaşım 1: TTL'ye rastgele sapma eklemek

En ucuz önlem. Her anahtara sabit bir süre yerine, sürenin yüzde on ile yirmisi kadar rastgele sapma eklenmiş bir süre verin. Böylece aynı anda yazılan anahtarlar zamana yayılarak düşer.

import random

def ttl_with_jitter(base_seconds, spread=0.15):
    return int(base_seconds * (1 + random.uniform(-spread, spread)))

Bu, eşzamanlı süre dolumunu çözer ama tek sıcak anahtara hiçbir şey yapmaz: o anahtar yine bir anda düşer ve yine yüz istek aynı anda kaynağa gider. Sapmayı her durumda ekleyin, ama tek önlem olarak görmeyin.

Yaklaşım 2: İstekleri birleştirmek

Aynı süreç içinde aynı anahtar için gelen eşzamanlı isteklerden yalnızca biri kaynağa gitsin, diğerleri onun sonucunu beklesin. Go'da bunun hazır aracı golang.org/x/sync/singleflight paketidir:

var group singleflight.Group

func getProduct(id string) (Product, error) {
    if p, ok := cache.Get(id); ok {
        return p, nil
    }
    v, err, _ := group.Do(id, func() (interface{}, error) {
        p, err := db.LoadProduct(id)
        if err == nil {
            cache.Set(id, p, ttlWithJitter(time.Hour))
        }
        return p, err
    })
    if err != nil {
        return Product{}, err
    }
    return v.(Product), nil
}

Etkisi büyüktür ama sınırı nettir: birleştirme süreç başınadır. Uygulamanın kırk kopyası çalışıyorsa sıcak anahtar düştüğünde kaynağa yine kırk sorgu gider. Çoğu sistem için kırk, bin istekten çok daha iyidir ve bu kadarı yeterlidir. Yetmiyorsa bir sonraki yaklaşıma geçilir.

Yaklaşım 3: Dağıtık kilit ile tek hesaplayıcı

Bütün kopyalar arasında yalnızca birinin yeniden hesaplamasını istiyorsanız paylaşılan bir kilit gerekir. Redis'te bunun yolu atomik SET ile NX ve PX seçenekleridir:

import uuid

def rebuild_with_lock(r, key, compute, ttl, lock_ms=10000):
    token = str(uuid.uuid4())
    if r.set(f"lock:{key}", token, nx=True, px=lock_ms):
        try:
            value = compute()
            r.set(key, value, ex=ttl)
            return value
        finally:
            release_if_owner(r, f"lock:{key}", token)
    return None

Kilidi alamayan isteğin ne yapacağı asıl karardır. Kısa aralıklarla tekrar bakıp beklemek, isteği hesaplama süresi kadar geciktirir ve bekleyen istekler bağlantı havuzunu doldurur. Daha iyisi, kilidi alamayan isteğe eski değeri vermektir. Bu da bizi bir sonraki yaklaşıma getirir.

Kilidi bırakırken yalnızca kendi koyduğunuz kilidi silin. Hesaplama kilit süresinden uzun sürerse kilit kendiliğinden düşer, başka bir istek onu alır ve siz düz bir DEL ile onun kilidini silersiniz. Karşılaştırıp silmeyi tek adımda yapmak için küçük bir Lua betiği kullanılır:

if redis.call("GET", KEYS[1]) == ARGV[1] then
  return redis.call("DEL", KEYS[1])
end
return 0

Yaklaşım 4: Eskiyi sun, arkada yenile

Bizim önerdiğimiz varsayılan budur. Fikir, önbellekteki değerin iki ayrı süresi olmasıdır: mantıksal süre ve fiziksel süre. Değerin içine "şu andan sonra bayat sayılır" zamanını yazarsınız, önbelleğe ise bundan daha uzun bir TTL verirsiniz.

İstek geldiğinde değer tazeyse doğrudan dönülür. Bayatsa yine de dönülür, ama aynı anda tek bir yenileme tetiklenir (süreç içi birleştirme ya da dağıtık kilitle). Kullanıcı hiçbir zaman hesaplamayı beklemez, kaynak da sıcak anahtar başına tek sorgu görür. Fiziksel süre de dolmuşsa, yani değer gerçekten yoksa, ancak o zaman bekleyerek hesaplanır.

Bu yaklaşımın bedeli bayatlıktır ve bunu bilinçli olarak kabul etmek gerekir. Ürün açıklaması, sayaçlar, listeler, öneri sonuçları için birkaç dakikalık bayatlık sorun değildir. Bakiye, stok rezervasyonu ya da yetki kararı için sorundur; bu tür verileri bu yaklaşımla önbelleğe koymayın.

HTTP katmanında aynı fikrin standart karşılığı vardır. Cache-Control: max-age=60, stale-while-revalidate=300 başlığı, destekleyen önbelleklere değeri altmış saniye taze, sonraki beş dakika boyunca arkada yenilenirken sunulabilir saymasını söyler. nginx'i önbellek olarak kullanıyorsanız aynı davranış yapılandırmayla kurulur:

proxy_cache_lock on;
proxy_cache_lock_timeout 5s;
proxy_cache_use_stale updating error timeout http_500 http_502 http_503;
proxy_cache_background_update on;

proxy_cache_lock aynı anahtar için kaynağa yalnızca bir istek gönderir, updating bayat değeri yenileme sürerken sunar, proxy_cache_background_update yenilemeyi istemciyi bekletmeden yapar.

Yaklaşım 5: Olasılıksal erken yenileme

Kilit kullanmak istemiyorsanız, istekleri süre dolmadan önce rastgele erken yenilemeye ikna edebilirsiniz. Literatürde XFetch adıyla geçen yöntemde her istek şu koşula bakar:

now - delta * beta * ln(random()) >= expiry

Burada delta son hesaplamanın ne kadar sürdüğü, beta agresifliği ayarlayan katsayıdır (genelde 1). ln(random()) negatif olduğu için sol taraf şimdiki zamandan biraz ileridedir; süre dolumuna yaklaştıkça koşulun tutma olasılığı artar. Sonuçta trafik yüksekse bir istek süre dolmadan kısa süre önce yenilemeyi yapar, diğerleri taze değeri görmeye devam eder.

Güzel bir yöntemdir ama iki koşulu var: değerin yanında hesaplama süresini de saklamanız gerekir ve trafiğin yeterince yüksek olması gerekir. Düşük trafikte erken yenileme olmaz, çığ da zaten olmaz. Ekip içinde anlatması ve hata ayıklaması stale-while-revalidate'ten zordur; özel bir nedeniniz yoksa önce dördüncü yaklaşımı kurun.

Tuzaklar

Hatayı önbelleğe koymak ya da hiç koymamak. Kaynak hata döndüğünde hiçbir şey yazmazsanız, kaynak çöktüğünde her istek onu yeniden dener ve çığ kesintiye dönüşür. Hata yanıtını uzun süre yazarsanız düzelmiş bir kaynağı dakikalarca görmezsiniz. Hataları birkaç saniyelik kısa bir süreyle yazın ya da hata durumunda eski değeri sunmaya devam edin.

Sonsuz bayatlık. Eskiyi sunan sistem, kaynak saatlerce çökük kalırsa saatler öncesinin verisini sessizce sunar. Bayat sunma süresine bir üst sınır koyun ve bu sınıra yaklaşan anahtar sayısını metrik olarak izleyin.

Paylaşılan iptal. Birleştirilmiş istekte hesaplamayı ilk gelen isteğin bağlamıyla (context, iptal jetonu) çalıştırırsanız, ilk kullanıcı sayfayı kapattığında bekleyen herkes iptal hatası alır. Paylaşılan hesaplamayı isteğin değil, kendi zaman aşımına sahip ayrı bir bağlamın altında çalıştırın.

Olmayan anahtarlar. Veritabanında bulunmayan bir kimlik için sorgu, sonuç boş olduğu için önbelleğe hiç yazılmaz ve her istekte kaynağa gider. Kötü niyetli ya da hatalı bir istemci rastgele kimliklerle bunu sömürür. "Bulunamadı" sonucunu da kısa bir TTL ile önbelleğe yazın.

Dağıtımda anahtar biçimini değiştirmek. Anahtara sürüm öneki eklemek temiz bir geçersiz kılma yöntemidir, ama yeni sürüm açıldığı anda bütün önbellek fiilen boştur. Bu, yoğun saatte yapılan bir soğuk başlangıçtır. Böyle bir değişikliği düşük trafikte yapın, mümkünse en sıcak anahtarları önceden ısıtın.

Görünmezlik. Çığı yaşadığınızı ancak isabet oranı ve eşzamanlı yeniden hesaplama sayısını ölçüyorsanız görürsünüz. Ortalama isabet oranı yüzde doksan dokuz olan bir sistem, en sıcak anahtarın düştüğü saniyede kaynağı yine ezebilir. Önbellek kaçırma sayısının yanında "şu anda kaç yeniden hesaplama sürüyor" sorusunu cevaplayan bir sayaç tutun; çığın asıl izi odur.

Hangisini seçmeli

Bizim sıralamamız şöyle: TTL sapmasını her yerde ekleyin, maliyeti yoktur. Bunun üstüne süreç içi birleştirmeyi kurun; çoğu sistem burada durabilir. Sıcak anahtarların hesaplaması pahalıysa ve bayatlık kabul edilebiliyorsa eskiyi sunup arkada yenileyin. Dağıtık kilidi, kopya sayısı yüksek ve kaynak bir tek sorguyu bile kaldıramayacak kadar pahalıysa ekleyin.

Bu yaklaşımların hiçbirine ihtiyaç duymadığınız durumlar da var. Hesaplama birkaç milisaniye sürüyorsa, eşzamanlılık düşükse ya da kaynak zaten önbellekten daha hızlı ölçeklenebiliyorsa bu karmaşıklık size bir şey kazandırmaz. Tutarlılığın şart olduğu veride ise önbellek çığının çözümü önbelleği akıllandırmak değil, o veriyi önbelleğe hiç koymamaktır.