"There are only two hard things in Computer Science: cache invalidation and naming things."

-- Phil Karlton

"caching de nedir ki ya, bu kadar zor olan ne var?" diyorsaniz bu yazi tam size gore. proje buyudukce ve caching'e gercekten ihtiyaciniz olan durumlarda isin icine beklemediginiz problemler giriyor. (cache invalidation, dog piling) ya da aslinda farkinda olmadiginiz sorunlar icin
optimizasyonlar yapmaniz gerekebiliyor.

asagida genel bir caching pattern'i var, muhtemelen bu sekilde calisan kodu bircogumuz yazdik ve kullaniyoruz.

from django.core import cache

def get_post_data(post_id):
    cache_key = "post_{post_id}".format(post_id=post_id)

    post = cache.get(cache_key)

    if not post:
        post = Post.objects.get(pk=post_id)
        cache.set(cache_key, post, 1800)

    return post

anlik 40.000 trafigin oldugu bir web uygulamasi dusunun, anlik 40.000 kisi db'ye gitse eski ve guzel RDBMS'iniz sikinti yasayacak durumda. boyle bir problem yasanmansin diye ne var yok cache'e yukaridakine benzer bir pattern'le dayadiniz.

redis'le memcache ile concurrency'in dibine vuruyorsunuz, hatta sirketiniz yazilim etkinliklerine sponsor oluyor orada "nasil sistemi scale ettik?", "memcache'in ustunde nasil at kosturdum?" diye sunumlar yapiyorsunuz. groupie'leriniz oluyor, skype'da feyizli yazilim sohbetlerinde "abi scaling bizden sorulur, hatta mysql'i de ssd'ye aldik, parasini verdik ama degdi" diyorsunuz, buraya kadar her sey cok guzel.

problem #1: timeout

timeout, yuksek trafikli sistemlerde kullanilmamasi gereken bir ozellik. cache'leriniz mumkunse sonsuza kadar durmali ya da olabildigince uzun tutulmali sistemde.

dog piling -- veya thundering hard

tamamen yukaridaki kod uzerinden dusunun. post_42 key'li cache'iniz var, 1800 saniye gecti timeout oldu, bir kullanici geldi, cache sistemine baktiniz key yok, tekrar olusturdunuz, koydunuz sisteminize.

burada asil sikinti yuksek trafigin oldugu sistemlerde, post42 key'i database'den hesaplanip tekrar cache'e konulacak zamanda, bir degil, binlerce, onbinlerce kullanicinin o key'e istek yapmasi. bu da binlerce kullanici kadar database'e ayni anda gidis demek. bunu sadece bir post detayi icin dusunmeyin, anasayfadaki bir topusers widget'i icin de olabilirdi.

hesaplamasi 1 saniye suren, ciddi JOIN'lerin oldugu bir SQL sorgusu da olabilirdi.

halbuki, timeout vermeseydik, cache invalidation'i tembellik etmeyip kendimiz yapsaydik boyle bir problem olmayacakti.

cache'lediginiz objeler guncel olmayan bir duruma geldiginde (editorlerden biri yaziyi update etti) siz de otomatik olarak bir sinyalle objenin cache'inin uzerine yazacaksiniz. dolayisiyla, olabildiginca timeout kullanmaktan kacinip, DELETE kullanmadiginiz surece herhangi bir "cache miss" ile karsilasmayacaksiniz. yukaridaki kodu buna gore update edip biraz duzeltelim.

from django.core import cache

def get_post_data(post_id):
    cache_key = "post_{post_id}".format(post_id=post_id)

    post = cache.get(cache_key)

    if not post:
        post = Post.objects.get(pk=post_id)
        cache.set(cache_key, post)

    return post

yeni yaklasimla -teoride- hicbir sekilde cache miss ile karsilasmamamiz gerekiyor, ama yine de if not post: kismini her ihtimale karsi tutuyoruz.

problem #2: hatali URL'ler

asthon kutcher projenizi gordu, twitter'ina atti, atarken bir yanlislik oldu, URL degisti. aslinda olmayan bir yaziya milyonlar twitterdan geldi. yukaridaki kodta, aslinda olmayan bir yazi icin onlarca cache ve db sorgusu demek. buradaki yuku azaltmak da mumkun.

from django.core import cache

NOT_FOUND = "404"

def get_post_data(post_id):
    cache_key = "post_{post_id}".format(post_id=post_id)

    post = cache.get(cache_key)

    if post == NOT_FOUND:
        raise Post.DoesNotExist

    if not post:
        try:
            post = Post.objects.get(pk=post_id)                
        except Post.DoesNotExist:
            post = NOT_FOUND

        cache.set(cache_key, post)        

    return post

1 sefer baktik database'e. eger boyle bir sey yoksa, belirledigimiz "icerik bulunamadi" kodunu cache key'ine atadik. ilk istekten sonra gelen tum istekler icin bundan sonra db'ye gidilmeyecek. aslinda bulunamayan yaziya post girilse dahi, key update edilecegi icin herhangi bir sikinti olmayacak.

iyilestirme #1: global bir cache_key listesi

buyuk projelerde onlarca belki de yuzlerce cache key'i olabilir. dolayisiyla cache key'lerini bunun gibi bir fonksiyon icerisinde hazirlamak yerine, cache_keys.py gibi ayri bir dosya tutmak ve gerektiginde import edip kullanmak daha mantikli.

su sekilde ayri bir dosyaniz oldugunu dusunun:

cache_keys.py

NOT_FOUND = "404"

keys = {
    "post": "post_{post_id}",
    "comment": "comment_{comment_id}",
}

daha sonra view dosyanizda bunu kullanmak kolay:

>>> import cache_keys
>>> cache_keys.keys["post"].format(post_id=42)
post_42

bu sekilde kullanmak insani hatalardan dolayi farkli key'lerle ugrasmanizi engeller, projeye sonradan katilan dev. icin de harika bir sey olur.

from django.core import cache
from cache_keys import keys
from cache_keys import NOT_FOUND

def get_post_data(post_id):
    cache_key = cache_keys.keys["post"].format(post_id=post_id)

    post = cache.get(cache_key)

    if post == NOT_FOUND:
        raise Post.DoesNotExist

    if not post:
        try:
            post = Post.objects.get(pk=post_id)
        except Post.DoesNotExist:
            post = NOT_FOUND

        cache.set(cache_key, post)        

    return post

kod ilk haline gore oldukca degisti. ama cok daha saglam.

--

genel oneriler

  • modellerin save/update gibi metodlarinda cache set/update gibi isler yapmayin. django icin model signals ile halledin ornegin.
  • birden fazla makineyi caching olarak kullaniyorsaniz consistent hashing kullanin. data invalidation oranini server ekleme/cikarma gibi durumlarda minimuma indirip guzel bir dagitim orani saglayacaktir.
  • memcache, redis gibi gibi sistemlerin portlari disariya acik olmamali. maalesef cok gozardi edilen bir durum.
  • in process caching/memoization kullanin. django orm'in bir cok durumda lazy sorgular atmasi buna guzel bir ornek.
  • varnish gibi upstream caching sistemleri az maliyetli ve cok etkili. kullanilabilecek yerlerde kullanmak cok mantikli.
  • caching'e gercekten ihtiyaciniz var mi emin olun. kodunuzu gereksiz yere karmasik hale getirmeniz her zaman gerekli olmayabilir.
Tags: python, caching, django