İçeriğe geç
academia.sh

Ders 14 / 18

Dağıtık İşlem Alternatifleri

İki aşamalı işlemenin gerçekleştirim maliyeti: oy ile karar arasında tutulan kilidin süresi, o sürede bloke kalan bağlantı ve reddedilen istek sayısı, eşgüdümcü düştüğünde katılımcılarda askıda kalan işlem — ve aynı işin tek turlu gerçekleştirmesiyle karşılaştırılması.

İçindekiler

Önceki konu sınırı çizdi, sınırın iki yanı arasındaki iletişimi kurdu ve adresin nasıl çözüleceğini gösterdi. Bu üç işin hiçbiri veriye dokunmadı. Katalog servisi kitabı, üyelik servisi ödünç sayacını kendi deposunda tutuyor; ama “ödünç ver” isteği hâlâ, iki yazmanın da birlikte olacağı ya da hiçbirinin olmayacağı varsayımıyla yazılıyor. O varsayımın dayanağı tek bir işlem sınırıydı ve sınır bölündüğünde kalktı.

Bu kaybın kendisi M16/K04’ün dağıtık işlem sorunu dersinde kurulmuş ve ölçülmüştür: iki ayrı depoya yapılan yazmalarda ikinci çağrı başarısız olunca birincisi kesinleşmiş kalır. O ölçüm burada tekrarlanmıyor. Burada ölçülen, o dersin bir cümleyle geçtiği klasik çözümün — iki aşamalı işleme — gerçekleştirim tarafındaki bedelidir: kilidin ne kadar süre tutulduğu, o sürede kaç isteğin bloke olduğu ve eşgüdümcü düştüğünde geride ne kaldığı.

Düzenek

İki servis gerçekten ayrı iki süreçtir; her biri kendi node:sqlite dosyasını açar ve aralarında yalnız yerel ağ üzerinden çağrı vardır.

DT1. Depo, yazma kilidini satır düzeyinde değil dosya düzeyinde tutar. Bu yüzden aşağıda sayılan “bloke istek” değeri bir üst sınırdır; satır kilitleyen bir depoda bloke olan küme, hazırlanmış işlemin dokunduğu satırlarla sınırlı kalır. Ölçümün biçimi — karar beklenirken kaynağın tutuluyor olması — iki durumda da aynıdır, büyüklüğü farklıdır.

DT2. Üyelik servisi oy vermeden önce sabit 400 ms harcar. Bu, yavaş katılımcının modelidir; gerçek bir ağdan ölçülmüş bir gecikme değildir. Yani kilit penceresinin büyüklüğü seçilmiş bir niceliktir; ölçülen şey, o büyüklükte bir pencerede ne olduğudur.

# kur.sh — onceki kosumu kapatir, iki servisin ayri depolarini bastan kurar
pkill -f katilimci.mjs 2>/dev/null; sleep 0.3
rm -f katalog.db* uyelik.db* servis.log
sqlite3 katalog.db >/dev/null <<'SQL'
PRAGMA journal_mode=WAL;
CREATE TABLE kitap (kitap_id INTEGER PRIMARY KEY, baslik TEXT NOT NULL, durumu TEXT NOT NULL);
INSERT INTO kitap VALUES (1,'Korluk','rafta'),(2,'Tutunamayanlar','rafta'),(3,'Kum Kitabi','rafta');
SQL
sqlite3 uyelik.db >/dev/null <<'SQL'
PRAGMA journal_mode=WAL;
CREATE TABLE uye (uye_id INTEGER PRIMARY KEY, ad TEXT NOT NULL, acik_odunc INTEGER NOT NULL, sinir INTEGER NOT NULL);
INSERT INTO uye VALUES (4,'Emre',2,3),(5,'Selin',3,3),(6,'Burak',0,9);
SQL

Katılımcı tarafı her iki servis için aynı modüldür; iş kuralı ve depo dışarıdan verilir. Ayırt edici nokta /hazirla ucudur: yazma yapılır, işlem kesinleştirilmez, bağlantı karar gelene kadar tutulur.

// katilimci.mjs — bir servisin katilimci tarafi; her servis ayri surec, ayri depo
import { createServer } from "node:http";
import { DatabaseSync } from "node:sqlite";

const [ad, dosya, port, isSql, rakipSql] = process.argv.slice(2);
const yavasMs = Number(process.env.YAVAS_MS ?? 0);
const havuz = Array.from({ length: 2 }, () => {
  const db = new DatabaseSync(dosya);
  db.exec("PRAGMA busy_timeout = 50");
  return { db, mesgul: false };
});
const askida = new Map();
const bekle = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
const bos = () => havuz.find((b) => !b.mesgul);

createServer((istek, yanit) => {
  const u = new URL(istek.url, "http://y");
  const y = (o) => yanit.end(JSON.stringify(o) + "\n");
  const islem = u.searchParams.get("islem");
  const anahtar = Number(u.searchParams.get("anahtar"));

  if (u.pathname === "/hazirla" || u.pathname === "/tekil") {
    const b = bos();
    if (!b) return y({ oy: "hayir", neden: "bos baglanti yok" });
    b.mesgul = true;
    if (yavasMs) bekle(yavasMs);
    b.db.exec("BEGIN IMMEDIATE");
    if (b.db.prepare(isSql).run(anahtar).changes !== 1) {
      b.db.exec("ROLLBACK"); b.mesgul = false; return y({ oy: "hayir", neden: "kosul saglanmadi" });
    }
    if (u.pathname === "/tekil") { b.db.exec("COMMIT"); b.mesgul = false; return y({ oy: "evet" }); }
    askida.set(islem, b);                  // kilit bu yanittan sonra da tutulmaya devam ediyor
    return y({ oy: "evet" });
  }
  if (u.pathname === "/kesinlestir" || u.pathname === "/iptal") {
    const b = askida.get(islem);
    if (!b) return y({ sonuc: "bilinmeyen islem" });
    b.db.exec(u.pathname === "/kesinlestir" ? "COMMIT" : "ROLLBACK");
    b.mesgul = false; askida.delete(islem);
    return y({ sonuc: u.pathname.slice(1) });
  }
  if (u.pathname === "/rakip") {           // isleme katilmayan, bagimsiz bir yerel yazma
    const b = bos();
    if (!b) return y({ sonuc: "bloke", neden: "bos baglanti yok" });
    b.mesgul = true;
    try { b.db.exec("BEGIN IMMEDIATE"); b.db.prepare(rakipSql).run(anahtar); b.db.exec("COMMIT");
          return y({ sonuc: "yazildi" }); }
    catch (h) { return y({ sonuc: "bloke", neden: h.code ?? h.message }); }
    finally { b.mesgul = false; }
  }
  if (u.pathname === "/askida")
    return y({ servis: ad, askida: [...askida.keys()], tutulan: havuz.filter((b) => b.mesgul).length });
  y({ hata: "bilinmeyen uc" });
}).listen(Number(port), () => console.log(`${ad} ayakta: ${port}`));
# baslat.sh — iki servis, iki ayri surec, iki ayri depo; uyelik yavas katilimci olarak kurulur
node katilimci.mjs katalog katalog.db 8801 \
  "UPDATE kitap SET durumu='odunc' WHERE kitap_id=? AND durumu='rafta'" \
  "UPDATE kitap SET durumu = CASE durumu WHEN 'rafta' THEN 'odunc' ELSE 'rafta' END WHERE kitap_id=?" \
  >>servis.log 2>&1 &
YAVAS_MS=400 node katilimci.mjs uyelik uyelik.db 8802 \
  "UPDATE uye SET acik_odunc=acik_odunc+1 WHERE uye_id=? AND acik_odunc<sinir" \
  "UPDATE uye SET acik_odunc=acik_odunc WHERE uye_id=?" \
  >>servis.log 2>&1 &
sleep 1

Aynı İş, İki Gerçekleştirme

Eşgüdümcü tek dosyadır ve aynı iş akışını iki biçimde yürütür. --sirayla kipi her servisin kendi işlemini hemen kesinleştirmesidir — M16/K04’te tutarsızlığı ölçülen düzen. Öntanımlı kip iki turludur: önce bütün katılımcılardan oy toplanır, sonra karar dağıtılır.

// esgudumcu.mjs — ayni is, iki gerceklestirme: --iki-asamali ve --sirayla
const kip = process.argv.includes("--sirayla") ? "sirayla" : "iki-asamali";
const cok = process.argv.includes("--cok");          // karar dagitilmadan once esgudumcu duser
const [kitapId, uyeId] = process.argv.slice(2).filter((a) => !a.startsWith("--")).map(Number);
const islem = `i-${kitapId}-${uyeId}`;
const katilimcilar = [
  { ad: "katalog", uc: "http://localhost:8801", anahtar: kitapId },
  { ad: "uyelik",  uc: "http://localhost:8802", anahtar: uyeId },
];
let mesaj = 0;
const cagir = async (k, yol) => {
  mesaj += 1;
  const c = await fetch(`${k.uc}/${yol}?islem=${islem}&anahtar=${k.anahtar}`);
  return c.json();
};
const ms = (a, b) => (b - a).toFixed(1);

if (kip === "sirayla") {                              // tek tur: her servis kendi isini kesinlestirir
  const t0 = performance.now();
  for (const k of katilimcilar) {
    const c = await cagir(k, "tekil");
    console.log(`  ${k.ad}: ${c.oy}${c.neden ? " (" + c.neden + ")" : ""}`);
  }
  console.log(`sirayla | tur=1 mesaj=${mesaj} | esgudumcu kilit penceresi=0 ms | sure=${ms(t0, performance.now())} ms`);
} else {                                              // iki tur: once oy, sonra karar
  const oylar = [];
  const t0 = performance.now();
  for (const k of katilimcilar) {
    const c = await cagir(k, "hazirla");
    oylar.push([k, c, performance.now()]);
    console.log(`  ${k.ad}: oy=${c.oy}${c.neden ? " (" + c.neden + ")" : ""}`);
  }
  const karar = oylar.every(([, c]) => c.oy === "evet") ? "kesinlestir" : "iptal";
  if (cok) {
    console.log(`esgudumcu karar dagitmadan dustu (karar=${karar} hicbir katilimciya ulasmadi)`);
    process.exit(1);
  }
  for (const [k, c] of oylar) if (c.oy === "evet") await cagir(k, karar);
  const t1 = performance.now();
  const [, , tOy] = oylar[0];
  console.log(`iki asamali | tur=2 mesaj=${mesaj} | karar=${karar}`);
  console.log(`  katalog kilit penceresi=${ms(tOy, t1)} ms | toplam=${ms(t0, t1)} ms (bu kosumda)`);
}

Bloke kalan kaynağı sayabilmek için sisteme, işleme katılmayan sıradan bir yük verilir.

// rakip.mjs — isleme katilmayan istekler; pencere boyunca duzenli araliklarla yazmayi dener
const [uc, anahtar, adet, aralik] = process.argv.slice(2);
let yazildi = 0, bloke = 0, toplamBekleme = 0;
for (let n = 0; n < Number(adet); n++) {
  const t = performance.now();
  const c = await (await fetch(`${uc}/rakip?anahtar=${anahtar}`)).json();
  toplamBekleme += performance.now() - t;
  c.sonuc === "yazildi" ? (yazildi += 1) : (bloke += 1);
  await new Promise((r) => setTimeout(r, Number(aralik)));
}
console.log(`rakip: istek=${adet} yazildi=${yazildi} bloke=${bloke}` +
            ` | ortalama bekleme=${(toplamBekleme / Number(adet)).toFixed(1)} ms (bu kosumda)`);

Kilit Penceresinde Bloke Kalan Kaynak

Aynı yük, aynı iş, iki gerçekleştirme. Yük katalog servisine gider; ödünç isteğiyle ilgisi yoktur, yalnız aynı depoya yazmak ister.

sh kur.sh; sh baslat.sh
node rakip.mjs http://localhost:8801 2 40 10 &
node esgudumcu.mjs 1 4
wait
  katalog: oy=evet
  uyelik: oy=evet
iki asamali | tur=2 mesaj=4 | karar=kesinlestir
  katalog kilit penceresi=459.5 ms | toplam=489.6 ms (bu kosumda)
rakip: istek=40 yazildi=34 bloke=6 | ortalama bekleme=12.4 ms (bu kosumda)
sh kur.sh; sh baslat.sh
node rakip.mjs http://localhost:8801 2 40 10 &
node esgudumcu.mjs 1 4 --sirayla
wait
  katalog: evet
  uyelik: evet
sirayla | tur=1 mesaj=2 | esgudumcu kilit penceresi=0 ms | sure=443.0 ms
rakip: istek=40 yazildi=40 bloke=0 | ortalama bekleme=2.6 ms (bu kosumda)

Koşumdan bağımsız iki nicelik burada belirleyicidir: tur sayısı 2’ye karşı 1, mesaj sayısı 4’e karşı 2. Katılımcı sayısı arttıkça mesaj sayısı 2n olarak büyür ve pencere en yavaş katılımcının hızına bağlanır.

Koşuma bağlı olanlar da tutarlıdır: katalog kilidi 459,5 ms tutuldu ve o pencerede 40 rakip isteğin 6’sı bloke oldu, ortalama bekleme 2,6 ms’ten 12,4 ms’e çıktı. Katalog kendi işini 30,1 ms’de bitirmişti; kilidi 459,5 ms tutmasının nedeni kendi işi değil, başka bir servisin oy vermesini bekliyor olmasıdır.

Eşgüdümcü Düştüğünde

İki aşamalı işlemenin asıl bedeli buradadır. Katılımcı “evet” dedikten sonra tek başına karar veremez: kesinleştirse öteki iptal etmiş olabilir, iptal etse öteki kesinleştirmiş olabilir. Kararı yalnız eşgüdümcü bilir.

sh kur.sh; sh baslat.sh
node esgudumcu.mjs 1 4 --cok
echo "-- katilimcilarin gorunumu --"
curl -s "http://localhost:8801/askida"; curl -s "http://localhost:8802/askida"
echo "-- disaridan okuma ve rakip yazma --"
sqlite3 katalog.db "SELECT kitap_id, durumu FROM kitap WHERE kitap_id=1;"
node rakip.mjs http://localhost:8801 2 10 10
echo "-- karar disaridan verilirse --"
ISLEM=$(curl -s "http://localhost:8801/askida" | sed 's/.*\["//;s/"\].*//')
curl -s "http://localhost:8801/kesinlestir?islem=$ISLEM"
curl -s "http://localhost:8802/kesinlestir?islem=$ISLEM"
node rakip.mjs http://localhost:8801 2 10 10
  katalog: oy=evet
  uyelik: oy=evet
esgudumcu karar dagitmadan dustu (karar=kesinlestir hicbir katilimciya ulasmadi)
-- katilimcilarin gorunumu --
{"servis":"katalog","askida":["i-1-4"],"tutulan":1}
{"servis":"uyelik","askida":["i-1-4"],"tutulan":1}
-- disaridan okuma ve rakip yazma --
1|rafta
rakip: istek=10 yazildi=0 bloke=10 | ortalama bekleme=67.4 ms (bu kosumda)
-- karar disaridan verilirse --
{"sonuc":"kesinlestir"}
{"sonuc":"kesinlestir"}
rakip: istek=10 yazildi=10 bloke=0 | ortalama bekleme=3.8 ms (bu kosumda)

İki serviste de 1’er askıda işlem, ikişer bağlantılık havuzların 1’er bağlantısı tutulmuş durumda. Kitabın durumu dışarıdan hâlâ rafta görünüyor — yazma görünmez, ama kilit gerçek: 10 rakip isteğin 10’u bloke oldu ve ortalama bekleme 67,4 ms’e çıktı. Bu sayı eşgüdümcü ayağa kalkana kadar düşmez. Kilit süresi artık bir gecikme değil, bir kesinti uzunluğudur.

Son iki çağrı kararı dışarıdan verir. Bu adım gerçek sistemlerde bir insanın ya da bir kurtarma yordamının işidir ve doğru kararı bilmek zorundadır; askıda kalan işlemin kesinleştirilmesi mi iptali mi gerektiği, katılımcıların elindeki bilgiden çıkarılamaz.

Karşılaştırma

Ölçü Sırayla yerel kesinleştirme İki aşamalı işleme
Tur sayısı 1 2
Mesaj sayısı (2 katılımcı) 2 4
Kilit penceresi 0 ms 459,5 ms (bu koşumda)
Pencerede bloke rakip istek 0 / 40 6 / 40 (bu koşumda)
Eşgüdümcü düşerse askıda işlem 0 2 (servis başına 1)
Eşgüdümcü düşerse bloke rakip istek 0 / 10 10 / 10

Neyi ucuzlattı: iki aşamalı işleme, kısmi sonucu ortadan kaldırdı. --sirayla kipinde katalog kesinleşip üyelik reddedildiğinde ortada sahibi olmayan bir ödünç kitap kalır; iki aşamalı kipte hiçbir katılımcı ötekinin oyunu görmeden kesinleştirmediği için bu durum oluşmaz. Uygulama kodunda telafi mantığı yazılmaz.

Neyi pahalılaştırdı: tur sayısı ikiye, mesaj sayısı 2n’e çıktı; kaynak, sistemin en yavaş katılımcısı kadar süre tutuldu ve o sürede işle ilgisiz istekler reddedildi.

Hangi yeni arıza kipi doğdu: belirsiz durum. Oyunu vermiş ama kararı almamış katılımcı, kilidi süresiz tutar; kaynağı serbest bırakmanın tek yolu dışarıdan verilecek bir karardır ve o kararı verecek olan, doğru yanıtı yerel bilgiyle bulamaz. Bu, sırayla kesinleştirmede bulunmayan bir kiptir.

Alternatifler bu bedelin nereye taşınacağını seçer. Telafi eden düzen kilidi hiç tutmaz, tutarsızlığı bir süre görünür kılar ve sıralamayı kısıtlar. Giden kutusu düzeni ise atomikliği tek bir deponun içine hapseder ve yayını ayrı bir işe devreder. İkisi de “ya hep ya hiç”i bırakıp “sonunda doğru”yu seçer.

Özet

  • İki aşamalı işleme iki servis süreci üzerinde kuruldu; oy turu ile karar turu arasında katalog servisi kilidini 459,5 ms tuttu, oysa kendi işi 30,1 ms sürmüştü.
  • Pencerenin uzunluğunu en yavaş katılımcı belirler; koşumdan bağımsız maliyet 1 tur yerine 2 tur ve n mesaj yerine 2n mesajdır.
  • O pencerede işle ilgisiz 40 isteğin 6’sı bloke oldu (bu koşumda) ve ortalama bekleme neredeyse beş katına çıktı; tek turlu gerçekleştirmede bloke istek sayısı sıfırdı.
  • Eşgüdümcü kararı dağıtmadan düştüğünde her katılımcıda 1 askıda işlem ve 1 tutulu bağlantı kaldı; rakip isteklerin tamamı bloke oldu ve bu durum dışarıdan karar verilene kadar sürdü.
  • Askıda kalan işlemin kesinleştirilmesi mi iptali mi gerektiği katılımcıların yerel bilgisinden çıkarılamaz; bu, tek turlu gerçekleştirmede olmayan yeni bir arıza kipidir.

Sonraki Adım

Kilit penceresini sıfırlamanın yolu, adımları hazırlamak yerine tek tek kesinleştirmek ve başarısızlıkta tamamlananları geri sarmaktır. M16/K04 bu düzenin bir telafi adımını kurmuştu, M19/K05 telafi edici işlemin bedelini ölçmüştü. Geriye ikisinin de değinmediği bir kısıt kalıyor: telafi edilebilirliği eşit olmayan adımlar bir sıraya dizilmek zorundadır ve o sıra keyfî değildir. Sonraki ders aynı ödünç akışını iki farklı adım sırasıyla koşturup her sırada geri alınamayan yan etkiyi sayar, sonra aynı akışı koreografi ve yürütücü olarak iki kez gerçekleştirip kod tarafındaki farkı — dokunulan servis ve bilinen adım sayısını — ölçer.

İ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