İçeriğe geç
academia.sh

Ders 06 / 25

HTTP Önbellekleme

Yanıtın kendi önbellek kurallarını taşıması: ETag ve Last-Modified doğrulayıcılarının koşullu isteklerle 304 üretmesi, gövde aktarımının sıfıra inmesi, kaynak değiştiğinde doğrulayıcının değişmesi ve public, private, max-age, no-store ile yeniden doğrulayarak sunma yönergelerinin kaynak türüne göre seçilmesi.

İçindekiler

Önceki derslerin bütün önbellekleri uygulamanın içindeydi: anahtar kodda üretiliyor, girdi kodda konuyor, geçersizleştirme kodda tetikleniyordu. Yanıt istemciye gönderildiği anda bu denetim biter. İlk derste “denetlenemez” diye anılan katmanlar — tarayıcı önbelleği, aradaki paylaşılan önbellekler, kenar katmanı — kodun ne düşündüğünü bilmez; yalnız yanıtın başlıklarında yazana bakar.

Bu yüzden HTTP’nin kendi önbellek sözlüğü vardır ve iki soruyu ayrı ayrı yanıtlar: bir yanıt ne kadar süre taze sayılır, tazeliği bittiğinde saklanan kopyanın hâlâ geçerli olup olmadığı kaynağa gövde indirmeden nasıl sorulur. Bu ders iki soruyu da gerçek istekler üzerinde yanıtlar.

Doğrulayıcı Üreten Bir Sunucu

Aşağıdaki sunucu kütüphane API’sinin dört uç noktasını sunar. Her yanıt gövdesinin özetinden bir doğrulayıcı (validator) üretilir ve koşullu istekler bu değere göre yanıtlanır. Yanıtlardaki tarih başlığı sabit tutulmuştur; böylece ölçümün çıktısı her koşuda aynıdır.

// http-onbellek.mjs — kutuphane API'si: dogrulayicilar ve onbellek denetim yonergeleri
import { createServer } from "node:http";
import { createHash } from "node:crypto";

const kitaplar = new Map([[7, { kitapId: 7, baslik: "Kum: Borges Seçkisi", yazar: "Derleme", rafta: 3 }]]);
const stok = { subeId: 1, raftaki: 137 };
const gecmis = { uyeId: 4, toplam: 12 };
const DEGISIM = new Map([[7, new Date("2025-07-01T09:00:00Z")]]);   // son degisiklik anlari

const etiket = (govde) => `"${createHash("sha256").update(govde).digest("hex").slice(0, 16)}"`;

function yanitla(istek, yanit, { govde, denetim, sonDegisim = null, vary = null }) {
  const metin = JSON.stringify(govde);
  const etag = etiket(metin);
  const basliklar = { "cache-control": denetim, etag };
  if (sonDegisim) basliklar["last-modified"] = sonDegisim.toUTCString();
  if (vary) basliklar.vary = vary;

  const gelenEtag = istek.headers["if-none-match"];
  const gelenTarih = istek.headers["if-modified-since"];
  const etagUyuyor = gelenEtag !== undefined && gelenEtag.split(",").map((e) => e.trim()).includes(etag);
  const tarihUyuyor = !etagUyuyor && gelenTarih !== undefined && sonDegisim !== null &&
    Math.floor(sonDegisim.getTime() / 1000) <= Math.floor(Date.parse(gelenTarih) / 1000);

  if (etagUyuyor || tarihUyuyor) { yanit.writeHead(304, basliklar); return yanit.end(); }
  yanit.writeHead(200, { ...basliklar, "content-type": "application/json; charset=utf-8",
    "content-length": Buffer.byteLength(metin) });
  yanit.end(metin);
}

const sunucu = createServer((istek, yanit) => {
  yanit.sendDate = false;                                          // ciktinin belirlenimci kalmasi icin
  const yol = new URL(istek.url, "http://yerel").pathname;

  if (istek.method === "POST" && yol === "/kitap/7") {              // kitap guncellenir: dogrulayici degisir
    const k = kitaplar.get(7);
    k.rafta -= 1;
    DEGISIM.set(7, new Date("2025-07-02T14:30:00Z"));
    yanit.writeHead(204, { "cache-control": "no-store" });
    return yanit.end();
  }
  if (yol === "/kitap/7") {                                         // seyrek degisen genel kaynak
    return yanitla(istek, yanit, { govde: kitaplar.get(7), denetim: "public, max-age=60",
      sonDegisim: DEGISIM.get(7), vary: "Accept-Language" });
  }
  if (yol === "/sube/1/stok") {                                     // sik degisen genel kaynak
    return yanitla(istek, yanit, { govde: stok, denetim: "public, max-age=5, stale-while-revalidate=30" });
  }
  if (yol === "/uye/4/gecmis") {                                    // kisiye ozel kaynak
    return yanitla(istek, yanit, { govde: gecmis, denetim: "private, max-age=30" });
  }
  if (yol === "/odunc/onay") {                                      // hic saklanmamasi gereken yanit
    return yanitla(istek, yanit, { govde: { onay: "U-4711", kalan: 2 }, denetim: "no-store" });
  }
  yanit.writeHead(404, { "cache-control": "no-store" });
  yanit.end();
});

sunucu.listen(8547, "127.0.0.1", () => console.log("kutuphane API 127.0.0.1:8547"));

Sunucuyu başlatan ve ölçen blok ayrıdır. Doğrulayıcı ile son değişiklik tarihi yanıttan okunur, sonraki isteklere koşul başlığı olarak konur.

# Yanitlarin onbellek basliklari ve kosullu isteklerin sonucu
node http-onbellek.mjs & sunucu=$!
sleep 0.5
K=http://127.0.0.1:8547

basliklar() { curl -sS -D - -o /dev/null "$@" | tr -d '\r' | grep -iE '^(HTTP/|etag|last-modified|cache-control|vary)'; }
olc() { curl -sS -o /dev/null -w "durum=%{http_code} indirilen=%{size_download} bayt\n" "$@"; }

echo "== 1. ilk istek: dogrulayicilar ve yonergeler"
basliklar "$K/kitap/7"

ETAG=$(curl -sS -D - -o /dev/null "$K/kitap/7" | tr -d '\r' | awk '/^[Ee]tag:/ {print $2}')
TARIH=$(curl -sS -D - -o /dev/null "$K/kitap/7" | tr -d '\r' | sed -n 's/^[Ll]ast-[Mm]odified: //p')
echo "== 2. ayni kaynak kosullu istenirse"
olc "$K/kitap/7"
olc -H "If-None-Match: $ETAG" "$K/kitap/7"
olc -H "If-Modified-Since: $TARIH" "$K/kitap/7"

echo "== 3. kaynak degisince eski dogrulayici tutmuyor"
curl -sS -o /dev/null -X POST "$K/kitap/7"
olc -H "If-None-Match: $ETAG" "$K/kitap/7"
basliklar "$K/kitap/7" | grep -iE '^(etag|last-modified)'

echo "== 4. kaynak turune gore yonergeler"
for yol in /kitap/7 /sube/1/stok /uye/4/gecmis /odunc/onay; do
  printf '%-16s %s\n' "$yol" "$(curl -sS -D - -o /dev/null "$K$yol" | tr -d '\r' | sed -n 's/^[Cc]ache-[Cc]ontrol: //p')"
done

kill $sunucu
kutuphane API 127.0.0.1:8547
== 1. ilk istek: dogrulayicilar ve yonergeler
HTTP/1.1 200 OK
cache-control: public, max-age=60
etag: "c5397b0c31b92e65"
last-modified: Tue, 01 Jul 2025 09:00:00 GMT
vary: Accept-Language
== 2. ayni kaynak kosullu istenirse
durum=200 indirilen=73 bayt
durum=304 indirilen=0 bayt
durum=304 indirilen=0 bayt
== 3. kaynak degisince eski dogrulayici tutmuyor
durum=200 indirilen=73 bayt
etag: "211739eb1d385801"
last-modified: Wed, 02 Jul 2025 14:30:00 GMT
== 4. kaynak turune gore yonergeler
/kitap/7         public, max-age=60
/sube/1/stok     public, max-age=5, stale-while-revalidate=30
/uye/4/gecmis    private, max-age=30
/odunc/onay      no-store

Tazelik ve Doğrulama

Çıktının ikinci bölümü iki farklı işi ayırıyor. Koşulsuz istek 200 döndürdü ve 73 bayt indirildi. Aynı kaynak If-None-Match başlığıyla istendiğinde yanıt 304 oldu ve indirilen gövde sıfır bayta indi. Üçüncü satırda aynı sonuç If-Modified-Since ile alındı.

304 Not Modified yanıtının kazandırdığı şey gövdedir, gidiş-dönüş değil. İstek yine sunucuya gitti, sunucu yine doğrulayıcıyı hesapladı. Kazanç, gövde büyüdükçe büyür: yetmiş üç baytlık bir kitap kaydında ihmal edilebilir, elli kilobayttan bir liste yanıtında belirleyicidir.

Tazelik ise doğrulamadan önce gelir. max-age=60 yazan bir yanıt altmış saniye boyunca taze sayılır ve o süre içinde önbellek sunucuya hiç sormaz. Süre dolduğunda kopya atılmaz, bayat duruma geçer ve bir sonraki istekte doğrulanır. Doğrulama 304 dönerse kopya yeniden taze sayılır. İki mekanizma birlikte çalışır: max-age gidiş-dönüşü, doğrulayıcı gövdeyi ortadan kaldırır.

Üçüncü bölüm doğrulayıcının anlamını kanıtlıyor. Kitap POST isteğiyle güncellendikten sonra elde tutulan eski doğrulayıcıyla yapılan koşullu istek 304 değil 200 döndü ve gövde yeniden indirildi. Yeni yanıtın hem etiketi hem son değişiklik tarihi değişmiştir. Doğrulayıcı, kaynağın içeriğine bağlı olduğu için bu davranış kendiliğinden doğrudur.

İki doğrulayıcı arasında bir ayrım vardır. Son değişiklik tarihinin çözünürlüğü bir saniyedir: aynı saniye içinde iki kez değişen bir kayıt için eski kopya geçerli sanılabilir. Gövde özetinden üretilen etiketin böyle bir sınırı yoktur. Buna karşılık etiketin hesaplanması gövdenin üretilmesini gerektirir; tarih ise çoğu zaman kaydın kendi sütunundan okunur ve sorgu çalıştırmadan yanıtlanabilir.

Yönergelerin Anlamı

Son bölümdeki dört satır, dört farklı kaynak türü için verilmiş dört karardır.

public, max-age=60 — kitap ayrıntısı her kullanıcıya aynı döner ve seyrek değişir. public, yanıtın paylaşılan önbelleklerde saklanabileceğini söyler.

public, max-age=5, stale-while-revalidate=30 — şube stok sayısı sık değişir. Beş saniye taze sayılır; sonraki otuz saniye boyunca bayat kopya sunulmayı sürdürür ve önbellek arka planda yenilemeyi yapar. Bu, önceki derste süreç içinde kurulan davranışın HTTP’deki karşılığıdır; aradaki fark, kararı artık uygulamanın değil aradaki önbelleğin uygulamasıdır.

private, max-age=30 — üyenin ödünç geçmişi kişiye özeldir. private, yanıtın yalnız son kullanıcının kendi önbelleğinde saklanabileceğini, paylaşılan bir önbelleğe konamayacağını söyler. Anahtar Tasarımı dersindeki kiracı sızıntısının HTTP karşılığı budur ve tek satırlık bir başlıkla önlenir.

no-store — ödünç onayı hiçbir yerde saklanmamalıdır. Bu yönerge sıkça no-cache ile karıştırılır: no-cache saklamayı yasaklamaz, yalnız kullanmadan önce doğrulamayı zorunlu kılar. Saklanmaması gereken yanıt için doğru yönerge no-store’dur.

Paylaşılan önbellekler için ayrı bir süre verilebilir: s-maxage, yalnız paylaşılan önbellekleri bağlar ve max-age’i geçersiz kılar. Kenar katmanında uzun, tarayıcıda kısa bir süre istendiğinde kullanılan yol budur.

Vary ve Paylaşılan Anahtar

Kitap yanıtındaki Vary: Accept-Language başlığı, paylaşılan önbelleğin anahtarını değiştirir. Anahtar Tasarımı dersinde önbellek anahtarının yanıtı değiştiren her girdiyi taşıması gerektiği ölçülmüştü; HTTP’de bu girdiler istek başlıklarıdır ve Vary onları adıyla bildirir. Başlık verilmezse önbellek anahtar olarak yalnız adresi kullanır ve Türkçe isteyen bir kullanıcıya İngilizce yanıt sunulabilir.

Vary başlığının tehlikeli iki değeri vardır. Vary: Cookie ya da Vary: Authorization yazan bir yanıt teknik olarak doğrudur ama pratikte önbelleklenemez: her kullanıcının çerezi farklı olduğu için her istek ayrı bir anahtara düşer ve isabet oranı sıfıra iner. Bu, Anahtar Tasarımı dersinde ölçülen “fazla alan” satırının aynısıdır. Kişiye özel yanıtlarda doğru çözüm Vary değil private’tır.

Özet

  • HTTP önbelleklemesi iki ayrı mekanizmadır: max-age yanıtın ne kadar süre taze sayılacağını, doğrulayıcı ise bayat kopyanın hâlâ geçerli olup olmadığını belirler.
  • Koşullu istek 304 döndürdüğünde indirilen gövde 73 bayttan 0 bayta indi; gidiş-dönüş yine yapıldı. Kazanç gövdededir ve gövde büyüdükçe artar.
  • Kaynak güncellendiğinde gövde özetinden üretilen etiket ve son değişiklik tarihi değişti; eski doğrulayıcıyla yapılan istek 304 yerine 200 aldı.
  • Son değişiklik tarihinin çözünürlüğü bir saniyedir; gövde özetinden üretilen etiketin böyle bir sınırı yoktur ama üretilmesi gövdenin hesaplanmasını gerektirir.
  • public paylaşılan önbelleğe izin verir, private yalnız son kullanıcının önbelleğine, no-store hiçbir yere. no-cache saklamayı değil, kullanmadan önce doğrulamamayı yasaklar.

Sonraki Adım

Konu boyunca her ders bir sayı üretti: kaynağa giden sorgu sayısı, bayat okuma sayısı, isabet ve ıska, indirilen bayt. Bu sayılar tek tek anlamlıydı ama hiçbiri “önbellek işe yarıyor mu” sorusunu tek başına yanıtlamaz. Anahtar Tasarımı dersinde en yüksek isabet oranı yanlış gerçekleştirime aitti; bir başka kurulumda yüksek isabet oranı, hiç okunmayan ucuz anahtarlardan gelebilir. Sonraki ders bu sayıları bir ölçüt kümesine bağlar: isabet oranı nasıl hesaplanır, neyi gizler, ıska cezası ile birleştiğinde ortalama yanıt süresine ne yapar ve önbellek kapasitesi değiştikçe isabet oranı nasıl davranır.

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat