İçeriğe geç
academia.sh

Ders 05 / 16

İstek Yaşam Döngüsü

Bir isteğin bağlantı kabulünden son baytın yazılmasına uzanan yolu yedi aşamaya bölünür; her aşamaya zaman damgası konularak sıra ve süreler ölçülür, bağlantı ile isteğin ayrı şeyler olduğu ve durum satırının ne zaman geri alınamaz hâle geldiği gösterilir.

İçindekiler

Önceki ders sürecin dışını ölçtü: kaç kopya çalışır, istek hangi kopyaya düşer, kopyalar neyi paylaşmaz. Sürecin içinde ise tek bir isteğin geçtiği bir yol vardır ve o yol şimdiye kadar tek bir kutu gibi ele alındı. Bu ders kutuyu açar.

İstek işleyicisi, dışarıdan bakıldığında “isteği al, yanıtı ver” biçiminde tek adımlı görünür. Gerçekte arada, her biri kendi zamanında olan ve her biri sonrakini kısıtlayan aşamalar vardır. Aşamaları görmenin en doğrudan yolu, her birine bir zaman damgası koyup sırayı yazdırmaktır. Kütüphane ödünç servisinin iki ucu — kitap ödünç verme ve katalog listeleme — bu ölçüm için kullanılacak.

Yedi Aşama

Bir isteğin sunucu tarafındaki yolu şu adımlardan geçer:

  1. Bağlantı kabulü. İşletim sistemi bir TCP bağlantısını dinleyen sokete koyar, uygulama onu kabul eder. Bu noktada henüz hiçbir HTTP verisi okunmamıştır.
  2. Başlık satırlarının çözülmesi. İstek satırı ve başlıklar okunup ayrıştırılır. Yöntem, yol ve başlıklar artık bilinir; gövde bilinmez.
  3. Gövdenin okunması. Gövde parça parça gelir; son parça geldiğinde istek tamdır.
  4. Yönlendirme. Yöntem ve yol bir işleyiciye eşlenir. Eşleşme yoksa isteğin karşılığı 404 olur.
  5. İşleyicinin çalışması. İş kuralı uygulanır, veri okunur veya yazılır, yanıt gövdesi üretilir.
  6. Yanıt başlığının yazılması. Durum kodu ve başlıklar sokete yazılır.
  7. Yanıtın tamamlanması. Gövdenin son baytı yazılır ve bağlantı ya kapanır ya da sonraki istek için açık kalır.

Bu sıra bir kural değil, bir gözlemdir; aşağıdaki sunucu her aşamayı gerçekleştiği anda kaydeder.

Aşamaları Ölçmek

// yasam-dongusu.mjs — bir istegin gectigi asamalari zaman damgasiyla kaydeder ve yazdirir
import { createServer } from "node:http";
import { readFile } from "node:fs/promises";

const KATALOG = new Map([["978-0262033848", { ad: "Algoritmalara Giris", adet: 2 }]]);
const t0 = () => process.hrtime.bigint();
const ms = (a, b) => Number(b - a) / 1e6;                    // nanosaniye farki -> milisaniye

let baglantiNo = 0;

const sunucu = createServer();

sunucu.on("connection", (soket) => {                        // 1. TCP baglantisi kabul edildi
  soket.kabulAni = t0();
  soket.no = ++baglantiNo;
  soket.istekSayisi = 0;
});

sunucu.on("request", (istek, yanit) => {
  const soket = istek.socket;
  soket.istekSayisi++;
  const asamalar = [["baglanti kabulu", soket.kabulAni]];
  const kaydet = (ad) => asamalar.push([ad, t0()]);

  kaydet("baslik satirlari cozuldu");                       // 2. istek olayi = basliklar hazir

  const parcalar = [];
  istek.on("data", (parca) => parcalar.push(parca));
  istek.on("end", async () => {
    kaydet("govde okundu");                                 // 3. govdenin son baytini aldik

    const yol = new URL(istek.url, "http://yerel").pathname;
    const isleyici = yol === "/odunc" && istek.method === "POST" ? "oduncVer"
      : yol === "/kitaplar" ? "katalogListele" : null;
    kaydet(`yonlendirme -> ${isleyici ?? "yok"}`);           // 4. yol bir isleyiciye eslendi

    let durum = 404, govde = { hata: "yol_yok" };
    if (isleyici === "oduncVer") {
      const istem = JSON.parse(Buffer.concat(parcalar).toString() || "{}");
      const kitap = KATALOG.get(istem.isbn);
      await readFile(new URL(import.meta.url));              // is: gercek bir girdi-cikti
      durum = kitap && kitap.adet > 0 ? 201 : 409;
      govde = durum === 201 ? { isbn: istem.isbn, uye: istem.uye, kalan: --kitap.adet }
        : { hata: "raf_bos" };
    } else if (isleyici === "katalogListele") {
      durum = 200;
      govde = { kitaplar: [...KATALOG].map(([isbn, k]) => ({ isbn, ...k })) };
    }
    kaydet("isleyici bitti");                                // 5. yanit govdesi hazir

    const metin = JSON.stringify(govde);
    yanit.writeHead(durum, { "content-type": "application/json; charset=utf-8",
      "content-length": Buffer.byteLength(metin) });
    kaydet("baslik yazildi");                                // 6. durum satiri artik kesin

    yanit.end(metin);
    yanit.on("finish", () => {                               // 7. son bayt cekirdege verildi
      kaydet("yanit tamamlandi");
      const baslangic = asamalar[0][1];
      console.log(`\n#${soket.no}.${soket.istekSayisi}  ${istek.method} ${istek.url} -> ${durum}`);
      for (const [ad, an] of asamalar) {
        console.log(`  ${String(ms(baslangic, an).toFixed(3)).padStart(9)} ms  ${ad}`);
      }
    });
  });
});

sunucu.listen(8432, "127.0.0.1", () => console.log("yasam dongusu 127.0.0.1:8432"));

Ölçüm üç istek yapar: aynı bağlantı üzerinde bir ödünç verme ve bir katalog listeleme, sonra ayrı bir bağlantıda bilinmeyen bir yol. --next seçeneği aynı curl çağrısı içinde ikinci bir istek tanımlar ve bağlantı yeniden kullanılır.

#!/usr/bin/env bash
# yasam-dongusu.mjs'yi baslatir; tek baglantida iki istek ve ayri baglantida bir istek yapar.
node yasam-dongusu.mjs & sunucu=$!
sleep 0.6

echo "### tek baglanti uzerinde iki istek (curl baglantiyi yeniden kullanir)"
curl -sS -o /dev/null \
  -X POST -H 'content-type: application/json' \
  -d '{"isbn":"978-0262033848","uye":"U-4711"}' http://127.0.0.1:8432/odunc \
  --next -sS -o /dev/null http://127.0.0.1:8432/kitaplar
sleep 0.3

echo "### ayri baglantida bilinmeyen yol"
curl -sS -o /dev/null http://127.0.0.1:8432/raflar
sleep 0.3

kill "$sunucu"
yasam dongusu 127.0.0.1:8432
### tek baglanti uzerinde iki istek (curl baglantiyi yeniden kullanir)

#1.1  POST /odunc -> 201
      0.000 ms  baglanti kabulu
      1.378 ms  baslik satirlari cozuldu
      2.192 ms  govde okundu
      2.240 ms  yonlendirme -> oduncVer
      3.534 ms  isleyici bitti
      5.434 ms  baslik yazildi
      6.882 ms  yanit tamamlandi

#1.2  GET /kitaplar -> 200
      0.000 ms  baglanti kabulu
      7.580 ms  baslik satirlari cozuldu
      7.702 ms  govde okundu
      7.728 ms  yonlendirme -> katalogListele
      7.779 ms  isleyici bitti
      7.823 ms  baslik yazildi
      8.088 ms  yanit tamamlandi
### ayri baglantida bilinmeyen yol

#2.1  GET /raflar -> 404
      0.000 ms  baglanti kabulu
      0.180 ms  baslik satirlari cozuldu
      0.243 ms  govde okundu
      0.278 ms  yonlendirme -> yok
      0.280 ms  isleyici bitti
      0.334 ms  baslik yazildi
      0.541 ms  yanit tamamlandi

Süreler her çalıştırmada değişir; ilk isteğin ölçümleri modül yüklemesinin ve ilk dosya okumasının ısınma maliyetini de taşır. Değişmeyen, aşamaların sırası ve aralarındaki büyüklük ilişkisidir.

Üçüncü ölçüm bir taban çizgisi verir: eşleşmeyen bir yol için toplam yarım milisaniyedir ve bu sürenin tamamı protokol işidir — bağlantı kabulü, ayrıştırma, yanıt yazımı. Uygulamanın kendi işi sıfırdır. İlk ölçümde isleyici bitti ile yonlendirme arasındaki 1,3 milisaniye ise gerçek iştir: dosya okuması ve iş kuralı. Bir isteğin yavaş olduğu söylendiğinde sorulacak ilk soru, sürenin bu iki bölümden hangisinde geçtiğidir.

Bağlantı ile İstek Ayrı Şeylerdir

İkinci ölçümün ilk satırı 0.000 ms baglanti kabulu der, ama ikinci satırı 7.580 ms gösterir. Arada yeni bir bağlantı kabulü yoktur; ikinci istek, birinci isteğin bittiği bağlantıdan gelmiştir. #1.1 ve #1.2 etiketleri bunu doğrudan söyler: aynı bağlantı, ikinci istek. Üçüncü istek #2.1 etiketiyle yeni bir bağlantı numarası almıştır.

Bu ayrım pratik sonuçlar taşır. Bağlantı başına yapılan iş bir kez yapılır: TCP el sıkışması, güvenli bağlantı kurulumu ve buna bağlı gecikme, bağlantı açık kaldığı sürece sonraki isteklere yayılır. Buna karşılık bağlantıya bağlanan durum tehlikelidir: bir soket üzerinde birden çok üyenin isteği geçebilir, çünkü aradaki vekil bağlantıyı yeniden kullanabilir. Kimlik bilgisi bağlantıya değil, isteğe aittir.

Ayrım kaynak yönetimini de belirler. Açık bağlantı sayısı, işlenen istek sayısından bağımsızdır; hiç istek göndermeyen bir istemci de bir bağlantı tutar. Sunucunun bağlantı sayısına ve boşta bekleme süresine ayrı sınır koymasının nedeni budur.

Gövde Bir Değer Değil, Bir Akıştır

Kodda gövde data olaylarıyla parça parça toplanır ve end olayıyla tamamlanır. Bu, ölçüm çıktısında baslik satirlari cozuldu ile govde okundu arasındaki aralık olarak görünür. Küçük bir JSON gövdesinde bu aralık bir milisaniyenin altındadır; büyük bir yüklemede saniyeler sürebilir.

Buradan iki karar çıkar. Birincisi, yönlendirme gövdeden önce yapılabilir. Bu örnekte yönlendirme kararı gövde okunduktan sonra kaydedilmiştir, çünkü kod okunaklı olsun diye işleyici mantığı tek yerde toplanmıştır. Yöntem ve yol ikinci aşamada zaten bilindiği için yönlendirme oraya alınabilir; alındığında eşleşmeyen bir istek gövdesi hiç okunmadan reddedilir.

İkincisi, gövde sınırlanmalıdır. Sunucu gövdeyi belleğe topladığı sürece, gönderilen bayt sayısı sunucunun bellek tüketimini belirler. Sınır konmazsa tek bir istemci süreci belleksiz bırakabilir. Sınırın nasıl konduğu ve aşıldığında hangi durum kodunun döndüğü, bu kursun Dosya Yükleme ve Depolama dersinde ele alınır.

Kararın Geri Alınamadığı An

Altıncı aşama bir eşiktir. Durum satırı ve başlıklar sokete yazıldıktan sonra istemci onları almıştır; geri alınamaz. Bunu doğrulamak için yanıt yazımına başladıktan sonra başlık eklemeyi ve durum kodunu değiştirmeyi deneyen bir sunucu yeterlidir.

// baslik-sonrasi.mjs — durum satiri yazildiktan sonra kararin geri alinamadigini gosterir
import { createServer } from "node:http";

createServer((istek, yanit) => {
  yanit.sendDate = false;
  yanit.writeHead(201, { "content-type": "application/json; charset=utf-8" });
  yanit.write('{"durum":"odunc verildi"');            // govde akmaya basladi

  for (const [ad, deneme] of [
    ["setHeader", () => yanit.setHeader("x-uyari", "gec kalindi")],
    ["writeHead", () => yanit.writeHead(500)],
  ]) {
    try {
      deneme();
      console.log(`${ad}: gecti`);
    } catch (hata) {
      console.log(`${ad}: ${hata.code}`);
    }
  }

  yanit.end(',"not":"durum kodu 201 olarak kaldi"}');
}).listen(8433, "127.0.0.1", () => console.log("baslik sonrasi 127.0.0.1:8433"));
#!/usr/bin/env bash
# baslik-sonrasi.mjs'yi baslatir; yanitin basliklarini ve govdesini alir, sunucu kutugunu gosterir.
node baslik-sonrasi.mjs & sunucu=$!
sleep 0.6
curl -sS -D - http://127.0.0.1:8433/odunc
echo
kill "$sunucu"
baslik sonrasi 127.0.0.1:8433
setHeader: ERR_HTTP_HEADERS_SENT
writeHead: ERR_HTTP_HEADERS_SENT
HTTP/1.1 201 Created
content-type: application/json; charset=utf-8
Connection: keep-alive
Keep-Alive: timeout=5
Transfer-Encoding: chunked

{"durum":"odunc verildi","not":"durum kodu 201 olarak kaldi"}

Sunucunun kütük satırları ile curl çıktısı aynı uçbirime yazıldığı için ilk iki satır sunucudan, kalanı istemciden gelir.

İki deneme de aynı hata koduyla reddedilmiştir ve yanıt 201 olarak kalmıştır. Çıktıda ayrıca content-length yerine Transfer-Encoding: chunked görünür: gövde parça parça yazıldığı için sunucu uzunluğu önceden bildirememiştir.

Sonucun kural hâline gelmiş biçimi şudur: yanıtla ilgili bütün kararlar ilk baytı yazmadan önce alınmış olmalıdır. İşleyicinin ortasında yazmaya başlayıp sonra bir hatayla karşılaşan kod, o hatayı istemciye bildiremez — yarım bir gövde ve yanlış bir durum kodu kalır. Yanıt gövdesini önce bellekte üretip sonra tek seferde yazmanın, ya da akışlı gönderimde hata olasılığını akış başlamadan tüketmenin nedeni budur.

Özet

  • Bir isteğin yolu yedi aşamadan geçer: bağlantı kabulü, başlıkların çözülmesi, gövdenin okunması, yönlendirme, işleyicinin çalışması, başlığın yazılması ve yanıtın tamamlanması.
  • Ölçümde eşleşmeyen bir yol yarım milisaniyede tamamlanmıştır; bu süre protokol işidir. Uygulamanın kendi işi, yönlendirme ile işleyicinin bitişi arasındaki aralıkta görünür.
  • Bağlantı ile istek ayrı yaşam sürelerine sahiptir: aynı bağlantı üzerindeki ikinci istek yeni bir kabul aşaması içermez, bu yüzden kimlik ve durum bağlantıya değil isteğe bağlanır.
  • Gövde parça parça gelir; yönlendirme gövdeden önce yapılabilir ve gövdenin boyutu sınırlanmadığında sunucunun bellek tüketimini istemci belirler.
  • Başlıklar yazıldıktan sonra başlık eklemek ve durum kodunu değiştirmek ERR_HTTP_HEADERS_SENT ile reddedilir; yanıtla ilgili kararlar ilk bayttan önce alınır.

Sonraki Adım

Ölçülen aşamaların hepsi tek bir işleyicinin içindeydi ve her aşama koda elle yazılmıştı. Gerçek bir uygulamada aynı işler — kimliğin çözülmesi, gövdenin ayrıştırılması, isteğin kütüğe yazılması, hatanın yakalanması — her işleyicide tekrarlanır. Bu tekrarları tek bir yerde toplayan yapı, isteği sırayla geçiren bir zincirdir. Sonraki ders o zinciri kurar: halkaların sırası değiştiğinde sonucun nasıl değiştiğini, kimlik denetimiyle günlüklemenin hangi sırayla dizilmesi gerektiğini ve hata yakalayıcının zincirin neresinde durduğunu ölçerek gösterir.

İ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