← Vesti

Zašto smo Tavla42 server za bekgemon u realnom vremenu ponovo napisali u Go-u

Kako smo naš onlajn bekgemon ubrzali pod opterećenjem — realtime deo prebacili smo na mali servis napisan u Go-u.

technical 1. јул 2026.

Svaka onlajn igra živi ili umire zbog jedne stvari: kako se oseća kada dodirnete figuru i sačekate da se tabla pomeri. Ako je taj trenutak trenutan, igra deluje živo. Ako zapinje — makar za sekundu — sve deluje pokvareno, bez obzira koliko je sve ostalo dobro.

Ovo je priča o tome kako smo taj trenutak učinili bržim i stabilnijim u Tavla42 — našoj besplatnoj onlajn multiplejer igri bekgemona — premeštanjem dela našeg bekenda koji radi u realnom vremenu sa okvira koji pokreće ostatak aplikacije na mali, namenski servis napisan u Go-u. Napisano je za dve vrste čitalaca: ako samo želite da znate zašto igra sada deluje bolje, prva polovina je za vas. Ako želite inženjerske detalje — testove opterećenja, arhitekturu, kompromise — sve je to u drugoj polovini, jasno označeno. A ako biste radije odmah igrali bekgemon onlajn, samo napred — ostatak ovoga će i dalje biti ovde posle.

Kratka verzija

Naš bekend je bila jedna Dart aplikacija izgrađena na Serverpod okviru. Radila je sve: naloge, prijatelje, turnire, kupovine unutar aplikacije, čet — i samu igru uživo. Serverpod je zaista odličan za ovo — omogućio je malom timu da brzo isporuči kompletan, tipiziran klijent-server proizvod, i dan-danas pokreće većinu Tavla42. Ali igra uživo je fundamentalno drugačija vrsta posla od ostatka, i kako je sve više ljudi igralo istovremeno, to dvoje je počelo da se bori za iste resurse. Deo sistema kome je najviše potrebna brzina — pomeranje figura u meču uživo — delio je traku sa svime ostalim.

Zato smo deo za igru uživo izdvojili u sopstveni namenski servis. On obrađuje samo vruću putanju u realnom vremenu: kockice, poteze, stanje table i održavanje oba igrača sinhronizovanim. Sve ostalo je ostalo tačno tamo gde je bilo. Rezultat je da užurbano veče uparivanja igrača više ne usporava trenutak kada odigrate potez — i dve polovine sistema sada mogu da rastu nezavisno.

Zašto je čekanje bitno

U meču onlajn multiplejer bekgemona, poruke koje lete između vašeg telefona i našeg servera su sitne i konstantne: bacio sam kockice, evo šta je palo; pomerio sam ovu figuru; da li je to dozvoljen potez; evo nove table; ti si na potezu. Desetine njih po partiji, sve osetljive na kašnjenje, sve moraju da stignu do oba igrača gotovo trenutno — to je ono što multiplejer u realnom vremenu znači kada bacate kockice protiv živog protivnika.

To je veoma drugačiji oblik posla od, recimo, učitavanja vaše liste prijatelja ili obrade kupovine. Te stvari se dešavaju povremeno i mogu da podnesu nekoliko stotina milisekundi. Potezi uživo ne mogu — kašnjenje od jedne sekunde na potezu je razlika između igre koja deluje responzivno i one koja deluje trapavo.

Kada jedan sistem obrađuje oboje, povremeni-ali-teški posao može da stane na put konstantnom-ali-lakom poslu. To je upravo ono što smo počeli da primećujemo.


Tehnička priča

Odavde pa nadalje ulazi se u inženjering. Ako to nije vaša stvar, zaključak iznad je čitava poenta: igra je brža jer njen vremenski najosetljiviji posao sada radi zasebno.

Polazna tačka

Originalni bekend je Serverpod aplikacija napisana u Dart-u, i vredi to reći jasno: Serverpod je odličan okvir. Dao nam je mnogo besplatno — generisane klijentske SDK-ove, tipiziran protokol deljen između Flutter aplikacije i servera, autentifikaciju, pristup bazi podataka, WebSocket tokove — i omogućio je malom timu da se kreće izuzetno brzo. I dan-danas pokreće većinu proizvoda: autentifikaciju, korisnike, prijatelje, turnire, kupovine unutar aplikacije, čet i alate za administraciju, i nemamo planove da to menjamo.

Igra u realnom vremenu radila je unutar tog istog procesa. Server je bio autoritativan: bacao je kockice, validirao svaki potez prema pravilima bekgemona, držao kanonsko stanje table i gurao ažuriranja ka oba klijenta preko WebSocket tokova. Ispravno i uredno — ali delio je runtime sa svakim drugim endpointom u proizvodu.

Pronalaženje zida

Pre nego što bilo šta promenimo, želeli smo brojeve, a ne pretpostavke. Zato smo izgradili sistem za testiranje opterećenja koji stvara sintetičke igrače koji se autentifikuju, ulaze u uparivanje, budu upareni u prave partije i igraju nasumične dozvoljene poteze od početka do kraja — prolazeći kroz potpuno iste WebSocket tokove koje koristi pravi Flutter klijent. Bez prečica, bez lažiranih endpointa.

Nekoliko namernih odluka učinilo je rezultate pouzdanim:

  • Različiti regioni, odvojen hardver. Generator opterećenja radio je u jednom data centru (Nirnberg) i pogađao server u drugom (Helsinki), otprilike 30 ms razdaljine. To je bliže pravom korisniku nego benchmark na istoj mašini, i garantovalo je da generator opterećenja ne može da ukrade CPU od servera koji smo merili.
  • Automatsko povećavanje konkurentnosti. Sistem je udvostručavao broj istovremenih igrača po fiksnom rasporedu i beležio snimak svakih pet sekundi: kašnjenje autentifikacije, kašnjenje uparivanja i kašnjenje potvrde poteza na p50/p95/p99, plus broj grešaka i isteka vremena partija.
  • Iskreni pragovi kvara. Definisali smo “pokvareno” unapred — p95 potvrde poteza iznad 1,5 s, ili stopa grešaka poteza iznad 2% — tako da je test objektivno proglasio neuspeh umesto da mi odokativno gledamo grafikon.

Oblik rezultata bio je jasan. Pri niskoj konkurentnosti server je bio savršeno zdrav: sa nekoliko desetina igrača, potvrde poteza vraćale su se za otprilike 100–180 ms. Ali kako se sistem povećavao — 80, pa 160, pa 320 istovremenih igrača — putanja u realnom vremenu se naglo pogoršavala. p95 potvrde poteza penjao se u opseg od više sekundi, p99 rep se razvlačio mnogo gore, uparivanje je usporavalo, i partije su počele da umiru na tajmeru predaje od četiri minuta: nekoliko na početku, pa desetine. Negde oko oznake od 320 igrača, pragovi kvara su se aktivirali.

Važan uvid nije bio “Dart je spor” ili “Serverpod to ne može da izdrži” — nijedno nije tačno, i oba udobno pokreću ostatak Tavla42 do dana današnjeg. Bilo je jednostavnije: model obrade zahteva opšte namene, koji istovremeno nosi svaki težak endpoint u proizvodu, nije idealan oblik za bujicu sitnih, na kašnjenje kritičnih poruka igre. To važi za skoro svaki all-in-one okvir — vruća putanja je razvila sopstveni skup zahteva, i bili su joj potrebni i model konkurentnosti podešen tačno za to i sopstveni runtime da prestane da se bori sa svime ostalim.

Odluka: sidecar, a ne prepisivanje

Primamljiv potez — prepisati čitav bekend — bio je i pogrešan. Većina Serverpod-ove površine (nalozi, IAP, turniri, čet) radila je dobro i nije imala nikakve veze sa uskim grlom. Prepisivanje bi bilo ogroman rizik za nulti dobitak.

Umesto toga koristili smo sidecar obrazac. Izgradili smo mali, odvojen servis koji poseduje samo vruću putanju igre u realnom vremenu, a Serverpod smo ostavili da poseduje sve ostalo. Ta dva rade jedan pored drugog na istom VPS-u i dele isti Postgres i Redis, tako da nema problema sa sinhronizacijom podataka između njih — samo čista podela odgovornosti. Radijus eksplozije bio je mali, uvođenje je moglo biti postepeno, i ako bi krenulo naopako mogli smo da se vratimo.

Za taj servis izabrali smo Go, konkretno zbog načina na koji obrađuje konkurentnost.

Arhitektura: jedna goroutine po sobi

Osnovni dizajn je goroutine po sobi za igru.

U Go-u, goroutine je izuzetno lagana jedinica konkurentnog izvršavanja — košta nekoliko kilobajta, a ne čitav nit operativnog sistema — i Go runtime multipleksira mnoge hiljade njih preko dostupnih CPU jezgara. To svojstvo je ono što čini dizajn mogućim: svaki aktivan meč dobija sopstvenu goroutine koja ekskluzivno poseduje stanje tog meča.

Ovo donosi dve stvari koje su ogromno bitne za server igre:

  • Bez zaključavanja na vrućoj putanji. Pošto jedna goroutine poseduje stanje svake sobe, svako bacanje kockica, potez i ažuriranje table za taj meč obrađuje se jedno po jedno od strane te goroutine. Drugi delovi sistema razgovaraju sa sobom slanjem komande, a ne posezanjem unutra i diranjem njenih podataka. Nema deljenog promenljivog stanja oko kog bi se takmičilo na vrućoj putanji, što znači nema takmičenja za zaključavanje i nema trka za podacima — klasa grešaka konkurentnosti je dizajnirana da nestane umesto da se debaguje.
  • Prirodna izolacija. Jedna zauzeta soba ili soba koja se loše ponaša ne može da pokvari ili blokira drugu. Deset hiljada tihih soba košta gotovo ništa dok čekaju.

Oko tih goroutine soba nalazi se tanka pomoćna postavka:

  • Hub soba za ceo proces — jednostavna mapa iza zaključavanja optimizovanog za čitanje — koji daje O(1) pretragu sobe po ID-u. Zaključavanje se drži samo za mikroskopsku pretragu; sav pravi posao po sobi dešava se iza sopstvene goroutine svake sobe, tako da hub nikada ne postaje usko grlo.
  • Neblokirajući WebSocket sloj. Petlja čitanja veze radi što manje: dekodira dolaznu poruku i predaje je pravoj goroutine sobe ili čekaonice. Težak posao se nikada ne dešava na putanji čitanja soketa, tako da spora operacija ne može da zaustavi vezu.
  • Skup veza ka Postgres-u (preko pgx) i kompaktan, verzionisan wire protokol čiji tipovi poruka namerno preslikavaju Serverpod-ove enume. Taj poslednji detalj je ono što je omogućilo Flutter aplikaciji da razgovara sa novim servisom koristeći isti deljeni rečnik koji je već poznavala — dodali smo namenski klijent za igru na strani aplikacije i uspeli da migriramo saobraćaj uživo na Go servis partiju po partiju, umesto u jednom rizičnom prebacivanju.

Sve se to gradi u sitan kontejner: višefazni Docker build koji proizvodi distroless runtime sliku. Mala slika, brzo pokretanje i skroman memorijski otisak — što je bitno kada deli mašinu sa glavnim bekendom.

Zašto je ovo brže pod opterećenjem — iskreno

Ovde smo namerno pažljivi: imamo rigoroznu osnovu za staru putanju (testovi opterećenja iznad), i nećemo citirati precizne “posle” brojeve za koje nismo objavili podatke. Ali razlozi zbog kojih se novi dizajn bolje drži pod konkurentnošću su strukturni, a ne puka nada:

  • Konkurentnost koja odgovara opterećenju. Partija uživo se prirodno modelira kao nezavisni, dugoveki, na porukama zasnovani akter. Goroutine-po-sobi to preslikava jedan-na-jedan. Hiljade istovremenih mečeva postaju hiljade jeftinih goroutine koje runtime raspoređuje preko svakog jezgra — umesto hiljada zahteva koji čekaju u redu kroz model izgrađen za drugačiji oblik saobraćaja.
  • Bez takmičenja na vrućoj putanji. Serijalizacija stanja svake sobe kroz sopstvenu goroutine uklanja zaključavanje iz dela sistema koji se pokreće najčešće i najviše je bitan. Manje troškova koordinacije znači predvidljivije kašnjenje, posebno u p99 repu gde je stara putanja najviše bolela.
  • Izolacija resursa. Servis u realnom vremenu i glavni bekend više ne dele runtime. Nalet uparivanja u užurbano veče ne može da izgladni kod koji validira vaš potez, i obrnuto. Svaki se može skalirati, profilisati i o njemu se rasuđivati zasebno.
  • Kompajliran, predvidljiv runtime. Nativni Go binarni fajl sa sakupljačem otpada niskog kašnjenja ravnomerno obrađuje nalete sitnih, čestih poruka — upravo profil saobraćaja bekgemona uživo.

Šta smo zadržali, i šta bismo vam rekli da kopirate

Deo kojim smo najzadovoljniji nije izbor jezika — to je suzdržanost. Nismo prepisali sistem koji radi. Identifikovali smo jednu komponentu čiji su se zahtevi razišli od svega oko nje, dali toj komponenti runtime koji joj je zaista bio potreban, i ostavili ostalo na miru. Deljena baza podataka, deljeni keš, kompatibilan protokol, postepena migracija, lako vraćanje.

Ako uzmete jednu stvar iz ovoga: pre nego što posegnete za prepisivanjem, testirajte opterećenje dovoljno iskreno da pronađete pravi zid, a onda premestite samo ono što stvarno udara u njega.


Želite sami da osetite razliku? Možete igrati bekgemon onlajn besplatno — bez potrebe za nalogom — protiv pravih protivnika u brzim, pouzdanim multiplejer mečevima, direktno u vašem pregledaču na play.table-online.ro. Više volite telefon? Preuzmite Tavla42, našu onlajn bekgemon igru sa kockicama, sa App Store-a ili Google Play-a i bacajte kockice protiv igrača širom sveta.

Igrajte dobro. 🎲

Spreman za igru?

Igraj odmah
Igraj odmah