NetMQ ile Brokerless Mesajlaşma

NetMQ, ZeroMQ mesajlaşma modelini .NET tarafında broker zorunluluğu olmadan sunar. Bu sadelik queue sınırı, socket ownership, backpressure ve failure semantics kararlarını uygulamanın üzerine bırakır.

Broker olmaması, kuyruk olmadığı anlamına gelmez.

ZeroMQ ailesini ilk kez kullananlarda en sık gördüğüm zihinsel model hatası bu. Ortada RabbitMQ veya Kafka gibi ayrı bir sunucu olmadığı için mesajın doğrudan socket'ten socket'e aktığı ve bütün hata davranışının TCP'ye bırakıldığı sanılıyor. Oysa NetMQ kendi queue, messaging pattern ve state-machine semantiğine sahip.

.NET tarafında hafif bir messaging katmanı gerektiğinde bu yaklaşım çok güçlü olabilir; fakat basitliğin karşılığında bazı mimari kararlar sizin sorumluluğunuza geçer.

NetMQ ne sağlar?

NetMQ, ZeroMQ yaklaşımının native C# implementasyonudur. Request/Reply, Publish/Subscribe, Push/Pull, Router/Dealer gibi messaging pattern'leri sağlar.

Bu pattern'ler TCP socket kullanımını daha yüksek seviyeli bir protokole dönüştürür.

Örneğin RequestSocket ile bir mesaj gönderip ikinci mesajı ilk yanıt gelmeden gönderemezsiniz. Bu kısıt API tuhaflığı değildir; REQ/REP state machine'inin parçasıdır.

Pattern seçimi bu nedenle class seçimi değil, communication contract seçimidir.

Brokerless tasarımın avantajı

Arada ayrı broker process'i olmadığında deployment daha küçüktür. Özellikle aynı host veya kontrollü LAN içinde çalışan bileşenlerde bu ciddi sadelik sağlar.

Ek broker kurulacak servis, izlenecek process, disk/persistence politikası, cluster configuration ve authentication/authorization yüzeyi demektir.

Bunların hiçbirine ihtiyaç olmayan transient mesaj akışında ZeroMQ modeli daha uygun olabilir.

Ancak broker'ın ortadan kalkması broker'ın sağladığı durability, replay ve centralized observability özelliklerini de ortadan kaldırabilir.

Socket ownership kritik

NetMQ socket'leri genel amaçlı thread-safe nesne gibi paylaşılmamalıdır. Socket'in hangi thread/poller tarafından sahiplenildiği açık olmalıdır.

Bu kuralı ihlal etmek başlangıçta çalışıyor görünen, yük altında nondeterministic davranan kod üretir.

Ben bu tür API'lerde ownership'i constructor seviyesinde değil lifecycle seviyesinde tanımlamayı daha doğru buluyorum:

Worker Thread
  -> owns socket
  -> reads command queue
  -> performs send/receive
  -> publishes result

Başka thread doğrudan socket'e dokunmak yerine owner thread'e iş aktarır.

Bu model biraz daha fazla kod gibi görünür fakat concurrency semantiğini açık hale getirir.

High-water mark backpressure sınırıdır

Mesajlar sonsuza kadar buffer'lanamaz. ZeroMQ/NetMQ tarafında high-water mark queue büyümesini sınırlar.

Bu ayarı yalnız tuning parametresi olarak görmek hatalıdır.

HWM dolduğunda ne olacağı socket türü ve send biçimine bağlıdır. Uygulama blocking, timeout veya drop davranışını bilmelidir.

Uzun süreli sistemlerde bounded queue kullanmayı tercih etmemin nedeni burada da aynı: consumer yavaşsa problem belleğe gizlenmemelidir.

Queue occupancy observability metriği olmalıdır.

PUB/SUB veri kaybını kabul eder

Publisher, subscriber'ın mesajı gerçekten işlediğini bilmez. Subscriber bağlantıyı kurmadan önce yayımlanan mesajları da kaçırabilir.

Bu davranış telemetri, canlı durum veya yeniden üretilebilir state snapshot'ı için kabul edilebilir olabilir.

Finansal işlem veya kalıcı iş emri için aynı pattern güvenli değildir.

Bir mesajın önemli olması pattern'i değiştirebilir: transient observation için PUB/SUB, work distribution için PUSH/PULL, synchronous command için REQ/REP, custom async routing için DEALER/ROUTER daha doğal olabilir.

Pattern isimlerini ezberlemekten daha önemli olan delivery beklentisini tanımlamaktır.

TCP ACK uygulama onayı değildir

Alt katmanda TCP kullanılması mesajın business operation olarak işlendiğini garanti etmez.

TCP karşı tarafın kernel buffer'ına kadar ilerlemiş olabilir. Process mesajı okumadan ölebilir. Process okuyup transaction commit etmeden de ölebilir.

Gerçek "işlendi" garantisi gerekiyorsa application-level acknowledgment ve gerekiyorsa idempotent retry tasarlanmalıdır.

Bu nokta yalnız NetMQ'ya özgü değildir; messaging sistemlerinde transport semantics ile business semantics arasındaki sınırdır.

Poller ile event loop

NetMQPoller birden fazla socket ve timer'ı tek event loop içinde yönetmek için kullanışlıdır.

Doğru kullanıldığında her bağlantı için thread açma ihtiyacını azaltabilir. Ancak callback içinde uzun süren CPU veya blocking I/O yapılırsa event loop durur.

Bu yüzden poller callback'i işin kendisini değil, mümkün olduğunca state transition ve dispatch işlemini yapmalıdır.

Ağır iş başka worker'a aktarılacaksa geri dönüş kanalının da bounded olması gerekir. Aksi halde "event loop bloklanmasın" diye oluşturulan worker pool yeni sınırsız queue üretir.

Failure ve reconnect

Network partition, peer restart veya bağlantı kopması normal durum olarak tasarlanmalıdır.

Uygulama peer yeniden gelince socket'in nasıl toparlanacağını, arada üretilen mesajların ne olacağını, retry sırasında duplicate ihtimalini ve process restart sonrası in-memory queue kaybını bilmelidir.

Eğer mesajın kaybı kabul edilmiyorsa brokerless in-memory tasarımın üstüne persistence katmanı eklemek gerekebilir. Bu noktada ayrı broker kullanmak yeniden daha doğru seçenek haline gelebilir.

Nerede kullanırım?

NetMQ'yu en anlamlı bulduğum alan, bileşenlerin sahipliğinin net olduğu ve mesajların çoğunun transient olduğu düşük gecikmeli servislerdir.

Aynı makinedeki process'ler, kontrollü worker topolojileri, telemetri, command routing ve edge sistemleri buna örnek olabilir.

Buna karşılık audit log, uzun süreli replay, consumer group koordinasyonu ve kalıcı event stream gerekiyorsa farklı altyapılar daha doğal olabilir.

Mühendislik kararı "broker hızlı mı yavaş mı?" değildir. İstenen delivery semantics hangi bileşende tutulacak sorusudur.

NetMQ broker'ı kaldırarak sistemi küçültür; sorumluluğu kaldırmaz.

Kaynakça

Bu sayfanın QR kodu