Ders 10 / 25
Yayımla–Abone Ol
Tek iletinin birden çok ilgiliye ulaştırılması: konu ile abonelik ayrımı, tek yayımın abone sayısına göre kaç teslime dönüştüğünün sayılması, yavaş abonenin birikiminin diğerlerinden yalıtılması, süzgeçle elenen kopyaların ölçülmesi ve yayımdan sonra kurulan aboneliğin geçmişi görememesi.
İçindekiler
Önceki ders kuyruğun vermediklerini üç maddede toplamıştı; sonuncusu yayındı. Bir ileti tam olarak bir tüketiciye gider, çünkü alınan ileti diğerleri için görünmez olur. Oysa ödünç verme olayı üç ayrı tarafı ilgilendiriyor: üyeye gecikme bildirimi gönderen taraf, şube stok özetini yeniden hesaplayan taraf ve aylık rapora satır ekleyen taraf. Aynı olayın üçüne birden ulaşması gerekiyor.
Bu dersin sorusu, o “üçüne birden”in nereye yazılacağıdır. Yanıt yalnız bir dağıtım düzeneği değil, aynı zamanda üreticinin kimin dinlediğini bilmemesini sağlayan bir ayrımdır.
Üreticinin Üç Hedefi Sorunu
En kısa çözüm, üreticinin üç kuyruğa üç kopya yazmasıdır. Ölçülebilir bir bedeli vardır: üretici artık üç hedefin adını bilir. Dördüncü ilgili taraf — diyelim ki arama dizini — eklendiğinde ödünç verme kodunun değişmesi gerekir. Beşincisi eklendiğinde yine değişir. Kuyruk üretici ile tüketiciyi zamanda ayırmıştı, ama üretici hâlâ tüketicilerin listesini taşıyor.
İş Mantığı Yerleşimi konusunda alan olaylarının çözdüğü bağlanma sorunu budur: yayımlayan taraf dinleyicileri saymaz, yalnız olduğu şeyi bildirir. Yayımla–abone ol (publish–subscribe), aynı ayrımı süreç sınırının ötesine taşır. Üretici tek bir konuya (topic) yazar; kimin okuyacağını konu bilir.
Konu ve Abonelik
Ayrımın iki adı vardır ve karıştırılmamaları gerekir. Konu, iletilerin yazıldığı adlı kanaldır. Abonelik (subscription), bir ilgili tarafın o konudaki kendi teslim sırasıdır. Yayımlanan her ileti, yayım anında var olan her aboneliğe birer kopya olarak düşürülür; abonelikler birbirinin ilerlemesini etkilemez.
Aşağıdaki model bu ikisini tutar. Zaman gerçek saatten değil, her abonelik için açıkça harcanan bir bütçeden okunur; böylece ölçümler makineden bağımsız kalır.
// konu.mjs — konu ve abonelikler: yayimlanan ileti her abonelige birer kopya dusurur export class Konu { constructor() { this.abonelikler = new Map(); // ad -> {kuyruk, sure, suzgec, islenen, elenen} this.yayin = 0; // yayimlanan ileti sayisi this.kopya = 0; // aboneliklere dusurulen kopya sayisi this.teslim = 0; // aboneler tarafindan islenen kopya sayisi } aboneOl(ad, { sure = 1, suzgec = () => true } = {}) { this.abonelikler.set(ad, { ad, kuyruk: [], sure, suzgec, islenen: 0, elenen: 0 }); } yayimla(tur, govde) { // uretici tarafi: tek cagri const ileti = { id: ++this.yayin, tur, govde }; for (const a of this.abonelikler.values()) { if (!a.suzgec(tur)) { a.elenen += 1; continue; } a.kuyruk.push(ileti); this.kopya += 1; } return ileti.id; } ilerlet(butce) { // her abonelik kendi hizinda ilerler for (const a of this.abonelikler.values()) { let kalan = butce; while (a.kuyruk.length > 0 && kalan >= a.sure) { a.kuyruk.shift(); a.islenen += 1; this.teslim += 1; kalan -= a.sure; } } } bekleyen(ad) { return this.abonelikler.get(ad).kuyruk.length; } islenen(ad) { return this.abonelikler.get(ad).islenen; } }
Üreticinin kaç çağrı yaptığı ile sistemin kaç teslim yaptığı ayrı iki sayıdır. Aşağıdaki koşum abone sayısını birden dörde çıkarırken üretici tarafını hiç değiştirmez.
// tek-yayin.mjs — tek yayimla cagrisinin abone sayisina gore kac teslime dondugu import { Konu } from "./konu.mjs"; for (const aboneler of [["bildirim"], ["bildirim", "stok-ozeti"], ["bildirim", "stok-ozeti", "rapor"], ["bildirim", "stok-ozeti", "rapor", "arama-dizini"]]) { const k = new Konu(); for (const ad of aboneler) k.aboneOl(ad); for (const n of [1, 2, 3, 4]) k.yayimla("odunc_verildi", { oduncId: n }); k.ilerlet(1000); console.log(`abone=${aboneler.length} yayimla cagrisi=${k.yayin} ` + `dusurulen kopya=${k.kopya} islenen teslim=${k.teslim}`); }
node tek-yayin.mjs
abone=1 yayimla cagrisi=4 dusurulen kopya=4 islenen teslim=4 abone=2 yayimla cagrisi=4 dusurulen kopya=8 islenen teslim=8 abone=3 yayimla cagrisi=4 dusurulen kopya=12 islenen teslim=12 abone=4 yayimla cagrisi=4 dusurulen kopya=16 islenen teslim=16
Dört satırın hepsinde yayım çağrısı dört; düşürülen kopya sayısı abone sayısıyla çarpılıyor. Bu çarpım yayımla–abone ol modelinin hem gücü hem maliyetidir. Gücü: üretici dört aboneli durumda da tek satır yazar, ilgili taraf eklemek üreticiye dokunmaz. Maliyeti: sistemin taşıdığı iş, yayım sayısı çarpı abone sayısı kadar büyür. Yayma (fan-out) çarpanı, iletiyi üretmenin değil taşımanın maliyetini belirler.
Aboneliğin kendi teslim sırası olması, önceki dersteki rekabet eden tüketici düzenini geçersiz kılmaz; onunla birleşir. Bir abonelik içinde birden çok işçi çalışabilir ve o aboneliğe düşen kopyaları aralarında paylaşır. İki düzenin ayrımı tek cümleyle tutulur: abonelikler arasında kopyalama, abonelik içinde paylaşma vardır.
Yavaş Abone Diğerlerini Bekletir mi
Ayrı teslim sırasının asıl sınavı budur. Aylık rapor abonesi diğerlerinden yavaş çalışsın: her iletiyi işlemesi üç zaman birimi sürsün, diğerlerininki bir. Her tur dört olay yayımlanır ve her abonelik turda altı zaman birimi harcayabilir.
// yavas-abone.mjs — rapor abonesi digerlerinden uc kat yavas; birikim abonelik basina olculur import { Konu } from "./konu.mjs"; const k = new Konu(); k.aboneOl("bildirim", { sure: 1 }); k.aboneOl("stok-ozeti", { sure: 1 }); k.aboneOl("rapor", { sure: 3 }); const satir = (...v) => v.map((x) => String(x).padStart(12)).join(""); console.log(satir("tur", "bildirim", "stok-ozeti", "rapor"), " (islenen/bekleyen)"); for (let tur = 1; tur <= 6; tur++) { for (let i = 0; i < 4; i++) k.yayimla("odunc_verildi", { tur, i }); k.ilerlet(6); // her abonelik turda 6 zaman birimi harcayabilir const g = (ad) => `${k.islenen(ad)}/${k.bekleyen(ad)}`; console.log(satir(tur, g("bildirim"), g("stok-ozeti"), g("rapor"))); }
node yavas-abone.mjs
tur bildirim stok-ozeti rapor (islenen/bekleyen)
1 4/0 4/0 2/2
2 8/0 8/0 4/4
3 12/0 12/0 6/6
4 16/0 16/0 8/8
5 20/0 20/0 10/10
6 24/0 24/0 12/12
Altı turun sonunda bildirim ve stok özeti yirmi dörder iletiyi bitirmiş, hiçbiri beklemiyor. Rapor abonesi on iki iletide kalmış ve on iki ileti biriktirmiş. Birikim yalnız yavaş aboneliktedir; hızlı olanların ilerlemesi hiç yavaşlamamıştır.
Bu yalıtımı sağlayan şey abonelik başına ayrı sıradır. Üç abone tek bir sırayı paylaşsaydı, en yavaş olan diğerlerinin önünde durur ve tüm konunun hızını belirlerdi. Ayrı sıralar bunu önler, ama sorunu yok etmez: yavaş abonenin birikimi bellekte ya da diskte gerçek bir yer kaplar ve büyümesi sınırsızdır. Önceki dersin kuyruk derinliği ölçütü burada abonelik başına izlenir; hangi abonenin geride kaldığı toplam sayıya bakarak görülmez.
Her Abone Her İletiyi İstemez
Konuya yalnız ödünç verme olayı yazılmaz. İade, üye kaydı ve şube eklenmesi de aynı kanaldan geçer. Bildirim abonesi üye kaydıyla ilgilenmez, rapor abonesi yalnız ödünç verme satırı yazar. İlgilenmediği iletiyi alıp atmak, aboneliğe düşürülen her kopyanın yazma ve okuma maliyetini boşa harcamak demektir.
Aboneliğe bir süzgeç verilir: iletinin türüne bakarak kopyanın düşürülüp düşürülmeyeceğine karar eden koşul. Süzgecin yayım tarafında çalışması belirleyicidir; elenen ileti aboneliğe hiç yazılmaz.
// suzgec.mjs — abonelik yalniz ilgilendigi turleri alir; elenen ileti kopyalanmaz import { Konu } from "./konu.mjs"; const TURLER = ["odunc_verildi", "iade_alindi", "uye_kaydedildi", "odunc_verildi", "sube_eklendi", "iade_alindi", "uye_kaydedildi", "odunc_verildi"]; function kosum(suzgecli) { const k = new Konu(); const s = (kume) => suzgecli ? (t) => kume.has(t) : () => true; k.aboneOl("bildirim", { suzgec: s(new Set(["odunc_verildi", "iade_alindi"])) }); k.aboneOl("stok-ozeti", { suzgec: s(new Set(["odunc_verildi", "iade_alindi", "sube_eklendi"])) }); k.aboneOl("rapor", { suzgec: s(new Set(["odunc_verildi"])) }); for (const t of TURLER) k.yayimla(t, {}); k.ilerlet(1000); return k; } for (const suzgecli of [false, true]) { const k = kosum(suzgecli); console.log(`suzgec=${suzgecli ? "var" : "yok"} yayin=${k.yayin} dusurulen kopya=${k.kopya} ` + `bildirim=${k.islenen("bildirim")} stok-ozeti=${k.islenen("stok-ozeti")} ` + `rapor=${k.islenen("rapor")}`); }
node suzgec.mjs
suzgec=yok yayin=8 dusurulen kopya=24 bildirim=8 stok-ozeti=8 rapor=8 suzgec=var yayin=8 dusurulen kopya=14 bildirim=5 stok-ozeti=6 rapor=3
Sekiz yayım, süzgeçsiz durumda yirmi dört kopya üretiyor; süzgeçle on dört. Rapor abonesi sekiz yerine üç ileti işliyor ve atacağı beş iletiyi hiç görmüyor. Süzgecin bir de gizli faydası vardır: aboneliğin ne istediği kodun içinde değil, aboneliğin tanımında durur. Bildirim abonesinin hangi olaylarla ilgilendiği, tüketici kodunu okumadan görülür.
Süzgecin sınırı, karar verebilmek için iletinin başlığında ne olduğuna bağlıdır. Türe bakan bir süzgeç ucuzdur. Gövdenin içindeki bir alana bakan süzgeç — “yalnız Merkez şubesinin ödünçleri” — iletinin çözülmesini gerektirir ve maliyeti abonelik sayısıyla çarpılır. Bu yüzden ayrım ölçütü genelde iletinin gövdesine değil, yayım anında verilen türe ya da konuya konur.
Yayımla–Abone Ol’un Sınırı: Geçmiş Yoktur
Modelin en belirleyici özelliği ölçümde açığa çıkar. Kopya, yayım anında var olan aboneliklere düşürülür. O anda olmayan bir abonelik için ileti hiç yazılmamıştır.
// gec-abone.mjs — abonelik yayimdan sonra kurulursa onceki iletiler o abonelige hic dusmez import { Konu } from "./konu.mjs"; const k = new Konu(); k.aboneOl("bildirim"); k.aboneOl("stok-ozeti"); for (const n of [1, 2, 3]) k.yayimla("odunc_verildi", { oduncId: n }); k.ilerlet(1000); console.log(`uc yayin sonra : bildirim=${k.islenen("bildirim")} stok-ozeti=${k.islenen("stok-ozeti")}`); k.aboneOl("arama-dizini"); // yeni ilgili taraf sonradan baglandi for (const n of [4, 5]) k.yayimla("odunc_verildi", { oduncId: n }); k.ilerlet(1000); console.log(`bes yayin sonra: bildirim=${k.islenen("bildirim")} stok-ozeti=${k.islenen("stok-ozeti")} ` + `arama-dizini=${k.islenen("arama-dizini")}`); console.log(`toplam yayin=${k.yayin} arama-dizininin goremedigi=${k.yayin - k.islenen("arama-dizini")}`);
node gec-abone.mjs
uc yayin sonra : bildirim=3 stok-ozeti=3 bes yayin sonra: bildirim=5 stok-ozeti=5 arama-dizini=2 toplam yayin=5 arama-dizininin goremedigi=3
Arama dizini beş ödünç olayının ikisini gördü. Kaçırdığı üçü bir yerde bekliyor değil; o kopyalar hiç var olmadı. Aynı boşluk ikinci bir biçimde de ortaya çıkar: işlenmiş bir kopya aboneliğin sırasından silinir, dolayısıyla arama dizini bir hata yüzünden kendi verisini kaybederse gördüğü iletileri de yeniden okuyamaz.
Bu, modelin kusuru değil tanımıdır. Yayımla–abone ol iletiyi dağıtır, saklamaz. Saklamak isteyen abone, aldığını kendi tarafında biriktirmek zorundadır. Yeni bir ilgili tarafın geçmişe bakarak kendini kurabilmesi ya da bir abonenin geriye dönüp yeniden okuyabilmesi gerekiyorsa, ihtiyaç duyulan şey dağıtım değil, kalıcı bir kayıttır.
Özet
- Konu iletilerin yazıldığı adlı kanaldır; abonelik bir ilgili tarafın o konudaki kendi teslim sırasıdır. Abonelikler arasında kopyalama, abonelik içinde paylaşma olur.
- Dört yayım çağrısı, abone sayısı birden dörde çıktığında dört yerine on altı kopyaya dönüştü; üretici kodu hiç değişmedi. Yayma çarpanı, taşıma maliyetini belirler.
- Ayrı teslim sıraları yavaş aboneyi yalıtır: altı turun sonunda rapor abonesi on iki ileti biriktirirken bildirim ve stok özeti yirmi dörder iletiyi bitirmiş ve sıfır bekleyenle kalmıştı.
- Süzgeç yayım tarafında çalışır: sekiz yayım süzgeçsiz yirmi dört kopya üretirken süzgeçle on dört kopya üretti, rapor abonesi ilgilenmediği beş iletiyi hiç görmedi.
- Kopya yalnız yayım anında var olan aboneliklere düşürülür; sonradan bağlanan arama dizini beş olayın üçünü hiç göremedi. Model iletiyi dağıtır, saklamaz.
Sonraki Adım
Kuyruk da konu da aynı varsayımı paylaşıyor: teslim edilmiş ileti tüketilmiştir, tüketilmiş ileti silinir. Bu varsayımın bedeli son ölçümde göründü — sonradan bağlanan taraf geçmişi göremiyor, hata sonrası hiçbir tüketici geriye dönemiyor. Sonraki ders, tüketmeyi silmekten ayıran ikinci taşıma modelini kurar: iletilerin sona eklendiği ve okunduğunda yerinde kaldığı kalıcı sıralı bir günlük. Aynı iş yükü hem kuyrukta hem günlükte koşturulur; yeniden okumanın neden birinde olanaksız, ötekinde bir konum değerini geri almak kadar ucuz olduğu sayıyla gösterilir.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.