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:
- 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.
- 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.
- Gövdenin okunması. Gövde parça parça gelir; son parça geldiğinde istek tamdır.
- Yönlendirme. Yöntem ve yol bir işleyiciye eşlenir. Eşleşme yoksa isteğin karşılığı
404olur. - İşleyicinin çalışması. İş kuralı uygulanır, veri okunur veya yazılır, yanıt gövdesi üretilir.
- Yanıt başlığının yazılması. Durum kodu ve başlıklar sokete yazılır.
- 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_SENTile 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.