

Tek bir sunucu üzerinde 65.535 ephemeral port limitine veya CPU darboğazına çarptığınızda, WebSocket altyapınızı yatayda ölçeklemeniz (horizontal scaling) kaçınılmazdır. Load balancer (HAProxy, ALB veya Nginx) arkasına 5 adet Node.js veya Go instance’ı yerleştirdiğiniz an, mimari yeni bir probleme gebe kalır: State izolasyonu. A sunucusuna bağlı olan bir istemci, B sunucusuna bağlı olan bir istemciye doğrudan mesaj iletemez. Bu iletişim kopukluğunu çözmek için sistemdeki tüm WebSocket sunucularını birbirine bağlayacak bir mesaj veri yolu, yani backplane entegre edilmesi gerekir.
Sektörde bu ihtiyacı karşılamak için en sık başvurulan iki teknoloji Redis Pub/Sub ve NATS’tır. İki teknoloji de mesajları dağıtma işlevini yerine getirse de, fault tolerance (hata toleransı), mesaj iletim garantileri (delivery semantics) ve ağ kopmaları (network partition) durumundaki davranışları taban tabana zıttır. 250.000 eşzamanlı (concurrent) bağlantıyı yönettiğimiz bir borsa veri akışı projesinde, yanlış backplane seçimi ve konfigürasyon eksiklikleri nedeniyle p99 gecikmelerinin 12ms’den 1840ms’ye fırladığına bizzat şahit oldum.
Bu karar rehberinde, Redis 7.2 ve NATS 2.10 (JetStream dahil) üzerinden WebSocket yatay ölçekleme mimarilerini parçalarına ayıracağız. Hangi senaryoda hangi teknolojinin seçilmesi gerektiğini spesifik throughput metrikleri, bellek ayak izi değerleri ve üretim ortamı (production) konfigürasyonlarıyla inceleyeceğiz.
Sisteminize bir mesaj kuyruğu veya pub/sub mekanizması eklemeden önce, iş gereksinimlerinizin hangi iletim garantisini talep ettiğini netleştirmeniz gerekir. WebSocket üzerinden anlık konum verisi mi gönderiyorsunuz, yoksa kripto para alım-satım emirleri mi? Bu soru, aşağıdaki üç semantikten birini seçmenizi zorunlu kılar:
Redis Pub/Sub, in-memory veri yapısı sunucusunun en eski özelliklerinden biridir. Mesajlar bellekte tutulmaz, doğrudan bağlı olan abonelere (subscribers) iletilir. Bağlı olmayan aboneler mesajı sonsuza dek kaçırır.
Redis’te PUBLISH komutunun zaman karmaşıklığı O(N+M)‘dir. Burada N, kanala abone olan istemci sayısını, M ise abone olunan toplam pattern sayısını ifade eder. Saniyede 150.000 mesaj (msg/sec) işleme kapasitesine rahatlıkla ulaşabilir (tek core üzerinde, 10 gigabit ağ bağlantısıyla). Ancak Redis’in single-threaded (tek iş parçacıklı) event loop mimarisi, yüksek throughput durumunda CPU darboğazına neden olabilir. Özellikle Redis 7.2.4 cluster topolojisinde, pub/sub mesajları cluster’daki tüm node’lara broadcast edilir (Redis 7.0 ile gelen Sharded Pub/Sub hariç). Bu da ağ bant genişliğini ciddi şekilde tüketir.
Node.js ortamında ioredis kullanarak kurulan tipik bir WebSocket backplane implementasyonu aşağıdadır. Burada kritik olan nokta, kopan bağlantıların yeniden kurulma stratejisidir (reconnect strategy).
const Redis = require('ioredis');
// Publisher instance
const pub = new Redis({
host: '10.0.1.45',
port: 6379,
maxRetriesPerRequest: 3,
enableReadyCheck: true,
retryStrategy(times) {
const delay = Math.min(times * 50, 2000); // Exponential backoff max 2s
return delay;
}
});
// Subscriber instance (Ayrı bir connection gerektirir)
const sub = new Redis({
host: '10.0.1.45',
port: 6379
});
sub.subscribe('ws:global_chat', (err, count) => {
if (err) console.error('Subscribe hatası: %s', err.message);
console.log(`%d adet kanala abone olundu.`, count);
});
sub.on('message', (channel, message) => {
// Gelen mesajı bu sunucuya bağlı tüm WebSocket client'larına dağıt
wss.clients.forEach(client => {
if (client.readyState === WebSocket.OPEN) {
client.send(message);
}
});
});
// Mesaj gönderme örneği (1.2ms gecikme ile)
pub.publish('ws:global_chat', JSON.stringify({ userId: 412, text: 'Selam!' }));
NATS, başından beri sadece mesajlaşma (messaging) odaklı tasarlanmış, C ve sonrasında Go ile yazılmış yüksek performanslı bir sistemdir. NATS Core tamamen “At-most-once” prensibiyle çalışırken, sisteme dahil olan JetStream modülü disk destekli, kalıcı (persistent) ve “At-least-once / Exactly-once” garantileri sunan bir yapıya dönüşür.
NATS 2.10, multi-threaded bir mimariye sahiptir. 3 node’lu bir cluster konfigürasyonunda saniyede 18 milyon mesaj (msg/sec) yönlendirme (routing) kapasitesini ölçümledik. NATS’ın en büyük avantajı Subject-Based Addressing mantığıdır. Redis’teki düz (flat) kanal isimleri yerine hiyerarşik yapı (örn: chat.room1.user4) ve wildcard desteği (chat.room1.*) sunar. Bu, pub/sub eşleşme (matching) operasyonlarını son derece az CPU döngüsüyle gerçekleştirir.
Aşağıdaki Node.js kodunda, mesaj kaybına tahammülü olmayan (örneğin borsa tahta verisi) bir WebSocket sunucusunun NATS JetStream ile “Pull Consumer” modeli kullanarak mesajları nasıl tükettiğini (consume) görebilirsiniz. ackWait ve maxDeliver parametreleri sistemin kalbidir.
import { connect, StringCodec } from 'nats';
async function initNatsBackplane() {
// NATS Cluster'a bağlan
const nc = await connect({ servers: ['nats://10.0.2.10:4222', 'nats://10.0.2.11:4222'] });
const jsm = await nc.jetstreamManager();
const js = nc.jetstream();
const sc = StringCodec();
// Stream tanımı (Eğer yoksa oluşturur)
await jsm.streams.add({
name: 'WS_EVENTS',
subjects: ['ws.events.*'],
retention: 'limits',
max_age: 1000 * 60 * 60, // 1 saatlik retention
storage: 'file', // Disk tabanlı kalıcılık
replicas: 3 // 3 node'a replike et (Fault tolerance)
});
// Consumer oluştur
const consumer = await js.consumers.getPushConsumer('WS_EVENTS', {
durable_name: 'node_instance_1',
deliver_subject: 'deliver.node_instance_1',
ack_policy: 'explicit',
ack_wait: 2000 * 1000000, // 2 saniye (nanosaniye cinsinden)
max_deliver: 5 // Max 5 kere tekrar dene
});
// Mesajları dinle ve işle
const iter = await consumer.consume();
for await (const m of iter) {
try {
const data = JSON.parse(sc.decode(m.data));
// WebSocket client'larına gönder...
// İşlem başarılıysa NATS'a ACK gönder
m.ack();
} catch (e) {
// Hata varsa NAK gönder, NATS ack_wait beklemeden anında tekrar iletsin
m.nak();
}
}
}
Mimari seçiminizi yaparken kullanabileceğiniz, production verilerine dayalı karar matrisi aşağıdaki gibidir:
| Kriter / Metrik | Redis Pub/Sub (Core) | NATS Core | NATS JetStream |
|---|---|---|---|
| İletim Garantisi | At-most-once | At-most-once | At-least-once / Exactly-once |
| Max Throughput (3 Node) | ~400K msg/sec | ~18M msg/sec | ~2.4M msg/sec (File Storage) |
| p99 Gecikme (Latency) | 2.4ms | 0.8ms | 3.2ms (Disk Flush O.H.) |
| CPU Mimarisi | Single-threaded (v7.2) | Multi-threaded | Multi-threaded |
| Mesaj Boyutu Limiti | 512 MB (Pratikte <10KB önerilir) | 1 MB (Varsayılan) | 1 MB (Varsayılan) |
| Network Partition Davranışı | Mesajlar kaybolur. | Mesajlar kaybolur. | Quorum (Raft) sağlanana kadar bekler, kaybolmaz. |
Elinizdeki sistemin doğasına göre teknoloji seçimi yapmak için bu formülleri kullanabilirsiniz:
eu.tr.istanbul.chat) ve belirli abonelerin sadece *.tr.*.chat pattern’ini dinlemesi gerekiyorsa -> NATS kullanın. Redis’in PSUBSCRIBE komutu büyük pattern listelerinde O(N) maliyetiyle CPU spike’larına sebep olur.2021 yılında, canlı bir trivia (bilgi yarışması) oyununun WebSocket altyapısını yönetirken Redis Pub/Sub ile acı bir tecrübe yaşadık. Sistemde anlık 250.000 kullanıcı bağlıydı ve 8 adet Node.js WebSocket sunucusu Redis’e SUBSCRIBE olmuş durumdaydı. Yarışma esnasında saniyede 8.000 mesaj üretiliyordu.
Sunuculardan birinde, anlık bir Garbage Collection (GC) duraksaması nedeniyle Redis’ten gelen pub/sub mesajlarını işleme hızı (consume rate) yavaşladı. Redis, mesajları iletemediği için bu istemci adına bellekte bir output buffer oluşturmaya başladı. Redis konfigürasyonundaki şu satırı gözden kaçırmıştık:
# redis.conf default ayarı
client-output-buffer-limit pubsub 32mb 8mb 60
Bu konfigürasyon şu anlama gelir: Eğer bir pub/sub istemcisinin (bizim Node.js sunucumuz) output buffer’ı anlık olarak 32MB’ı aşarsa veya 60 saniye boyunca 8MB’ın üzerinde kalırsa, Redis bu istemcinin TCP bağlantısını acımasızca keser (kill eder).
Node.js sunucumuz yavaşladığı için buffer 32MB limitine ulaştı. Redis bağlantıyı kesti. İstemci (Node.js) koptuğunu anlayıp tekrar bağlanana kadar geçen 1.5 saniyelik sürede gelen tüm mesajlar kayboldu. Bu sunucuya bağlı olan ~31.250 kullanıcı, yarışmanın o turundaki soruyu cihazlarında göremediler. Bu olayın ardından, kalıcılık ve ACK mekanizması sunan NATS JetStream altyapısına geçiş yaparak problemi kökünden çözdük.
Hangi teknolojiyi seçerseniz seçin, canlı ortamda (production) patlamamak için aşağıdaki checklist’i uygulayın:
sysctl net.ipv4.tcp_keepalive_time=60 ayarı, load balancer (AWS NAT Gateway gibi) timeout’larından önce boşta kalan (idle) bağlantıların düşmesini engeller.ulimit -n değerini en az 1048576 (1 milyon) olarak ayarlayın. Too many open files (EMFILE) hatası, scale-out senaryolarının bir numaralı katilidir.redis_pubsub_channels ve redis_client_longest_output_list metriklerini kesinlikle alarm (alert) mekanizmasına bağlayın. NATS içinse resmi Prometheus exporter’ı kullanarak nats_messages_dropped_total ve nats_slow_consumers metriklerini 7/24 izleyin.Kafka yüksek throughput sunsa da, WebSocket gibi on binlerce geçici (ephemeral) konu (topic) veya abonenin (consumer) sürekli yaratılıp silindiği senaryolara uygun değildir. Kafka’da bir partition açmak pahalı bir operasyondur, Zookeeper/KRaft üzerinde yük yaratır. NATS’ta konu (subject) açmak anlıktır ve maliyetsizdir.
Evet, Redis 5.0 ile gelen Streams (XADD, XREAD) kalıcılık (persistence) ve Consumer Group mantığı sağlar. Ancak NATS JetStream kadar cluster içi replikasyon ve partition toleransında olgunlaşmamıştır ve bellek maliyeti NATS’ın disk maliyetinden çok daha yüksektir.
Hiçbirini. 5 MB boyutunda bir veriyi pub/sub kanalından iletmek anti-pattern’dir. Büyük dosyayı S3 gibi bir object storage’a yükleyip, Redis/NATS üzerinden sadece dosyanın indirme URL’sini (veya presigned URL) JSON olarak 100 byte boyutunda iletmelisiniz.
İç ağda (VPC içerisinde) backplane iletişimi yapıyorsanız, TLS şifrelemesi NATS throughput’unda yaklaşık %32 düşüşe neden olur. Sektör standardı olarak, dış dünya ile load balancer arasında TLS sonlandırması (termination) yapılır, iç ağdaki broker iletişimi plaintext (veya mTLS, ancak donanım destekli AES-NI ile) bırakılır.
WebSocket mimarilerinde yatay ölçekleme yaparken backplane olarak Redis Pub/Sub veya NATS seçimi, tamamen işinizin mesaj kaybına olan toleransına bağlıdır. Sadece “fire-and-forget” mantığında, geçici veriler taşıyorsanız ve halihazırda Redis altyapısına sahipseniz, Redis basitliği ve konfigürasyon kolaylığı ile işinizi görecektir.
Ancak, mesajların sıralamasının (ordering), kesinlikle ulaştığından emin olmanın (acknowledgement) ve ağ kopmalarında verilerin diskte tamponlanmasının (buffering) istendiği kritik sistemler tasarlıyorsanız, NATS JetStream tek doğru yoldur. Mimarinizi şekillendirmeden önce, yukarıdaki karar matrisini ekibinizle inceleyin ve mutlaka bir yük testi (load test) ile max_deliver ve client-output-buffer-limit parametrelerinin sınırlarını kendi ortamınızda zorlayın.

Endüstriyel AI, PLC verilerinin doğru ve zamanında akışına bağlıdır. Modbus ve OPC UA entegrasyonuyla düşük gecikmeli, güvenli ve semantik veri hatları oluşturarak üretim operasyonlarını optimize etmenin yollarını keşfedin.