Nyulak szigete: hány játékost bír a szerver?
Terhelésteszt a studio-1 VPS-en, 2026-10-25. Robotok a laptopról, a valódi úton (Kívánjáték-átjáró).
A VPS kb. 150 játékost bír egyszerre jól; 200 felett a játék egyetlen szála (a 20 Hz-es tick) akad el, 250 felett a szerver lemarad, 500-nál már másodpercekig nem érkezik adat a játékosokhoz. A mostani termékkorlát (48 játékos) ennek harmada.
A szerver egy tickje
A játék 20-szor számol másodpercenként, egy tickre 50 ms jut. Ami fölötte van, ott a szerver lemarad.
Állapotcsomag másodpercenként, játékosonként
A szerver 20-at küld; ha kevesebb jön meg, a többiek mozgása szaggat.
A teljes VPS processzora (Hetzner API)
A két mag összesen, 200% = mindkét mag tele. Lépcsőnként a mérési perc átlaga.
A VPS processzora a teljes teszt alatt (Hetzner API, 10 mp)
Minden sáv egy lépcső. A pihenőkben a gép visszaesik.
A szomszédok válaszideje közben
Az élő Nyulak és a kivanjatek.hu, 3 mp-enként mérve. Egyik sem hibázott, és nem indult újra.
A teljes jelentés
Újraindulás, kifagyás, OOM nem volt sehol (se a tesztben, se az élő szolgáltatásoknál). Részletek lent.
Mit mértünk, hogyan
- Gép: a
studio-1Hetzner VPS (2 megosztott vCPU). Rajta fut az élő Nyulak, a TatAI: Front, az Akkufalu, a Kívánjáték webapp + Postgres, és az egészet a Caddy szolgálja ki. A teszt alatt ezek mind futottak, élő játékos egyiken sem volt. - Tesztpéldány: az élő kiadás (0.8.1-bb92068) pontos másolata, külön unit (
nyulak-stress, 127.0.0.1:24791), külön állapotmappa, ugyanaz a memóriahatár (1,5 GB) mint az élőnek. Az élő porthoz és állapothoz nem nyúltunk. A termékkorlátokat (maxConns 96, perIp 8, core.maxPlayers 48) a teszt szerver felemelte, a terheléses visszavétel (perf.shed) ki volt kapcsolva: a nyers költséget mértük. A terméken aperf.shedbe van kapcsolva, az valamennyit segítene (nem mértük). - Útvonal: a laptopról a valódi websocketen át, ahogy a játékosok jönnek:
wss://nyulak-stress.kivanjatek.hu/ws, vagyis a Caddy és a Kívánjáték-átjáró is benne van az útban. Az átjáró saját IP-határa egyik lépésnél sem állított meg (0 elutasított kapcsolat). A laptop és a VPS közti hálózat sem volt szűk keresztmetszet 12 MB/s-ig. - Játékosok: fejetlen botok (
tools/stress.mjs,tools/botclient.mjs): belépnek, csapatot és kezdőpontot választanak, mozognak, lőnek, 20 Hz-en küldenek parancsot. A pálya élő hadserege (kb. 300 NPC) közben él. A játékosok véletlenül szóródnak a bázisokon. - Lépcsők: 5, 10, 25, 50, 100, 150, 200, 250, 300, 500 játékos; mindegyik kb. 15-50 mp belépés + 10-20 mp pihenő + 60 mp mérés. Plusz egy 5 perces futás 100 játékossal (tartósság). A botok a laptopon teljes dekódolással futottak (a valódi kliens munkáját végzik), kivéve 250-nél és 500-nál, ahol light módban (csak számolják a bájtokat és a réseket, nem dekódolnak). 300-nál mindkettő megvan: ott a dekódoló botok folyamata megakadt a laptopon (1,2 s eseményhurok-késés), ezért a 300-as kliensoldali számokra a light sor érvényes. A szerveroldali számok a két módban gyakorlatilag azonosak.
- Mit figyeltünk: a szerver
/healthés/stressvégpontját (tick, rendszerenkénti idő, CPU, steal, RSS, hálózat), a Hetzner Cloud API szervermetrikáit (CPU, hálózat, lemez, 10 mp-es felbontás), az élő Nyulak/healthés a kivanjatek.hu válaszidejét 3 mp-enként, a szolgáltatások újraindulás-számlálóját (systemctl show) előtte és utána, a kernelnaplót OOM-ra. - Mockolt szám:
tools/hostbench.mjs n<N>a VPS-en, hamis kapcsolatokkal (nincs hálózat, nincs átjáró), amikor az élő szerveren 0 játékos volt. - Költségvetés: a játék 20 Hz-en tickel, tickenként 50 ms jut. Ha az átlag 50 fölé megy, a szerver lemarad.
Eredmény lépcsőnként
A tick a szerver saját /health mérése. A „Játék-folyamat CPU” egy magra vetítve (100 = egy teljes mag); a Hetzner CPU a két vCPU összege (max 200). Nyugalmi VPS CPU a teszt előtt/után: kb. 2 (a 200-ból).
| Játékos | Mód | Tick átl. ms | p95 | max | Lemaradt tick | Játék-folyamat CPU % | Egész VPS CPU % (Hetzner, max 200) | Steal % | RSS MB |
|---|---|---|---|---|---|---|---|---|---|
| 5 | decode | 8,6 | 13,1 | 29 | 0 | 36 | 44 | 0 | 205 |
| 10 | decode | 9,5 | 14,3 | 24 | 0 | 24 | 41 | 0 | 211 |
| 25 | decode | 14,5 | 20,1 | 28 | 0 | 35 | 62 | 0 | 219 |
| 50 | decode | 20,1 | 27,4 | 53 | 0 | 47 | 91 | 0 | 241 |
| 100 | decode | 26,6 | 36,2 | 73 | 0 | 58 | 110 | 0 | 287 |
| 100 (5 perc) | decode | 27,1 | 37,7 | 79 | 0 | 57 | 114 | 0 | 826 |
| 150 | decode | 32,7 | 46,3 | 104 | 0 | 65 | 139 | 0 | 879 |
| 200 | decode | 39,8 | 60,5 | 117 | 0 | 78 | 163 | 0 | 341 |
| 250 | light | 52,0 | 74,6 | 200 | 186 | 80 | 175 | 0 | 823 |
| 300 | light | 67,0 | 97,4 | 214 | 444 | 77 | 178 | 0 | 413 |
| 300 | decode | 67,9 | 98,3 | 232 | 401 | 77 | 176 | 0 | 382 |
| 500 | light | 94,9 | 124,4 | 503 | 692 | - (a /stress nem válaszolt) | 184 | - | 939 |
(Az RSS oszlop a folyamat összesített memóriája: a Node nem adja vissza az operációs rendszernek, ami egyszer kellett, ezért a 150-es és a 100/5 perces sor az 500-as lépcső után magas, nem a játékosszámtól. A tesztszerver a teljes teszt végén, üresen is kb. 680 MB volt.)
| Játékos | Ki MB/s (Hetzner) | KiB/s/játékos | Snapshot/s a botnál | Bot ping átl. / max ms | Leghosszabb snapshot-rés ms | Élő Nyulak ms átl. / max | KJ ms átl. / max |
|---|---|---|---|---|---|---|---|
| 5 | 0,04 | 9,3 | 20,2 | 31 / 67 | 128 | 33 / 64 | 33 / 63 |
| 10 | 0,09 | 9,5 | 20,2 | 29 / 45 | 149 | 35 / 75 | 34 / 74 |
| 25 | 0,26 | 9,7 | 20,1 | 30 / 52 | 128 | 37 / 90 | 36 / 88 |
| 50 | 0,52 | 12 | 20,0 | 32 / 61 | 138 | 35 / 72 | 34 / 70 |
| 100 | 1,58 | 15 | 20,1 | 37 / 91 | 230 | 35 / 76 | 38 / 105 |
| 100 (5 perc) | 1,44 | 14 | 20,1 | 37 / 121 | 398 | 34 / 97 | 35 / 99 |
| 150 | 3,02 | 18 | 20,0 | 48 / 171 | 523 | 37 / 80 | 37 / 80 |
| 200 | 4,76 | 20 | 20,1 | 67 / 580 | 960 | 47 / 119 | 73 / 268 |
| 250 | 7,55 | 22 | 17,2 | 157 / 19 831 | 28 782 | 36 / 72 | 45 / 153 |
| 300 (light) | 8,66 | 21 | 12,9 | 207 / 48 209 | 54 908 | 51 / 131 | 66 / 293 |
| 300 (decode) | 9,82 | 23 | 15,1 | 289 / 60 526 | 32 065 | 44 / 125 | 62 / 191 |
| 500 | 12,64 | - | 3,8 | 27 030 / 100 496 | 109 659 | 79 / 395 | 188 / 953 |
A „Snapshot/s” a játékosonkénti állapotcsomag, a szerver 20-at küld; 20 alatt a játékos mozgása szaggat. A ping a játék saját ping-pongja (a tick-sorban várakozik, ezért a lemaradást is mutatja). A játékos oldali alap-ping ~30 ms (laptop - Caddy - VPS). Az élő Nyulak és a KJ válaszideje nyugalomban is ~35 ms (ugyanaz az útvonal, a saját mérésünk).
A mockolt szám (hostbench, a VPS-en, hálózat nélkül, az élő szerver üres volt)
| Játékos | Tick átl. ms | p95 | max | GC ms/tick | RSS MB | átl. ms: players / net / npc / vehicles |
|---|---|---|---|---|---|---|
| 5 | 3,0 | 5,2 | 22 | 0,05 | 252 | 0,13 / 0,65 / 1,1 / 0,5 |
| 10 | 4,1 | 6,7 | 19 | 0,06 | 253 | 0,60 / 1,0 / 1,3 / 0,6 |
| 25 | 6,0 | 9,2 | 29 | 0,14 | 267 | 1,4 / 1,7 / 1,4 / 0,8 |
| 50 | 9,0 | 13,2 | 39 | 0,35 | 296 | 3,1 / 2,7 / 1,4 / 0,7 |
| 100 | 15,2 | 24,0 | 106 | 1,7 | 376 | 5,9 / 5,1 / 1,9 / 0,9 |
| 200 | 28,2 | 56,6 | 117 | 17,7 | 434 | 12,3 / 10,2 / 2,7 / 1,2 |
| 500 | 72,0 | 235,3 | 404 | 58,8 | 717 | 30,0 / 34,1 / 2,8 / 1,7 |
A mockolt szám a tiszta játéklogika (a hamis kliensek ugyanabban a folyamatban dekódolnak, így a GC-t felfelé torzítják), a valódi mérés ennél 30-70%-kal magasabb 100-200 játékosnál: ez a websocket-küldés, a Caddy és az átjáró ára a valódi tick mellett. Mindkét mérés ugyanazt mondja: a költség ~lineárisan nő, 200 felett a players és net rendszer (a játékosonkénti állapot összerakása és a küldés) gyorsabban, mint a játékosszám.
Mi akad el először
- A tick (egy szál). A Node egyetlen szálon számolja az egész világot és építi minden játékos állapotcsomagját. Ez 200 játékosnál p95-ben átlépi az 50 ms-ot, 250-nél az átlag is, innentől a szerver tickjei lemaradnak (
behind), a játékosok kevesebb mint 20 állapotot kapnak másodpercenként (300-nál 13-15, 500-nál 4), a ping másodpercekre nő. A két vCPU-ból a játék csak az egyiket használja (a játék-folyamat 80% körül tetőzik), a másikon a Caddy, a kernel hálózati rétege és a többi szolgáltatás osztozik. - A VPS egésze 200-nál 80%, 250 fölött 88-92% (Hetzner CPU). A steal (a hipervizor elvonása) a teljes teszt alatt 0 volt: a megosztott vCPU nem lassult be mástól.
- Hálózat nem szűk: 12,6 MB/s kifelé a 500-nál (kb. 100 Mbit/s), játékosonként ~20 KiB/s (~160 kbit/s). 100 játékossal kb. 135 GB/nap kimenő forgalom lenne, ha egész nap tele lenne.
- Memória nem szűk: a játék 340 MB (200 játékosnál) - 940 MB (500-nál); a VPS memóriája nem fogyott el (nem volt OOM). A tesztunit csúcsa 976 MB volt a 1,5 GB-os határ alatt.
- Lemez nem látszott szűk keresztmetszetnek a Hetzner lemezmetrikái szerint.
Újraindulás, kifagyás
- Nem volt. Az élő szolgáltatások (
nyulak-szigete,tatai-front,akkufalu,kivanjatek-app, Postgres, Caddy)NRestarts=0a teszt előtt és után, azActiveEnterTimestamp-ek nem változtak (az élő Nyulak/healthuptime-ja a mérés alatt végig nőtt), a gép 9 napja nem indult újra. A kernelnapló és a journal: nincs OOM, nincs hung task, nincs hiba a Caddy vagy a KJ naplóban. - A szomszédok válaszideje: az élő Nyulak és a kivanjatek.hu egyetlen mintában sem hibázott (1170 minta, 0 nem-200). 150 játékosig nem mozdultak (~35 ms). 200 felett a KJ válasza lassul: 200-nál átlag 73 ms (max 268), 500-nál átlag 188 ms (max 953 ms). Vagyis ha a Nyulak ténylegesen 200+ játékossal menne, a közös gép miatt a Kívánjáték weboldala is észrevehetően lassabb lenne, de nem állna le.
- Az SSH: a teszt közben SSH-n nem mértem, mert az sshd-t a saját mérőm tévedésből kiverte (lásd lent). A VPS a teszt után azonnal válaszolt, a terhelés közben a Hetzner metrikák és a webes szolgáltatások végig válaszoltak.
- A tesztszerver maga: 500 játékosnál a
/healthés a/stresstöbb mp-ig nem válaszolt (a játék-folyamat tele a tick-sorral), a szerver nem omlott össze: a botok leállása után pár másodperc alatt 0 játékosra ürült és újra aludt. Az 500-as lépcső után a teszt folytatható volt (150, 250, 100/5 perc futott utána hiba nélkül).
Termékkorlát és a hardver határa
| Érték | |
|---|---|
| Termékkorlát most | core.maxPlayers 48 (állítható 1-64), perIp 8, maxConns 96 |
| 48 játékosnál a tick | kb. 20 ms átlag, p95 27 ms (az 50 ms-nak 40%-a) |
| Kényelmes határ (p95 < 50 ms, 0 lemaradás) | kb. 150 játékos |
| Szélén (p95 60 ms, de még 20 snapshot/s) | 200 játékos |
| Szétesik | 250 és fölötte |
A termékkorlát tehát a hardvernél jóval szigorúbb: a 48 játékos mellett a VPS-nek ~60% tartaléka van. A maxPlayers max-értéke a beállításokban 64 (a jelenlegi kód szerint), amit a VPS a mérés szerint bírna (50 és 100 között a tick 20-27 ms átlag). Tovább menni (96-150) a core.maxPlayers, core.perIp (egy IP-ről több játékos: kollégium, mobilháló) és maxConns együttes emelését kéri, és 100+ fölött érdemes az perf.shed-et megtartani bekapcsolva.
Két figyelmeztetés: a botok véletlenszerűen mozognak, a valódi játékosok csoportosulnak (egy ponton sokan: több lövedék, több látható entitás egy játékosnak - ez rosszabb lehet a mostaninál, a battle hostbench-szcenáriót érdemes külön futtatni); és a mérés alatt a KJ-n és az Akkufaluban nem volt valódi forgalom, a számok ehhez képest „üres szomszédok” esetét mutatják.
Mivel lehetne többet
- Több szál: az állapotcsomag-építés és a küldés (
net, kb. 25-35% a tickből 200 fölött) worker szálba vihető; aplayersrendszer (játékosonkénti láthatóság) is, így a tick két magra oszlana. Ez a legnagyobb nyereség ezen a hardveren. - Láthatóság/ritkítás: a távoli entitások ritkább küldése (a
perf.shedezt csinálja), kisebb látótávolság sok játékosnál; alacsonyabb snapshot-ráta (15 Hz) 150+ játékosnál. - Külön gép: a Nyulak a saját VPS-én (vagy dedikált vCPU-s típus, pl. CCX) ne osztozzon a Caddy-vel, a Postgres-sel és a többivel; a megosztott vCPU-n 0 steal volt, de a Caddy és a kernel a második magot viszi.
- Az NPC-hadsereg: 300 NPC kb. 4-5 ms; nem ez viszi a költséget, a játékosszám igen.
Ami nem ment simán (a teljesség kedvéért)
- A
ssh -Lportátirányítás nem működött: a VPS sshd-je tiltja (AllowTcpForwarding no, a keményítés része). Nem kerültem meg; a vezető felvette a terhelés útvonalának a Caddy-átjárósnyulak-stress.kivanjatek.huideiglenes címét, így mértem. (A tunnel- eszközt nem vettem be a repóba; a_tools/hcloud/hcloud.mjs-ből atunnelparancsot kivettem, a fájl egyébként a más által módosított állapotában maradt.) - A saját megfigyelőm 3 mp-enként megnyitotta az sshd portját (banner-olvasás), a fail2ban ezt támadásnak vette, és kb. egy órára kizárta a laptop IP-jét. A terhelés mérését ez nem érintette (a webes szolgáltatások és a Hetzner API más úton megy), de az újraindulás-ellenőrzés és a takarítás SSH-n ezért csak a kitiltás lejárta után (09:26 UTC) készült. A
stress-watch.mjsazóta nem érinti az SSH-t. - 500 játékos belépésénél az első kísérlet a
/healthidőtúllépésén elhasalt a mérőben (a szerver 10 mp-ig sem válaszolt); a mérő azóta ezt elviseli. A 500-as sor a második kísérlet; az első eredménye nem szerepel.
Takarítás
A VPS-en leállítva és törölve: a nyulak-stress és nyulak-hostbench unit, /srv/studio/nyulak-stress, /srv/studio/nyulak-stress-data; a 24791-es port szabad, nincs maradék folyamat vagy unit; a lemez és a memória visszaállt az indulás előtti szintre. Az ideiglenes nyulak-stress.kivanjatek.hu Caddy-útvonalat a vezető távolítja el. A laptopon nem fut tunnel, nincs maradék bot.
Nyers adatok és eszközök
docs/stress-2026-10-25.json: lépcsőnként a mért sor (steps), a mockolt futások (hostbench), a Hetzner idősorok 10 mp-es felbontásban (hetznerSeries: CPU % a két vCPU-n, hálózat B/s, lemez), a szomszédok mintái 3 mp-enként (watch), az újraindulás-ellenőrzés (extra). Időbélyegek ms-ban (a lépcsőktStart/tMeasure0/tEnd), a Hetzner idősoroké is ms-ban.- Eszközök a
tools/alatt:stress-server.mjs(a tesztszerver),stress.mjs(a terhelő),stress-watch.mjs(a szomszédok figyelése),stress-hcloud.mjs(a Hetzner metrikák),stress-report.mjs(az összefésülés), és abotclient.mjs/hostbench.mjskiegészítései.