Gerçek zamanlı iletişim artık olsa iyi olur özellik değil. Kullanıcılar anlık güncellemeler, canlı bildirimler ve kesintisiz işbirliği bekliyor. Ancak WebSocket bağlantılarını ölçeklemek, HTTP endpoint’lerini ölçeklemekten temelden farklı.
Zorluk
Bir e-spor platformu için gerçek zamanlı sohbet sistemi inşa ederken somut bir problemle karşılaştım: 100ms altı mesaj teslimi ile 2.000+ eşzamanlı WebSocket bağlantısını desteklemek. Teori burada gerçeklikle buluşuyor.
Mimari Genel Bakış
Son mimari şöyle görünüyordu:
┌─────────────────────────────────────────────────────────┐
│ Load Balancer │
│ (Sticky Sessions) │
└─────────────────────────────────────────────────────────┘
│
┌──────────────────┼──────────────────┐
│ │ │
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Node.js │ │ Node.js │ │ Node.js │
│ Server │ │ Server │ │ Server │
└────┬────┘ └────┬────┘ └────┬────┘
│ │ │
└──────────────────┼──────────────────┘
│
┌──────┴──────┐
│ Redis │
│ Pub/Sub │
└─────────────┘
Temel Tasarım Kararları
1. Bağlantı Yönetimi
Her WebSocket bağlantısı bellek tüketir. Binlerce bağlantıyla bu hızla birikir. Mesaj geçmişini bellekte saklamadan metadata takibi yapan bir bağlantı yöneticisi uyguladım:
interface ConnectionMetadata {
odId: string;
odName: string;
odChannel: string;
connectedAt: Date;
lastActivity: Date;
}
class ConnectionManager {
private connections: Map<string, WebSocket> = new Map();
private metadata: Map<string, ConnectionMetadata> = new Map();
register(ws: WebSocket, odId: string, channel: string) {
this.connections.set(odId, ws);
this.metadata.set(odId, {
odId,
odName,
odChannel,
connectedAt: new Date(),
lastActivity: new Date()
});
}
broadcast(channel: string, message: object) {
const recipients = this.getChannelMembers(channel);
const payload = JSON.stringify(message);
for (const odId of recipients) {
const ws = this.connections.get(odId);
if (ws?.readyState === WebSocket.OPEN) {
ws.send(payload);
}
}
}
}
2. Çoklu Sunucu İletişimi için Redis Pub/Sub
Birden fazla Node.js sunucunuz olduğunda, bir sunucuya gönderilen mesajın diğer sunuculara bağlı istemcilere ulaşması gerekir. Redis Pub/Sub bunu zarif bir şekilde çözer:
import Redis from 'ioredis';
const publisher = new Redis();
const subscriber = new Redis();
// Bir sunucuda mesaj alındığında
function handleIncomingMessage(channel: string, message: ChatMessage) {
// Tüm sunucuların alması için Redis'e yayınla
publisher.publish(`chat:${channel}`, JSON.stringify(message));
}
// Tüm sunucular ilgili kanallara abone olur
subscriber.psubscribe('chat:*');
subscriber.on('pmessage', (pattern, channel, message) => {
const channelId = channel.replace('chat:', '');
const parsed = JSON.parse(message);
connectionManager.broadcast(channelId, parsed);
});
3. Heartbeat ve Ölü Bağlantı Temizliği
WebSocket bağlantıları sessizce ölebilir (ağ sorunları, istemci çökmeleri). Heartbeat’ler uygulamak kaynak sızıntılarını önler:
const HEARTBEAT_INTERVAL = 30000; // 30 saniye
const CONNECTION_TIMEOUT = 60000; // 60 saniye
setInterval(() => {
const now = Date.now();
for (const [odId, metadata] of connectionManager.metadata) {
const ws = connectionManager.connections.get(odId);
if (!ws || ws.readyState !== WebSocket.OPEN) {
connectionManager.remove(odId);
continue;
}
if (now - metadata.lastActivity.getTime() > CONNECTION_TIMEOUT) {
ws.terminate();
connectionManager.remove(odId);
continue;
}
// Ping gönder
ws.ping();
}
}, HEARTBEAT_INTERVAL);
Performans Optimizasyonları
Mesaj Gruplama
Her mesajı anında göndermek yerine, küçük bir zaman penceresi içinde gelen mesajları grupla:
class MessageBatcher {
private queue: Map<string, Message[]> = new Map();
private timers: Map<string, NodeJS.Timeout> = new Map();
add(channel: string, message: Message) {
if (!this.queue.has(channel)) {
this.queue.set(channel, []);
}
this.queue.get(channel)!.push(message);
if (!this.timers.has(channel)) {
this.timers.set(channel, setTimeout(() => {
this.flush(channel);
}, 50)); // 50ms gruplama penceresi
}
}
private flush(channel: string) {
const messages = this.queue.get(channel) || [];
this.queue.delete(channel);
this.timers.delete(channel);
if (messages.length > 0) {
connectionManager.broadcast(channel, {
type: 'batch',
messages
});
}
}
}
Binary Protokol
Yüksek frekanslı güncellemeler (oyun durumu gibi) için JSON yerine binary protokoller kullanmayı düşünün:
// JSON yerine MessagePack kullanma
import { encode, decode } from '@msgpack/msgpack';
function sendBinary(ws: WebSocket, data: object) {
const binary = encode(data);
ws.send(binary);
}
İzleme ve Metrikler
Ölçemediğinizi optimize edemezsiniz. Takip edilmesi gereken temel metrikler:
- Sunucu başına bağlantı sayısı
- Mesaj verimi (mesaj/saniye)
- Gecikme yüzdelikleri (p50, p95, p99)
- Bağlantı başına bellek kullanımı
- Redis Pub/Sub gecikmesi
// Prometheus tarzı metrikler
const connectionGauge = new Gauge({
name: 'websocket_connections_total',
help: 'Toplam aktif WebSocket bağlantıları'
});
const messageCounter = new Counter({
name: 'websocket_messages_total',
help: 'İşlenen toplam mesajlar',
labelNames: ['type', 'channel']
});
const latencyHistogram = new Histogram({
name: 'websocket_message_latency_ms',
help: 'Milisaniye cinsinden mesaj teslim gecikmesi',
buckets: [5, 10, 25, 50, 100, 250, 500]
});
Öğrenilen Dersler
-
Sticky session’lar şart: Bunlar olmadan, yeniden bağlanan istemciler farklı sunuculara gider ve oturum durumlarını kaybeder.
-
Zarif bozulma: Yoğun yük altında, kritik olanlardan (gerçek mesajlar) önce kritik olmayan mesajları (yazıyor göstergeleri) bırakın.
-
İstemci tarafı yeniden bağlanma: Sunucu kurtarmasında thundering herd’i önlemek için jitter ile exponential backoff uygulayın.
-
Erken yük testi: Prodüksiyondan önce binlerce bağlantıyı simüle etmek için Artillery veya k6 gibi araçlar kullanın.
Sonuç
WebSocket’leri ölçeklemek sihir değil—dikkatli mimari, darboğazlarınızı anlama ve amansız optimizasyon. İster sohbet sistemi, ister canlı spor güncellemeleri veya işbirlikçi düzenleme inşa ediyor olun, buradaki prensipler geçerli. Bunları ustalaştırın ve gerçek zamanlı özellikler cephaneliğinizde sadece başka bir araç haline gelir.