İçeriğe geç
academia.sh

Ders 13 / 16

Oturum ve Yapışkanlık

Oturumun üç yerleşimi aynı düzenek üzerinde ölçülür: süreç belleğinde sessizce boşalan sepet, düğüm eklendiğinde taşınan üyenin kaybolan oturumu, paylaşılan deponun istek başına okuması, imzalı belirtecin çerez boyu ve iptal edilemezliği.

İçindekiler

Önceki ders durumu uygulamadan çıkardı: yerel dosyalar, süreç içi sayaçlar ve düğüme bağlanan ne varsa dışarı taşındı, düğüm yerine konabilir hâle geldi. Geriye kullanıcıya bağlı tek bir durum kaldı: oturum. Oturum silinemez, yalnızca bir yere konur. Bu ders o yerin üç seçeneğini tek düzenek üzerinde ölçer.

Ölçülen nesne bölünmüş ödünç sisteminin ödünç sepetidir: üyenin ayırttığı ama henüz onaylamadığı kitaplar. Sepet oturumda durur, oturum da üç yerden birinde: süreç belleğinde, paylaşılan bir depoda ya da istemcideki imzalı bir belirteçte.

  • DP10. Ödünç düğümleri aynı sürümü çalıştırır; aralarında ağ ya da donanım farkı yoktur. Ölçümler arasında değişen tek şey oturumun yerleşimidir.
  • DP11. İstemci çerez taşır: her yanıttaki çerezi saklar, sonraki istekte geri gönderir.
  • DP12. Paylaşılan depo aynı makinede bir node:sqlite dosyasıdır; gerçek dağıtımda ayrı bir süreç olur, istek başına okuma sayısı değişmez.

Üç Yerleşim, Tek Düğüm

Düğüm üç yerleşimi de tanır; hangisini kullanacağı OTURUM_YERI ile seçilir. Üçünün de işi aynı: çerezden oturumu okumak, yazıp yeni çerezi döndürmek, iptal etmek. Ne yaptıkları değil, nerede yaptıkları farklıdır.

// dugum.mjs — odunc sepeti dugumu; oturumun nerede durdugu OTURUM_YERI ile secilir
import { createServer } from 'node:http';
import { createHmac, randomUUID, timingSafeEqual } from 'node:crypto';
import { DatabaseSync } from 'node:sqlite';

const AD = process.env.DUGUM_ADI;
const ANAHTAR = process.env.IMZA_ANAHTARI;
const LISTE = process.env.IPTAL_LISTESI === 'acik';
const sayac = { depoOkuma: 0, imzaDogrulama: 0 };

const depo = new DatabaseSync(process.env.DEPO_YOLU);
depo.exec('PRAGMA journal_mode = WAL');
depo.exec('CREATE TABLE IF NOT EXISTS oturum (kimlik TEXT PRIMARY KEY, sepet TEXT, iptal INTEGER)');
depo.exec('CREATE TABLE IF NOT EXISTS iptal (kimlik TEXT PRIMARY KEY)');
const oku = (sorgu, d) => { sayac.depoOkuma += 1; return depo.prepare(sorgu).get(d); };

const imzala = (govde) => createHmac('sha256', ANAHTAR).update(govde).digest('base64url');
const bos = () => ({ kimlik: randomUUID(), sepet: [], yeni: true });

const YERLESIM = {
  // 1. Surec bellegi: oturum yalniz bu surecin yigininda yasar.
  surec: {
    bellek: new Map(),
    oku(cerez) {
      const bulunan = this.bellek.get(cerez.oturum);
      return bulunan ? { kimlik: cerez.oturum, sepet: bulunan, yeni: false } : bos();
    },
    yaz(o) { this.bellek.set(o.kimlik, o.sepet); return `oturum=${o.kimlik}`; },
    iptal(o) { this.bellek.delete(o.kimlik); },
  },
  // 2. Paylasilan depo: oturum butun dugumlerin gordugu tek yerde durur.
  depo: {
    oku(cerez) {
      const s = cerez.oturum && oku('SELECT sepet, iptal FROM oturum WHERE kimlik = ?', cerez.oturum);
      return !s || s.iptal ? bos() : { kimlik: cerez.oturum, sepet: JSON.parse(s.sepet), yeni: false };
    },
    yaz(o) {
      depo.prepare('INSERT INTO oturum VALUES (?, ?, 0) ON CONFLICT(kimlik) DO UPDATE SET sepet = excluded.sepet')
        .run(o.kimlik, JSON.stringify(o.sepet));
      return `oturum=${o.kimlik}`;
    },
    iptal(o) { depo.prepare('UPDATE oturum SET iptal = 1 WHERE kimlik = ?').run(o.kimlik); },
  },
  // 3. Imzali belirtec: oturum istemcide durur, dugum yalniz imzayi dogrular.
  belirtec: {
    oku(cerez) {
      const [govde, imza] = String(cerez.belirtec ?? '').split('.');
      if (!govde || !imza) return bos();
      sayac.imzaDogrulama += 1;
      const beklenen = Buffer.from(imzala(govde));
      const gelen = Buffer.from(imza);
      if (beklenen.length !== gelen.length || !timingSafeEqual(beklenen, gelen)) return bos();
      const o = JSON.parse(Buffer.from(govde, 'base64url').toString());
      if (LISTE && oku('SELECT 1 FROM iptal WHERE kimlik = ?', o.kimlik)) return bos();
      return { ...o, yeni: false };
    },
    yaz(o) {
      const govde = Buffer.from(JSON.stringify({ kimlik: o.kimlik, sepet: o.sepet })).toString('base64url');
      return `belirtec=${govde}.${imzala(govde)}`;
    },
    iptal(o) { if (LISTE) depo.prepare('INSERT OR IGNORE INTO iptal VALUES (?)').run(o.kimlik); },
  },
}[process.env.OTURUM_YERI];

const cerezAyristir = (b) => Object.fromEntries((b ?? '').split(';').map((p) => p.trim())
  .filter(Boolean).map((p) => [p.slice(0, p.indexOf('=')), p.slice(p.indexOf('=') + 1)]));

createServer((istek, yanit) => {
  const yol = new URL(istek.url, 'http://yerel');
  const bas = { 'content-type': 'application/json' };
  if (yol.pathname === '/sayac') return yanit.writeHead(200, bas).end(JSON.stringify(sayac));
  const oturum = YERLESIM.oku(cerezAyristir(istek.headers.cookie));
  if (yol.pathname === '/cikis') {
    YERLESIM.iptal(oturum);
    return yanit.writeHead(200, bas).end(JSON.stringify({ dugum: AD, cikis: true }));
  }
  if (istek.method === 'POST') oturum.sepet = [...oturum.sepet, yol.searchParams.get('kitap')];
  const cerez = YERLESIM.yaz(oturum);
  yanit.writeHead(200, { ...bas, 'set-cookie': cerez });
  yanit.end(JSON.stringify({ dugum: AD, sepet: oturum.sepet.length, yeni: oturum.yeni, cerezBoyu: cerez.length }));
}).listen(Number(process.env.PORT), () => console.log(`hazir ${AD}`));

Aşağıdaki koşum paylaşılan depoyu seçiyor: iki ekleme, bir okuma.

OTURUM_YERI=depo DUGUM_ADI=a PORT=8719 DEPO_YOLU=oturum.db \
  IMZA_ANAHTARI=kutuphane-imza-anahtari node dugum.mjs > /dev/null &
SUNUCU=$!
sleep 0.6
curl -s -X POST -c cerez.txt 'http://127.0.0.1:8719/sepet?kitap=K-101'; echo
curl -s -X POST -b cerez.txt -c cerez.txt 'http://127.0.0.1:8719/sepet?kitap=K-102'; echo
curl -s -b cerez.txt 'http://127.0.0.1:8719/sepet'; echo
kill $SUNUCU
{"dugum":"a","sepet":1,"yeni":true,"cerezBoyu":43}
{"dugum":"a","sepet":2,"yeni":false,"cerezBoyu":43}
{"dugum":"a","sepet":2,"yeni":false,"cerezBoyu":43}

Sepet ikiye çıktı, çerez 43 bayt kaldı. Tek düğümde yerleşim kararının gözlenebilir bir sonucu yoktur; kararın neden ertelendiğini bu açıklar.

Dağıtım Değişince Sepete Ne Oluyor

Ölçüm iki düğümle başlar. Dağıtıcı istekleri sırayla dağıtır; istemci çerez taşır.

// dagitici.mjs — dugumleri baslatir, istegi sirayla ya da yapiskan bicimde dagitir
import { spawn } from 'node:child_process';
import { createHash } from 'node:crypto';
import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';

const kok = mkdtempSync(join(tmpdir(), 'oturum-'));
export const temizle = () => rmSync(kok, { recursive: true, force: true });
export const durdur = (d) => d.forEach((x) => x.cocuk.kill('SIGKILL'));

export async function dugumBaslat(yer, ad, port, ek = {}) {
  const cocuk = spawn(process.execPath, ['dugum.mjs'], { stdio: ['ignore', 'pipe', 'inherit'],
    env: { ...process.env, OTURUM_YERI: yer, DUGUM_ADI: ad, PORT: String(port),
           IMZA_ANAHTARI: 'kutuphane-imza-anahtari', DEPO_YOLU: join(kok, 'oturum.db'), ...ek } });
  await new Promise((c) => cocuk.stdout.once('data', c));
  return { ad, port, cocuk };
}

// Cerez kavanozu tasiyan istemci: her yanittaki cerezi saklar, bir sonrakinde geri gonderir.
export function istemci(kimlik) {
  const kavanoz = new Map();
  return { kimlik, async cagir(dugum, yol) {
    const cerez = [...kavanoz].map(([a, d]) => `${a}=${d}`).join('; ');
    const yanit = await fetch(`http://127.0.0.1:${dugum.port}${yol}`,
      { method: yol.includes('kitap=') ? 'POST' : 'GET', headers: cerez ? { cookie: cerez } : {} });
    for (const ham of yanit.headers.getSetCookie()) {
      kavanoz.set(ham.slice(0, ham.indexOf('=')), ham.slice(ham.indexOf('=') + 1));
    }
    return yanit.json();
  } };
}

export const sirayla = (d, n) => d[n % d.length];
export const yapiskan = (d, kimlik) => d[createHash('sha256').update(kimlik).digest()[0] % d.length];

Birinci ölçümde tek üye sekiz kitabı, sırayla dağıtılan sekiz istekle sepete ekler. İkincisi yapışkan dağıtımı açar — aynı üye hep aynı düğüme gider — ve düğüm sayısını üçe çıkarır.

// yerlesim-olcumu.mjs — sirayla dagitilan sekiz ekleme, sonra yapiskan dagitimda ucuncu dugum
import { dugumBaslat, durdur, temizle, istemci, sirayla, yapiskan } from './dagitici.mjs';

const KITAP = ['K-101', 'K-102', 'K-103', 'K-104', 'K-105', 'K-106', 'K-107', 'K-108'];
const UYELER = Array.from({ length: 12 }, (_, i) => `U-${String(i + 1).padStart(4, '0')}`);

async function sirayliOlcum(yer) {
  const d = [await dugumBaslat(yer, 'a', 8710), await dugumBaslat(yer, 'b', 8711)];
  const uye = istemci('U-0001');
  const bildirilen = [];
  for (const [i, kitap] of KITAP.entries()) {
    bildirilen.push((await uye.cagir(sirayla(d, i), `/sepet?kitap=${kitap}`)).sepet);
  }
  const son = await uye.cagir(d[0], '/sepet');
  durdur(d);
  return `${yer.padEnd(10)} ${bildirilen.join(' ').padEnd(24)} ${son.sepet}`;
}

async function yapiskanOlcum(yer) {
  const iki = [await dugumBaslat(yer, 'a', 8712), await dugumBaslat(yer, 'b', 8713)];
  const istemciler = UYELER.map(istemci);
  for (const uye of istemciler) {
    for (const kitap of ['K-201', 'K-202', 'K-203']) await uye.cagir(yapiskan(iki, uye.kimlik), `/sepet?kitap=${kitap}`);
  }
  const uc = [...iki, await dugumBaslat(yer, 'c', 8714)];
  let tasinan = 0, bosalan = 0;
  for (const uye of istemciler) {
    if (yapiskan(uc, uye.kimlik).ad !== yapiskan(iki, uye.kimlik).ad) tasinan += 1;
    if ((await uye.cagir(yapiskan(uc, uye.kimlik), '/sepet')).sepet === 0) bosalan += 1;
  }
  durdur(uc);
  return `${yer.padEnd(10)} ${String(tasinan).padEnd(9)} ${String(bosalan).padEnd(9)} ${UYELER.length - bosalan}`;
}

console.log('sirayla dagitim, tek uye, sekiz ekleme (hicbir istek hata donmedi)');
console.log('yerlesim   eklemede bildirilen sepet  son okuma');
for (const yer of ['surec', 'depo', 'belirtec']) console.log(await sirayliOlcum(yer));
console.log('');
console.log('yapiskan dagitim, on iki uye, dugum sayisi ikiden uce cikiyor');
console.log('yerlesim   tasinan   bosalan   sepeti duran');
for (const yer of ['surec', 'depo', 'belirtec']) console.log(await yapiskanOlcum(yer));
temizle();
sirayla dagitim, tek uye, sekiz ekleme (hicbir istek hata donmedi)
yerlesim   eklemede bildirilen sepet  son okuma
surec      1 1 1 1 1 1 1 1          0
depo       1 2 3 4 5 6 7 8          8
belirtec   1 2 3 4 5 6 7 8          8

yapiskan dagitim, on iki uye, dugum sayisi ikiden uce cikiyor
yerlesim   tasinan   bosalan   sepeti duran
surec      7         7         5
depo       7         0         12
belirtec   7         0         12

Birinci tablonun surec satırı bu dersin çekirdeğidir. Sekiz istek gönderildi, sekizi de başarıyla döndü ve her biri sepette bir kitap bildirdi: her istek bir önceki düğümün bilmediği bir çerezle gelince düğüm yeni bir oturum açıyor ve kitabı ona ekliyor. Sepet hiç birikmiyor, son okumada görünen sayı sıfır. Uygulama kayıp bildirmedi çünkü kendi bakış açısından kayıp yok: her istek geçerli bir oturumla bitti.

İkinci tablo yapışkanlığın neyi satın alıp neyi almadığını gösteriyor. Yapışkan dağıtımda süreç belleği çalışır görünür; sorun düğüm kümesi değiştiğinde çıkar. Üçüncü düğüm eklenince on iki üyenin yedisi başka bir düğüme taşındı ve süreç belleği yerleşiminde o yedi sepet boşaldı. Taşınan üye sayısı üç yerleşimde de aynı — dağıtım kuralı aynı kural — ama sepeti boşalan üye sayısı yalnız süreç belleğinde sıfırdan farklı. Yapışkanlık oturumu düğüme bağlar; düğüm kümesi değiştiği anda o bağ kopar. Yayın da düğüm kümesini değiştirir.

İstek Başına Ödenen Bedel

Paylaşılan depo ve imzalı belirteç ikinci tabloda aynı sonucu verdi. Farkları üç yerde: istek başına yapılan iş, çerezde taşınan bayt ve çıkıştan sonrası.

// maliyet.mjs — istek basina dogrulama isi, cerez boyu ve cikistan sonra gecen istek
import { dugumBaslat, durdur, temizle, istemci } from './dagitici.mjs';

const ISTEK = 200;
const BOY = [1, 8, 32, 128];

async function olc(yer, ek = {}, ad = yer) {
  const dugum = await dugumBaslat(yer, 'a', 8715, ek);
  const uye = istemci('U-0001');
  const boy = new Map();
  for (let i = 1; i <= ISTEK; i += 1) {
    const yanit = await uye.cagir(dugum, `/sepet?kitap=K-${300 + i}`);
    if (BOY.includes(i)) boy.set(i, yanit.cerezBoyu);
  }
  const sayac = await fetch(`http://127.0.0.1:${dugum.port}/sayac`).then((y) => y.json());
  await uye.cagir(dugum, '/cikis');
  let gecen = 0;
  for (let i = 0; i < 5; i += 1) if ((await uye.cagir(dugum, '/sepet')).sepet > 0) gecen += 1;
  durdur([dugum]);
  return { ad, ...sayac, boy, gecen };
}

const o = [await olc('surec'), await olc('depo'), await olc('belirtec'),
           await olc('belirtec', { IPTAL_LISTESI: 'acik' }, 'belirtec+liste')];

console.log(`${ISTEK} istekte yapilan is ve cikistan sonra kabul edilen bes istek`);
console.log('yerlesim        depo okuma  imza dogrulama  cikistan sonra gecen');
for (const s of o) {
  console.log(`${s.ad.padEnd(15)} ${String(s.depoOkuma).padEnd(11)} ${String(s.imzaDogrulama).padEnd(15)} ${s.gecen} / 5`);
}
console.log('');
console.log('cerezde tasinan bayt, sepetteki kitap sayisina gore');
console.log(`yerlesim        ${BOY.map((n) => `${n} kitap`.padEnd(10)).join('')}`);
for (const s of o) console.log(`${s.ad.padEnd(15)} ${BOY.map((n) => String(s.boy.get(n)).padEnd(10)).join('')}`);
temizle();
200 istekte yapilan is ve cikistan sonra kabul edilen bes istek
yerlesim        depo okuma  imza dogrulama  cikistan sonra gecen
surec           0           0               0 / 5
depo            199         0               0 / 5
belirtec        0           199             5 / 5
belirtec+liste  199         199             0 / 5

cerezde tasinan bayt, sepetteki kitap sayisina gore
yerlesim        1 kitap   8 kitap   32 kitap  128 kitap 
surec           43        43        43        43        
depo            43        43        43        43        
belirtec        143       217       473       1497
belirtec+liste  143       217       473       1497

İki yüz istekte 199 okuma ya da 199 doğrulama sayıldı; eksik olan tek istek çerezsiz gelen ilkidir. Sayılar koşumdan bağımsız: depo istek başına bir okuma, belirteç istek başına bir doğrulama yapar. Bedelleri aynı yerde ödenmez — depo okuması ağ üzerinden gider ve düğüm sayısı arttıkça deponun üstüne biner, imza doğrulaması düğümle birlikte ölçeklenir.

Çerez sütunu belirtecin bedelidir. Kimlikle yetinen iki yerleşimde çerez sepet ne kadar büyürse büyüsün 43 baytta duruyor; belirteçte sepet 128 kitaba çıktığında 1497 bayta çıkıyor ve bu bayt her istekte iki yönde taşınıyor.

Son sütun kararı belirleyen yerdir. Çıkıştan sonra gönderilen beş istek paylaşılan depoda sıfır kez, imzalı belirteçte beşinde de kabul edildi: düğüm imzayı doğruluyor, imza geçerli, oturum yaşıyor. Belirteç kendi başına iptal edilemez. IPTAL_LISTESI açıldığında iptal çalışıyor — ama belirtec+liste satırı hem 199 doğrulama hem 199 depo okuması gösteriyor: iptal edilebilirlik, belirtecin depodan kaçma üstünlüğünü geri veriyor.

Yanlış Anahtarın Sessizliği

Belirteç yerleşiminin doğru ayarı, bütün düğümlerin aynı imza anahtarını taşımasıdır. Üç düğümden birine başka bir anahtar verildiğinde ne olduğu sayılabilir.

// yanlis-anahtar.mjs — uc dugumden birine baska imza anahtari verilirse ne olur
import { dugumBaslat, durdur, temizle, istemci, sirayla } from './dagitici.mjs';

async function olc(ucuncununAnahtari) {
  const d = [await dugumBaslat('belirtec', 'a', 8716), await dugumBaslat('belirtec', 'b', 8717),
             await dugumBaslat('belirtec', 'c', 8718, { IMZA_ANAHTARI: ucuncununAnahtari })];
  const uye = istemci('U-0001');
  let sifirlanan = 0, hataKodu = 0;
  const izlek = [];
  for (let i = 0; i < 12; i += 1) {
    const yanit = await uye.cagir(sirayla(d, i), `/sepet?kitap=K-${400 + i}`);
    if (yanit.yeni && i > 0) sifirlanan += 1;
    izlek.push(`${yanit.dugum}:${yanit.sepet}`);
  }
  durdur(d);
  return { sifirlanan, hataKodu, izlek: izlek.join(' ') };
}

for (const [etiket, anahtar] of [['dogru ayar    ', 'kutuphane-imza-anahtari'],
                                 ['yanlis anahtar', 'eski-imza-anahtari']]) {
  const s = await olc(anahtar);
  console.log(`${etiket}: sessizce sifirlanan oturum ${s.sifirlanan}/11, hata kodu donen istek ${s.hataKodu}/12`);
  console.log(`  dugum:sepet  ${s.izlek}`);
}
temizle();
dogru ayar    : sessizce sifirlanan oturum 0/11, hata kodu donen istek 0/12
  dugum:sepet  a:1 b:2 c:3 a:4 b:5 c:6 a:7 b:8 c:9 a:10 b:11 c:12
yanlis anahtar: sessizce sifirlanan oturum 7/11, hata kodu donen istek 0/12
  dugum:sepet  a:1 b:2 c:1 a:1 b:2 c:1 a:1 b:2 c:1 a:1 b:2 c:1

İzlek satırı farkı tek bakışta veriyor: doğru ayarda sepet birden on ikiye kadar sayıyor, yanlış anahtarla ikiyi geçemiyor ve her üçüncü istekte başa dönüyor. On iki isteğin tamamı her iki koşumda da başarıyla döndü; hata kodu sayısı sıfır. Anahtarı farklı olan düğüm hata bildirmiyor, doğrulayamadığı belirteci tanımadı sayıp yeni bir oturum açıyor — sonra kendi anahtarıyla imzaladığı çerezi geri gönderiyor ve diğer iki düğüm de onu tanımıyor. Ayar hatası, kaybolan sepet olarak görünüyor.

Ayarın Yeri

Yerleşim kararı tek bir ayar değildir; kaç yere dağıldığı yerleşime göre değişir.

Ayar Katman Kaç yerde durur
oturum yerleşimi uygulama kodu her düğümde bir
yapışkanlık kuralı istek dağıtıcı tek yerde
depo adresi ortam değişkeni her düğümde bir
imza anahtarı ortam değişkeni her düğümde bir

Dört düğümlü bir kurulumda süreç belleği beş ayar noktası ister (dört düğüm artı dağıtıcıdaki yapışkanlık kuralı), paylaşılan depo sekiz, imzalı belirteç sekiz, iptal listeli belirteç on iki; yirmi düğümde yirmi bir, kırk, kırk ve altmış. Anlamlı olan toplam değil, her düğümde bir satırlarıdır: oradaki bir değerin tek bir düğümde farklı kalması yukarıdaki sessiz sonucu üretir. Bu yüzden imza anahtarı ve depo adresi tek kaynaktan dağıtılır, düğüm başına elle girilmez.

Özet

  • Sepet oturumda durur; oturum üç yerden birine konur: süreç belleği, paylaşılan depo, istemcideki imzalı belirteç. Tek düğümde üçü de aynı davranır.
  • Sırayla dağıtılan sekiz ekleme, süreç belleğinde sepeti hiç biriktirmedi: sekiz istek de başarıyla döndü, son okumada sepet sıfır çıktı, hiçbiri hata bildirmedi.
  • Düğüm sayısı ikiden üçe çıktığında on iki üyenin yedisi taşındı; süreç belleğinde o yedi sepet boşaldı, diğer iki yerleşimde sıfır.
  • Paylaşılan depo istek başına bir okuma yapar ve iptali destekler; imzalı belirteç okuma yapmaz ama çerezi 43 bayttan 1497 bayta çıkarır ve çıkıştan sonraki beş isteğin beşini de kabul eder. İptal listesi eklenince depo okuması geri gelir.
  • Üç düğümden birine farklı imza anahtarı verildiğinde on bir istekten yedisi sessizce yeni oturum açtı; hata kodu dönen istek sayısı sıfır kaldı.

Sonraki Adım

Bu dersin son ölçümü düğüm sayısını ikiden üçe çıkardı ve bunun oturumlara ne yaptığını saydı. Düğüm kümesini değiştiren tek olay ölçekleme değildir: her yayın da düğümleri durdurur, yenisini başlatır, bir süre ikisini birlikte çalıştırır. Sonraki ders yayının kendisini ölçer: sabit hızda istek akarken sürüm değiştirildiğinde kaç istek düşer, bu kaç saniyelik kesintiye denk gelir, açık bağlantıları boşaltmak bu sayıyı ne kadar düşürür ve geri alma kaç adımda tamamlanır.

İ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