tolgacelik.net
Tüm yazılar
·3 dk

WebSocket'i Ölçekte Yönetmek: Bağlantı, State, Broadcast

Bir HTTP API'yi 10 binlerce kullanıcıya ölçeklemek görece kolay. Aynı sayıda kullanıcının WebSocket bağlantısını yönetmek bambaşka bir oyun. Multiplayer üretiminden 4 yıllık deneyim.

websocketreal-timescalingkubernetesmultiplayer

HTTP request'ler stateless. Bir API'ye 10.000 RPS verebilirsin, her request bağımsız, sunucular arası dağıtılır, ölçeklenir. Tertemiz.

WebSocket aynı dünyadan değil. Her bağlantı persistent: kullanıcı bağlandı, sunucu o kullanıcıyı tutuyor, mesaj geldikçe ona iletiyor. 10.000 eşzamanlı kullanıcı = 10.000 açık socket. Bu, ölçekleme problemi olarak HTTP'den fundamental olarak farklı.

Multiplayer platformumuzda 4+ yıldır 10.000+ eşzamanlı WebSocket bağlantısı yöneten bir sistem işletiyoruz. İşte gerçek dersler.

1. Sticky session veya pub/sub — biri olmalı

Kullanıcı pod-A'ya bağlandı. Diğer kullanıcı pod-B'de. Birbirlerine mesaj göndermek istiyorlar. Pod-A'daki kullanıcının mesajı pod-B'deki kullanıcıya nasıl ulaşır?

İki yaklaşım:

Sticky session — bir kullanıcının tüm bağlantıları aynı pod'a yönlendirilir. Aynı oda kullanıcıları aynı pod'da. Pod-internal broadcast yeterli.

Sıkıntı: pod restart edildiğinde bütün oda dağılır. Yatay ölçekleme da zor — sticky'ye sıkışıyorsun.

Pub/sub — pod sayısı önemli değil. Her pod, her oda için Redis Pub/Sub'a abone olur. Mesaj bir pod'a gelir, Redis'e yayınlar, tüm pod'lar mesajı kendilerindeki kullanıcılara dağıtır.

Bizde bu kullanıyoruz. Sebep: yatay ölçekleme + pod restart sırasında oda hayatta kalır.

2. Connection state — pod'un sorumluluğu mu, dağıtık mı

Her bağlantı için memory tutuluyor: kullanıcı id, hangi odada, ne kadar süredir bağlı, son ping. 10.000 bağlantı için 50-100 MB memory.

İki yaklaşım:

Pod-local state — her pod kendi bağlantılarını bilir, başkasınınkini bilmez. Basit, hızlı.

Distributed state — Redis veya benzeri'nde global bağlantı haritası. Hangi kullanıcı hangi pod'da, herkes biliyor.

Bizde yine pod-local + Redis pub/sub. Distributed state senkronize tutmak operasyonel olarak pahalı. Pod-local ile yaşıyoruz, çünkü sticky değiliz, pub/sub broadcast yeterli.

Bir kullanıcının "online mi?" sorusunu cevaplamak için ayrı bir Redis hash kullanıyoruz: online_users set. Bağlanıldığında SET, koparıldığında SREM, periyodik cleanup.

3. Heartbeat ve dead connection detection

WebSocket bağlantılar sessizce ölür. Kullanıcı internet'i kesilir, NAT timeout'u, cihaz kapanır — sunucuya hiç haber gelmeden. Pod, kullanıcının hâlâ orada olduğunu sanır, mesaj göndermeye çalışır, başarısız olur ya da memory'de zombi tutar.

Çözüm: heartbeat.

Her 30 saniyede client → server bir ping mesajı yollar. 90 saniyedir ping gelmemişse, sunucu bağlantıyı zombie kabul edip kapatır.

Server'dan client'a da ters yönde ping gönderilebilir. Biz client'tan ping aldığımız için pasif yaklaşımı tercih ediyoruz — server CPU'sunu yormaz.

Ek bir önlem: TCP keepalive'ı OS seviyesinde aç. Ama keepalive default 2 saat — mobile clientlar için faydasız. Application-level heartbeat genelde gerekli.

4. Graceful shutdown

Pod'un yeniden başlatılması gerekiyor. Bağlı 1.000 kullanıcı var. Pod aniden ölürse 1.000 kişi anında düşer.

Çözüm: graceful shutdown.

1. Pod "shutting down" sinyali alır (SIGTERM)
2. Yeni bağlantı kabul etmeyi bırakır (load balancer'dan da düşürülür)
3. Mevcut bağlantılara "yeniden bağlan" mesajı gönderir
4. Kullanıcılar diğer pod'lara reconnect eder
5. 30-60 saniye bekler, hâlâ kalanları kapatır
6. Pod ölür

Kubernetes'te terminationGracePeriodSeconds: 60 ayarı bu süreyi yönetir. Bizde oyun pod'ları için 90 saniye — orta-tur maçlar bitsin diye.

5. Broadcast scaling — Pub/Sub ölçeği

10.000 kullanıcı bir global event'e abone (örn. "günün lider"i değişti). Mesaj hepsine gitmeli. Pub/sub mesaj 10.000 socket'a kopyalanır — 10.000 send().

Pratik:

  • Mesajı küçük tut (JSON yerine ikili format kullanmak gerekir mi?)
  • Send'i async yap, paralel
  • Slow consumer'ları kop, drop, log

Bir slow consumer (yavaş cihaz) yüzünden tüm broadcast'i durdurma. Buffer dolduğunda mesajı drop et, log at.

6. Authentication ve hijacking

WebSocket bağlantısı bir kere kurulduktan sonra, "bu kullanıcı gerçekten X mi?" sorusu zor. Token expire'ı yok, refresh yok.

Pratiğimiz:

  • Bağlantı kurulurken token doğrula (initial handshake)
  • Token süresi dolduğunda kullanıcıyı kop, yeniden bağlanmasını iste
  • Periodically (her 5 dakika) token'ı re-validate et

Bu detay üretim ortamında kritik — özellikle ödeme veya yetki etkileri olan WebSocket akışları varsa.

Ölçeklenebilirlik kontrol listesi

WebSocket sistemini production'a alırken sorduğum:

  • Pod-local state mi, distributed mi (planı net mi)?
  • Pub/sub mu, sticky mi (planı net mi)?
  • Heartbeat var mı? Threshold ne?
  • Graceful shutdown var mı? Süresi ne?
  • Slow consumer için drop politikası var mı?
  • Connection limit per pod ne? (memory + CPU sınırı)
  • Reconnection logic client tarafında var mı? (exponential backoff)
  • Load balancer WebSocket-aware mı? (NLB/ALB değil, Layer-7)

Sonuç

WebSocket scaling, klasik HTTP scaling'den farklı bir mental model gerektirir. Connection-as-resource düşüncesine geçtiğinde rahat. Çoğu ekibin tökezlediği yer: HTTP zihniyetiyle WebSocket'e başlayıp, ilk traffic dalgasında alçaktan uçma.

Bizim setup'ımız 5 yılda olgunlaştı, ama bugün benzer bir sistemi kurmaya başlasam, bu 6 madde ilk gün listesi olurdu.

PaylaşLinkedInX
Bir sonraki yazı doğrudan kutuna düşsün

Distributed sistemler, üretim stabilitesi, AI destekli geliştirme üzerine uzun yazılar. Spam yok, çıkmak kolay.

Sadece e-posta. Bu sunucuda saklanır. Hiçbir yerde paylaşılmaz, satılmaz.

Yorumlar

Henüz yorum yok. İlk yorumu sen bırak.

Yorum bırak

Hiçbir yerde gösterilmez. Sadece gerekirse cevap vermek için.