İçeriğe geç
academia.sh

Ders 04 / 19

Artımlı Yeniden Üretim

Statik çıktının seçici tazelenmesi; tazelik penceresi, arka plan tazeleme kuyruğu, ıska–isabet sayımı ve pencere uzunluğunun üretim yükü ile içerik gecikmesi arasında kurduğu değiş tokuş.

İçindekiler

Statik üretimin üç sınırından biri, tazeliğin derleme sıklığına bağlanmasıydı. Bir sayfa derleme anında üretilmiş olabilir ama sonsuza kadar geçerli sayılmak zorunda değildir: kendisine bir geçerlilik süresi tanınabilir, süre dolduğunda bir sonraki istek eski kopyayı alırken arka planda yeni sürümü hazırlanabilir.

Bu düzenek Önbellek Stratejileri dersinde tarayıcı tarafında görülen yeniden doğrulayarak sunma davranışının sunucu tarafındaki karşılığıdır. Bu ders onu bir oluşturma modeli olarak kurar ve iki büyüklüğü ölçer: kaç isteğin ne kadar eski içerik gördüğü ve kaç yeniden üretim işi doğduğu.

Üç Yanıt Durumu

Bir sayfa isteği geldiğinde depoya bakılır ve üç durumdan biri gerçekleşir.

Iska: depoda o sayfanın hiçbir kopyası yoktur. İstek, üretimin bitmesini bekler ve sunucuda oluşturmanın maliyetini ödemiş olur. Bu durum her sayfa için en çok bir kez yaşanır.

Taze isabet: depodaki kopya, tazelik penceresi içinde üretilmiştir. Doğrudan sunulur; hiçbir üretim işi doğmaz.

Eski sunum: kopya vardır ama penceresi dolmuştur. Kopya olduğu gibi sunulur, yeniden üretim arka plan kuyruğuna alınır. İstek bekletilmez; tazelenme bir sonraki isteklere yansır.

Bu üç durumun bir sunucuda nasıl göründüğü doğrudan gözlenebilir.

// artimli-sunucu.mjs — tazelik penceresi ve arka plan tazelemesi olan sunucu
import { createServer } from "node:http";

const PENCERE = 3000;   // ms, kopyanin taze sayildigi sure
const URETIM = 1000;    // ms, sayfanin uretilme maliyeti

let kaynakSurum = 0;
setInterval(() => { kaynakSurum += 1; }, 1000).unref(); // veri her saniye degisir

let kayit = null;       // { govde, surum, uretim }
let tazeleniyor = false;

const bekle = (ms) => new Promise((c) => setTimeout(c, ms));

async function uret() {
  await bekle(URETIM);
  return { govde: `olcum listesi surum ${kaynakSurum}\n`, surum: kaynakSurum, uretim: Date.now() };
}

createServer(async (istek, yanit) => {
  yanit.sendDate = false;
  if (istek.url !== "/olcumler") return yanit.writeHead(404).end();

  let durum;
  if (!kayit) {
    durum = "iska";                       // kopya yok: istek uretimi bekler
    kayit = await uret();
  } else if (Date.now() - kayit.uretim < PENCERE) {
    durum = "taze";                       // pencere icinde: dogrudan sunulur
  } else {
    durum = "eski";                       // pencere disi: eski kopya sunulur
    if (!tazeleniyor) {                   // tazeleme bir kez kuyruga alinir
      tazeleniyor = true;
      uret().then((yeni) => { kayit = yeni; tazeleniyor = false; });
    }
  }
  yanit.setHeader("X-Durum", durum);
  yanit.setHeader("Content-Type", "text/plain; charset=utf-8");
  yanit.setHeader("Content-Length", Buffer.byteLength(kayit.govde));
  yanit.writeHead(200).end(kayit.govde);
}).listen(8174, "127.0.0.1", () => console.log("dinleniyor: 127.0.0.1:8174"));

Aşağıdaki betik bu dosyayla aynı dizinde çalıştırılır. Bağlantı noktası 8174 keyfîdir ve boş olmalıdır; kullanımdaysa iki dosyada da değiştirilir.

#!/usr/bin/env bash
# artimli-sunucu.mjs'yi baslatir, dort ayri anda istek yapar, sonra durdurur.
node artimli-sunucu.mjs > /dev/null &
sunucu=$!
sleep 1

iste() {
  printf '%-34s' "$1"
  curl -sS -D basliklar -w '   sure %{time_total} s   ' http://127.0.0.1:8174/olcumler | tr -d '\n'
  grep -i '^x-durum' basliklar | tr -d '\r'
}

iste "1) ilk istek"
iste "2) hemen ardindan"
sleep 4
iste "3) pencere dolduktan sonra"
sleep 2
iste "4) tazeleme bittikten sonra"
rm -f basliklar

kill "$sunucu"
1) ilk istek                      olcum listesi surum 1   sure 1.007820 s   X-Durum: iska
2) hemen ardindan                 olcum listesi surum 1   sure 0.000844 s   X-Durum: taze
3) pencere dolduktan sonra        olcum listesi surum 1   sure 0.000695 s   X-Durum: eski
4) tazeleme bittikten sonra       olcum listesi surum 7   sure 0.001069 s   X-Durum: taze

Süre alanları makineye bağlıdır. Belirleyici olan oran şudur: ıska yaklaşık bir saniye sürerken diğer üç istek milisaniyenin altında kalır — aradaki fark yaklaşık bin kattır. Üçüncü istek penceresi dolmuş bir kopyayı sunar ve yine hızlıdır; bekleyen yalnızca tazeliktir, kullanıcı değil.

Dördüncü satır düzeneğin karakteristik davranışını gösterir: sunulan sürüm 1’den 7’ye atlamıştır. Arka plandaki üretim, başladığı andaki değil bittiği andaki kaynağı yakalar; bu arada kaynak birkaç sürüm ilerlemiştir.

Tazelik Penceresi Gerçek Yaşı Söylemez

Yanıt üzerindeki “taze” damgası, kopyanın kaynağıyla aynı olduğunu değil, pencere içinde üretildiğini bildirir. İkisi aynı şey değildir. Aşağıdaki benzetim bu ayrımı sanal bir saat üzerinde açığa çıkarır; çıktı makineden bağımsızdır.

// artimli.mjs — tazelik penceresi ve arka plan tazeleme kuyrugu modeli
// Sanal saat kullanilir: cikti belirlenimcidir, makineye bagli degildir.

const PENCERE_VARSAYILAN = 60;   // saniye
const URETIM_SURESI = 2;         // saniye, arka planda bir sayfanin yeniden uretilmesi
const SURE = 1800;               // saniye, benzetimin uzunlugu
const ISTEK_SAYISI = 300;

// Kaynak verinin surumu: olcum listesi her 60 saniyede bir degisir, arsiv hic degismez.
const kaynakSurum = (yol, t) => (yol === "/olcumler" ? Math.floor(t / 60) : 0);

// Belirlenimci sozde rastgele dizi (dogrusal esleskli uretec).
function uretec(tohum) {
  let x = tohum;
  return () => (x = (x * 1103515245 + 12345) % 2147483648) / 2147483648;
}

// Istek akisi: yollar agirliklarina gore secilir, zamanlar artan sirada uretilir.
function istekAkisi() {
  const rastgele = uretec(20260728);
  const agirlik = [["/olcumler", 6], ["/gun-1", 2], ["/gun-2", 1], ["/gun-3", 1]];
  const havuz = agirlik.flatMap(([yol, n]) => Array(n).fill(yol));
  const istekler = [];
  for (let i = 0; i < ISTEK_SAYISI; i++) {
    istekler.push({
      t: Math.floor(rastgele() * SURE),
      yol: havuz[Math.floor(rastgele() * havuz.length)],
    });
  }
  return istekler.sort((a, b) => a.t - b.t);
}

function calistir(pencere, gunlukTut) {
  const depo = new Map();      // yol -> { surum, uretim }
  const kuyruk = new Map();    // yol -> tamamlanma zamani
  const sayac = { taze: 0, eski: 0, iska: 0, uretim: 0 };
  const gecikmeler = [];
  const gunluk = [];

  for (const { t, yol } of istekAkisi()) {
    // Kuyruktaki tazeleme suresi dolduysa depoya yazilir.
    const biter = kuyruk.get(yol);
    if (biter !== undefined && biter <= t) {
      depo.set(yol, { surum: kaynakSurum(yol, biter), uretim: biter });
      kuyruk.delete(yol);
      sayac.uretim += 1;
    }

    const kayit = depo.get(yol);
    let sonuc;
    if (!kayit) {
      // Iska: kopya yok, istek uretimi bekler.
      depo.set(yol, { surum: kaynakSurum(yol, t), uretim: t });
      sayac.iska += 1; sayac.uretim += 1;
      sonuc = "iska";
    } else if (t - kayit.uretim < pencere) {
      sayac.taze += 1;
      sonuc = "taze";
    } else {
      // Eski kopya sunulur, tazeleme kuyruga alinir (zaten kuyruktaysa yenisi eklenmez).
      sayac.eski += 1;
      if (!kuyruk.has(yol)) kuyruk.set(yol, t + URETIM_SURESI);
      sonuc = "eski";
    }

    const sunulan = depo.get(yol);
    const gecikme = kaynakSurum(yol, t) - sunulan.surum;
    gecikmeler.push(gecikme);
    if (gunlukTut && yol === "/olcumler" && gunluk.length < 14) {
      gunluk.push(`${String(t).padStart(4)}s  ${sonuc.padEnd(7)}` +
        `sunulan surum ${String(sunulan.surum).padStart(2)}   kaynak surum ` +
        `${String(kaynakSurum(yol, t)).padStart(2)}   gecikme ${gecikme}` +
        (kuyruk.has(yol) ? "   [kuyrukta]" : ""));
    }
  }
  const ortalama = gecikmeler.reduce((a, b) => a + b, 0) / gecikmeler.length;
  return { sayac, ortalama, enKotu: Math.max(...gecikmeler), gunluk };
}

const ilk = calistir(PENCERE_VARSAYILAN, true);
console.log(`--- /olcumler istekleri, ilk 14 (pencere ${PENCERE_VARSAYILAN} s) ---`);
for (const satir of ilk.gunluk) console.log(satir);

console.log(`\n--- ${ISTEK_SAYISI} istek, ${SURE} s, pencere degistikce ---`);
console.log("pencere".padStart(8) + "taze".padStart(7) + "eski".padStart(7) + "iska".padStart(7) +
  "uretim".padStart(9) + "ort. gecikme".padStart(14) + "en kotu".padStart(9));
for (const pencere of [10, 60, 300, 1800]) {
  const { sayac, ortalama, enKotu } = calistir(pencere, false);
  console.log(`${pencere} s`.padStart(8) + String(sayac.taze).padStart(7) +
    String(sayac.eski).padStart(7) + String(sayac.iska).padStart(7) +
    String(sayac.uretim).padStart(9) + ortalama.toFixed(2).padStart(14) +
    String(enKotu).padStart(9));
}
--- /olcumler istekleri, ilk 14 (pencere 60 s) ---
   5s  iska   sunulan surum  0   kaynak surum  0   gecikme 0
   9s  taze   sunulan surum  0   kaynak surum  0   gecikme 0
  20s  taze   sunulan surum  0   kaynak surum  0   gecikme 0
  28s  taze   sunulan surum  0   kaynak surum  0   gecikme 0
  37s  taze   sunulan surum  0   kaynak surum  0   gecikme 0
  90s  eski   sunulan surum  0   kaynak surum  1   gecikme 1   [kuyrukta]
  92s  taze   sunulan surum  1   kaynak surum  1   gecikme 0
  92s  taze   sunulan surum  1   kaynak surum  1   gecikme 0
  98s  taze   sunulan surum  1   kaynak surum  1   gecikme 0
 107s  taze   sunulan surum  1   kaynak surum  1   gecikme 0
 116s  taze   sunulan surum  1   kaynak surum  1   gecikme 0
 117s  taze   sunulan surum  1   kaynak surum  1   gecikme 0
 146s  taze   sunulan surum  1   kaynak surum  2   gecikme 1
 154s  eski   sunulan surum  1   kaynak surum  2   gecikme 1   [kuyrukta]

--- 300 istek, 1800 s, pencere degistikce ---
 pencere   taze   eski   iska   uretim  ort. gecikme  en kotu
    10 s     98    198      4      179          0.14        1
    60 s    216     80      4       73          0.38        2
   300 s    276     20      4       23          1.42        5
  1800 s    296      0      4        4          9.38       29

Günlüğün 146. saniyedeki satırı dersin can alıcı noktasıdır: yanıt taze damgalıdır ama gecikme birdir. Kopya 92. saniyede üretilmiştir, 60 saniyelik pencere 152. saniyeye kadar sürer; oysa kaynak 120. saniyede bir sürüm ilerlemiştir. Pencere, kopyanın yaşını ölçer, içeriğin doğruluğunu değil.

Doksanıncı saniyedeki satır kuyruğun işleyişini gösterir: eski kopya sunulmuş, tazeleme kuyruğa alınmış, iki saniye sonra tamamlandığı için 92. saniyedeki istek yeni sürümü görmüştür.

Pencere Uzunluğu Bir Değiş Tokuştur

Alt tablo pencere uzunluğunun iki büyüklüğü ters yönde çektiğini gösterir. Pencere onda bir saniyeye indirildiğinde ortalama gecikme 0,14 sürüme düşer ama üretim sayısı 179’a çıkar. Pencere otuz dakikaya çıkarıldığında üretim dörde iner — her sayfa için yalnızca ilk ıska — ama ortalama gecikme 9,38 sürüme, en kötü durum 29 sürüme fırlar.

Aradaki ilişki doğrusal değildir. Pencereyi 10 saniyeden 60 saniyeye çıkarmak üretimi 2,5 kat azaltırken ortalama gecikmeyi yalnızca 2,7 kat artırır; 300 saniyeden 1800 saniyeye çıkarmak ise üretimi 5,8 kat azaltırken gecikmeyi 6,6 kat artırır. Kazanç eğrisi düzleşir: belirli bir noktadan sonra pencereyi uzatmak, tasarruf ettiğinden fazlasını tazelikten alır.

Iska sayısının bütün satırlarda dört olması da anlamlıdır: dört ayrı yol vardır ve her yol için ıska yalnızca bir kez yaşanır. Bekleyen istek sayısı, sayfa sayısına bağlıdır, istek hacmine değil.

Uygulamada Dikkat Edilen Noktalar

Kuyruk tekilleştirilir. Modelde bir yol kuyruktayken ikinci bir tazeleme eklenmez. Bu koruma olmasaydı, penceresi dolmuş popüler bir sayfaya gelen yüz eşzamanlı istek yüz üretim işi doğururdu. Aynı anda çok sayıda kopyanın penceresi dolarsa üretim yükü ani bir sıçrama yapar; pencerelere küçük bir rastgele pay eklemek bu sıçramayı yayar.

İlk ıska öngörülebilir kılınır. Sayfa ilk kez istendiğinde tam üretim maliyeti ödenir. Trafiği bilinen sayfalar derleme sırasında önceden üretilerek bu maliyet yayından önce alınır; kalanlar ilk istekte doğar.

Geçersiz kılma penceresi tamamlar. Pencere zamana dayalı bir tahmindir. İçeriğin değiştiği kesin olarak biliniyorsa — bir ölçüm düzeltildiğinde, bir sayfa yayından kaldırıldığında — ilgili kaydın doğrudan geçersiz kılınması beklemeyi ortadan kaldırır. Zaman tabanlı pencere ile olay tabanlı geçersiz kılma birlikte kullanılır.

Eski içerik arayüzde gizlenmez. Kullanıcı bir ölçüm listesine bakarken gördüğü değerin ne zaman üretildiğini bilmelidir. Üretim zamanının sayfada yazılması, gecikmeyi ortadan kaldırmaz ama yanıltıcı olmaktan çıkarır.

Özet

  • Artımlı yeniden üretimde bir istek üç durumdan birine düşer: ıska üretimi bekletir, taze isabet doğrudan sunulur, eski sunum kopyayı verip tazelemeyi kuyruğa alır.
  • Ölçüm, ıskanın üretim maliyetini ödediğini, diğer iki durumun ise binde bir mertebesinde sürdüğünü gösterir; bekleyen tazeliktir, kullanıcı değil.
  • Tazelik penceresi kopyanın yaşını ölçer, kaynağa uygunluğunu değil; pencere içinde üretilmiş bir kopya kaynağın gerisinde olabilir.
  • Pencereyi kısaltmak gecikmeyi azaltır ve üretim yükünü artırır; kazanç eğrisi düzleştiği için belirli bir uzunluktan sonra tasarruf tazelikten daha pahalıya alınır.
  • Iska sayısı sayfa sayısına bağlıdır, istek hacmine değil; kuyruğun tekilleştirilmesi eşzamanlı isteklerin üretim yükünü çoğaltmasını önler.

Sonraki Adım

Buraya kadarki dört model, işaretlemenin nerede üretileceğini tartıştı ve hepsinde bir şey ortak kaldı: yanıt tek parçada üretilir, tek parçada gönderilir ve istemciye ulaştığında etkileşimsizdir. Sunucuda oluşturma dersinde ölçülen boşluk — içeriğin göründüğü an ile düğmenin çalıştığı an arasındaki aralık — hâlâ yerinde durur. Sonraki ders bu aralığı adlandırır ve iki yönden daraltır: yanıtın parça parça gönderilmesi ve etkileşimin sayfanın tamamına değil, gerçekten gereken bölümlerine verilmesi.

İ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