Database Engineering

Trafik Dalgalanmalarından Hayatta Kalmak: Redis Önbellek Damgalarını Önlemek İçin Teknik Bir Rehber

Yüksek kullanılabilirlikli sistem tasarımında "Önbellek Damgası" (ayrıca "Gök Gürültüsü Sürüsü" olarak da bilinir), en tehlikeli performans düşmanlarından biridir. Yüksek trafikli bir önbellek anahtarı aynı anda sona erdiğinde oluşur ve binlerce eşzamanlı isteğin önbelleği atlayarak arka uç veritabanına veya üst hizmete ulaşmasına neden olur. Bu ani artış altyapınızı aşırı yükleyebilir, gecikmenin artmasına, veritabanı bağlantı havuzunun tükenmesine ve potansiyel hizmet kesintilerine yol açabilir.

Orta düzeyden ileri düzey geliştiriciler için, modern uygulamaların yüksek isabet oranı gereksinimleri nedeniyle kısa bir sona erme süresi ayarlamak tek başına geçerli bir çözüm değildir. Bunun yerine, bu riski azaltmak için sağlam stratejiler uygulamalıyız. Bu yazıda, birbirini tamamlayan iki teknik inceleyeceğiz: Dağıtılmış Kilitleme ve Olasılıksal Erken Sonlandırma.

Bir Önbellek Damgasının Anatomisi

Problemi çözmek için önce mekanizmasını anlamamız gerekir. Popüler bir ürün sayfasının önbellek TTL'sinin (Yaşam Süresi) 60 saniye olduğu bir senaryoyu ele alalım. t=60s anında anahtar sona erer. Eğer 10.000 kullanıcı bu sayfayı o milisaniyede isterse, 10.000 iş parçacığı önbelleği kontrol eder, hiçbir şey bulamaz ve veritabanına geçer. Saniyede 1.000 sorguyu işleyebilen bir veritabanınız olsa bile, 10.000 eşzamanlı sorgu birikintisine neden olabilir ve uygulamayı çökertme potansiyeli taşır.

Hedef, veriyi yeniden oluşturmakla yalnızca bir isteğin sorumlu olması, diğer isteklerin ise beklemesi veya eski bir sürümü almasıdır.

Strateji 1: Redlock ile Dağıtılmış Kilitleme

En basit yaklaşım, önbelleği yeniden oluşturmaya çalışmadan önce dağıtılmış bir kilit almaktır. Kilit alınırsa, istek veriyi oluşturur ve önbelleği günceller. Kilit zaten başkası tarafından tutuluyorsa, istek kısa bir süre bekler veya önbellekteki eski değeri döndürür.

Bunu Redis'te uygularken, yarış durumlarını (race conditions) önlemek için atomik işlemler kullanmak çok önemlidir. NX (Eğer Yoksa Ayarla - Set if Not eXists) ve EX (Sona Erme) bayraklarıyla SET komutu, kilidi güvenli bir şekilde uygulamanın standart yoludur.

import redis

def get_data_with_lock(redis_client, key, data_fetcher):
    # Ölümcül kilitlenmeyi (deadlock) önlemek için NX ve sona erme süresi ile kilidi almaya çalış
    # EX, kilidi saniye cinsinden TTL ayarlar ve işlem çökerse kilidin serbest kalmasını sağlar
    if redis_client.set(lock_key, "locked", nx=True, ex=5):
        try:
            # Yalnızca bir iş parçacığı buraya girer
            data = data_fetcher()
            redis_client.set(key, data, ex=60) # Önbellek TTL'sini ayarla
            return data
        finally:
            # Kilidi serbest bırak
            redis_client.delete(lock_key)
    else:
        # Başka bir iş parçacığı önbelleği yeniliyor
        # Seçenek A: Bekle ve tekrar dene (bloke edici)
        # Seçenek B: Eski önbelleği veya hatayı döndür (bloke edici olmayan)
        return redis_client.get(key)

Bu yaklaşım etkili olsa da, bekleyen tüm iş parçacıkları için gecikme getirir. Yüksek eşzamanlılık senaryolarında, bloke olan iş parçacıkları önemli kaynaklar tüketebilir. Okuma ağırlıklı iş yükleri için daha zarif bir çözüm Olasılıksal Sonlandırmadır.

Strateji 2: Olasılıksal Erken Sonlandırma

Önbelleğin t=60 anında ani olarak sona ermesine izin vermek yerine, olasılıksal bir yaklaşım kullanabiliriz. Önbellek TTL'sini, sonlanmanın nominal süreden biraz önce gerçekleşme olasılığının yüksek olduğu değişken bir aralığa ayarlarız. Bu, yeniden oluşturma isteklerini bir zaman penceresine yayarak veritabanındaki yükü dengeler.

Ayrıca bir "tembel yenileme" mekanizması uygulayabiliriz. Bir anahtara erişildiğinde, sona ermeye yaklaşıp yaklaşmadığı kontrol edilir (örneğin, TTL'nin son %10'u içinde). Eğer öyleyse, mevcut isteğin mevcut (hafifçe eski) veriyi döndürürken, veriyi eşzamansız olarak yenilemek için arka planda bir iş parçacığı veya görev başlatılır.

import time
import random

def get_data_probabilistic(redis_client, key, data_fetcher):
    data = redis_client.get(key)
    
    if not data:
        # Önbellek isabeti değil: Yeniden oluştur ve ayarla
        new_data = data_fetcher()
        # TTL'ye küçük bir rastgele gürültü ekle (örn. 60s ile 70s arası)
        ttl = random.randint(60, 70)
        redis_client.set(key, new_data, ex=ttl)
        return new_data
    
    # Önbellek isabeti: Erken yenileme gerekip gerekmediğini kontrol et
    ttl_remaining = redis_client.ttl(key)
    
    # Kalan TTL, orijinal maksimum TTL'nin %10'undan azsa, eşzamansız olarak yenile
    if ttl_remaining < 7: 
        # Üretim ortamında burada bir mesaj kuyruğu veya eşzamansız görev çalıştırıcı kullanın
        schedule_async_refresh(key, data_fetcher)
        
    return data

def schedule_async_refresh(key, data_fetcher):
    # Bu işlev, ana istek iş parçacığını bloke etmemek için
    # ayrı bir iş parçacığında/işlemde çalışmalıdır
    new_data = data_fetcher()
    redis_client.set(key, new_data, ex=60)

Sonuç

Önbellek damgalarını önlemek, tek bir mükemmel çözümü seçmekle ilgili değil, belirli trafik desenleriniz için doğru aracı seçmekle ilgilidir. Dağıtılmış kilitleme güçlü tutarlılık sağlar ancak gecikme getirebilir. Olasılıksal sonlandırma, hafif veri eskimesi pahasına yüksek kullanılabilirlik ve düşük gecikme sunar.

Çoğu üretim sistemi için en iyi yaklaşım hibrit bir yöntemdir: Sonlanmayı yaymak için gürültülü kısa TTL'ler kullanmak ve bunları eşzamansız arka plan yenilemeleriyle birleştirmek. Bu stratejileri uygulayarak, altyapınızın en ağır yükler altında bile dirençli kalmasını ve trafik dalgalanmaları sırasında bile pürüzsüz bir kullanıcı deneyimi sunmasını sağlarsınız.

Share: