Bahadır Bilir

mino

Oyuncuların gezip sohbet ettiği, tarayıcıda ortak bir 2B alan. Node.js ve uWebSockets.js üzerinde gerçek zamanlı bir sunucu, aynı dünyayı Redis üzerinden paylaşan birden çok süreç ve WebGPU ile çizen bir istemci.

Her konumun sahibi sunucu: istemci yalnız yürümek istediği yönü gönderir; sunucu her oyuncuyu saniyede 20 kez hareket ettirir ve snapshot gönderir. Aynı makinede ya da farklı makinelerde çalışan birden çok sunucu süreci alanı Redis üzerinden paylaşır: her biri kendi oyuncularını simüle eder, snapshot’larını, sohbetini ve presence bilgisini yayınlar, diğerleri bunları değiştirmeden iletir. Wire protokolü FlatBuffers, yalnız binary. Tarayıcı istemcisi bütün oyuncuları WebGPU ile tek draw call’da, en yeni snapshot’ın yaklaşık 100 ms gerisinden, interpolasyonla çizer.

  • Sunucusu ölen oyuncu başka bir sunucuda devam eder: resume bileti, session ve her shard’ın son snapshot’ı Redis’te. AWS’de, Cloudflare üzerinden, ilk makinedeki süreç öldürüldüğünde oyuncu socket’i kapandıktan 436 ms sonra ikinci makinede karşılandı, 473 ms’de yerindeydi.
  • Sınırına gelen süreç, kendi işi yüzünden tick’leri gecikmeye başlayınca yeni oyuncuları “server busy” ile reddeder; bütün tick’lerin kaymasına izin vermez.
  • Şimdilik misafir oyuncular; ilk mesajda identity-service access token’ı için yer var.

AWS’de ölçüldü

İki c7a.large makine, her birinde bir sunucu süreci, önlerinde TLS ile nginx ve aynı subnet’te bir yük üreteci. Her oyuncu bir ile üç saniyede bir yeni bir yöne yürür.

Snapshot'lar arası süre, p95, iki makinede oyuncu sayısına göre
Göster
  • iki c7a.large
  • bir tick, 50 ms

snapshot aralığı p95, ms

oyuncu

snapshot aralığı p95, ms
oyuncuiki c7a.large
10051
25051
50052
1.00058
1.500104
2.000237,5

1000 oyuncuda snapshot'lar zamanında ve her makine yaklaşık %30 dolu. 1500 ve 2000'de yük üreteci %70'in üstünde doluydu, bu iki adım temiz değil; 2500 ve 3000 geçersiz.

Bir oyuncuya giden bir frame'in maliyeti, her makinede
Göster
  • sunucu
  • nginx
  1. 100 oyuncu8,9 µs12,6 µs
  2. 250 oyuncu6 µs13,7 µs
  3. 500 oyuncu5,9 µs15,4 µs
  4. 1000 oyuncu6,6 µs19,6 µs
  5. 1500 oyuncu8,6 µs31,5 µs
  6. 2000 oyuncu9 µs32,1 µs
sunucunginx
100 oyuncu8,9 µs12,6 µs
250 oyuncu6 µs13,7 µs
500 oyuncu5,9 µs15,4 µs
1000 oyuncu6,6 µs19,6 µs
1500 oyuncu8,6 µs31,5 µs
2000 oyuncu9 µs32,1 µs

nginx her frame'i şifreler ve kopyalar, maliyeti frame'in boyutuyla artar; sunucu bir frame'i bir kez kurar ve aynı byte'ları her socket'e verir. 1000 oyuncuda nginx her makinede 0,39, sunucu 0,13 çekirdek kullandı.

Tek makinede ölçüldü

Bir masaüstü, sunucu süreçleri çekirdeklere sabitlenmiş: bir süreç nerede duruyor ve neden.

Tek çekirdekte bir süreç: oyuncu sayısına göre saniyede tick
Göster
  • loopback TCP
  • Unix socket
  • tick hızı, saniyede 20

saniyede tick

oyuncu

saniyede tick
oyunculoopback TCPUnix socket
1.50020,120
2.00020,120,1
2.50012,219,4
3.0009,913,6

Unix socket'te bir frame sunucuya yaklaşık %30 daha ucuz; bir süreç saniyede 20 tick'te yaklaşık 2000 yerine 2500 oyuncu taşıyor. Bu masaüstünün sayıları, busy kuralı kapalı.

  • Girişlerin maliyeti oyuncu sayısının karesiyle artıyordu: her join her shard’ı okuyor ve bütün oyuncu listesini encode ediyordu. Saniyede yaklaşık 280 gelen 2500 oyuncuyla değişiklikten önce 671 join zaman aşımına uğradı, sonra hiçbiri; bellek 297 MB’tan 142 MB’a indi.
  • Maliyet byte başına değil, write başına. Bir istemcinin frame’leri tick başına tek write’ta birleşince iki süreç 1642 yerine 2000 oyuncunun hepsini, 0,97 yerine 0,65 çekirdekle aldı.
  • Tek süreç ve tek çekirdekte yarım milyon boşta websocket: her biri süreçte yaklaşık 1,3 KB, kernel’de 2,7 ile 4 KB. Sınır gönderimde: hepsine bir mesaj göndermek bir çekirdeğin 5,7 ile 12 saniyesini alıyor.
  • Tek alanda byte’lar oyuncu sayısının karesiyle artıyor, çünkü her oyuncu diğer her oyuncunun konumunu saniyede 20 kez alıyor. Birkaç binin üstünde sınır ağ; aşmanın yolu her oyuncuya yalnız görebildiğini göndermek.