← Haberler

Tavla24'ün gerçek zamanlı online tavla sunucusunu neden Go ile yeniden yazdık

Online çok oyunculu tavlamızı yük altında nasıl hızlandırdık: gerçek zamanlı sıcak yolu küçük bir Go servisine taşıdık.

technical 1 Temmuz 2026

Her online oyun tek bir şeyle yaşar ya da ölür: bir pulu dokunduğunuzda ve tahtanın hareket etmesini beklediğinizde nasıl hissettirdiği. O an anlıksa, oyun canlı hisseder. Takılırsa — bir saniyeyle bile olsa — geri kalan her şey ne kadar iyi olursa olsun her şey bozuk gibi gelir.

Bu, o anı Tavla24’te — ücretsiz online çok oyunculu tavla oyunumuzda — nasıl daha hızlı ve daha kararlı hale getirdiğimizin hikayesi: backend’imizin gerçek zamanlı kısmını, uygulamanın geri kalanını çalıştıran framework’ten alıp Go ile yazılmış küçük, amaca özel bir servise taşıyarak. İki tür okur için yazıldı: sadece oyunun neden artık daha iyi hissettirdiğini bilmek istiyorsanız, ilk yarısı sizin için. Mühendislik ayrıntısını — yük testlerini, mimariyi, ödünleşimleri — istiyorsanız, hepsi ikinci yarıda, açıkça işaretlenmiş halde. Ve şu anda tercihiniz doğrudan online tavla oynamak ise, buyurun — geri kalanı sonra da burada olacak.

Kısa versiyon

Backend’imiz Serverpod framework’ü üzerine kurulu tek bir Dart uygulamasıydı. Her şeyi yapıyordu: hesaplar, arkadaşlar, turnuvalar, uygulama içi satın almalar, sohbet — ve canlı oyunun kendisi. Serverpod bunun için gerçekten mükemmel — küçük bir ekibin tam, tipli bir istemci-sunucu ürününü hızlıca yayınlamasını sağladı ve Tavla24’ün büyük kısmını bugün hâlâ çalıştırıyor. Ama canlı oyun, geri kalanından temelde farklı türde bir iş ve aynı anda daha fazla insan oynadıkça, ikisi aynı kaynaklar için rekabet etmeye başladı. Sistemin en hızlı olması gereken kısmı — canlı bir maçta pulları hareket ettirmek — diğer her şeyle aynı şeridi paylaşıyordu.

Bu yüzden canlı oyun kısmını kendi özel servisine ayırdık. Yalnızca gerçek zamanlı sıcak yolu yönetiyor: zarlar, hamleler, tahta durumu ve iki oyuncuyu senkronize tutmak. Diğer her şey tam olduğu yerde kaldı. Sonuç şu: yoğun bir eşleştirme akşamı artık hamle yaptığınız anı yavaşlatmıyor — ve sistemin iki yarısı artık birbirinden bağımsız büyüyebiliyor.

Bekleme neden önemli

Online çok oyunculu bir tavla maçında, telefonunuzla sunucumuz arasında uçuşan mesajlar minik ve süreklidir: Zarları attım, işte gelenler; bu pulu oynadım; bu geçerli bir hamle mi; işte yeni tahta; sıra sende. Oyun başına düzinelercesi, hepsi gecikmeye duyarlı, hepsinin neredeyse anında her iki oyuncuya ulaşması gerekiyor — canlı bir rakibe karşı zar atarken gerçek zamanlı çok oyunculu tam olarak budur.

Bu, örneğin arkadaş listenizi yüklemek ya da bir satın almayı işlemekten çok farklı bir iş biçimidir. Onlar ara sıra olur ve birkaç yüz milisaniyeyi tolere edebilir. Canlı hamleler edemez — bir hamlede bir saniyelik gecikme, duyarlı hisseden bir oyunla takılan bir oyun arasındaki farktır.

Tek bir sistem her ikisini de yönettiğinde, ara sıra olan ama ağır iş, sürekli olan ama hafif işin önüne geçebilir. Görmeye başladığımız tam olarak buydu.


Teknik hikaye

Buradan itibaren iş mühendisliğe giriyor. Bu sizin tarzınız değilse, yukarıdaki çıkarım her şeyin özü: oyun daha hızlı çünkü en zamana duyarlı işi artık kendi başına çalışıyor.

Başlangıç noktası

Orijinal backend, Dart ile yazılmış bir Serverpod uygulaması ve bunu açıkça söylemek gerekir: Serverpod harika bir framework. Bize bir sürü şeyi bedavaya verdi — üretilmiş istemci SDK’ları, Flutter uygulaması ile sunucu arasında paylaşılan tipli bir protokol, kimlik doğrulama, veritabanı erişimi, WebSocket akışları — ve küçük bir ekibin dikkate değer ölçüde hızlı ilerlemesini sağladı. Ürünün büyük kısmını bugün hâlâ çalıştırıyor: kimlik doğrulama, kullanıcılar, arkadaşlar, turnuvalar, uygulama içi satın almalar, sohbet ve yönetim araçları — ve bunu değiştirmeyi planlamıyoruz.

Gerçek zamanlı oyun aynı süreç içinde çalışıyordu. Sunucu otoriterdi: zarları o atıyor, her hamleyi tavla kurallarına göre doğruluyor, kanonik tahta durumunu tutuyor ve güncellemeleri WebSocket akışları üzerinden iki istemciye de gönderiyordu. Doğru ve temiz — ama üründeki diğer her endpoint’le bir runtime paylaşıyordu.

Duvarı bulmak

Herhangi bir şeyi değiştirmeden önce sezgi değil, sayılar istedik. Bu yüzden kimlik doğrulaması yapan, eşleştirmeye giren, gerçek oyunlara eşleştirilen ve baştan sona rastgele geçerli hamleler oynayan sentetik oyuncular üreten bir yük testi düzeneği kurduk — gerçek bir Flutter istemcisinin kullandığı tam aynı WebSocket akışlarından geçerek. Kestirme yok, sahte endpoint yok.

Birkaç bilinçli seçim sonuçları güvenilir kıldı:

  • Bölgeler arası, ayrı donanım. Yük üreteci bir veri merkezinde (Nürnberg) çalışıyor ve sunucuyu başka birinde (Helsinki) yaklaşık 30 ms uzaklıkta vuruyordu. Bu, aynı makinede yapılan bir kıyaslamadan çok gerçek bir kullanıcıya daha yakın ve yük üretecinin ölçtüğümüz sunucudan CPU çalamayacağını garantiledi.
  • Otomatik artan eşzamanlılık. Düzenek, eşzamanlı oyuncu sayısını sabit bir programa göre iki katına çıkarıyor ve her beş saniyede bir anlık görüntü kaydediyordu: kimlik doğrulama gecikmesi, eşleştirme gecikmesi ve p50/p95/p99’da hamle onay gecikmesi, ayrıca hata ve oyun zaman aşımı sayıları.
  • Dürüst kırılma eşikleri. “Bozuk”u baştan tanımladık — hamle onayı p95’i 1,5 s üzerinde ya da hamle hata oranı %2 üzerinde — böylece test bir grafiği gözle tahmin etmemiz yerine başarısızlığı nesnel olarak ilan etti.

Sonuçların şekli netti. Düşük eşzamanlılıkta sunucu mükemmel derecede sağlıklıydı: birkaç düzine oyuncuyla hamle onayları kabaca 100–180 ms’de geri geliyordu. Ama düzenek arttıkça — 80, sonra 160, sonra 320 eşzamanlı oyuncu — gerçek zamanlı yol keskin biçimde bozuldu. Hamle onayı p95’i çoklu-saniye aralığına tırmandı, p99 kuyruğu çok daha kötü uzadı, eşleştirme yavaşladı ve oyunlar dört dakikalık hükmen kayıp zaman aşımıyla ölmeye başladı: önce bir avuç, sonra düzinelerce. 320 oyuncu civarında bir yerde kırılma eşikleri aşıldı.

Önemli içgörü “Dart yavaş” ya da “Serverpod bunu kaldıramaz” değildi — ikisi de doğru değil ve ikisi de Tavla24’ün geri kalanını bugüne dek rahatça çalıştırıyor. Daha basitti: aynı anda üründeki her ağır endpoint’i taşıyan genel amaçlı bir istek işleme modeli, minik, gecikmeye kritik oyun mesajlarından oluşan bir su hortumu için ideal biçim değildir. Bu, hemen hemen her hepsi-bir-arada framework için geçerlidir — sıcak yol kendi gereksinim setini geliştirmişti ve hem tam olarak buna göre ayarlanmış bir eşzamanlılık modeline hem de her şeyle rekabet etmeyi bırakması için kendi runtime’ına ihtiyacı vardı.

Karar: yeniden yazma değil, bir sidecar

Cazip hamle — tüm backend’i yeniden yazmak — aynı zamanda yanlış olandı. Serverpod’un yüzeyinin çoğu (hesaplar, IAP, turnuvalar, sohbet) gayet iyi çalışıyordu ve darboğazla hiçbir ilgisi yoktu. Onu yeniden yazmak sıfır fayda için devasa bir riskti.

Bunun yerine sidecar desenini kullandık. Yalnızca gerçek zamanlı oyun sıcak yoluna sahip küçük, ayrı bir servis kurduk ve diğer her şeyin sahipliğini Serverpod’a bıraktık. İkisi aynı VPS üzerinde yan yana çalışıyor ve aynı Postgres ile Redis’i paylaşıyor, dolayısıyla aralarında veri senkronizasyonu sorunu yok — sadece sorumlulukların temiz bir ayrımı. Etki alanı küçüktü, sunum kademeli olabilirdi ve ters giderse geri dönebilirdik.

O servis için Go’yu seçtik, özellikle eşzamanlılığı nasıl ele aldığı için.

Mimari: oda başına bir goroutine

Çekirdek tasarım oyun odası başına bir goroutine.

Go’da bir goroutine, son derece hafif bir eşzamanlı yürütme birimidir — tam bir işletim sistemi iş parçacığı değil, birkaç kilobayta mal olur — ve Go runtime’ı bunların binlercesini mevcut CPU çekirdekleri arasında çoğullar. Tasarımı mümkün kılan tam da bu özellik: her aktif maç, o maçın durumuna özel olarak sahip olan kendi goroutine’ını alır.

Bu, bir oyun sunucusu için muazzam derecede önemli olan iki şey satın alır:

  • Sıcak yolda kilit yok. Her odanın durumuna tek bir goroutine sahip olduğu için, o maç için her zar atışı, hamle ve tahta güncellemesi o goroutine tarafından teker teker işlenir. Sistemin diğer kısımları bir odaya, verisine uzanıp dokunarak değil, ona bir komut göndererek konuşur. Sıcak yolda üzerinde çekişilecek paylaşılan değiştirilebilir durum yoktur; bu da kilit çekişmesi ve veri yarışı olmaması demektir — eşzamanlılık hata sınıfı, ayıklanarak değil tasarımla dışarıda bırakılır.
  • Doğal yalıtım. Yoğun ya da hatalı davranan bir oda bir diğerini bozamaz ya da bloke edemez. On bin sessiz oda beklerken neredeyse hiçbir maliyeti olmaz.

Bu oda goroutine’larının etrafında ince bir destek kadrosu oturur:

  • Kimliğe göre O(1) oda araması sağlayan, süreç genelinde bir oda hub’ı — okuma için optimize edilmiş bir kilit arkasında basit bir map. Kilit yalnızca mikroskobik arama için tutulur; tüm gerçek oda başına iş her odanın kendi goroutine’ı arkasında olur, dolayısıyla hub asla bir darboğaz haline gelmez.
  • Bloke etmeyen bir WebSocket katmanı. Bağlantının okuma döngüsü mümkün olduğunca az iş yapar: gelen bir mesaj zarfını çözer ve doğru oda ya da lobi goroutine’ına devreder. Soket okuma yolunda asla ağır iş olmaz, dolayısıyla yavaş bir işlem bağlantıyı durduramaz.
  • Postgres’e bir bağlantı havuzu (pgx aracılığıyla) ve mesaj tipleri kasıtlı olarak Serverpod’un enum’larını yansıtan derli toplu, sürümlenmiş bir wire protokolü. Bu son ayrıntı, Flutter uygulamasının yeni servisle zaten bildiği aynı paylaşılan kelime dağarcığını kullanarak konuşmasını sağlayan şeydi — uygulama tarafına özel bir oyun istemcisi ekledik ve canlı trafiği riskli tek bir geçişle değil, oyun oyun Go servisine taşıyabildik.

Bütün mesele minik bir konteynere derlenir: distroless bir runtime imajı üreten çok aşamalı bir Docker derlemesi. Küçük imaj, hızlı başlangıç ve mütevazı bir bellek ayak izi — ki bu, ana backend ile bir kutuyu paylaştığında önemlidir.

Bu yük altında neden daha hızlı — dürüstçe

Burada bilinçli olarak dikkatliyiz: eski yol için titiz bir taban çizgimiz var (yukarıdaki yük testleri) ve verilerini yayınlamadığımız kesin “sonra” rakamları alıntılamayacağız. Ama yeni tasarımın eşzamanlılık altında neden daha iyi dayandığının nedenleri umut dolu değil, yapısaldır:

  • İş yüküne uyan eşzamanlılık. Canlı bir oyun, doğal olarak bağımsız, uzun ömürlü, mesaj güdümlü bir aktör olarak modellenir. Oda başına goroutine bunu bire bir eşler. Binlerce eşzamanlı maç, runtime’ın her çekirdeğe dağıttığı binlerce ucuz goroutine olur — farklı bir trafik biçimi için kurulmuş bir modelden kuyrukta bekleyen binlerce istek yerine.
  • Sıcak yol çekişmesi yok. Her odanın durumunu kendi goroutine’ı üzerinden serileştirmek, sistemin en sık çalışan ve en önemli kısmından kilitlemeyi kaldırır. Daha az koordinasyon yükü daha öngörülebilir gecikme demektir — özellikle eski yolun en çok acıttığı p99 kuyruğunda.
  • Kaynak yalıtımı. Gerçek zamanlı servis ile ana backend artık bir runtime paylaşmıyor. Yoğun bir akşamda eşleştirmedeki bir dalgalanma, hamlenizi doğrulayan kodu açlığa mahkûm edemez ve tersi de geçerli. Her biri kendi başına ölçeklenebilir, profillenebilir ve akıl yürütülebilir.
  • Derlenmiş, öngörülebilir bir runtime. Düşük gecikmeli bir çöp toplayıcıya sahip yerel bir Go ikili dosyası, minik, sık mesaj patlamalarını eşit biçimde ele alır — tam olarak canlı tavlanın trafik profili.

Neyi koruduk ve size neyi kopyalamanızı söyleriz

En memnun olduğumuz kısım dil seçimi değil — özkontrol. Çalışan bir sistemi yeniden yazmadık. Gereksinimleri çevresindeki her şeyden ayrışan tek bileşeni belirledik, o bileşene gerçekten ihtiyacı olan runtime’ı verdik ve geri kalanını rahat bıraktık. Paylaşılan veritabanı, paylaşılan önbellek, uyumlu protokol, kademeli geçiş, kolay geri dönüş.

Bundan tek bir şey çıkaracaksanız o da şu: yeniden yazmaya uzanmadan önce, gerçek duvarı bulacak kadar dürüstçe yük testi yapın ve sonra yalnızca gerçekten ona çarpan şeyi taşıyın.


Farkı kendiniz hissetmek ister misiniz? Ücretsiz online tavla oynayabilirsiniz — hesap gerekmez — gerçek rakiplere karşı hızlı, güvenilir çok oyunculu maçlarda, doğrudan tarayıcınızda play.table-online.ro adresinde. Telefonunuzu mu tercih edersiniz? Online tavla zar oyunumuz Tavla24’ü App Store veya Google Play’den indirin ve dünyanın dört bir yanındaki oyunculara karşı zar atın.

İyi zarlar. 🎲

Oynamaya hazır mısın?

Şimdi Oyna
Şimdi Oyna