Kullanıcı hâlâ eski fiyatı görüyor: önbellek katmanları, Cache-Control ve geçersiz kılma
Ekranda eski fiyatın kalması tek bir önbelleğin arızası değil, birbirinden habersiz katmanların farklı kopyaları tutmasıdır. Bir isteğin yolu üzerinde en az dört ayrı kopya durur: tarayıcının kendi önbelleği, CDN'in paylaşılan önbelleği, uygulamanın içindeki anahtar-değer önbelleği ve veritabanının önüne konmuş sonuç önbelleği. Her birinin kendi anahtarı, kendi ömrü ve kendi temizleme yöntemi var; hiçbiri diğerinin ne yaptığını bilmiyor. Bu yüzden "önbelleği temizleyelim" cümlesi çözüm değil, sorunun kendisi. Aşağıda önce kopyayı hangi katmanın tuttuğunu bulmayı, sonra her katmana ne yazılacağını, en sonda da önbelleğin sessizce veri sızdırdığı yeri ele alıyoruz.
Önce kopyayı hangi katman tutuyor
Tarayıcı önbelleği özeldir: yalnızca o kullanıcıya aittir ve dışarıdan temizleyemezsiniz. CDN önbelleği paylaşılandır: bir kullanıcıya verdiği yanıtı başkasına da verir, buna karşılık bir API çağrısıyla silinebilir. Uygulama içindeki önbellek tamamen sizin kontrolünüzdedir ama genelde en az belgelenen katmandır, çünkü çoğu zaman "şuraya bir Redis koyduk" diye başlar ve kimse ömrünü yazmaz.
Ayrımı yapmanın hızlı yolu başlıklara bakmaktır. curl -sI https://siteniz.com/urun/123 çıktısında Age başlığı varsa yanıt bir ara önbellekten geliyor demektir ve değeri o kopyanın kaç saniyedir orada durduğunu söyler. Cloudflare arkasındaysanız cf-cache-status başlığı HIT, MISS, EXPIRED gibi bir değer döner. Bu iki başlık da yoksa ve içerik hâlâ eskiyse kopya ya tarayıcıda ya da kendi uygulamanızın içindedir. Gizli pencerede aynı adresi açmak bu ikisini beş saniyede birbirinden ayırır.
no-cache "önbelleğe alma" demek değil
Bu yanlış anlama sahada gördüğümüz en yaygın önbellek hatası. Güncel standart Haziran 2022'de yayımlanan RFC 9111 ve direktiflerin anlamı şöyle: no-store yanıtın hiçbir önbellekte saklanmamasını söyler, no-cache ise saklanmasına izin verir ama her kullanımdan önce sunucuya doğrulatılmasını şart koşar. Yani no-cache yazan bir yanıt diskte durur, sadece körlemesine servis edilmez. must-revalidate bir adım daha dar: yanıt taze olduğu sürece serbestçe kullanılabilir, bayatladığı anda doğrulama zorunlu olur. private yanıtı yalnızca tarayıcı önbelleğine bırakır, public ise paylaşılan önbelleklere de açar.
Bir de ömür direktifleri var. max-age saniyeyi yanıtın oluşturulma anından itibaren sayar, alındığı andan değil; araya giren bir önbellek Age başlığıyla geçen süreyi bildirir ve tarayıcı bunu ömürden düşer. s-maxage yalnızca paylaşılan önbellekler için geçerlidir ve onlar için max-age değerini ezer. Pratikte üç reçete işinizin çoğunu görür:
- İçerik özetiyle adlandırılmış statik dosyalar:
public, max-age=31536000, immutable.immutabledirektifi RFC 8246'dan gelir ve tazeyken dosyanın hiç değişmeyeceğini söyleyerek gereksiz doğrulama isteklerini keser. - HTML ve sık değişen JSON:
no-cacheartı birETag. Kopya tutulur, her seferinde doğrulanır, değişmemişse 304 döner. - Girişten sonraki her şey:
private, no-store. Burada cimri olmayın.
Başlık yazmazsanız kararı sizin yerinize başkası verir
Cache-Control yoksa önbellekler tahmin yürütür, buna sezgisel tazelik denir ve sonucu tahmin edilebilir olmaz. CDN'ler de kendi varsayılanlarını uygular. Cloudflare örneğinde varsayılan davranış oldukça belirgin: CDN, HTML ve JSON'u varsayılan olarak önbelleğe almaz, buna karşılık görsel, betik, stil, yazı tipi ve belge uzantılarından oluşan sabit bir listeyi alır. Yanıtta önbellek başlığı hiç yoksa kenar sunucusu duruma göre bir süre uydurur: 200, 206 ve 301 için 120 dakika, 302 ve 303 için 20 dakika, 404 ve 410 için 3 dakika. Ayrıca Cache-Control değeri private, no-store, no-cache ya da max-age=0 ise, veya yanıtta Set-Cookie varsa önbelleğe almaz.
Buradan çıkan kural basit: her rota için ne istediğinizi açıkça yazın. Yazmadığınız her uç noktada davranış, CDN sağlayıcınızın varsayılanına ve tarayıcının sezgisine kalıyor, ikisi de sizin iş kurallarınızı bilmiyor.
Doğrulama ucuz, ama bedava değil
ETag ve Last-Modified içeriğin parmak izini taşır. Tarayıcı bir sonraki istekte If-None-Match ile bu değeri gönderir, içerik değişmemişse sunucu gövdesiz bir 304 döner. Kazanç bant genişliğindedir, gidiş dönüş süresinde değil: istek yine ağa çıkar, yine sunucunuzu meşgul eder. Yavaş bir mobil bağlantıda 304 de pahalıdır.
İki tuzağa dikkat edin. Birincisi, aynı içeriği farklı sunucu örnekleri farklı ETag ile döndürüyorsa doğrulama sürekli başarısız olur ve her istek tam yanıtı yeniden indirir; yük dengeleyici arkasındaki kurulumlarda bunu ölçmeden fark etmezsiniz. İkincisi, sıkıştırma katmanının ETag'i değiştirip değiştirmediğidir. İkisi de sessiz kaybettirir, ikisi de basit bir curl turuyla görünür hale gelir.
Önbellek anahtarını siz tasarlamazsanız isabet oranınız düşer
Bir önbellek, yanıtı bir anahtara karşı saklar. Anahtar varsayılan olarak adrestir, ama Vary başlığı onu genişletir. Vary: Accept-Encoding makul, çünkü sıkıştırılmış ve sıkıştırılmamış sürümler gerçekten farklı. Vary: User-Agent ise anahtarı binlerce parçaya böler ve isabet oranını yerle bir eder. Vary: * yazan bir yanıt ise hiçbir sonraki istek için yeniden kullanılamaz, yani önbelleği tamamen kapatmış olursunuz.
Sorgu parametreleri de aynı işi yapar. ?utm_source= ile gelen her ziyaretçi, aynı sayfanın ayrı bir kopyasını oluşturur; pazarlama kampanyası açıldığı gün kaynak sunucunuzun yükünün neden ikiye katlandığının cevabı genelde buradadır. CDN'lerin önbellek anahtarını düzenleme ayarları tam olarak bunun için var: izleme parametrelerini anahtarın dışında bırakın, sıralamayı normalleştirin, çerezlerden yalnızca gerçekten yanıtı değiştirenleri dahil edin. Dil seçimi için Vary: Accept-Language yerine her dile ayrı adres vermek hem önbellek hem arama motoru tarafında daha temiz sonuç verir, ayrıntısını çok dilli site mimarisi yazısında ele almıştık.
Geçersiz kılmanın üç yolu: bekle, sil, anahtarı değiştir
Birincisi süreyle beklemek. Kısa TTL vermek her şeyi çözer gibi görünür ama çözmez: 60 saniyelik bir ömür, kaynak sunucunuzun dakikada bir tam yükü karşılaması demek. İkincisi hedefli silme. Adrese göre silmek küçük ölçekte yeterli, ancak "bu ürünü güncelledim, ürünün geçtiği bütün liste ve kategori sayfaları da bayatladı" durumunda adres saymakla bitmez. Bunun için önbellek etiketleri var: yanıtınıza Cache-Tag: urun-123, kategori-9 gibi bir başlık koyarsınız, sonra tek çağrıyla o etiketi taşıyan bütün kopyaları silersiniz. Cloudflare'in sınırları belgelenmiş durumda: Cache-Tag başlığı toplamda 16 KB'ı aşamaz (yaklaşık 1000 etiket), API çağrısında bir etiket en fazla 1024 karakter olabilir ve tek seferde en fazla 100 etiket temizlenebilir.
"Hepsini temizle" düğmesi ise acil durum aracıdır. Bastığınız anda kaynak sunucunuz, o güne kadar CDN'in emdiği bütün trafiği aynı anda karşılamaya başlar; yoğun saatte basılan bir "purge everything", kendi kendinize düzenlediğiniz küçük bir yük saldırısıdır.
Üçüncü yol en sağlamı: geçersiz kılmayın, anahtarı değiştirin. Statik dosyaları içerik özetiyle adlandırırsanız (app.9f2c1d.js) yeni sürüm yeni bir adres olur, eskisinin bir yıl önbellekte kalması hiçbir zarar vermez. Bu yaklaşım sıfır kesintili yayına alma sırasında iki sürümün bir süre yan yana çalışmasını da mümkün kılar, çünkü eski sayfa hâlâ eski dosyayı bulabilir.
Bayat yanıt her zaman kötü değil
RFC 5861'in getirdiği iki uzantı, önbellekten alınacak en ucuz kazançlardan biri. stale-while-revalidate bayatlamış kopyanın kullanıcıya anında verilmesine, tazelemenin arka planda yapılmasına izin verir; kullanıcı bekleme süresini hiç görmez. Cache-Control: max-age=60, stale-while-revalidate=600 yazan bir sayfa dakikada bir tazelenir ama hiçbir ziyaretçi kaynak sunucuyu beklemez.
stale-if-error ise kaynak sunucunuz 500, 502, 503 veya 504 döndürdüğünde bayat kopyayı servis etmeye devam eder. Yayına alma sırasında kısa süreli bir arıza yaşandığında sitenin ayakta kalması ile beyaz ekran arasındaki fark çoğu zaman bu tek satır oluyor.
Bir de tarayıcıya ve CDN'e farklı ömür verme meselesi var. Tarayıcı önbelleğini silemezsiniz, CDN önbelleğini silebilirsiniz; dolayısıyla mantıklı kurulum CDN'e uzun, tarayıcıya kısa ömür vermektir. Haziran 2022'de yayımlanan RFC 9213 bunun için CDN-Cache-Control başlığını tanımlıyor: CDN o başlığı okur, tarayıcı klasik Cache-Control'ü. Böylece kenar sunucuda saatlerce duran bir sayfayı, gerektiğinde tek bir temizleme çağrısıyla dünya genelinde güncelleyebilirsiniz.
Uygulama önbelleğinde asıl tehlike: sürü etkisi
Yaygın desen şudur: veriyi önbellekte ara, yoksa hesapla ve yaz. Sorun anahtarın süresi dolduğu anda ortaya çıkar. O anda gelen yüz istek de önbellekte bulamaz, yüzü birden aynı ağır sorguyu çalıştırır ve veritabanı diz çöker. Redis belgelerinde bu duruma sürü etkisi (thundering herd), önbellek kütüphanelerinde köpek yığılması (dogpile), CDN tarafında önbellek izdihamı (cache stampede) deniyor; üçü de aynı şey.
Dört pratik önlem işe yarıyor. İsteklerin tekilleştirilmesi, yani aynı anahtar için yalnızca bir hesaplamanın çalışması ve diğerlerinin onun sonucunu beklemesi. Ömre rastgele bir sapma eklemek, böylece aynı anda yazılan bin anahtar aynı anda ölmez. Süre dolmadan önce arka planda tazelemek. Ve bulunamayan sonucu da kısa süreliğine önbelleğe almak, yoksa var olmayan bir kaydı soran bot trafiği doğrudan veritabanınıza iner. Şunu da not düşelim: yavaş bir sorgunun sonucunu önbelleğe koymak performans sorununu çözmez, sadece ertelenir. Sorgunun kendisini de düzeltmek gerekir, indeks ve sorgu planı tarafını ayrı bir yazıda ele almıştık.
Tarayıcılar artık önbelleği paylaşmıyor
Uzun yıllar geçerli olan bir taktik artık çalışmıyor: "jQuery'yi genel bir CDN'den çekelim, kullanıcının önbelleğinde zaten vardır." Chrome 86 ile birlikte, Ekim 2020'de, HTTP önbelleği bölümlendi. Kaynaklar artık yalnızca adrese göre değil, üst seviye sitenin de dahil olduğu bir anahtara göre saklanıyor. A sitesinde indirilen yazı tipi, B sitesinde görünmez ve yeniden indirilir. Gerekçe gizlilik ve siteler arası sızıntı saldırılarıydı; Chrome'un kendi ölçümlerinde ağ kullanımı yaklaşık %4, ilk ve en büyük içerikli boyama süreleri yaklaşık %0,3 arttı.
Sonucu şu: üçüncü taraf kaynaklardan dosya çekmenin önbellek gerekçesi kalmadı. Yazı tiplerini ve kütüphaneleri kendi alan adınızdan servis etmek artık hem daha hızlı hem daha az bağımlılık demek. Sayfa hızı metrikleri tarafında da fark ölçülebilir düzeyde.
Önbellek yanlış kişiye doğru cevabı verirse
Önbelleğin güvenlik tarafı, performans tarafından daha az konuşuluyor ama sonuçları daha ağır. Paylaşılan bir önbelleğe düşen kişiselleştirilmiş bir sayfa, sıradaki ziyaretçiye başkasının verisini gösterir. Yanıtta Set-Cookie varken önbelleğe alınan bir sayfa, oturum çerezini de dağıtır. Bu yüzden girişli her rota için private, no-store varsayılan olmalı, istisna değil.
Daha sinsi bir sınıf da var. Önbellek ile kaynak sunucu aynı adresi farklı ayrıştırırsa, saldırgan dinamik bir sayfayı statikmiş gibi önbelleğe attırabilir; buna önbellek aldatması deniyor. PortSwigger Research'ün 8 Ağustos 2024'te yayımladığı "Gotta cache 'em all" çalışması bu farkları sistematik olarak taradı ve Cloudflare, Akamai, CloudFront, Azure, Imperva, Google Cloud ve Fastly dahil birçok sağlayıcıda ayrıştırma farkları buldu. Örnekler somut: Spring noktalı virgülü matris değişkeni ayırıcısı sayıyor, Rails noktayı biçim ayırıcısı olarak yorumluyor, bazı sunucular satır sonu veya boş bayt karakterini farklı ele alıyor. /profil;dosya.css gibi bir adres kaynak sunucuya profil sayfası, önbelleğe bir stil dosyası gibi görünebiliyor.
Savunma tarafı sade. Dinamik ve kişiye özel her yanıtta no-store, private olsun. CDN kuralları kaynak sunucunun Cache-Control başlığını ezmesin, özellikle "her şeyi önbelleğe al" tipi geniş kurallardan kaçının. Sağlayıcınızın önbellek aldatması korumasını açın. En önemlisi, bunu bir kere test edin: giriş yapmış bir oturumla /hesap/siparisler adresinin sonuna /x.css ekleyip isteyin, sonra aynı adresi oturumsuz bir tarayıcıda açın. Kendi siparişlerinizi görüyorsanız açık zaten oradadır. Uç nokta tarafındaki akrabalarını API güvenliği ve OWASP API Security Top 10 yazısında ele almıştık.
Bu haftaya sığan ilk adım
En çok trafik alan 20 adresi listeleyin ve her biri için curl -sI çıktısındaki Cache-Control, Age, ETag ve varsa CDN durum başlığını bir tabloya yazın. Muhtemelen üç şey göreceksiniz: hiç başlık yazmadığınız rotalar, girişli olduğu halde private işaretlenmemiş yanıtlar ve bir yıl yerine bir gün önbelleklenen içerik özetli statik dosyalar. Üçünü de düzeltmek bir günlük iş. Ardından okuma ağırlıklı iki üç uç noktaya stale-while-revalidate ekleyip kaynak sunucuya düşen istek sayısındaki değişimi ölçün; izlenebilirlik tarafında zaten bir gösterge panonuz varsa fark ilk günde görünür. Etiketleme altyapısını da ihtiyacınız olmadan önce kurun, çünkü ilk acil güncelleme geldiğinde elinizde tek kalan seçenek "hepsini temizle" olmasın.
Bu konuda yardıma mı ihtiyacınız var?