CDN Önbellek Stratejileri ve Cache-Control Başlıkları
Yıllar önce bir e-ticaret müşterisinin sitesi bayram kampanyası sırasında çöktü. Sunucular değil, CDN yanlış yapılandırılmıştı ve her istek origin’e ulaşıyordu. O gün Cache-Control başlıklarını doğru yapılandırmanın ne kadar kritik olduğunu acı bir şekilde öğrendim. Bu yazıda o deneyimden ve sonrasında edindiğim bilgilerden yola çıkarak CDN önbellek stratejilerini ve Cache-Control başlıklarını gerçek dünya senaryolarıyla ele alacağım.
CDN Önbellekleme Neden Bu Kadar Önemli?
CDN (Content Delivery Network), içeriği coğrafi olarak dağıtılmış edge node’larında önbelleğe alarak son kullanıcıya en yakın noktadan sunmayı sağlar. Ama bu sistemin gerçekten işe yaraması için HTTP başlıklarını doğru yapılandırmak şart. Yanlış yapılandırılmış bir CDN, ya hiçbir şeyi önbelleğe almaz (origin’i boğar) ya da çok agresif önbelleğe alır (kullanıcılar eski içerik görür).
Temel sorun şu: CDN’ler, hangi içeriği ne kadar süre tutacaklarını genellikle sunucunuzun gönderdiği HTTP başlıklarına bakarak belirler. Bu başlıkları yönetmesini bilmiyorsanız, pahalı bir CDN aboneliği için para ödeyip aslında hiçbir şeyi önbelleğe almıyor olabilirsiniz.
Cache-Control Başlığı: Temelden İleri Seviyeye
Cache-Control başlığı, hem tarayıcıya hem de CDN edge node’larına içeriğin nasıl önbelleğe alınması gerektiğini söyler. En sık kullanılan direktifleri şöyle sıralayabiliriz:
- max-age=N: İçeriğin N saniye boyunca geçerli olduğunu belirtir
- s-maxage=N: Sadece paylaşımlı önbellekler (CDN, proxy) için max-age’i override eder
- no-cache: İçeriği önbelleğe al ama kullanmadan önce origin’den doğrula
- no-store: İçeriği hiç önbelleğe alma
- public: İçerik herkese açık, CDN ve tarayıcı önbelleğe alabilir
- private: Sadece tarayıcı önbelleğe alabilir, CDN alamaz
- stale-while-revalidate=N: Arka planda yenilerken N saniye boyunca eski içeriği sun
- stale-if-error=N: Origin hata verirse N saniye boyunca eski içeriği sun
- must-revalidate: Süresi dolmuş içerik asla kullanılmasın
- immutable: İçerik hiç değişmeyecek, tarayıcı doğrulama yapmasın
Bu direktifler birlikte kullanılabilir ve CDN ile tarayıcı davranışını ince ayarla kontrol etmenizi sağlar.
Nginx ile Cache-Control Başlıklarını Yapılandırma
Nginx kullanıyorsanız, farklı içerik tipleri için farklı önbellek stratejileri tanımlamak oldukça kolay:
# /etc/nginx/sites-available/example.com
server {
listen 80;
server_name example.com;
root /var/www/html;
# Statik asset'ler - uzun süreli önbellek
location ~* .(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, max-age=31536000, immutable";
add_header Vary "Accept-Encoding";
}
# HTML dosyaları - kısa önbellek, revalidation ile
location ~* .html$ {
expires 1h;
add_header Cache-Control "public, max-age=3600, stale-while-revalidate=60, stale-if-error=86400";
}
# API endpoint'leri - önbelleğe alma
location /api/ {
add_header Cache-Control "no-store, no-cache, must-revalidate";
add_header Pragma "no-cache";
}
# Kullanıcıya özel içerik
location /dashboard/ {
add_header Cache-Control "private, no-store";
}
}
Burada dikkat edilmesi gereken önemli bir nokta: immutable direktifi, tarayıcıya “bu dosyanın içeriği hiç değişmeyecek, hard refresh yapsan bile doğrulama” demiyor. Bu yüzden bunu sadece fingerprint ya da hash içeren dosya isimleriyle kullanın. Örneğin app.a3f8c2d1.js gibi.
Apache ile Cache-Control Yapılandırması
Apache kullanıcıları için benzer yapılandırma .htaccess veya VirtualHost bloğu üzerinden yapılabilir:
# /etc/apache2/sites-available/example.com.conf veya .htaccess
<IfModule mod_expires.c>
ExpiresActive On
# Varsayılan
ExpiresDefault "access plus 1 month"
# HTML
ExpiresByType text/html "access plus 1 hour"
# CSS ve JavaScript
ExpiresByType text/css "access plus 1 year"
ExpiresByType application/javascript "access plus 1 year"
# Görseller
ExpiresByType image/jpeg "access plus 1 year"
ExpiresByType image/png "access plus 1 year"
ExpiresByType image/webp "access plus 1 year"
ExpiresByType image/svg+xml "access plus 1 year"
# Font dosyaları
ExpiresByType font/woff2 "access plus 1 year"
ExpiresByType application/font-woff "access plus 1 year"
</IfModule>
<IfModule mod_headers.c>
# Fingerprint'li dosyalar için immutable ekle
<FilesMatch ".[0-9a-f]{8,}.(js|css)$">
Header set Cache-Control "public, max-age=31536000, immutable"
</FilesMatch>
# API rotaları için önbelleği kapat
<LocationMatch "^/api/">
Header set Cache-Control "no-store, no-cache, must-revalidate"
Header unset ETag
</LocationMatch>
</IfModule>
CDN Katmanında s-maxage Kullanımı
s-maxage direktifi, CDN’lerin davranışını tarayıcıdan bağımsız olarak kontrol etmenizi sağlar. Bu özellikle içeriğinizin CDN’de uzun süre, tarayıcıda kısa süre önbelleklenmesini istediğinizde çok işe yarar.
Gerçek dünya senaryosu: Haber sitesi düşünün. Ana sayfa içeriği CDN’de 10 dakika boyunca tutulsun (binlerce eş zamanlı ziyaretçi için origin’i koruyor), ama tarayıcı her 1 dakikada bir kontrol etsin:
# Haber sitesi ana sayfa örneği
Cache-Control: public, max-age=60, s-maxage=600, stale-while-revalidate=30
# Bu başlık şunu söylüyor:
# Tarayıcı: 60 saniye önbellekle
# CDN: 600 saniye önbellekle
# CDN: Süresi dolarken 30 saniyelik isteklerde eski içeriği sun, arka planda yenile
Bunu Nginx’te dinamik olarak uygulamak için:
# /etc/nginx/conf.d/caching.conf
map $request_uri $cache_control {
default "public, max-age=60, s-maxage=600, stale-while-revalidate=30";
~^/api/ "no-store, no-cache";
~^/user/ "private, no-store";
~^/static/ "public, max-age=31536000, immutable";
~^/news/breaking "public, max-age=30, s-maxage=60";
}
server {
listen 80;
server_name example.com;
location / {
add_header Cache-Control $cache_control;
proxy_pass http://backend;
}
}
Vary Başlığı ve CDN ile Etkileşimi
Vary başlığı, CDN’e hangi istek başlığına göre farklı önbellek versiyonları tutması gerektiğini söyler. Yanlış kullanıldığında önbellek verimliliğini ciddi ölçüde düşürebilir.
# İyi kullanım: Encoding'e göre farklı versiyon tut
add_header Vary "Accept-Encoding";
# Dikkatli kullanılmalı: User-Agent'a göre önbellek (çok sayıda farklı versiyon oluşur)
# add_header Vary "User-Agent"; # Bu CDN hit rate'ini mahveder
# Dil içeriği için
add_header Vary "Accept-Language";
# Mobil/masaüstü ayrımı için (mümkünse URL bazlı yapın, bu yerine)
# /m/page vs /page gibi
Cloudflare gibi CDN’ler bazı Vary başlıklarını yoksayar. Bu yüzden CDN dokümantasyonunuzu mutlaka okuyun. Örneğin Cloudflare, Vary: User-Agent başlığını görmezden gelir çünkü bu başlık önbellek verimliliğini mahveder.
Purge ve Cache Invalidation Stratejileri
İçerik güncellediğinizde CDN’deki eski versiyonları temizlemek kritik. Bu işleme “cache purge” veya “cache invalidation” denir. Farklı CDN sağlayıcılarının API’leri farklıdır:
# Cloudflare Cache Purge - Tek URL
curl -X POST "https://api.cloudflare.com/client/v4/zones/ZONE_ID/purge_cache"
-H "Authorization: Bearer API_TOKEN"
-H "Content-Type: application/json"
--data '{"files":["https://example.com/page.html","https://example.com/style.css"]}'
# Cloudflare - Tüm cache'i temizle (dikkatli kullanın!)
curl -X POST "https://api.cloudflare.com/client/v4/zones/ZONE_ID/purge_cache"
-H "Authorization: Bearer API_TOKEN"
-H "Content-Type: application/json"
--data '{"purge_everything":true}'
# AWS CloudFront Invalidation
aws cloudfront create-invalidation
--distribution-id DISTRIBUTION_ID
--paths "/index.html" "/css/*" "/js/*"
# Fastly Purge - Surrogate-Key ile
curl -X POST "https://api.fastly.com/service/SERVICE_ID/purge"
-H "Fastly-Key: API_TOKEN"
-H "Surrogate-Key: product-123 category-electronics"
Surrogate Key / Cache Tag stratejisi çok güçlüdür. Fastly, Varnish ve bazı diğer CDN’ler bu özelliği destekler. İçeriğinize tag ekler, sonra o tag’e göre toplu purge yaparsınız:
# Origin sunucunuzdan bu başlığı gönderin
Surrogate-Key: product-123 category-electronics homepage
# Sonra sadece product-123 ile ilgili tüm cache'leri temizleyin
curl -X POST "https://api.fastly.com/service/SERVICE_ID/purge"
-H "Fastly-Key: API_TOKEN"
-H "Surrogate-Key: product-123"
Gerçek Dünya Senaryosu: E-Ticaret Sitesi Optimizasyonu
Başta bahsettiğim o e-ticaret krizi sonrasında oluşturduğum stratejiyi paylaşayım. Problem şuydu: Kampanya günü tüm istekler origin’e düşüyordu çünkü product sayfaları Cache-Control: private ile işaretliydi (sepet bilgisinin de aynı sayfada render edilmesinden dolayı).
Çözüm, sayfayı parçalara ayırmaktı:
# Nginx yapılandırması - E-ticaret senaryosu
server {
listen 80;
server_name shop.example.com;
# Ürün sayfaları - CDN'de önbellekle, sepet bilgisini AJAX ile yükle
location ~* ^/product/ {
add_header Cache-Control "public, max-age=300, s-maxage=3600, stale-while-revalidate=60, stale-if-error=86400";
add_header Vary "Accept-Encoding";
proxy_pass http://backend;
}
# Sepet API'si - asla önbelleğe alma
location /api/cart/ {
add_header Cache-Control "no-store, no-cache, must-revalidate";
add_header Pragma "no-cache";
proxy_pass http://backend;
}
# Stok bilgisi API'si - çok kısa önbellek
location /api/stock/ {
add_header Cache-Control "public, max-age=30, s-maxage=30";
proxy_pass http://backend;
}
# Statik dosyalar - maksimum önbellek
location /static/ {
add_header Cache-Control "public, max-age=31536000, immutable";
root /var/www;
}
# Kategori sayfaları
location ~* ^/category/ {
add_header Cache-Control "public, max-age=600, s-maxage=7200, stale-while-revalidate=120";
proxy_pass http://backend;
}
}
Bu yapılandırmayla origin yükü kampanya günü %80 oranında düştü. Sepet ve kullanıcı verisi AJAX ile ayrı endpoint’ten çekildiği için ürün sayfaları artık güvenle önbelleklenebiliyordu.
Cache-Control Başlıklarını Test Etme
Yaptığınız yapılandırmanın gerçekten çalışıp çalışmadığını test etmek için birkaç pratik yöntem:
# curl ile header'ları kontrol et
curl -I https://example.com/product/123
# Çıktıda şunlara bak:
# Cache-Control: public, max-age=300, s-maxage=3600
# X-Cache: HIT (CDN'den geldi)
# X-Cache: MISS (Origin'den geldi)
# Age: 245 (CDN'de kaç saniyedir tutulduğu)
# Verbose modda tüm request/response header'larını gör
curl -v https://example.com/static/app.a3f8c2d1.js 2>&1 | grep -E "(Cache-Control|X-Cache|Age|ETag|Last-Modified)"
# Birden fazla istek yaparak CDN HIT/MISS durumunu izle
for i in {1..5}; do
echo "Request $i:"
curl -s -o /dev/null -w "Status: %{http_code}, Time: %{time_total}sn"
-H "Cache-Control: no-cache"
https://example.com/product/123
done
# CDN'i bypass edip origin'den doğrudan cache header'larını kontrol et
curl -I -H "Fastly-Debug: 1" https://example.com/
curl -I -H "CF-Cache-Status: " https://example.com/ # Cloudflare
# Cache header'larını daha okunabilir görmek için
curl -sI https://example.com/ | grep -i cache
Conditional Requests ve ETag Yönetimi
ETag ve Last-Modified başlıkları, “stale” içeriklerin gereksiz yere tekrar indirilmesini önler. CDN katmanında bu mekanizmanın doğru çalışması, bant genişliğinden ciddi tasarruf sağlar:
# Nginx'te ETag aktifleştirme (genellikle varsayılan açık)
etag on;
# PHP uygulamanızda ETag oluşturma örneği
# (Bu bash değil ama konsept için gösterdim, gerçek uygulamada backend kodu olur)
# Apache'de ETag yapılandırması
# FileETag direktifi ile ne kullanılacağını belirt
FileETag MTime Size
# Büyük cluster'larda inode bazlı ETag sorun yaratabilir, kapat:
FileETag MTime Size
# ya da tamamen kapat ve kendi ETag mekanizmanı kullan:
Header unset ETag
FileETag None
Varnish veya kendi reverse proxy’nizi kullanıyorsanız, conditional request’leri doğru handle etmek önemli:
# Varnish VCL - Conditional request handling
# /etc/varnish/default.vcl
sub vcl_hash {
hash_data(req.url);
if (req.http.host) {
hash_data(req.http.host);
} else {
hash_data(server.ip);
}
return (lookup);
}
sub vcl_backend_response {
# s-maxage varsa onu kullan, yoksa max-age'i kullan
if (beresp.http.Cache-Control ~ "s-maxage=(d+)") {
set beresp.ttl = std.duration(regsub(beresp.http.Cache-Control,
".*s-maxage=(d+).*", "1") + "s", 0s);
}
# Hata durumlarında kısa süre önbellekle
if (beresp.status >= 500) {
set beresp.ttl = 5s;
set beresp.grace = 30s;
}
}
Performans İzleme ve Cache Hit Rate Takibi
CDN yatırımınızın karşılığını alıp almadığınızı anlamak için cache hit rate’i izlemeniz gerekir. İdeal olarak bu değer %85-95 arasında olmalıdır:
# Nginx access log'larından cache hit/miss analizi
# Önce log formatını ayarla
log_format cdn_cache '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'cache_status="$upstream_cache_status"';
# Log'u analiz et
awk '{print $NF}' /var/log/nginx/access.log | sort | uniq -c | sort -rn
# Cloudflare Analytics API ile cache hit rate çek
curl -X GET "https://api.cloudflare.com/client/v4/zones/ZONE_ID/analytics/dashboard"
-H "Authorization: Bearer API_TOKEN"
-H "Content-Type: application/json"
-G
--data-urlencode "since=-1440"
--data-urlencode "until=0" | jq '.result.totals.bandwidth.cached / .result.totals.bandwidth.all * 100'
# AWS CloudFront cache statistics
aws cloudfront get-distribution-config --id DISTRIBUTION_ID
# CloudWatch metrics ile cache hit rate izleme
aws cloudwatch get-metric-statistics
--namespace AWS/CloudFront
--metric-name CacheHitRate
--dimensions Name=DistributionId,Value=DISTRIBUTION_ID
--start-time 2024-01-01T00:00:00Z
--end-time 2024-01-02T00:00:00Z
--period 3600
--statistics Average
Yaygın Hatalar ve Nasıl Kaçınılır
Yıllar içinde gördüğüm en sık hataları paylaşmak istiyorum:
no-cacheileno-storekarıştırmak:no-cacheönbelleğe alır ama kullanmadan önce doğrular.no-storehiç almaz. Gizli veriler içinno-storekullanın.
- Query string’li URL’ler için önbellek yönetimini ihmal etmek:
/product?id=123ve/product?id=123&ref=emailfarklı cache key’leri oluşturur. CDN yapılandırmanızda hangi query string parametrelerinin cache key’e dahil edileceğini belirtin.
Set-Cookieheader’ı olan response’ları önbelleklemeye çalışmak: Çoğu CDN,Set-Cookieiçeren response’ları otomatik olarak önbelleklemez. Bu iyi bir şey ama farkında olun.
- Authorization header kontrolünü atlamak:
Authorizationbaşlığı olan istekler CDN tarafından genellikle bypass edilir. Bunu bilin ve tasarımınızı buna göre yapın.
- WWW vs. non-WWW:
example.comvewww.example.comfarklı cache key’leri oluşturur. Canonical URL’nizi belirleyin ve redirect uygulayın.
- HTTP ve HTTPS için ayrı cache: CDN, HTTP ve HTTPS trafiğini genellikle ayrı önbelleğe alır.
https://example.comısıttıysanızhttp://example.comyeniden MISS olacaktır.
Sonuç
CDN önbellek stratejileri, başlangıçta karmaşık görünse de temel prensipleri kavradıktan sonra oldukça sistematik bir yapıya oturuyor. Özetle şunları aklınızda tutun:
- Statik asset’leri agresif önbellekleyin, dosya isimine hash ekleyin ve
immutablekullanın - Dinamik içerik için
s-maxageile CDN ve tarayıcı davranışını ayrı ayrı kontrol edin - Kullanıcıya özel içeriği asla CDN’e bırakmayın,
privateveyano-storekullanın stale-while-revalidateile kullanıcı deneyimini bozmadan arka planda yenileme yapın- Surrogate Key/Cache Tag stratejisiyle granüler purge yapın, “purge everything” tuşuna basmayın
- Cache hit rate’inizi düzenli izleyin, %70’in altındaysanız bir şeyler yanlış gidiyor
CDN’i doğru yapılandırmak hem kullanıcı deneyimini iyileştirir hem de sunucu maliyetlerini ciddi ölçüde düşürür. O e-ticaret müşterisinin kampanya sırasındaki trafiği, doğru yapılandırma sonrasında origin’in dörtte biri yükle karşılandı. Aynı trafik, dört kat daha az sunucu kaynağıyla. Bu rakamlar çoğu zaman konfigürasyon dosyanızdaki birkaç satır değişikliğinden ibaret.
