İçeriğe geç
academia.sh

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.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat