İçeriğe geç
academia.sh

Ders 05 / 18

Sunucusuz Yaklaşım

Olay tetiklemeli işlev modelinin kısıtlarının ölçülmesi: iki çağrı arasında bellekte hiçbir şey tutulamamasının getirdiği ek depo çağrıları, çağrı başına konan süre sınırının iş akışını bölmesi ve süreç içinde kalmış son yerel çağrıların da dış çağrıya dönüşmesi.

İçindekiler

Önceki dersin beş süreci uzun ömürlüydü: bir kez başlatılıyor, istek gelsin gelmesin ayakta duruyor ve üye kayıtlarını kendi belleğinde tutuyordu. Bu ders dağıtım birimini bir adım daha küçültür ve uzun ömürlülüğü kaldırır. Birim artık bir süreç değil, yalnız bir olay tetiklediğinde var olan bir işlevdir.

Bu düzenin adı sunucusuz (serverless). Biçemin tanımı ve ilk kurulum maliyeti Mimari Biçemler kursunda ele alındı; burada ölçülen şey işlev modelinin çalışma kısıtlarıdır: iki çağrı arasında bellekte hiçbir şey kalmaması, çağrı başına bir süre sınırı bulunması ve süreç içinde kalmış son yerel çağrıların da dışarı çıkması.

MS6. İşlev çalıştırıcısı yerel bir modeldir: her olay için node ile yeni bir süreç başlatır ve o sürece sabit bir süre sınırı uygular. Modelin taşıdığı üç özellik şudur — çağrılar arasında bellek paylaşılmaz, aşılan süre sınırı işi ortasında keser, işlev dışındaki her şeye ağ üzerinden erişilir.

Çalıştırıcı ve Depo

Durum bir yerde durmak zorunda. İşlevler tutamadığı için, tek uzun ömürlü süreç bir anahtar deposudur.

// depo-servisi.mjs — islevler arasinda durum tutan tek uzun omurlu surec
import { createServer } from "node:http";
const port = Number(process.argv[2]);
if (!port) { console.log("kullanim: node depo-servisi.mjs <port>"); process.exit(0); }
const kutu = { "kitap:1": { durum: "rafta" },
               "uye:4": { ad: "Emre", acikOdunc: 1, sinir: 5, borc: 0 },
               gecikme: ["g1", "g2", "g3"], islenen: [] };
let cagri = 0;
const kez = {};
createServer(async (istek, yanit) => {
  const u = new URL(istek.url, "http://y");
  const a = u.searchParams.get("a");
  const say = () => { cagri += 1; kez[a] = (kez[a] ?? 0) + 1; };
  if (u.pathname === "/al") { say(); return yanit.end(JSON.stringify(kutu[a] ?? null)); }
  if (u.pathname === "/koy") {
    say();
    let g = "";
    for await (const p of istek) g += p;
    kutu[a] = JSON.parse(g);
    return yanit.end("{}");
  }
  if (u.pathname === "/sayac") {
    const y = { cagri, uye: kez["uye:4"] ?? 0 };
    if (u.searchParams.get("sifirla")) { cagri = 0; for (const k in kez) delete kez[k]; }
    return yanit.end(JSON.stringify(y));
  }
  yanit.statusCode = 404;
  yanit.end("{}");
}).listen(port, () => console.log(`depo ${port}`));
// calistirici.mjs — islev cagirici: her olay icin yeni surec, her cagriya sure siniri
import { spawnSync } from "node:child_process";
export const SINIR_MS = 400;
export function cagir(islev, ...olay) {
  const s = spawnSync("node", [islev, ...olay], { encoding: "utf8", timeout: SINIR_MS });
  return { kesildi: s.signal !== null, cikti: s.stdout.trim() };
}

Dört İşlev

Her işlev olayı komut satırından alır, işini yapar, sonucu basar ve ölür. Hiçbiri bir sonraki çağrıya bir şey bırakamaz.

// islev-uygunluk.mjs — olay tetiklemeli islev; her cagrida bellegi bos baslar
const D = process.env.DEPO;
if (!D) { console.log("DEPO ortam degiskeni gerekli"); process.exit(0); }
const uye = await (await fetch(`http://127.0.0.1:${D}/al?a=uye:${process.argv[2]}`)).json();
console.log(JSON.stringify({ uygun: uye.acikOdunc < uye.sinir }));
// islev-isaretle.mjs — kitabi odunc isaretler, uyenin sayacini artirir
const D = process.env.DEPO;
if (!D) { console.log("DEPO ortam degiskeni gerekli"); process.exit(0); }
const [kitapNo, uyeNo] = process.argv.slice(2);
const yaz = (a, d) => fetch(`http://127.0.0.1:${D}/koy?a=${a}`, { method: "POST", body: JSON.stringify(d) });
const kitap = await (await fetch(`http://127.0.0.1:${D}/al?a=kitap:${kitapNo}`)).json();
if (kitap.durum !== "rafta") { console.log(JSON.stringify({ hata: "kitap rafta degil" })); process.exit(0); }
await yaz(`kitap:${kitapNo}`, { durum: "odunc" });
const uye = await (await fetch(`http://127.0.0.1:${D}/al?a=uye:${uyeNo}`)).json();
uye.acikOdunc += 1;
await yaz(`uye:${uyeNo}`, uye);
console.log(JSON.stringify({ durum: "odunc" }));
// islev-ucret.mjs — gecikme ucretini hesaplar ve uyenin borcuna yazar
const D = process.env.DEPO;
if (!D) { console.log("DEPO ortam degiskeni gerekli"); process.exit(0); }
const [uyeNo, gun] = process.argv.slice(2).map(Number);
const ucret = gun > 0 ? gun * 2 : 0;
if (ucret > 0) {
  const uye = await (await fetch(`http://127.0.0.1:${D}/al?a=uye:${uyeNo}`)).json();
  uye.borc += ucret;
  await fetch(`http://127.0.0.1:${D}/koy?a=uye:${uyeNo}`, { method: "POST", body: JSON.stringify(uye) });
}
console.log(JSON.stringify({ ucret }));
// islev-bildirim.mjs — uyenin adini depodan okur ve bildirimi basar
const D = process.env.DEPO;
if (!D) { console.log("DEPO ortam degiskeni gerekli"); process.exit(0); }
const [uyeNo, metin] = process.argv.slice(2);
const uye = await (await fetch(`http://127.0.0.1:${D}/al?a=uye:${uyeNo}`)).json();
console.log(`bildirim -> ${uye.ad}: ${metin}`);
// akis.mjs — odunc is akisi dort islev cagrisina bolunur; her cagri yeni bir surec
import { cagir } from "./calistirici.mjs";
const D = process.env.DEPO;
if (!D) { console.log("DEPO ortam degiskeni gerekli"); process.exit(0); }
const sayac = async () => (await (await fetch(`http://127.0.0.1:${D}/sayac?sifirla=1`)).json());
await sayac();
const [kitapNo, uyeNo, gun] = ["1", "4", "3"];
let islev = 0;
const c = (ad, ...olay) => { islev += 1; return JSON.parse(cagir(ad, ...olay).cikti || "null"); };
const u = c("islev-uygunluk.mjs", uyeNo);
if (u.uygun) {
  c("islev-isaretle.mjs", kitapNo, uyeNo);
  const { ucret } = c("islev-ucret.mjs", uyeNo, gun);
  islev += 1;
  console.log(cagir("islev-bildirim.mjs", uyeNo, `kitap ${kitapNo}`).cikti);
  console.log(`odunc verildi: kitap ${kitapNo} -> uye ${uyeNo}, ucret ${ucret}`);
}
const s = await sayac();
console.log(`islev cagrisi: ${islev}   yeni surec: ${islev}   depo cagrisi: ${s.cagri}   uye:4 erisimi: ${s.uye}`);
# baslat.sh — durumu tutan tek uzun omurlu surec ayaga kalkar, sonra is akisi kosar
node depo-servisi.mjs 8796 >depo.log 2>&1 & echo $! >depo.pid
for i in $(seq 40); do curl -s -o /dev/null "http://127.0.0.1:8796/" && break; sleep 0.2; done
echo "uzun omurlu surec: 1 (depo)"
DEPO=8796 node akis.mjs
uzun omurlu surec: 1 (depo)
bildirim -> Emre: kitap 1
odunc verildi: kitap 1 -> uye 4, ucret 6
islev cagrisi: 4   yeni surec: 4   depo cagrisi: 8   uye:4 erisimi: 6

Durum Tutamamanın Bedeli

Son satır üç sayı taşıyor.

Yeni süreç: 4. Önceki derste bir ödünç isteği hiçbir süreç başlatmıyordu; beş süreç zaten ayaktaydı ve isteği karşılıyordu. Burada her istek dört süreç doğurur ve dördü de biter. Bu, dağıtım biriminin çağrı süresince var olmasının doğrudan sonucudur.

Depo çağrısı: 8. Mikroservis düzeninde üye kaydı üyelik sürecinin belleğindeydi; kayda erişim süreç içi bir nesne okumasıydı. Burada hiçbir işlev kaydı taşıyamaz, bu yüzden her işlev kendi çağrısında onu depodan çekmek zorundadır.

uye:4 erişimi: 6. Aynı üye kaydı bir ödünç iş akışı içinde altı kez ağdan geçti: uygunluk okudu, işaretle okuyup yazdı, ücret okuyup yazdı, bildirim okudu. Önceki derste üyelik servisine yapılan çağrı üçtü ve üçü de bellekteki tek bir nesne üzerinde çalışıyordu. Fark, işlev modelinin en somut maliyetidir: bellekte tutulamayan her şey ağdan geçer.

Buna bağlı ikinci bir sonuç var. Önceki derste ucret-servisi gecikme oranını kendi belleğinde tutuyor ve hesabı süreç içinde yapıyordu; burada da hesap süreç içinde yapılıyor, ama sonucu yazmak için üye kaydını çekmek ve geri yazmak gerekiyor. Süreç içinde kalmış son yerel adımlar da dış çağrıya dönüştü.

Süre Sınırı

İkinci kısıt, çağrı başına konan süre sınırıdır. Gecikmiş üç kitabın bildirimini üreten bir iş, kayıt başına 300 ms sürüyor; sınır 400 ms.

// islev-gecikme.mjs — gecikme bildirimlerini tek cagrida sirayla isler
const D = process.env.DEPO;
if (!D) { console.log("DEPO ortam degiskeni gerekli"); process.exit(0); }
const kayitlar = await (await fetch(`http://127.0.0.1:${D}/al?a=gecikme`)).json();
const bitti = [];
for (const k of kayitlar) {
  await new Promise((c) => setTimeout(c, 300));
  bitti.push(k);
  await fetch(`http://127.0.0.1:${D}/koy?a=islenen`, { method: "POST", body: JSON.stringify(bitti) });
}
console.log(JSON.stringify({ islenen: bitti.length }));
// islev-gecikme-tek.mjs — tek kayit isler; durum cagrilar arasinda depoda tutulur
const D = process.env.DEPO;
if (!D) { console.log("DEPO ortam degiskeni gerekli"); process.exit(0); }
const kayitlar = await (await fetch(`http://127.0.0.1:${D}/al?a=gecikme`)).json();
await new Promise((c) => setTimeout(c, 300));
const bitti = await (await fetch(`http://127.0.0.1:${D}/al?a=islenen`)).json();
bitti.push(kayitlar[Number(process.argv[2])]);
await fetch(`http://127.0.0.1:${D}/koy?a=islenen`, { method: "POST", body: JSON.stringify(bitti) });
console.log(JSON.stringify({ islenen: bitti.length }));
// sure.mjs — ayni is once tek cagrida, sonra cagrilara bolunerek kosturulur
import { cagir, SINIR_MS } from "./calistirici.mjs";
const D = process.env.DEPO;
if (!D) { console.log("DEPO ortam degiskeni gerekli"); process.exit(0); }
const gec = (yol) => fetch(`http://127.0.0.1:${D}${yol}`);
const sifirla = () => fetch(`http://127.0.0.1:${D}/koy?a=islenen`, { method: "POST", body: "[]" });
const say = async () => (await (await gec("/sayac?sifirla=1")).json()).cagri;
const islenen = async () => (await (await gec("/al?a=islenen")).json()).length;

await sifirla(); await say();
const tek = cagir("islev-gecikme.mjs");
console.log(`tek cagri : kesildi=${tek.kesildi}  depo cagrisi=${await say()}  islenen=${await islenen()}/3  sinir=${SINIR_MS} ms`);

await sifirla(); await say();
let kesilen = 0;
for (const i of ["0", "1", "2"]) if (cagir("islev-gecikme-tek.mjs", i).kesildi) kesilen += 1;
console.log(`bolunmus  : cagri=3  kesilen=${kesilen}  depo cagrisi=${await say()}  islenen=${await islenen()}/3`);
DEPO=8796 node sure.mjs
tek cagri : kesildi=true  depo cagrisi=2  islenen=1/3  sinir=400 ms
bolunmus  : cagri=3  kesilen=0  depo cagrisi=9  islenen=3/3

Tek çağrıda iş yarım kaldı: üç kayıttan biri işlendi ve süreç ortasından kesildi. Kesilme bir hata değil, çalıştırıcının kuralıdır — kod hatasız, iş eksik. Burada dikkat çeken şey, kesilen çağrının kısmi etki bırakmış olmasıdır: bir kayıt depoya yazılmıştı.

Bölünmüş sürümde aynı iş üç çağrıya dağıldı ve tamamlandı. Bedeli depo çağrısı sayısında görünüyor: 2’den 9’a çıktı. Sebebi, işin durumunun artık çağrılar arasında taşınamamasıdır — her çağrı hem girdi listesini hem de o ana kadar işlenenleri yeniden okumak zorunda. Süre sınırı iş akışını bölünce, bölünmenin maliyetini durum taşıma ödedi.

Bu sayılar iki nedenle ortama görece dayanıklıdır: kayıt başına iş 300 ms, sınır 400 ms olarak seçildi; koşumdan bağımsız nicelikler kesilen çağrı sayısı, işlenen kayıt sayısı ve depo çağrısı sayısıdır.

# durdur.sh — depo surecini durdurur
kill "$(cat depo.pid)" 2>/dev/null
echo "durduruldu"
durduruldu

Üç Sütun

Sunucusuz
Ucuzlattığı Dağıtım birimi tek dosyaya indi; ayakta tutulan süreç 5’ten 1’e düştü, işlev çağrı dışında hiç var olmuyor
Pahalılaştırdığı İstek başına 4 yeni süreç; aynı üye kaydına 6 ağ erişimi; iş bölününce depo çağrısı 2’den 9’a çıktı
Doğan arıza kipi Süre sınırı işi ortasından keser: kod hatasız, sonuç eksik ve kısmi etki depoda kalmış olur

Özet

  • İşlev modelinde dağıtım birimi çağrı süresince var olur: bir ödünç iş akışı 4 işlev çağrısı ve 4 yeni süreç üretti; önceki derste bu sayı sıfırdı.
  • İki çağrı arasında bellekte hiçbir şey kalmadığı için durum depoya taşındı; iş akışı 8 depo çağrısı yaptı ve tek bir üye kaydına 6 kez ağdan erişildi.
  • Süreç içinde kalmış son yerel adımlar da dış çağrıya dönüştü; hesap süreç içinde yapılsa bile sonucu yazmak okuma ve yazma gerektiriyor.
  • Çağrı başına süre sınırı işi ortasından kesti: üç kayıttan biri işlendi, kesilen çağrı geride kısmi etki bıraktı.
  • İşi çağrılara bölmek sınırı aştırmadı, ama durum taşıma maliyetini doğurdu: depo çağrısı 2’den 9’a çıktı.

Sonraki Adım

Bu noktada aynı ödünç iş akışı beş biçimde kuruldu ve her biri kendi üç sütununu yazdı. Geriye tek bir soru kalıyor: elde çalışan bir tek parça varken bu biçimlerden birine nasıl geçilir. Sonraki ders geçişin kendisini ele alır: yolun aşamalı olarak ayrıştırılması, boğucu incir kalıbıyla eski ve yeni yolun bir süre birlikte koşması ve bu sürenin kod tarafındaki bedeli — iki kod tabanında tutulan ortak mantık, çift bakım maliyeti ve geri alınabilirlik.

İ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