Ders 11 / 18
Ön Yüz İçin Backend
İstemciye özel toplama katmanının gerçekleştirimi: tek genel uçtan beslenen iki istemcinin taşıdığı fazla veri ile yaptığı istek sayısının ölçülmesi, ayrı toplama katmanlarının bu iki sayıya etkisi ve ödediği yinelenen mantık satırı ile sessiz ayrışma riski.
İçindekiler
Önceki ders kenar sorumluluklarını tek bir yerde topladı: doğrulama, sınırlama ve yönlendirme artık her servisin içinde değil, önündeki tek katmanda duruyor. O katmanın sessiz kabulü şudur: sınırın arkasındaki servisler ne döndürüyorsa istemciye o gider. Oysa aynı uca bağlanan istemciler aynı yanıtı istemiyor. Kütüphane ödünç sisteminin web listesi bir üyenin bütün ödünç kayıtlarını kitap adı, yazar ve raf koduyla birlikte gösteriyor; mobil özet ekranı yalnız iki sayı ve bir başlık gösteriyor. İkisi de aynı uçtan besleniyor.
Bu dersin işi, o durumu ölçmek ve ön yüz için backend (backend for frontend) yerleşimiyle karşılaştırmaktır. Kalıbın tanımı Trafik Katmanı kursunda kuruldu ve orada trafik tarafından ölçüldü; burada ölçülen şey uygulama tarafıdır: istemcinin taşıdığı bayt, yaptığı istek ve toplama katmanının kod olarak ödettiği tekrar.
Düzeneğin varsayımları: SS10 iki istemci vardır (web listesi, mobil özet) ve üçüncü bir istemci yoktur. SS11 katalog, üyelik ve ödünç üç ayrı süreçtir; katalog kaydı on alan taşır. SS12 ölçüm süre ölçmez; istemci ağındaki istek sayısını, taşınan baytı ve alan sayısını sayar.
Üç Aşağı Servis ve Tek Genel Uç
Üç servis tek dosyadan, üç ayrı süreç olarak koşuyor. Her biri kendi istek ve bayt sayacını tutuyor; bu sayaçlar aşağı tarafın gördüğü yükü ölçmeye yarayacak.
// bff/servis.mjs — uc asagi servis, tek dosya: node bff/servis.mjs katalog 8741 import { createServer } from "node:http"; const [ad, port] = [process.argv[2], Number(process.argv[3])]; const KITAP = { "978-0": { isbn: "978-0", ad: "Kayip Zamanin Izinde", yazar: "M. Proust", cevirmen: "R. Hakmen", yayinevi: "Yapi Kredi", basimYili: 1913, sayfa: 542, tur: "roman", dil: "tr", raf: "PQ-2631" }, "978-1": { isbn: "978-1", ad: "Sessiz Ev", yazar: "O. Pamuk", cevirmen: null, yayinevi: "Iletisim", basimYili: 1983, sayfa: 320, tur: "roman", dil: "tr", raf: "PL-248" }, "978-2": { isbn: "978-2", ad: "Algoritmalara Giris", yazar: "T. Cormen", cevirmen: "A. Yilmaz", yayinevi: "MIT", basimYili: 2009, sayfa: 1312, tur: "ders", dil: "tr", raf: "QA-76" }, "978-3": { isbn: "978-3", ad: "Bozkirkurdu", yazar: "H. Hesse", cevirmen: "K. Sipal", yayinevi: "Can", basimYili: 1927, sayfa: 224, tur: "roman", dil: "tr", raf: "PT-2617" }, }; const UYE = { uyeNo: "U-41", ad: "Zeynep", soyad: "Aydin", eposta: "[email protected]", telefon: "+90-555-0000", adres: "Kordon Cad. 12, Izmir", uyelikTarihi: "2019-04-02", uyelikTuru: "arastirmaci", cezaBakiyesi: 0, kartDurumu: "etkin" }; const ODUNC = [ { isbn: "978-0", alinma: "2026-02-10", sonTeslim: "2026-03-10", uzatma: 1 }, { isbn: "978-1", alinma: "2026-02-24", sonTeslim: "2026-03-24", uzatma: 0 }, { isbn: "978-2", alinma: "2026-02-14", sonTeslim: "2026-03-14", uzatma: 2 }, { isbn: "978-3", alinma: "2026-03-02", sonTeslim: "2026-04-01", uzatma: 0 }, ]; let istek = 0, bayt = 0; // servis basina istek ve gonderilen govde bayti const VER = { katalog: (u) => KITAP[u.split("/")[2]], uyelik: () => UYE, odunc: () => ODUNC }; createServer((q, y) => { if (q.url === "/sayac") { y.writeHead(200, { "Content-Type": "application/json" }); return y.end(JSON.stringify({ ad, istek, bayt })); } const govde = JSON.stringify(VER[ad](q.url)); istek += 1; bayt += Buffer.byteLength(govde); y.writeHead(200, { "Content-Type": "application/json" }); y.end(govde); }).listen(port, "127.0.0.1", () => console.log(`${ad} 127.0.0.1:${port}`));
Genel uç, üyelik ile ödünç kayıtlarını birleştirip olduğu gibi döndürüyor. Katalog verisini eklemiyor, çünkü katalog başka bir sınırın arkasında ve bu uç ondan önce yazıldı. Sonuç şu: kitap adını göstermek isteyen her istemci, listeyi aldıktan sonra katalog servisine kendisi gitmek zorunda.
// bff/genel-uc.mjs — tek genel uc 8744: iki istemci de burayi cagirir, ayni govdeyi alir import { createServer } from "node:http"; const AL = async (u) => (await fetch(u)).json(); createServer(async (q, y) => { const uyeNo = q.url.split("/")[2]; const uye = await AL("http://127.0.0.1:8742/uye/" + uyeNo); const odunc = await AL(`http://127.0.0.1:8743/uye/${uyeNo}/odunc`); const govde = JSON.stringify({ uye, odunc }); y.writeHead(200, { "Content-Type": "application/json" }); y.end(govde); }).listen(8744, "127.0.0.1", () => console.log("genel-uc 127.0.0.1:8744"));
İki İstemci, İki Toplama Katmanı
İkinci gerçekleştirimde her istemcinin kendi toplama katmanı var. Web katmanı listeyi kurup gecikme gününü hesaplıyor; mobil katmanı aynı veriden iki sayı ve bir başlık çıkarıyor. İkisi de ayrı bir dağıtım birimidir ve birbirinin kodunu kullanmıyor.
// bff/bff-web.mjs — web listesi icin toplama katmani 8745 import { createServer } from "node:http"; const BUGUN = "2026-03-16"; const KATALOG = "http://127.0.0.1:8741/kitap/"; const UYELIK = "http://127.0.0.1:8742/uye/"; const ODUNC = "http://127.0.0.1:8743/uye/"; const AL = async (u) => (await fetch(u)).json(); const gunFarki = (a, b) => Math.round((Date.parse(a) - Date.parse(b)) / 86400000); createServer(async (q, y) => { const uyeNo = q.url.split("/")[2]; const uye = await AL(UYELIK + uyeNo); const odunc = await AL(`${ODUNC}${uyeNo}/odunc`); const kitap = await Promise.all(odunc.map((o) => AL(KATALOG + o.isbn))); const satir = odunc.map((o, i) => ({ baslik: kitap[i].ad, yazar: kitap[i].yazar, raf: kitap[i].raf, teslim: o.sonTeslim, gecikmeGunu: Math.max(0, gunFarki(BUGUN, o.sonTeslim)), })); const govde = JSON.stringify({ okuyucu: `${uye.ad} ${uye.soyad}`, satir }); y.writeHead(200, { "Content-Type": "application/json" }); y.end(govde); }).listen(8745, "127.0.0.1", () => console.log("bff-web 127.0.0.1:8745"));
// bff/bff-mobil.mjs — mobil ozet icin toplama katmani 8746 import { createServer } from "node:http"; const BUGUN = "2026-03-16"; const KATALOG = "http://127.0.0.1:8741/kitap/"; const UYELIK = "http://127.0.0.1:8742/uye/"; const ODUNC = "http://127.0.0.1:8743/uye/"; const AL = async (u) => (await fetch(u)).json(); const gunFarki = (a, b) => Math.round((Date.parse(a) - Date.parse(b)) / 86400000); createServer(async (q, y) => { const uyeNo = q.url.split("/")[2]; const uye = await AL(UYELIK + uyeNo); const odunc = await AL(`${ODUNC}${uyeNo}/odunc`); const gecikmis = odunc.filter((o) => gunFarki(BUGUN, o.sonTeslim) > 0).length; const yakin = odunc.slice().sort((a, b) => Date.parse(a.sonTeslim) - Date.parse(b.sonTeslim))[0]; const kitap = await AL(KATALOG + yakin.isbn); const govde = JSON.stringify({ okuyucu: uye.ad, gecikmis, enYakin: { baslik: kitap.ad, kalanGun: gunFarki(yakin.sonTeslim, BUGUN) } }); y.writeHead(200, { "Content-Type": "application/json" }); y.end(govde); }).listen(8746, "127.0.0.1", () => console.log("bff-mobil 127.0.0.1:8746"));
Ölçüm
Ölçüm betiği dört durumu koşuyor: iki istemci, iki gerçekleştirim. Her durumda istemcinin yaptığı istek sayısını, aldığı baytı ve aldığı gövdedeki yaprak alan sayısını topluyor; istemcinin gerçekten okuduğu adlara göre gövdeyi budayıp kullanılmayan baytı çıkarıyor. Son sütun sızıntı ölçüsüdür: istemcinin okuduğu adlardan kaçı aşağı servislerin kendi alan adıdır.
// bff/olc.mjs — iki istemci x iki gerceklestirme: istek, bayt, fazla veri, yinelenen satir import { readFileSync } from "node:fs"; const al = async (u) => { const m = await (await fetch(u)).text(); return { bayt: Buffer.byteLength(m), veri: JSON.parse(m) }; }; const SAYAC = [8741, 8742, 8743].map((p) => `http://127.0.0.1:${p}/sayac`); const toplam = async () => (await Promise.all(SAYAC.map(al))) .reduce((s, x) => s + x.veri.istek, 0); // Istemcilerin ekrana koymak icin gercekten okudugu alan adlari const ALAN = { "web/genel uc": ["ad", "soyad", "isbn", "sonTeslim", "yazar", "raf"], "mobil/genel uc": ["ad", "isbn", "sonTeslim"], "web/bff": ["okuyucu", "satir"], "mobil/bff": ["okuyucu", "gecikmis", "enYakin"], }; const SENARYO = { "web/genel uc": async () => { // genel uc + her kitap icin katalog cagrisi const g = await al("http://127.0.0.1:8744/uye/U-41/ozet"); const k = await Promise.all(g.veri.odunc.map((o) => al(`http://127.0.0.1:8741/kitap/${o.isbn}`))); return { istek: 1 + k.length, bayt: g.bayt + k.reduce((s, x) => s + x.bayt, 0), govde: { ...g.veri, kitap: k.map((x) => x.veri) } }; }, "mobil/genel uc": async () => { // ayni genel uc + yalniz en yakin kitap const g = await al("http://127.0.0.1:8744/uye/U-41/ozet"); const y = g.veri.odunc.slice().sort((a, b) => Date.parse(a.sonTeslim) - Date.parse(b.sonTeslim))[0]; const k = await al(`http://127.0.0.1:8741/kitap/${y.isbn}`); return { istek: 2, bayt: g.bayt + k.bayt, govde: { ...g.veri, kitap: [k.veri] } }; }, "web/bff": async () => { const g = await al("http://127.0.0.1:8745/uye/U-41/liste"); return { istek: 1, bayt: g.bayt, govde: g.veri }; }, "mobil/bff": async () => { const g = await al("http://127.0.0.1:8746/uye/U-41/ozet"); return { istek: 1, bayt: g.bayt, govde: g.veri }; }, }; const yaprak = (v) => Array.isArray(v) ? v.reduce((s, e) => s + yaprak(e), 0) : v && typeof v === "object" ? Object.values(v).reduce((s, e) => s + yaprak(e), 0) : 1; const buda = (v, k) => { // yalniz istemcinin okudugu adlari birak if (Array.isArray(v)) return v.map((e) => buda(e, k)); if (v && typeof v === "object") { const o = {}; for (const [a, d] of Object.entries(v)) { if (k.has(a)) o[a] = d; else { const alt = buda(d, k); if (alt && Object.keys(alt).length) o[a] = alt; } } return o; } return null; }; const anlamli = (dosya) => readFileSync(dosya, "utf8").split("\n") .map((s) => s.trim()).filter((s) => s && !s.startsWith("//") && /[A-Za-z]/.test(s)); // Asagi servislerin urettigi alan adlari: sizinti olcusunun tabani const ham = await Promise.all(["8742/uye/U-41", "8743/uye/U-41/odunc", "8741/kitap/978-0"] .map((y) => al(`http://127.0.0.1:${y}`))); const ASAGI = new Set(ham.flatMap((h) => Object.keys(Array.isArray(h.veri) ? h.veri[0] : h.veri))); const satir = []; for (const [ad, calistir] of Object.entries(SENARYO)) { const once = await toplam(); const s = await calistir(); const sonra = await toplam(); const k = new Set(ALAN[ad]); satir.push({ ad, govde: s.govde, istek: s.istek, ic: sonra - once, bayt: s.bayt, gelen: yaprak(s.govde), kullanilan: yaprak(buda(s.govde, k)), fazla: s.bayt - Buffer.byteLength(JSON.stringify(buda(s.govde, k))), sizan: ALAN[ad].filter((a) => ASAGI.has(a)).length }); } const B = ["istemci istegi", "asagi istek", "alinan B", "gelen yaprak", "kullanilan", "fazla B", "sizan ad"]; console.log(`${"istemci/gerceklestirme".padEnd(17)}${B.map((b) => b.padStart(15)).join("")}`); for (const s of satir) { console.log(`${s.ad.padEnd(17)}${[s.istek, s.ic, s.bayt, s.gelen, s.kullanilan, s.fazla, s.sizan] .map((v) => String(v).padStart(15)).join("")}`); } console.log(`\nmobil/bff govdesi: ${JSON.stringify(satir.at(-1).govde)}`); const w = anlamli("bff/bff-web.mjs"), m = anlamli("bff/bff-mobil.mjs"); const ortak = w.filter((s) => m.includes(s)); console.log(`\nbff-web anlamli satir = ${w.length}, bff-mobil = ${m.length}, ` + `ikisinde birebir ayni = ${ortak.length}`); console.log(`genel uc dosyasi = ${anlamli("bff/genel-uc.mjs").length} satir, ` + `toplama katmani dosyasi = ${w.length + m.length} satir`); console.log(`asagi servis alan adi = ${ASAGI.size}; toplama katmani = 2 dagitim birimi`);
for s in "servis.mjs katalog 8741" "servis.mjs uyelik 8742" "servis.mjs odunc 8743" \ "genel-uc.mjs" "bff-web.mjs" "bff-mobil.mjs"; do node bff/$s > /dev/null & done for p in 8741 8742 8743 8744 8745 8746; do curl -s --retry 30 --retry-connrefused --retry-delay 0 -o /dev/null http://127.0.0.1:$p/sayac done node bff/olc.mjs kill $(jobs -p)
istemci/gerceklestirme istemci istegi asagi istek alinan B gelen yaprak kullanilan fazla B sizan ad
web/genel uc 5 6 1216 66 26 691 6
mobil/genel uc 2 3 723 36 11 467 3
web/bff 1 6 442 21 21 0 0
mobil/bff 1 3 91 4 4 0 0
mobil/bff govdesi: {"okuyucu":"Zeynep","gecikmis":2,"enYakin":{"baslik":"Kayip Zamanin Izinde","kalanGun":-6}}
bff-web anlamli satir = 19, bff-mobil = 19, ikisinde birebir ayni = 13
genel uc dosyasi = 10 satir, toplama katmani dosyasi = 38 satir
asagi servis alan adi = 22; toplama katmani = 2 dagitim birimi
Sayıların Okunması
İlk iki satır tek genel ucun bedelini veriyor. Web listesi ekranı kurmak için beş istek yapıyor ve 1216 bayt alıyor; bu baytın 691’i, yani yüzde 57’si hiçbir zaman ekrana çıkmıyor. Mobil özet ekranı iki sayı ve bir başlık göstermek için iki istek yapıyor, 723 bayt alıyor ve bunun 467’si kullanılmıyor. Alan tarafında oran daha keskin: mobil istemci otuz altı yaprak değer alıyor ve on birini okuyor.
Alt iki satır toplama katmanının etkisini gösteriyor. İstemci isteği 5’ten ve 2’den 1’e iniyor. Alınan bayt web tarafında 1216’dan 442’ye, mobil tarafında 723’ten 91’e düşüyor — mobil ekran için taşınan veri sekizde bire iniyor. Fazla bayt iki durumda da 0’dır, çünkü katman gövdeyi ekranın istediği biçimde kuruyor.
Aşağı istek sütunu ise değişmiyor: web tarafında iki gerçekleştirimde de 6, mobil tarafında 3. Bu, kalıbın en çok yanlış anlaşılan yeridir. Toplama katmanı aşağı servislerin yükünü azaltmıyor; aynı işi yapıyor, yalnız nerede yaptığını değiştiriyor. Beş gidiş-dönüş istemcinin ağından iç ağa taşınıyor. Kazanç istemcinin ağındadır ve orada gidiş-dönüş bir sunucu içi çağrıdan çok daha pahalıdır.
Sızan ad sütunu bağlaşımı ölçüyor. Genel uç yerleşiminde web istemcisinin okuduğu altı adın
altısı da aşağı servislerin kendi alan adıdır; sonTeslim adı ödünç servisinin sözlüğünden
gelir ve istemci koduna kadar iner. Toplama katmanı yerleşiminde bu sayı 0’dır: istemci
teslim, gecikmeGunu, enYakin gibi ekranın sözlüğünü görür. Ödünç servisi alanını yeniden
adlandırdığında ilk yerleşimde istemci kodu düzeltilip yeniden yayınlanmak zorundadır; ikinci
yerleşimde düzeltme katmanda kalır.
Yinelenen Mantık ve Sessiz Ayrışma
Bedel son üç satırdadır. Genel uç dosyası on anlamlı satır; iki toplama katmanı toplam otuz
sekiz satır. Bunların on üçü iki dosyada birebir aynıdır: üç servis adresi, AL yardımcısı,
gunFarki, BUGUN sabiti, üye ile ödünç çağrıları. Yani her istemci için bir katman kurmak,
aynı mantığı istemci sayısınca kopyalamak demektir. Üçüncü bir istemci geldiğinde on üç satır
üçüncü kez yazılacaktır.
Bu tekrarın doğurduğu arıza kipi, kopyanın kendisi değil, kopyalardan yalnız birinin
güncellenmesidir. Ödünç servisi sonTeslim alanını teslimTarihi diye yeniden adlandırıyor ve
yalnız web katmanı güncelleniyor.
sed 's/sonTeslim/teslimTarihi/g' bff/servis.mjs > bff/servis-v2.mjs # odunc alani yeniden adlandirildi sed 's/sonTeslim/teslimTarihi/g' bff/bff-web.mjs > bff/bff-web-v2.mjs # yalniz web katmani guncellendi node bff/servis.mjs katalog 8741 > /dev/null & node bff/servis.mjs uyelik 8742 > /dev/null & node bff/servis-v2.mjs odunc 8743 > /dev/null & node bff/bff-web-v2.mjs > /dev/null & node bff/bff-mobil.mjs > /dev/null & for p in 8741 8742 8743 8745 8746; do curl -s --retry 30 --retry-connrefused --retry-delay 0 -o /dev/null http://127.0.0.1:$p/sayac done echo "web $(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8745/uye/U-41/liste) $(curl -s http://127.0.0.1:8745/uye/U-41/liste | head -c 96)" echo "mobil $(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8746/uye/U-41/ozet) $(curl -s http://127.0.0.1:8746/uye/U-41/ozet)" kill $(jobs -p)
web 200 {"okuyucu":"Zeynep Aydin","satir":[{"baslik":"Kayip Zamanin Izinde","yazar":"M. Proust","raf":"P
mobil 200 {"okuyucu":"Zeynep","gecikmis":0,"enYakin":{"baslik":"Kayip Zamanin Izinde","kalanGun":null}}
İki katman da 200 döndürüyor. Web listesi doğru; mobil özet ise gecikmis alanını 2 yerine 0,
kalanGun alanını -6 yerine null veriyor. Okunmayan alan undefined olduğu için tarih
karşılaştırması sessizce yanlış sonuç üretti ve hiçbir yerde hata kaydı oluşmadı. Tek genel uç
yerleşiminde aynı değişiklik iki istemciyi birden bozardı ve bu, fark edilmesi daha kolay bir
arızadır. Toplama katmanı yerleşiminde arıza istemciler arasında ayrışma biçiminde
görünür: bir ekran doğru, öteki sessizce yanlıştır.
Buradan çıkan kural, kalıbın sahipliğiyle ilgilidir. Toplama katmanı aşağı servisin değil, ekranı yazan ekibin dağıtım birimidir; aşağı servisin sözleşmesi değiştiğinde katman sayısınca düzeltme gerekir ve o düzeltmelerin hepsinin yapıldığını gösteren tek şey, katmanların sözleşmeye karşı sınanmasıdır.
Özet
- Tek genel uç yerleşiminde web listesi 5 istek ve 1216 bayt, mobil özet 2 istek ve 723 bayt taşıyor; bu baytların 691 ve 467’si ekrana çıkmıyor.
- İstemciye özel toplama katmanı istek sayısını 1’e, alınan baytı 442 ve 91’e indiriyor; fazla bayt iki istemcide de 0 oluyor.
- Aşağı servislere ulaşan istek değişmiyor (6 ve 3): katman yükü azaltmıyor, gidiş-dönüşü istemcinin ağından iç ağa taşıyor.
- Bağlaşım ölçüsü: genel uçta istemcinin okuduğu 6 addan 6’sı aşağı servisin alan adı; toplama katmanında 0.
- Ödenen kod: iki katman 38 anlamlı satır, bunların 13’ü birebir aynı; ayrıca 2 yeni dağıtım birimi.
- Doğan arıza kipi: aşağı alan adı değişip yalnız bir katman güncellendiğinde iki istemci
sessizce ayrışıyor — web 200 ve doğru, mobil 200 ve
gecikmis: 0.
Sonraki Adım
Toplama katmanı da, önündeki ağ geçidi de aynı türden bir çözümdür: bir sorumluluğu servis
kodundan alıp ayrı bir yere koymak. İkisinin de aldığı sorumluluk biçim ile ilgiliydi —
hangi alan kime gider, hangi gövde nasıl kurulur. Oysa servis kodunun içinde biçimle ilgisi
olmayan başka bir yığın da duruyor: yeniden deneme döngüsü, zaman aşımı süresi, taşımanın
güvenliği ve her çağrının ölçütünü toplayan sayaçlar. Bu ders boyunca yazılan üç katmanın her
birinde fetch çağrısı çıplaktır; ne zaman aşımı vardır, ne yeniden deneme, ne de bir sayaç.
Sonraki ders bu işleri servis kodundan çıkarıp ayrı bir sürece taşıyan yerleşimi ele alır ve
ölçüyü kod tarafında kurar: uygulama kodundan kaç satır silindi, geriye hangi sorumluluk kaldı
ve ayrı süreçte koşmanın bedeli ne oldu.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.