Çoğu mimari diyagramında önbellek, bir servis ile veritabanı arasındaki tek bir kutudur. O kutu, önbelleğin işe mi yarayacağını yoksa zarar mı vereceğini belirleyen soruları gizler: içine veriyi kim koyar, veritabanına kim yazar ve arada bir okuyucu ne görür?
Dört strateji bu sorulara farklı yanıtlar verir: cache-aside, read-through, write-through ve write-back. Bu rehber her birini aynı örnekle anlatıyor, birbirleriyle karşılaştırıyor, production'da ortaya çıkan hataları ele alıyor ve yeniden kullanabileceğiniz bir diyagramla bitiyor.
Her önbelleğin yanıtladığı iki soru
Her önbellekleme stratejisi bir yanıt çiftidir:
- Okumada veriyi önbelleğe kim yükler? Ya uygulama kodunuz ya da veritabanına nasıl ulaşacağını bilen bir önbellek katmanı.
- Veritabanına kim, ne zaman yazar? Uygulamanız, aynı istek içinde önbellek katmanı ya da daha sonra önbellek katmanı.
| Strateji | Okuma: önbelleği kim doldurur | Yazma: veritabanına kim yazar |
|---|---|---|
| Cache-aside | Uygulama, bir ıskadan sonra | Uygulama; ardından anahtarı geçersiz kılar |
| Read-through | Önbellek katmanı, bir ıskadan sonra | Strateji tanımlamaz; genellikle sonraki ikisinden biriyle eşleşir |
| Write-through | Genellikle read-through ya da cache-aside ile birleştirilir | Önbellek katmanı, aynı istek içinde |
| Write-back (write-behind) | Genellikle read-through ile birleştirilir | Önbellek katmanı, daha sonra ve çoğu zaman toplu olarak |
Örnek boyunca, Postgres'te bir products tablosu ve Redis önbelleği olan bir ürün API'si kullanılıyor. Bir ürün sayfası product:42 anahtarını okur, bir yönetim aracı da fiyatları günceller.
Cache-aside (lazy loading)
Uygulama iki depoyla da konuşur. Önce önbelleğe sorar; ıskada veritabanını okur ve sonucu bir yaşam süresiyle (TTL) önbelleğe koyar. AWS'nin ElastiCache dokümantasyonu aynı deseni lazy loading adıyla anlatır.
def get_product(product_id):
key = f"product:{product_id}"
cached = redis.get(key)
if cached is not None:
return json.loads(cached) # hit
row = db.fetch_one("SELECT * FROM products WHERE id = %s", product_id)
if row is not None:
redis.set(key, json.dumps(row), ex=300) # fill, with a 300 s TTL
return row
def update_price(product_id, price):
db.execute("UPDATE products SET price = %s WHERE id = %s", price, product_id)
redis.delete(f"product:{product_id}") # invalidate, don't overwrite
Yazmalarda yeni değeri önbelleğe yazmak yerine anahtarı silin. Sonraki okuma güncel satırı yükler. Microsoft'un cache-aside deseni de aynı yazma kuralını anlatır.
Kazancınız: yalnızca birinin istediği veri önbelleğe girer ve önbellek çökerse okumalar veritabanına giderek, yalnızca daha yavaş çalışmaya devam eder. Bedeli: her ıska üç gidiş-dönüş tutar (önbellek, veritabanı, önbellek) ve bir yarış durumu vardır. Iska yaşayan bir okuyucu eski satırı okuyabilir, yazıcı satırı güncelleyip anahtarı siler, ardından okuyucu eski değeri önbelleğe geri yazar. Değer TTL dolana kadar bayat kalır; cache-aside'ın her zaman TTL'e ihtiyaç duymasının bir nedeni budur.
Read-through
Uygulama yalnızca önbelleği çağırır. Iskada önbellek katmanı, yapılandırdığınız bir yükleyici üzerinden değeri veritabanından yükler, saklar ve döndürür. Okuma sonucu cache-aside ile aynıdır, ama yükleme kodu her çağırıcıda değil, tek bir yerde yaşar.
Düz bir Redis sunucusu veritabanınızdan kendi başına okuma yapmaz. Read-through bunu yapabilen bir katmandan gelir: yükleyici fonksiyonu olan bir önbellekleme kütüphanesi, bir proxy ya da tek bir veritabanı için yapılmış yönetilen bir önbellek. Amazon DynamoDB Accelerator (DAX) buna bir örnektir: önbellek ıskasında okumayı DynamoDB'ye iletir, sonucu döndürür ve önbelleğe alır.
Ödünleşim: önbellek katmanı artık okuma yolunun üzerindedir. Çökerse, istemcinin veritabanına bir geri dönüş yolu yoksa okumalar da başarısız olur.
Write-through
Uygulama önbellek katmanına yazar, katman da aynı işlem içinde veritabanına yazar. Uygulama, ancak iki yazma da başarılı olursa başarılı yanıtı alır. DAX tam olarak bunu belgeler: veri önce DynamoDB tablosuna, sonra önbelleğe yazılır ve çağrı yalnızca iki yazma da başarılı olursa başarılı olur.
Kazancınız: önbellek üzerinden yazılan veri önbellekte günceldir; yazmanın hemen ardından yapılan bir okuma onu görür. Bedeli: her yazma iki depoyu bekler ve önbellek, yazılan ama hiç okunmayan verilerle dolar. Bu verinin süresi dolsun diye bir TTL ekleyin. Yeni ve boş bir önbellek düğümünde de veri yeniden yazılana kadar hiçbir şey yoktur; write-through'un genellikle read-through ya da lazy loading ile birleştirilmesinin nedeni budur.
Write-back (write-behind)
Uygulama önbelleğe yazar ve başarılı yanıtı hemen alır. Önbellek katmanı veritabanına daha sonra, çoğu zaman toplu olarak yazar; aynı anahtara yapılan tekrarlı güncellemeler tek bir veritabanı yazmasına indirgenir.
Kazancınız: en düşük yazma gecikmesi ve yoğun dönemlerde çok daha az veritabanı yazması. Bedeli: tampon çoğaltılmıyor ya da kalıcı bir log ile desteklenmiyorsa, önbellek düğümü çöktüğünde henüz aktarılmamış her şey kaybolur. Veritabanı da önbelleğin gerisinde kalır; raporlar, diğer servisler ve yedekler daha eski veriyi görür. Write-back, görüntülenme sayaçları gibi birkaç saniyelik kaybın kabul edilebildiği verilere uyar.
Yan yana
| Cache-aside | Read-through | Write-through | Write-back | |
|---|---|---|---|---|
| Uygulama kodu veritabanını bilir | Evet | Hayır | Hayır | Hayır |
| Yazma gecikmesi | Veritabanı yazması artı bir önbellek silme | Yazma stratejisine bağlı | Önbellek ve veritabanı | Yalnızca önbellek |
| Bayat okumalar | Geçersiz kılma ya da TTL'e kadar | Geçersiz kılma ya da TTL'e kadar | Önbellek üzerinden yapılan yazmalarda nadir | Veritabanı önbelleğin gerisinde kalır |
| Önbellek düğümü kaybedilirse | Daha yavaş okumalar | Geri dönüş yolu yoksa okumalar başarısız | Yeniden yazılana kadar eksik veri | Aktarılmamış yazmalar kaybolur |
| Tipik kullanım | Redis ile varsayılan seçim | Bir kütüphane ya da yönetilen önbellek | Yazdıktan hemen sonra güncel okuma | Yazma yoğun sayaçlar ve ani yükler |
TTL'ler, tahliye ve üç klasik hata
TTL, bir değerin ne kadar bayatlayabileceğini sınırlar. Ürünün kabul ettiği bayatlığa göre her veri türü için ayrı seçin: bir ürün açıklaması dakikalarca yaşayabilir, stok seviyesi belki birkaç saniye. Süresi dolmuş bir anahtar okunduğunda hiç yokmuş gibi davranır.
Tahliye (eviction) ayrı bir mekanizmadır: önbelleğin belleği dolduğunda neyi çıkaracağı. Redis'te sınırı maxmemory, neyin çıkarılacağını maxmemory-policy belirler. redis.conf içindeki varsayılan değer noeviction'dır; bellek dolduğunda veri ekleyen komutlara hata döndürür. Saf bir önbellek için Redis dokümantasyonu iyi bir varsayılan olarak allkeys-lru önerir. Yönetilen servisler kendi varsayılanlarını belirler; sizinkini kontrol edin. İsabet oranını INFO stats çıktısından keyspace_hits / (keyspace_hits + keyspace_misses) olarak ölçün.
Neredeyse her önbellekte üç hata ortaya çıkar:
- İzdiham (stampede). Popüler bir anahtarın süresi dolar ve yüzlerce istek aynı anda ıskalayıp veritabanını sorgular. Değeri tek bir istek yeniden oluştursun; diğerleri kısa süre beklesin ya da eski değeri sunsun. Bir Redis kilidi bunu yapar:
SET lock:product:42 <token> NX PX 5000yalnızca tek bir çağırıcı için başarılı olur. Facebook'un memcache makalesi aynı sorunu lease'lerle çözer ve bu soruna yatkın anahtarlarda veritabanına gelen sorguların tepe değerinin saniyede 17 binden 1,3 bine düştüğünü bildirir. - Delinme (penetration). Var olmayan kimlikler için gelen istekler her zaman ıskalar ve her zaman veritabanına gider. "Bulunamadı" sonucunu kısa bir TTL ile önbelleğe alın ya da önce geçerli kimliklerden oluşan bir Bloom filtresine bakın.
- Çığ (avalanche). Örneğin toplu bir yüklemeden ya da önbellek yeniden başladıktan sonra çok sayıda anahtarın süresi aynı anda dolar ve veritabanı tüm yükü bir anda alır. TTL'lere rastgele sapma ekleyin, örneğin 300 saniye artı 60 saniyeye kadar, ve önbelleği kademeli olarak ısıtın.
Diyagram
Aynı üç katılımcıdan oluşan dört satır; fark kenarlarda: veritabanıyla kim, ne zaman konuşuyor.
Diyagram içeriği
- Product API
- Redis
- Postgres
- Cache layer
- 1 GET (miss)
- 2 SELECT
- 3 SET, TTL 300 s
- 1 read
- 2 on miss: SELECT
- 1 write
- 2 UPDATE, same request
- 1 write, OK at once
- 2 flush later, batched
Nasıl okunur:
- 1. satır, cache-aside. Ürün API'si Redis'i okur (1), ıskada Postgres'i kendisi okur (2) ve Redis'i bir TTL ile doldurur (3). Postgres'e giden kenar önbelleği atlar: iki çağrının da sahibi uygulamadır.
- 2. satır, read-through. API yalnızca önbellek katmanıyla konuşur; katman ıskada Postgres'ten yükler.
- 3. satır, write-through. Yazma, aynı istek içinde önbellek katmanı üzerinden Postgres'e gider.
- 4. satır, write-back. API başarılı yanıtı hemen alır. Kesikli kenar, sonradan yapılan toplu aktarımdır.
Önbellekler mimari diyagramlarında nasıl çizilir
- Stratejiyi ve TTL'i adlandırın. Önbellek düğümünde ya da kenarında "Cache-aside, TTL 300 s" yazması, bir inceleyicinin ilk sorusunu yanıtlar.
- Veritabanıyla kimin konuştuğunu çizin. Cache-aside'da servisin önbelleğe olduğu kadar veritabanına da bir kenarı vardır. Veritabanının önünde araya girmiş gibi çizilen bir önbellek read-through izlenimi verir ve okuyucular öyle varsayar.
- Geçersiz kılma yolunu çizin. Veritabanına doğrudan yazan toplu işler ve yönetim araçları dahil, anahtarları kimin sildiğini gösterin. Önbelleğe giden yolu olmayan bir yazıcı, bayat verinin kaynağıdır.
- Hata davranışını yazın. "Önbellek çökerse: okumalar Postgres'e gider" notu, nöbetçi mühendise ikinci bir önbellek ikonundan daha çok şey söyler.
- Sayıları tuvale koyun. İsabet oranı hedefi, bellek sınırı ve tahliye politikası, önbelleğin yanındaki küçük bir nota sığar.
- Asenkron olan her şeyi kesikli çizin. Bir write-back aktarımı ya da bir geçersiz kılma olayı isteğin parçası değildir.
Seçim
Cache-aside ve bir TTL ile başlayın. Her önbellek ve her veritabanıyla çalışır, önbellek çöktüğünde de yalnızca okumalar yavaşlar. Bir kütüphane ya da yönetilen önbellek size bir yükleyici sunduğunda read-through'a geçin. Yazmanın hemen ardından yapılan okumaların yeni değeri görmesi gerekiyorsa ve yazmalar ek gecikmeyi kaldırabiliyorsa write-through ekleyin. Write-back'i yalnızca aktarılmamış yazmaları kaybetmeyi ya da yeniden oynatmayı göze alabiliyorsanız kullanın.
Önbelleğin çevresindeki resim için Redis'in bir URL kısaltıcının okuma yoluna hizmet ettiği sistem tasarımı diyagramı örneğine, hangi servisin hangi veriye sahip olduğunu göstermek için de mikroservis diyagramı kurallarına bakın.
Archboard'da oluşturun
- Google ya da GitHub ile giriş yapın ve bir diyagram oluşturun.
- Her strateji için Temel paketinden bir Metin başlığı ekleyin, ardından üç düğümü yan yana koyun: ürün API'si için C4 Modeli paketinden bir Konteyner, önbellek için Veri paketinden bir Redis ve veritabanı için Veri paketinden bir Postgres. 2 ile 4. satırlarda önbelleği "Cache layer" olarak etiketleyin.
- Her satırı numaralı kenar etiketleriyle bağlayın: cache-aside için "1 GET (miss)", "2 SELECT" ve "3 SET, TTL 300 s", diğer satırlar için de okuma ya da yazma adımları. Cache-aside'da API'den Postgres'e giden kenarı önbelleğin üzerinden geçirin.
- Write-back aktarım kenarını kesikli yapın.
- Her satırın altına, yazma kuralını ve önbellek çöktüğünde ne olduğunu anlatan Temel paketinden bir Metin notu ekleyin.
Örnek 12 diyagram düğümü kullanıyor ve Free planın sınırları içinde kalıyor. Her mimari diyagramın arkasındaki genel yöntem için yazılım mimarisi diyagramı nasıl çizilir yazısına bakın.
SSS
Cache-aside ile lazy loading aynı şey mi? Evet. İki ad da uygulamanın veriyi önbelleğe yalnızca bir ıskadan sonra yüklemesini anlatır. AWS dokümantasyonu "lazy loading" der, diğer birçok kaynak ise "cache-aside".
Veri değiştiğinde önbellek anahtarını güncellemeli miyim, silmeli miyim? Cache-aside'da silin. Yeni değerlerini önbelleğe yazan iki yazıcı yanlış sırayla bitirip geride eski bir değer bırakabilir. Silmenin ardından taze bir yükleme bunu önler; TTL de yukarıda anlatılan daha nadir yarış durumunu sınırlar.
Redis read-through ya da write-through'u kendi başına yapabilir mi? Hayır. Bir Redis sunucusu, istemcilerin gönderdiğini saklar. Read-through ve write-through, veritabanınızı tanıyan bir katman gerektirir: bir önbellekleme kütüphanesi, bir proxy ya da tek bir veritabanı için yapılmış yönetilen bir önbellek.
Hangi TTL'i kullanmalıyım? Ürünün o veri türü için kabul ettiği en uzun bayatlığı. Ardından birlikte yüklenen anahtarların süresi birlikte dolmasın diye rastgele sapma ekleyin.
Write-back güvenli mi? Yalnızca aktarılmamış yazmaları kaybetmek ya da yeniden oynatmak kabul edilebilirse veya yazma tamponu çoğaltılıyor ya da kalıcı bir log ile destekleniyorsa. Ödemeler ya da siparişler gibi verilerde kaçının.