Cache Sistemlerinde “Dog-pile Effect” den Korunmak

Standart bir web uygulamasında, cache araçlarını kullanmak basit görünebilir. Bir değerin cache’de var olup olmadğı kontrol edilir, varsa değer alınır, yoksa yeniden üretilip daha sonraki isteklere sunulmak üzere cache’e yazılır. Fakat yüksek trafiğe sahip bir uygulamada, cache süresi bittiğinde aynı anda gelen istekler, cache’lenmiş içeriği eş zamanlı tazelemeye çalışabilirler. Bu da kullandığınız veri kaynağına (veritabanı, web servis vb..) extra yük getirecektir.

Örneğin, cevap vermesi 2 saniye süren bir veritabanı sorgusu olduğunu düşünelim. Sorgunun cevabını 5 dakikalık cache’lediğimizi varsayalım. İlk istekte sorgu sonucu cache’leniyor ve 5 dakika boyunca gelen isteklere cache’deki sonuç dönüyor. Uygulamamıza saniyede 100 anlık istek geldiğini varsayalım. Cache süresi bitimindeki ilk saniyede 100 istek (request), -cache’in süresi dolduğu için- içeriği tazelemeye çalışacaklardır. Sorgunun 2 saniye sürdüğünü göz önüne alırsak, ikinci saniyede gelen 100 kişi de aynı tazeleme işlemini yapmaya çalışacaktır. Bir isteğin veritabanına bağlanıp sorguyu çalıştırarak cache’i tazelemesi yeterliyken, veritabanına 200 bağlantı kurulup aynı sorgu 200 defa çalıştırılacaktır. Buna “dog-pile effect” deniyor.

Dog-pile effect’den korunmanın bir kaç yolu var. Bunlardan ilki mevcut cache’leme algoritmanızı biraz değiştirmenizi gerektiriyor. Bir istek cache’deki içeriği tazelerken başka bir isteğin aynı içeriği tazelemeye çalışmasını önleyerek bu sorunu aşabiliriz. Bunun için atomik bir kilit mekanizması kullanarak tek bir isteğin içeriği güncellemesini garanti altına alabiliriz. Cache’lenen içeriğin yaşam süresini (ttl) yenilenme süresinden (expire) uzun tutarak aradaki zaman farkında içeriği güncelleyebiliriz. Örneğin her 300 saniyede bir yenilenmesini istediğimiz bir içeriğimiz varsa ve içeriğin tazelenme süresi 2 saniye sürüyorsa, içeriğin cache’de kalma süresini en az 303 saniye olmalıdır. Cache’lenecek veri içeriğinde, asıl içerik yanında bir de içeriğin son kullanma zamanı yer almalıdır.

PHP ve Memcache kullanarak basit fonksiyon örneği yapalım:

<?php

function getCached($cacheKey)
{
    $memcached = new Memcached();
    $memcached->addServers([['127.0.0.1', 11211]]);

    // memcache'den key sonucunu isteyelim.
    $data = $memcached->get($cacheKey);

    // cache'de data varsa;
    if ($data) {
        $content = $data['content'];
        $expire  = $data['expire'];

        // içeriğin taze olup olmadığını kontrol edelim.
        if ($expire > time()) {
            // içerik var ve hala taze.
            return $content;
        }
    }

    // içerik tazeleme işlemi için kilitleme işlemini yapalım.
    // bunun için, mevcut key'in sonuna '.lock' string'ini ekleyerek memcache'in add() methodunu kullanacağız.
    // atomic bir method olan add() sayesinde aynı anda sadece bir client lock key'ini üretebilecektir.
    // add() methodu çağrıldığında, eğer bu key başka bir client tarafından eklenmişse false dönecektir.

    // içeriğin 60 saniye içinde hazırlanacağını varsayalım.
    $lockStatus = $memcached->add($cacheKey . '.lock', 1, time() + 60);

    if ($lockStatus) {
        // kilitleme işlemi başarılı.
        // içeriği yenileyelim.

        // $content değeri yenileniyor..
        $content = 'Burada güncellenmiş içerik var';

        // cache'e konulacak veriyi hazırlayalım.
        $data = [
            'content' => $content,
            'expire'  => time() + 300 // 5 dakika sonra tazeliğini kaybetsin.
        ];

        // cache'i güncelleyelim.
        $memcached->set($cacheKey, $data, time() + 600); // bu data cache üzerinde 10 dakika dursun.

        // kilidi açalım.
        $memcached->delete($cacheKey . '.lock');

        // içeriği dönelim.
        return $content;
    }

    if ($data) {
        // içerik var ama taze değil ve başkası tarafından yenileniyor.
        // eski içeriği dönelim.
        return $data['content'];
    }

    // cache'deki veri tamamen yok olduktan sonra ve
    // cache içeriği başka bir client tarafından yenilenmekteyken buraya düşüyor.
    // boş sonuç dönmek yerine belirli bir süre bekleyip içeriğin durumu tekrar kontrol edilebilir.
    return false;
}

Kilit işlemi için APC‘de apc_add() methodu, Redis için de aşağıdaki komut kullanılabilir:

SET lock_key "locked" EX lock_timeout NX

Bir diğer yöntem ise dog-pile’e maruz kalan kısımlarda expire süresi olmayan (cache forever) cache’ler kullanıp arka planda bir cron yardımıyla cache’leri tazelemek (pre-warm).

Bonuslar:
http://highscalability.com/strategy-break-memcache-dog-pile
http://www.sobstel.org/blog/preventing-dogpile-effect/