İçeriğe geç
academia.sh

Ders 04 / 34

Uzak Yordam Çağrısı ve gRPC

Sözleşmeyi önce yazan yaklaşım: şema dosyasından üretilen doğrulayıcı, alan numarasının tel üzerindeki kimlik olması, kendi ikili çerçevelemesiyle metin biçiminin bayt karşılaştırması ve gRPC'nin bu tasarımı akış türleriyle birlikte getirmesi.

İçindekiler

Önceki ders REST’in kısıtlarını sınadı. O kısıtların ortak yanı, sözleşmeyi ağın kurallarına yaslamalarıdır: adresler, yöntemler, durum kodları. Sözleşmenin ayrıntısı — bir alanın tipi, bir alanın zorunlu olup olmadığı — telin dışında, belgede kalır.

Bunun tersini yapan bir yaklaşım vardır. Uzak yordam çağrısı (remote procedure call), ağ üzerindeki bir işlemi yerel bir fonksiyon çağrısı gibi gösterir: istemci KitapGetir diye bir işlev çağırır, çağrı ağa iner, karşı tarafta çalışır ve sonuç döner. Bu gösterimin işleyebilmesi için iki tarafın çağrının imzası üzerinde anlaşması gerekir ve bu anlaşma belgede değil, makine okunur bir şemada durur.

Yordam Çağrısı Benzetmesinin Sınırı

Benzetme yararlıdır ama tam değildir ve nerede bozulduğunu bilmek arayüz tasarımını belirler. Yerel bir çağrı ölçülemeyecek kadar hızlıdır; uzak çağrı en iyi durumda bir ağ turu sürer. Yerel bir çağrı ya çalışır ya da bir istisna atar; uzak çağrı üçüncü bir sonuç üretebilir — kısmi başarısızlık: isteğin gidip gitmediği, işin yapılıp yapılmadığı bilinmez. Yerel çağrıda argümanlar bellekteki nesnelerdir; uzak çağrıda bayt dizisine çevrilmeleri gerekir ve bu çevirinin kuralları sözleşmenin kendisidir.

Bu üç fark, uzak yordam çağrısı biçemini kullanan arayüzlerin tasarımına yansır: çağrılar kaba taneli tutulur (çok sayıda küçük çağrı yerine az sayıda dolu çağrı), yeniden deneme davranışı açıkça tanımlanır ve veri biçimi sıkı biçimde belirlenir.

Sözleşme Önce Yazılır

Şema dosyası arayüzün tek doğruluk kaynağıdır. Aşağıdaki dosya bir mesaj tipi ile bir servis tanımlar; her alanın bir numarası, bir tipi, bir adı ve bir zorunluluk durumu vardır.

mesaj Kitap {
  1 dizgi    isbn      zorunlu
  2 dizgi    ad        zorunlu
  3 tamsayi  sayfa     istege-bagli
  4 mantiksal oduncte  zorunlu
}
servis Katalog {
  KitapGetir(KitapIstek) -> Kitap
  KitapAra(AramaIstek) -> akis Kitap
}

Bu dosyadan iki şey üretilir: alanların varlığını ve tipini denetleyen bir doğrulayıcı, ve nesneyi bayt dizisine çeviren bir kodlayıcı. Kodlayıcının biçimi, alan adlarını tele hiç çıkarmaz; her alan kendi numarasıyla anılır ve sayılar değişken uzunlukta yazılır.

// uret.mjs — sema dosyasindan dogrulayici ve ikili kodlayici uretir (sozlesme onceliklidir).
import { readFileSync } from "node:fs";

export const semaOku = (dosya) => {
  const mesajlar = {}, servisler = {};
  let acik = null;
  for (const ham of readFileSync(dosya, "utf8").split("\n")) {
    const satir = ham.trim();
    if (!satir) continue;
    let e;
    if ((e = /^mesaj (\w+) \{$/.exec(satir))) { acik = mesajlar[e[1]] = []; continue; }
    if ((e = /^servis (\w+) \{$/.exec(satir))) { acik = servisler[e[1]] = []; continue; }
    if (satir === "}") { acik = null; continue; }
    if ((e = /^(\d+) (\w+)\s+(\w+)\s+(\S+)$/.exec(satir)))
      acik.push({ numara: +e[1], tip: e[2], ad: e[3], zorunlu: e[4] === "zorunlu" });
    else if ((e = /^(\w+)\((\w+)\) -> (akis )?(\w+)$/.exec(satir)))
      acik.push({ yordam: e[1], girdi: e[2], akis: Boolean(e[3]), cikti: e[4] });
  }
  return { mesajlar, servisler };
};

const TIPLER = { dizgi: "string", tamsayi: "number", mantiksal: "boolean" };

// Uretilmis dogrulayici: alanlarin varligini ve tipini semaya gore denetler.
export const dogrulayiciUret = (alanlar) => (nesne) => {
  const kusur = [];
  for (const a of alanlar) {
    const v = nesne[a.ad];
    if (v === undefined) { if (a.zorunlu) kusur.push(`${a.ad}: zorunlu alan eksik`); continue; }
    if (typeof v !== TIPLER[a.tip]) kusur.push(`${a.ad}: ${a.tip} bekleniyordu, ${typeof v} geldi`);
  }
  for (const ad of Object.keys(nesne))
    if (!alanlar.some((a) => a.ad === ad)) kusur.push(`${ad}: semada yok`);
  return kusur;
};

// Degisken uzunluklu tam sayi: kucuk sayilar az bayt tutar.
const varintYaz = (n) => {
  const b = [];
  do { let p = n & 0x7f; n >>>= 7; b.push(n ? p | 0x80 : p); } while (n);
  return Buffer.from(b);
};
const varintOku = (b, i) => {
  let n = 0, k = 0;
  while (b[i] & 0x80) { n |= (b[i++] & 0x7f) << k; k += 7; }
  return [n | (b[i++] << k), i];
};

// Ikili cerceveleme: her alan (numara<<3 | tur) anahtariyla yazilir; alan adi tele hic cikmaz.
export const kodlayiciUret = (alanlar) => ({
  kodla(nesne) {
    const parcalar = [];
    for (const a of alanlar) {
      const v = nesne[a.ad];
      if (v === undefined) continue;
      const tur = a.tip === "dizgi" ? 2 : 0;
      parcalar.push(varintYaz((a.numara << 3) | tur));
      if (tur === 0) parcalar.push(varintYaz(a.tip === "mantiksal" ? (v ? 1 : 0) : v));
      else { const g = Buffer.from(v, "utf8"); parcalar.push(varintYaz(g.length), g); }
    }
    return Buffer.concat(parcalar);
  },
  cozumle(bayt) {
    const nesne = {};
    let i = 0;
    while (i < bayt.length) {
      let anahtar; [anahtar, i] = varintOku(bayt, i);
      const alan = alanlar.find((a) => a.numara === anahtar >> 3);
      if ((anahtar & 7) === 2) {
        let uzunluk; [uzunluk, i] = varintOku(bayt, i);
        nesne[alan?.ad ?? `bilinmeyen_${anahtar >> 3}`] = bayt.toString("utf8", i, i + uzunluk);
        i += uzunluk;
      } else {
        let sayi; [sayi, i] = varintOku(bayt, i);
        nesne[alan?.ad ?? `bilinmeyen_${anahtar >> 3}`] = alan?.tip === "mantiksal" ? Boolean(sayi) : sayi;
      }
    }
    return nesne;
  },
});

Ölçüm

Ölçüm dört soruyu yanıtlar: şemadan hangi yüzey üretilir, doğrulayıcı hangi kayıtları reddeder, ikili biçim ne kadar bayt tutar ve alan adı değiştiğinde tel üzerindeki baytlara ne olur?

// olc.mjs — sema oncelikli sozlesmeyi sinar: dogrulama, ikili/metin bayt karsilastirmasi, alan adi degisimi
import { writeFileSync } from "node:fs";
import { semaOku, dogrulayiciUret, kodlayiciUret } from "./uret.mjs";

const { mesajlar, servisler } = semaOku("kutuphane.sema");
const alanlar = mesajlar.Kitap;
const dogrula = dogrulayiciUret(alanlar);
const { kodla, cozumle } = kodlayiciUret(alanlar);

console.log("-- 1) semadan uretilen servis yuzeyi --");
for (const y of servisler.Katalog)
  console.log(`  ${y.yordam}(${y.girdi}) -> ${y.akis ? "akis " : ""}${y.cikti}`);

console.log("\n-- 2) uretilmis dogrulayici --");
const ORNEKLER = [
  { isbn: "978-0262033848", ad: "Algoritmalara Giris", sayfa: 1312, oduncte: false },
  { isbn: "978-0201896831", ad: "Programlama Sanati", oduncte: true },   // istege bagli alan yok
  { isbn: "978-0131103627", oduncte: true },                             // zorunlu alan eksik
  { isbn: "978-0596007126", ad: "Head First", sayfa: "272", oduncte: true },  // tip yanlis
  { isbn: "978-0000000000", ad: "Deneme", oduncte: true, raf: "R-12" },  // semada olmayan alan
];
for (const o of ORNEKLER) {
  const kusur = dogrula(o);
  console.log(`  ${JSON.stringify(o).slice(0, 52).padEnd(54)} ${kusur.length ? "RED  " + kusur[0] : "GECER"}`);
}

console.log("\n-- 3) ikili cerceveleme ile metin biciminin bayt sayisi --");
const gecerli = ORNEKLER.filter((o) => dogrula(o).length === 0);
for (const o of gecerli) {
  const ikili = kodla(o), metin = Buffer.from(JSON.stringify(o), "utf8");
  console.log(`  ${o.isbn}  ikili=${String(ikili.length).padStart(3)} B  ` +
    `metin=${String(metin.length).padStart(3)} B  kazanc=%${Math.round(100 * (1 - ikili.length / metin.length))}`);
}
const toplamIkili = gecerli.reduce((t, o) => t + kodla(o).length, 0);
const toplamMetin = Buffer.byteLength(JSON.stringify(gecerli));
console.log(`  toplam       ikili=${toplamIkili} B  metin=${toplamMetin} B  ` +
  `kazanc=%${Math.round(100 * (1 - toplamIkili / toplamMetin))}`);

console.log("\n-- 4) tel uzerinde alan adi yok: sema alan adini degistirir --");
const bayt = kodla(gecerli[0]);
console.log(`  eski semayla cozumlenen : ${JSON.stringify(cozumle(bayt))}`);
writeFileSync("yeni.sema", `mesaj Kitap {
  1 dizgi    isbn      zorunlu
  2 dizgi    baslik    zorunlu
  3 tamsayi  sayfa     istege-bagli
  4 mantiksal oduncte  zorunlu
}
`);
const yeni = kodlayiciUret(semaOku("yeni.sema").mesajlar.Kitap);
console.log(`  yeni semayla cozumlenen : ${JSON.stringify(yeni.cozumle(bayt))}`);
console.log(`  ayni baytlar, ${bayt.length} B; degisen tek sey alan numarasinin adi.`);
-- 1) semadan uretilen servis yuzeyi --
  KitapGetir(KitapIstek) -> Kitap
  KitapAra(AramaIstek) -> akis Kitap

-- 2) uretilmis dogrulayici --
  {"isbn":"978-0262033848","ad":"Algoritmalara Giris",   GECER
  {"isbn":"978-0201896831","ad":"Programlama Sanati","   GECER
  {"isbn":"978-0131103627","oduncte":true}               RED  ad: zorunlu alan eksik
  {"isbn":"978-0596007126","ad":"Head First","sayfa":"   RED  sayfa: tamsayi bekleniyordu, string geldi
  {"isbn":"978-0000000000","ad":"Deneme","oduncte":tru   RED  raf: semada yok

-- 3) ikili cerceveleme ile metin biciminin bayt sayisi --
  978-0262033848  ikili= 42 B  metin= 81 B  kazanc=%48
  978-0201896831  ikili= 38 B  metin= 66 B  kazanc=%42
  toplam       ikili=80 B  metin=150 B  kazanc=%47

-- 4) tel uzerinde alan adi yok: sema alan adini degistirir --
  eski semayla cozumlenen : {"isbn":"978-0262033848","ad":"Algoritmalara Giris","sayfa":1312,"oduncte":false}
  yeni semayla cozumlenen : {"isbn":"978-0262033848","baslik":"Algoritmalara Giris","sayfa":1312,"oduncte":false}
  ayni baytlar, 42 B; degisen tek sey alan numarasinin adi.

Ölçümün Söyledikleri

Doğrulama sözleşmeden üretilir, elle yazılmaz. Beş kaydın üçü reddedildi ve üç ret üç ayrı türdendir: eksik zorunlu alan, yanlış tip, şemada bulunmayan alan. Bu denetimlerin hiçbiri uygulama kodunda yazılmamıştır; şema dosyasındaki satırlardan çıkmıştır. Şema değiştiğinde doğrulama da değişir ve iki tarafın kuralları ayrışamaz.

İkili biçim aynı kaydı yaklaşık yarı bayta indirir. Kazancın kaynağı sıkıştırma değil, gereksiz olanın hiç yazılmamasıdır: alan adları, tırnak işaretleri, iki nokta ve virgüller tele çıkmaz. Sayılar da metin olarak değil, değişken uzunluklu tam sayı olarak yazılır — 1312 dört karakter yerine iki bayt tutar.

Alan numarası kimliktir, alan adı değil. Dördüncü bölüm, birinci derste kırıcı sayılan bir değişikliği tekrarlar: ad alanı baslik olarak yeniden adlandırılır. Metin biçiminde bu değişiklik tüketiciyi kırmıştı. İkili biçimde aynı 42 baytlık dizi, yeni şemayla sorunsuz çözümlenir; yalnız çıktıdaki anahtar değişir. Tel üzerindeki kimlik 2 sayısıdır.

Bunun aynadaki karşılığı da doğrudur ve daha tehlikelidir: numara değiştirmek sessizce kırar. İki alanın numaraları takas edilirse doğrulayıcı hiçbir şey söylemez, çünkü baytlar geçerlidir; yalnız değerler yanlış alanlara yerleşir. Şema öncelikli tasarımın değişmez kuralı buradan gelir: bir alan numarası bir kez verildikten sonra başka bir alana verilmez, kaldırılan numara yeniden kullanılmaz.

gRPC’nin Getirdikleri

Bu dersteki şema, doğrulayıcı ve çerçeveleme, bu tasarımın küçük bir örneğidir. gRPC aynı yapıyı hazır bir bütün olarak sunar: mesajlar Protocol Buffers şemasıyla tanımlanır, istemci ve sunucu tarafı bu şemadan üretilir, taşıma katmanı olarak HTTP/2 kullanılır.

HTTP/2’nin seçilmesi bir ayrıntı değil, dört çağrı türünü mümkün kılan şeydir. Şema dosyasındaki akis işareti bunu gösterir: bir çağrı tek istek–tek yanıt olabileceği gibi, sunucu tarafı akış (bir istek, çok yanıt), istemci tarafı akış (çok istek, tek yanıt) ya da çift yönlü akış olabilir. Kütüphane servisinde “raf sayımını akış olarak gönder” ya da “katalogda eşleşenleri geldikçe ver” gibi işler bu türlere oturur.

Bedeli de bellidir. Tel üzerindeki içerik gözle okunamaz; bir isteği incelemek için şemayı bilen bir araç gerekir. Tarayıcı bu protokolü doğrudan konuşamaz, araya bir çeviri katmanı girer. Ve arayüzü kullanmak için karşı tarafın şemasına erişmek zorunludur — REST’te bir adrese istek atıp yanıtı okumak yeterken, burada sözleşme olmadan tek bayt anlaşılmaz.

Bu bedeller, gRPC’nin en çok nerede kullanıldığını da açıklar: sistemin kendi bileşenleri arasında, iki tarafın da aynı ekipçe yönetildiği ve çağrı sayısının yüksek olduğu yerlerde.

Özet

  • Uzak yordam çağrısı, ağ üzerindeki işlemi yerel bir çağrı gibi gösterir; benzetme gecikme, kısmi başarısızlık ve bayta çevirme noktalarında bozulur ve tasarımı bu noktalar belirler.
  • Şema öncelikli tasarımda sözleşme makine okunur bir dosyadır; doğrulayıcı ve kodlayıcı ondan üretilir, böylece iki tarafın kuralları ayrışamaz.
  • Ölçümde ikili çerçeveleme aynı kayıtları metin biçimine göre yaklaşık yarı bayta indirdi; kazanç alan adlarının ve ayraçların tele hiç çıkmamasından gelir.
  • Tel üzerindeki kimlik alan numarasıdır: alan adı değiştiğinde aynı baytlar sorunsuz çözümlenir, ama numara değiştiğinde hata sessizce yanlış değer olarak görünür.
  • gRPC bu tasarımı Protocol Buffers şeması ve HTTP/2 taşımasıyla birleştirir; dört çağrı türünü mümkün kılar, karşılığında okunabilirlik ve tarayıcıdan doğrudan erişim kaybolur.

Sonraki Adım

Şema öncelikli tasarım yanıtın biçimini sözleşmede sabitler ve bu, ikinci derste ölçülen soruna geri döner: istemcinin gereksinimi değiştiğinde sunucu değişmek zorundadır. Bir de bunun tam tersini yapan yaklaşım vardır — biçimi istemcinin belirlediği, ama bunu gevşek bir seçim metniyle değil, sunucunun yayımladığı bir tip sistemi içinde yaptığı bir yaklaşım. Sonraki ders bu yaklaşımı kurar: tek uç nokta, tip sistemiyle sınırlanmış istemci güdümlü sorgu, alan başına çözümleme ve kısmen başarılı yanıt kavramı.

İ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