İçeriğe geç
academia.sh

Ders 16 / 18

Olay Güdümlü Mimari

Çağrı yönünün tersine çevrilmesi: yayıncının bildiği ve adıyla çağırdığı tüketici sayısının sayılması, yeni bir tüketici eklendiğinde yayıncı dosyasında düzenlenen satırın ölçülmesi, sınırdan geçen bayt ile dönüştürme noktasının tüketici sayısıyla nasıl büyüdüğünün hesaplanması, bir tüketici düştüğünde yanıtlanan istek sayısının karşılaştırılması ve aynı olay ikinci kez ulaştığında iki tüketici tasarımının sapmasının ölçülmesi.

İçindekiler

Buraya kadarki üç biçemde tek bir şey hiç değişmedi: çağrı yönü. Tek parçada birleştirme bir fonksiyon çağrısıydı, servis yönelimli düzende paylaşılan bir yüzeyden geçen bir çağrıydı, mikroservis düzeninde iki ağ çağrısının birleşimiydi. Üçünde de çağıran taraf karşısındaki birimi adıyla biliyor, çağırıyor ve yanıtını bekliyordu. Ölçülen bütün maliyetler bu iki yükümlülükten çıktı.

Olay güdümlü mimari (event-driven architecture) çağrı yönünü tersine çevirir: birim, olmuş bir olguyu ilan eder; kimin dinlediğini bilmez, dönüş değeri almaz. Ücretlendirme bağlamı artık operasyonu çağırmaz, gonderi-fiyatlandi olayını yayınlar. Alan olayı kavramı Alan Odaklı Tasarım kursunda kuruldu; o kursun Olay Kaynaklı Kalıcılık dersi olay dizisini bir kalıcılık biçimi olarak ölçtü. Buradaki soru başkadır: olay bir sistem düzeninin taşıyıcısı olduğunda, yani uygulama içinden çıkıp sistem sınırını geçtiğinde ne değişir.

Sınırı Geçen Olayın Üç Farkı

Uygulama içinde alan olayı bellekteki bir nesnedir. Yayıncı ile ele alan aynı çağrı yığıtında durur; ele alan bir hata attığında yayıncı bunu görür, hepsi bittiğinde yayıncı işin tamamlandığını bilir. Olay sistem sınırını geçtiğinde bu üç şeyin üçü de aynı anda gider.

Birincisi, olay serileşir: bir metne çevrilir, karşı tarafta yeniden okunur. Bu andan sonra olayın alan listesi yayınlanmış bir sözleşmedir; her alan sürüm uyumu izlenecek bir yükümlülük olur. İkincisi, yayıncı dönüş değeri almaz: işin yapıldığını değil, ilanın verildiğini bilir. Üçüncüsü, olayın varış sayısı 1 olarak varsayılamaz. Teslim anlamlarının ayrıntısı Önbellekleme, Kuyruklar ve Eşzamansız İşleme kursunun konusudur; buraya düşen kısım mimari kısıttır — tüketici, aynı olayın ikinci kez ulaşmasına göre tasarlanmak zorundadır.

Veriyolu bu üç farkı da açık eder. Yayımla–abone ol modeli aynı kursta kurulmuştu; buradaki gerçekleştirim onun ölçülebilir en küçük hâlidir.

// olay/veriyolu.mjs — olay veriyolu: yayimi serilestirir, her tuketiciyi yalitir, gecisleri sayar
export function olayVeriyolu(kayit) {
  const dinleyici = new Map();
  return {
    dinle(tur, tuketici) {
      if (dinleyici.has(tur) === false) dinleyici.set(tur, []);
      dinleyici.get(tur).push(tuketici);
    },
    yayimla(tur, govde) {
      const metin = JSON.stringify(govde);
      kayit.donusum += 1;
      kayit.olay.push([tur, metin]);
      for (const t of dinleyici.get(tur) ?? []) {
        kayit.donusum += 1;
        kayit.bayt += metin.length;
        kayit.gecis.push(`${tur} -> ${t.ad}`);
        try { t.ele(JSON.parse(metin)); } catch (e) { kayit.dusen.push(`${t.ad}: ${e.message}`); }
      }
    },
  };
}

yayimla bilinçli olarak hiçbir şey döndürmez ve tüketicinin hatasını yayıncıya geçirmez. Bu iki karar biçemin kendisidir, bir kolaylık değil.

Yayıncı yalnız kendi işini yapar ve olayı verir. Dosyada hiçbir tüketici adı geçmez.

// olay/ucret.mjs — ucretlendirme baglami: fiyatlar ve olayi yayinlar, dinleyeni bilmez
const TARIFE = { kademe: [[1, 3000], [5, 4800], [20, 9600]], bolge: { "34": 100, "06": 115, "35": 125 }, asgari: 2500 };

export function net(agirlik, bolge, oran) {
  const kademe = TARIFE.kademe.find(([k]) => agirlik <= k) ?? [0, 9600];
  const taban = Math.max(TARIFE.asgari, Math.round((kademe[1] * TARIFE.bolge[bolge]) / 100));
  return taban - Math.round(taban * Math.min(oran, 0.4));
}

export function ucretlendirme(veriyolu) {
  let sira = 0;
  return {
    fiyatla(g) {
      sira += 1;
      return veriyolu.yayimla("gonderi-fiyatlandi", {
        olayNo: `O${sira}`, id: g.id, agirlik: g.agirlik, bolge: g.bolge,
        sozlesmeNo: g.sozlesmeNo, net: net(g.agirlik, g.bolge, g.oran),
      });
    },
  };
}

Sözleşmeye eklenen olayNo alanı gönderinin değil olayın kendi kimliğidir; üçüncü farkın karşılığı budur ve ölçümde kullanılacaktır.

İki tüketici var. Teslimat operasyonu rotayı gönderi kimliğine yazar; aynı olay ikinci kez gelirse aynı anahtarın üzerine aynı değeri yazar.

// olay/operasyon.mjs — teslimat operasyonu tuketicisi: rotayi gonderi kimligine yazar
const AGAC = { "34": ["34"], "06": ["34", "06"], "35": ["34", "41", "35"] };

export function operasyonTuketicisi() {
  const plan = new Map();
  return {
    ad: "operasyon",
    ele(olay) {
      const rota = AGAC[olay.bolge] ?? ["34"];
      plan.set(olay.id, { rota, gun: rota.length, tasiyici: rota.length > 2 ? "MT" : "AN" });
    },
    durum() {
      return { plan: plan.size, gun: [...plan.values()].reduce((t, p) => t + p.gun, 0) };
    },
  };
}

İkinci tüketici hacim indirimi için sözleşme başına ağırlık toplar. Toplama işi doğası gereği biriktiricidir; aşağıda aynı işin iki tasarımı yan yana durur.

// olay/sozlesme.mjs — hacim indirimi tuketicisi: ayni isin biriktiren ve anahtarli tasarimi
function hacim(toplam) {
  return { sozlesme: toplam.size, hacim: [...toplam.values()].reduce((t, a) => t + a, 0) };
}

export function biriktiren() {
  const toplam = new Map();
  return {
    ad: "sozlesme-biriktiren",
    ele(olay) {
      if (olay.sozlesmeNo === null) throw new TypeError(`hacim indirimi sozlesme ister: ${olay.id}`);
      toplam.set(olay.sozlesmeNo, (toplam.get(olay.sozlesmeNo) ?? 0) + olay.agirlik);
    },
    durum() { return hacim(toplam); },
  };
}

export function anahtarli() {
  const toplam = new Map();
  const islenen = new Set();
  return {
    ad: "sozlesme-anahtarli",
    ele(olay) {
      if (olay.sozlesmeNo === null) throw new TypeError(`hacim indirimi sozlesme ister: ${olay.id}`);
      if (islenen.has(olay.olayNo)) return;
      islenen.add(olay.olayNo);
      toplam.set(olay.sozlesmeNo, (toplam.get(olay.sozlesmeNo) ?? 0) + olay.agirlik);
    },
    durum() { return hacim(toplam); },
  };
}

Karşılaştırma tarafı için aynı iş doğrudan çağrıyla da yazılır. İki düzen aynı sonucu üretir; ayrıldıkları yer ölçülecek olan yerdir.

// dogrudan/ucret.mjs — ayni is dogrudan cagriyla: ucretlendirme tuketicileri adiyla bilir
import { net } from "../olay/ucret.mjs";
import { operasyonTuketicisi } from "../olay/operasyon.mjs";
import { biriktiren } from "../olay/sozlesme.mjs";

export function dogrudanUcretlendirme(kayit) {
  const operasyon = operasyonTuketicisi();
  const sozlesme = biriktiren();
  return {
    tuketici: [operasyon, sozlesme],
    fiyatla(g) {
      const olay = {
        id: g.id, agirlik: g.agirlik, bolge: g.bolge,
        sozlesmeNo: g.sozlesmeNo, net: net(g.agirlik, g.bolge, g.oran),
      };
      kayit.gecis.push("ucret -> operasyon");
      operasyon.ele(olay);
      kayit.gecis.push("ucret -> sozlesme");
      sozlesme.ele(olay);
      return { id: g.id, net: olay.net, plan: operasyon.durum().plan };
    },
  };
}

Beş Ölçüm

Sürücü betik beş soruyu sırayla yanıtlar: yayıncı kaç tüketici biliyor ve kaçını adıyla çağırıyor; aynı üç gönderi iki düzende kaç sınırdan geçiyor, kaç dönüştürme noktası ve kaç bayt taşıyor; üçüncü bir tüketici eklendiğinde yayıncı dosyasında kaç satır düzenleniyor; bir tüketici düştüğünde kaç istek yanıtlanıyor; aynı olay ikinci kez ulaştığında iki tüketici tasarımı ne kadar sapıyor.

// olay/olc.mjs — bilme yukumlulugu, sinir olculeri, ariza yalitimi ve tekrar sapmasi
import { readFileSync } from "node:fs";
import { olayVeriyolu } from "./veriyolu.mjs";
import { ucretlendirme } from "./ucret.mjs";
import { operasyonTuketicisi } from "./operasyon.mjs";
import { biriktiren, anahtarli } from "./sozlesme.mjs";
import { dogrudanUcretlendirme } from "../dogrudan/ucret.mjs";

const GONDERI = [
  { id: "G1", agirlik: 4, bolge: "35", sozlesmeNo: "S7", oran: 0.15 },
  { id: "G2", agirlik: 1, bolge: "34", sozlesmeNo: "S7", oran: 0 },
  { id: "G3", agirlik: 12, bolge: "06", sozlesmeNo: "S9", oran: 0.25 },
];
const bosKayit = () => ({ gecis: [], donusum: 0, bayt: 0, olay: [], dusen: [] });
const kur = (kayit, tuketiciler) => {
  const yol = olayVeriyolu(kayit);
  for (const t of tuketiciler) yol.dinle("gonderi-fiyatlandi", t);
  return [yol, ucretlendirme(yol)];
};

const YOL = /from\s+"[^"]*(operasyon|sozlesme)\.mjs"/g;
for (const d of ["olay/ucret.mjs", "dogrudan/ucret.mjs"]) {
  const m = readFileSync(d, "utf8");
  console.log(`${d.padEnd(19)} bildigi tuketici = ${[...m.matchAll(YOL)].length}, cagirdigi tuketici = ${(m.match(/\.ele\(/g) ?? []).length}`);
}

const dk = bosKayit();
const dogrudan = dogrudanUcretlendirme(dk);
const dDonus = GONDERI.map((g) => dogrudan.fiyatla(g));
console.log(`dogrudan    : sinir gecisi = ${dk.gecis.length}, donusum noktasi = ${dk.donusum}, bayt = ${dk.bayt}, donus = ${dDonus.every((d) => d === undefined) ? "yok" : "nesne"}`);

const ok = bosKayit();
const [, oUcret] = kur(ok, [operasyonTuketicisi(), biriktiren()]);
const oDonus = GONDERI.map((g) => oUcret.fiyatla(g));
console.log(`olay gudumlu: sinir gecisi = ${ok.gecis.length}, donusum noktasi = ${ok.donusum}, bayt = ${ok.bayt}, donus = ${oDonus.every((d) => d === undefined) ? "yok" : "nesne"}`);
console.log(`  olay govdesi = ${ok.olay[0][1]}`);

const oncesi = readFileSync("olay/ucret.mjs", "utf8");
const uk = bosKayit();
const bolgeSayaci = {
  ad: "bolge-sayaci", sayi: new Map(),
  ele(o) { this.sayi.set(o.bolge, (this.sayi.get(o.bolge) ?? 0) + 1); },
};
const [, uUcret] = kur(uk, [operasyonTuketicisi(), biriktiren(), bolgeSayaci]);
for (const g of GONDERI) uUcret.fiyatla(g);
const duzenlenen = oncesi === readFileSync("olay/ucret.mjs", "utf8") ? 0 : 1;
console.log(`ucuncu tuketici: yayincida duzenlenen satir = ${duzenlenen}, bilesim kokune eklenen satir = 1, gordugu bolge = ${bolgeSayaci.sayi.size}`);
console.log(`  uc tuketiciyle: sinir gecisi = ${uk.gecis.length}, donusum noktasi = ${uk.donusum}, bayt = ${uk.bayt}`);

const SOZLESMESIZ = [GONDERI[0], { id: "G2", agirlik: 1, bolge: "34", sozlesmeNo: null, oran: 0 }, GONDERI[2]];
const ak = bosKayit();
const aDogrudan = dogrudanUcretlendirme(ak);
let aYanit = 0;
for (const g of SOZLESMESIZ) {
  try { aDogrudan.fiyatla(g); aYanit += 1; } catch (e) { ak.dusen.push(`yayinci: ${e.message}`); }
}
console.log(`dogrudan    : yanitlanan istek = ${aYanit}/3, dusen = ${ak.dusen.length} (${ak.dusen.join("; ")})`);
console.log(`  operasyon = ${JSON.stringify(aDogrudan.tuketici[0].durum())}, sozlesme = ${JSON.stringify(aDogrudan.tuketici[1].durum())}`);

const ek = bosKayit();
const eOp = operasyonTuketicisi();
const eSoz = biriktiren();
const [, eUcret] = kur(ek, [eOp, eSoz]);
let eYanit = 0;
for (const g of SOZLESMESIZ) {
  try { eUcret.fiyatla(g); eYanit += 1; } catch (e) { ek.dusen.push(`yayinci: ${e.message}`); }
}
console.log(`olay gudumlu: yanitlanan istek = ${eYanit}/3, dusen = ${ek.dusen.length} (${ek.dusen.join("; ")})`);
console.log(`  operasyon = ${JSON.stringify(eOp.durum())}, sozlesme = ${JSON.stringify(eSoz.durum())}`);

const tk = bosKayit();
const tOp = operasyonTuketicisi();
const tBirik = biriktiren();
const tAnahtar = anahtarli();
const [tYol, tUcret] = kur(tk, [tOp, tBirik, tAnahtar]);
for (const g of GONDERI) tUcret.fiyatla(g);
const IZLENEN = [[tOp, "operasyon", "gun"], [tBirik, "sozlesme-biriktiren", "hacim"], [tAnahtar, "sozlesme-anahtarli", "hacim"]];
const once = IZLENEN.map(([t, , k]) => t.durum()[k]);
const [tur, metin] = tk.olay[2];
tYol.yayimla(tur, JSON.parse(metin));
console.log(`ucuncu olay ikinci kez ulasti: ${metin}`);
IZLENEN.forEach(([t, ad, k], i) => {
  const sonra = t.durum()[k];
  console.log(`  ${ad.padEnd(20)} ${k} once = ${once[i]}, sonra = ${sonra}, sapma = ${sonra - once[i]}`);
});

const kaynak = readFileSync("olay/sozlesme.mjs", "utf8");
const satir = (ad) => kaynak.split(`export function ${ad}()`)[1].split("\n}")[0].split("\n").filter((s) => s.trim() !== "").length;
console.log(`tekrara dayanikli tasarimin bedeli = ${satir("anahtarli") - satir("biriktiren")} satir`);
node olay/olc.mjs
olay/ucret.mjs      bildigi tuketici = 0, cagirdigi tuketici = 0
dogrudan/ucret.mjs  bildigi tuketici = 2, cagirdigi tuketici = 2
dogrudan    : sinir gecisi = 6, donusum noktasi = 0, bayt = 0, donus = nesne
olay gudumlu: sinir gecisi = 6, donusum noktasi = 9, bayt = 476, donus = yok
  olay govdesi = {"olayNo":"O1","id":"G1","agirlik":4,"bolge":"35","sozlesmeNo":"S7","net":5100}
ucuncu tuketici: yayincida duzenlenen satir = 0, bilesim kokune eklenen satir = 1, gordugu bolge = 3
  uc tuketiciyle: sinir gecisi = 9, donusum noktasi = 12, bayt = 714
dogrudan    : yanitlanan istek = 2/3, dusen = 1 (yayinci: hacim indirimi sozlesme ister: G2)
  operasyon = {"plan":3,"gun":6}, sozlesme = {"sozlesme":2,"hacim":16}
olay gudumlu: yanitlanan istek = 3/3, dusen = 1 (sozlesme-biriktiren: hacim indirimi sozlesme ister: G2)
  operasyon = {"plan":3,"gun":6}, sozlesme = {"sozlesme":2,"hacim":16}
ucuncu olay ikinci kez ulasti: {"olayNo":"O3","id":"G3","agirlik":12,"bolge":"06","sozlesmeNo":"S9","net":8280}
  operasyon            gun once = 6, sonra = 6, sapma = 0
  sozlesme-biriktiren  hacim once = 17, sonra = 29, sapma = 12
  sozlesme-anahtarli   hacim once = 17, sonra = 17, sapma = 0
tekrara dayanikli tasarimin bedeli = 3 satir

Sayıların Okunması

İlk iki satır bilme yükümlülüğünü sayıya çeviriyor. Doğrudan çağrı düzeninde ücretlendirme dosyası 2 tüketici modülünü ithal ediyor ve 2 tüketiciyi adıyla çağırıyor; olay güdümlü düzende iki sayı da 0. Bu, uzantı maliyetinde doğrudan görünür: üçüncü tüketici eklendiğinde yayıncı dosyasında düzenlenen satır 0 oldu, bileşim kökünde 1 satır arttı. Doğrudan düzende aynı ekleme ücretlendirme dosyasında bir ithal, bir alan ve bir çağrı satırı ister.

Sınır geçişi sayısı iki düzende de 6. Bu, kolay atlanan bir sonuçtur: olay güdümlü biçem sınır sayısını azaltmaz, sınırda ne olduğunu değiştirir. Doğrudan düzende dönüştürme noktası 0 ve sınırdan geçen bayt 0’dı; olay güdümlü düzende dönüştürme noktası 9, bayt 476. Dokuz sayısının yapısı da okunabilir: her yayımda 1 serileştirme ve tüketici başına 1 çözümleme, yani üç gönderi için 3×(1+2)3 \times (1 + 2). Tüketici üçe çıktığında 3×(1+3)=123 \times (1 + 3) = 12 oldu ve bayt 476’dan 714’e çıktı — tam olarak 238×3238 \times 3. Yayma çarpanı doğrusaldır ve maliyeti tüketici sayısıyla büyür; yayıncının kodu ise sabit kalır.

Arıza satırları biçemin en keskin kazancını gösteriyor. Sözleşmesiz gönderi hacim indirimi tüketicisinde hata attı. Doğrudan düzende bu hata çağrı yığıtından yayıncıya yükseldi ve isteği düşürdü: yanıtlanan istek 2/3. Üstelik operasyon tüketicisi o gönderinin rotasını zaten yazmıştı — plan sayısı 3 kaldı. Yani istek başarısız görünürken sistemin bir bölümünde işi yapılmış durumdadır. Olay güdümlü düzende yanıtlanan istek 3/3; düşen tüketici sayısı yine 1 ama düşenin adı kayda geçti ve ücretlendirmenin yanıtı etkilenmedi. İki düzende tüketici durumları birebir aynı çıktı (plan 3, hacim 16); farklı olan tek şey hatanın kimin sorunu olduğudur.

Bu ayrım bir bedelle gelir. dusen listesi ölçümde göründü çünkü veriyolu aynı süreçteydi. Tüketici ayrı bir dağıtım birimine geçtiğinde o liste yayıncının erişemediği bir yerde tutulur.

Tekrarın Tüketiciye Yüklediği Şey

Son ölçüm üçüncü farkın faturasıdır. Aynı olay gövdesi ikinci kez veriyoluna verildiğinde operasyon tüketicisinin gün toplamı 6’dan 6’ya, yani hiç değişmedi: rota gönderi kimliğine yazıldığı için ikinci yazım birincinin üstüne aynı değeri koydu. Biriktiren tüketicinin hacmi 17’den 29’a çıktı, sapma 12. Bu sapma sözleşmeli müşterinin hacim indirimini olduğundan yüksek gösterir; kusur veriyolunda değil, tüketicinin tasarımındadır.

Anahtarlı tasarım aynı tekrarda 17’de kaldı. Farkı yapan şey olayNo alanının sözleşmede bulunması ve tüketicinin işlediği kimlikleri tutmasıdır. Bedeli ölçüldü: 3 satır. Bu üç satır biçemin tüketiciye yüklediği asıl yükümlülüktür — yayıncı varış sayısını garanti edemediği için her biriktirici tüketici kendi tekrar dayanıklılığını yazmak zorundadır. Tüketici sayısı arttıkça bu üç satır her biriktiricide yeniden görünür.

Biçemin Yerleştiği Yer

Sayılar bir kalite niteliği ödünleşimi betimliyor. Kazanılan nitelikler sürdürülebilirlik ve güvenilirliktir: yeni bir tüketici için yayıncıda düzenlenen satır 0, bir tüketici düştüğünde yanıtlanan istek 3/3. Bırakılan nitelik uçtan uca kesinliktir: yayıncı 476 baytlık bir sözleşme yayınlar, karşılığında hiçbir dönüş değeri almaz ve işin yapıldığını yanıt anında bilemez.

Bu yüzden biçem, işin yanıt anında bitmesi gerekmeyen alışverişler için yerindedir. Ücret hesabı gönderi kaydedilirken bitmek zorundadır; rota planı ve hacim indirimi bitmek zorunda değildir. Ücret hesabını da olaya çevirmek, ölçümdeki 2/3 satırının ücret yanıtına taşınması olur. Biçem seçimi bu yüzden sistem geneli için değil, alışveriş başına verilir.

Özet

  • Olay güdümlü mimaride birim olmuş bir olguyu ilan eder; yayıncı dosyasında bildiği tüketici 0, adıyla çağırdığı tüketici 0 ölçüldü, doğrudan çağrı düzeninde iki sayı da 2’ydi.
  • Olay sistem sınırını geçtiğinde üç şey birden değişir: gövde serileşip yayınlanmış sözleşme olur, yayıncı dönüş değeri almaz, varış sayısı 1 varsayılamaz.
  • Sınır geçişi iki düzende de 6 kaldı; değişen sınırda olan şeydir — dönüştürme noktası 0’dan 9’a, sınırdan geçen bayt 0’dan 476’ya çıktı ve tüketici üçe çıkınca 12 ile 714 oldu.
  • Üçüncü tüketici yayıncı dosyasında 0 satır düzenlenerek eklendi; maliyet bileşim kökündeki 1 satır ve doğrusal büyüyen yayma çarpanıdır.
  • Bir tüketici hata attığında doğrudan düzende yanıtlanan istek 2/3’e indi ve yarım kalan iş bıraktı; olay güdümlü düzende 3/3 kaldı, düşen tüketici adıyla kayda geçti.
  • Aynı olay ikinci kez ulaştığında anahtarlı yazan operasyon tüketicisinin sapması 0, biriktiren tüketicinin sapması 12 oldu; anahtarlı tasarımın bedeli 3 satırdır.

Sonraki Adım

Bu dersteki üç düzenin ortak bir örtük varsayımı var: ücretlendirme, operasyon ve veriyolu için bir süreç vardır ve o süreç istek gelmeden önce ayaktadır. Mikroservis dersinde üç süreç elle başlatıldı ve elle durduruldu; olay güdümlü düzende veriyolunun da bir yerde koşması gerekir. Sonraki biçem bu varsayımı kaldırır: kod, bir olay ya da bir istek geldiğinde çağrılan bir işleve indirgenir, süreci ayakta tutma işi yazılımın dışına çıkar. Sonraki ders o düzende neyin ölçüldüğünü sayar: uygulamanın kendi taşıdığı sunucu satırı, bir isteğin dokunduğu işlev sayısı, işlevler arasında paylaşılamayan durumun nereye taşındığı ve bileşimin yapılandırmaya kaydırılmasının düzenlenen dosya sayısına etkisi.

İ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