14 anni di Tribal Wars
Mantenimento e modernizzazione di un browser game legacy
Articolo e Infografica a cura di Jon Dawson, Senior Software Developer di Tribal Wars, forniteci da Innogames e Star2com.
Quando il gioco è stato reso disponibile al pubblico nel 2003 il mondo dello sviluppo web era un ambiente molto diverso. Internet Explorer aveva una quota di mercato del 95%, l’HTML5 non era ancora stato nemmeno immaginato e il supporto dei media interattivi online era molto limitato. Dopo aver lavorato allo sviluppo di Tribal Wars per 10 anni, tornerò indietro nel tempo ripercorrendo la storia del suo sviluppo, oltre a illustrare alcune delle sfide affrontate durante la modernizzazione degli ultimi anni. Descriverò inoltre alcuni degli sforzi compiuti per ridimensionare il gioco dalla presenza su un singolo server a diversi milioni di utenti attivi mensili, oltre a condividere le lezioni apprese lavorando a un prodotto legacy.
Il progetto hobby
Le versioni iniziali di backend del gioco (2003-2006) sono state sviluppate utilizzando PHP 4. La maggior parte della logica di gioco è stata gestita da funzioni raggruppate all’interno di file in base all’area del gioco a cui erano collegate. Le interazioni effettive da parte dei giocatori sono state implementate usando le classi per i controller e Smarty per le views. Tre erano i test più importanti da fare: uno per assicurarsi che il risultato di una battaglia tra due giocatori fosse esatto, uno per testare che l’invio di mail fosse corretto e uno per testare che la localizzazione (via Gettext) funzionasse. Tutti i dati sono stati memorizzati all’interno di un database MySQL 3.23.
Una delle caratteristiche più importanti del gioco è che le azioni venivano eseguite in tempo reale, anche quando non si stava giocando. Un worker PHP è stato utilizzato per selezionare tutte le azioni in sospeso (ad esempio un attacco ad un giocatore) una volta al secondo ed elaborarle.
Il frontend del gioco era semplice e reso esclusivamente dal server, principalmente utilizzando solo tabelle di dati e grafica limitata. L’unico contenuto interattivo è stato raggruppato insieme in un file javascript ~500 line che conteneva la logica per cose come i timer che facevano il conto alla rovescia fino allo zero.
Per quanto concerne la sua infrastruttura, il gioco ha iniziato a esistere su un server, nella stanza di uno dei fondatori. Poco dopo è stato trasferito su un piccolo numero di server affittati da un’azienda locale, ciascuno con un processore Pentium 4 e 2 GB di memoria. Abbiamo inizialmente utilizzato Apache, ma poi siamo passati a lighttpd come server web per la sua natura leggera e ad alte prestazioni. Grazie alla semplicità del gioco, le richieste impiegavano meno di 10 ms per essere completate.
A questo punto il gioco è stato sviluppato da un solo sviluppatore, un grafico e un designer.
Transizione a piccola startup
Nel 2006 Tribal Wars è diventato un gioco browser di successo e, un anno dopo, è stata fondata InnoGames GmbH come azienda che ne continuasse lo sviluppo. È solo da questo punto in poi che abbiamo una storia di sviluppo completa del progetto, a causa di un passaggio da CVS a Subversion all’inizio del 2007.
Nel backend si potrebbero apportare molti miglioramenti alla qualità del codice utilizzando le nuove funzionalità OOP introdotte in PHP 5. Molta attenzione è stata riposta nella stabilità del prodotto e nel garantire che potesse funzionare senza problemi in più mercati. Finora il gioco era disponibile in oltre 15 lingue/territori. Per migliorare le prestazioni abbiamo iniziato a utilizzare memcache a livello di memorizzazione per operazioni costose.
A questo punto sono stati sviluppati diversi “microservizi” molto utili per il supporto all’interno del gioco:
- Un servizio di “aiuto” indipendente è stato utilizzato come CMS per sviluppare manuali/guide per il gioco, che è stato quindi spinto a server di gioco utilizzando XMLRPC.
- È stato sviluppato uno strumento web di localizzazione per consentire ai gestori delle comunità di localizzare facilmente il gioco. Ha funzionato utilizzando i file di linguaggio Gettext dal repository, estraendo un elenco di frasi dal gioco, poi spingendo (impegnando) nel repository per la successiva distribuzione. 10 anni dopo la versione modernizzata di questo strumento viene ancora utilizzata per tutti i nostri giochi.
- Un servizio rudimentale di raccolta di statistiche è stato utilizzato per raccogliere tutte le informazioni di gioco da tutti i server e aggregarlo, consentendo una visualizzazione dettagliata dei numeri dei giocatori, delle tendenze ecc.
Nel frontend, il gioco è rimasto sostanzialmente invariato in termini di tecnologia. In alcune piccole aree del gioco abbiamo iniziato a utilizzare XMLHttpRequest (“AJAX”) per migliorare l’usabilità del gioco, ma mai come requisito. Il primo caso di utilizzo per questo consentiva ai giocatori di rinominare i loro villaggi senza dover ricaricare la pagina.
Il 7 marzo 2007 è stata rilasciata la prima versione di Mootools, un framework Javascript leggero orientato agli oggetti. Meno di due mesi dopo è stato aggiunto al gioco per aiutare a sostenere lo sviluppo del frontend. Ciò ha contribuito all’introduzione di nuove funzionalità, come drag and drop e un tutorial più interattivo per accogliere i giocatori all’interno del gioco.
Il team di sviluppo è rimasto piccolo, con un paio di sviluppatori attivi, un grafico e un amministratore di sistema.
Per supportare la crescita del gioco abbiamo trasferito l’hosting sui nostri server collocati in un data center vicino. I server di database erano 2u 2x Xeon (53xx) con 16GB di memoria e HDD 8x74GB in RAID 1 e server web 1x Xeon 3000/4 core con memoria da 4GB e un disco rigido.
Crescita del gioco
Nel 2009, il team che sta dietro Tribal Wars si era espanso fino a raggiungere il picco di 6 sviluppatori. Quasi tutto lo sviluppo avveniva con un approccio orientato agli oggetti e siamo passati a JIRA per una corretta gestione di bug, nuove funzionalità e del progetto nel suo complesso.
A questo punto il gioco aveva raggiunto milioni di giocatori attivi mensili e questo aveva portato a problemi di performance. Abbiamo utilizzato un mix di scala verticale e orizzontale lanciando regolarmente nuove istanze mondiali che consistevano di un server di database e di diversi webserver. Ciò ha portato a una struttura sbilanciata in quanto i giocatori si sarebbero avvicinati maggiormente ai più recenti mondi liberi lasciandone molto pochi sui mondi più datati. I nostri ultimi mondi avrebbero dovuto lottare per affrontare l’elevato carico di database da tanti utenti concorrenti.
Per cercare di risolvere questo problema parti del gioco sono state spostate in un database secondario in modo che ogni mondo fosse supportato da due server di database. Per essere chiari, non si trattava di una replica, ma di memorizzare diverse tabelle su ciascun server. Nei casi in cui il gioco avesse dovuto eseguire un JOIN tra tabelle su entrambi i server, l’applicazione avrebbe eseguito manualmente tutto ciò in base ai risultati di due query.
Questo ci ha permesso di continuare a far girare il gioco sull’hardware esistente per quasi due anni. Una volta che un hardware migliore fosse disponibile, potremmo tornare ad avere l’intero gioco su una istanza di database. Andando a ritroso, avrebbe potuto rivelarsi una scelta più azzeccata quella di investire in un hardware migliore invece di dividere il database in questo modo. Splittare i database e mantenere il codice è stato uno sforzo significativo per il team di sviluppo.
All’inizio del 2010, abbiamo deciso di rimuovere Mootools dal frontend e passare a jQuery. Il gioco ha cominciato a diventare più complicato poiché sono state aggiunte altre funzionalità con contenuti interattivi. In quel momento, task runner come Grunt o Gulp non esistevano, quindi sono state sviluppate custom mergers e minifiers personalizzate in PHP. Se questo vi sembra arcaico tenete presente che in quel momento Internet Explorer aveva ancora una quota di mercato superiore al 60% e il supporto per IE6 era ancora una necessità.
Passaggio al mobile
Nel 2011 il mercato del browser ha cominciato a evolversi verso giochi altamente grafici e abbiamo raggiunto il nostro numero massimo di giocatori attivi mensili. A questo punto, non molto era cambiato tecnologicamente nel backend del gioco. Il gioco ha avuto una piccola suite di test di unità per le funzionalità di base, ma molte cose non potevano essere testate facilmente a causa della progettazione del codice così datata.
La nostra infrastruttura si è modificata in Macchine Virtuali che girano su server blade, inizialmente ancora separati in server di database (collegati a una rete di storage) e server frontend (con un disco locale). Alla fine la SAN è stata rimossa e ora ciascun server dispone di SSD locali. Continuiamo a utilizzare la stessa configurazione anche oggi, nonostante abbiamo apportato molti altri miglioramenti delle infrastrutture non illustrati in questo articolo.
Abbiamo implementato un cambiamento maggiore al nostro stack, sostituendo lighttpd con nginx per il webserver. Abbiamo osservato un ritmo di sviluppo più lento rispetto a quello desiderato e abbiamo deciso di passare a qualcosa di più mainstream.
Per diversi anni avevamo offerto una versione ridotta del gioco per browser web mobili e nel 2011 abbiamo deciso che era giunto il momento di proporre le nostre applicazioni native per iOS e Android. Mentre il gioco era certamente giocabile sui browser è emerso chiaramente che gli utenti mobili necessitavano di un’esperienza più snella all’interno di un’app. Spingere notifiche sugli eventi all’interno dell’account, come ad esempio un nuovo attacco in arrivo, sono molto importanti in un gioco in tempo reale dove se mancano informazioni vitali come questa per alcune ore può fare la differenza tra una vittoria di successo o una sconfitta.
Questa è stata una delle maggiori sfide per noi: come si può adattare un gioco basato su browser che non dispone di API definito e fornisce un’esperienza mobile, senza dover riprogettare in modo significativo il backend o impegnare un grande numero di nuovi sviluppatori?
La prima versione della nostra applicazione iOS consisteva solamente di registrazione/autenticazione nativa, un bel menu, un webview, notifiche push e, naturalmente, pagamenti mobile. Nel backend abbiamo deciso di creare una nuova API solo per le applicazioni mobile che avrebbero agito come livello di compatibilità per il resto del gioco. Ripensandoci, avrebbe potuto essere un’idea migliore investire tempo nel rifatturare i nostri sistemi in un’unica API ben progettata che potesse essere utilizzata sia per il browser che per il mobile, ma ci sarebbe voluto molto più tempo. Abbiamo cercato quindi di stabilire velocemente una presenza sugli app store per vedere dove questo ci avrebbe portato.
Le notifiche di push e i pagamenti erano gestiti tramite microservizi indipendenti che erano anche in grado di gestire gli altri nostri giochi con il passare del tempo. Quando la nostra applicazione è stata annunciata abbiamo inviato poche migliaia di notifiche push a settimana: ora ne stiamo inviando oltre 100 milioni tramite lo stesso microservizio.
Con il passare degli anni abbiamo aggiunto altre funzionalità all’app iOS e abbiamo finito per avere una soluzione ibrida in cui alcune parti del gioco vengono servite dal nostro backend come contenuti HTML e visualizzati in una webview e altre funzionalità “core” come la mappa, la panoramica e la sede del villaggio e le messaggistica in-game sono implementate nativamente. L’applicazione può passare da uno dei due tipi di schermate e comunicare con il webview utilizzando javascript per assicurarsi che le pagine native e le pagine web siano mantenute in sincronia.
Uno dei vantaggi del nostro livello di compatibilità mobile è che, nonostante la prima versione dell’app iOS abbia più di 6 anni, si potrebbe utilizzarla ancora per accedere e giocare.
Grazie alla presenza stabile negli app store, più di un terzo delle nuove registrazioni ora provengono dalle nostre applicazioni.
Tribal Wars ai giorni nostri
Mentre il team di Tribal Wars ora è più piccolo rispetto al nostro picco (abbiamo due sviluppatori, un grafico, un ingegnere per la qualità, un game designer, un responsabile di prodotto, un lead community manager e un amministratore di sistema) abbiamo apportato svariate modernizzazioni negli ultimi anni nonostante le sfide dovute all’esperienza con il vecchio codice.
Ecco un riepilogo del nostro attuale status tecnologico:
- La maggior parte delle nuove funzionalità sono scritte in PHP (con le nuove funzionalità di PHP7 come i tipi scalari), hanno test di unità e di integrazione se applicabili, e sono progettate come applicazioni mini one-page sul frontend.
- Utilizziamo redis sia per la memorizzazione in cache che per la memorizzazione di alcuni dati (come ad esempio i classificatori dei giocatori) con circa 13 GB di dati in memoria.
- Utilizziamo nodejs e socket.io (usando redis) per abilitare gli aggiornamenti dal nostro backend ai giocatori e per facilitare il nostro sistema di chat.
- Utilizziamo RabbitMQ per eventi queued che non devono essere completati in tempo reale.
- Utilizziamo ancora PHP per elaborare gli eventi in background, anche se ora 4 di essi girano contemporaneamente su un mondo standard.
- Utilizziamo ancora MySQL (ora versione 5.6) come database primario con circa 1,7 terabytes di dati.
- Spediamo oltre 100 milioni di eventi al giorno in un cluster Hadoop per l’analisi.
- Abbiamo implementato HTTPS ovunque dal 2015.
- Abbiamo abilitato HTTP2 sin dall’inizio del 2016.
- Utilizziamo graphana per creare cruscotti e grafici per un rapido sguardo alla “salute” del gioco e alle nuove funzionalità.
- IPV6 è in arrivo 🙂
- Abbiamo una flotta di circa 1150 macchine virtuali per 330 mondi (tutti in esecuzione Debian Jessie).
- Possiamo distribuire una nuova versione del gioco su ciascun server in meno di 5 minuti e cercare di distribuire tutti i contenuti finali almeno una volta alla settimana. Spesso facciamo più implementazioni in uno stesso giorno.
Ora abbiamo meno giocatori rispetto a quanti ne avevamo 5 anni fa, ma possediamo un’infrastruttura molto più complessa, quindi dobbiamo ancora essere molto attenti a prestazioni e bilanciamento. La nostra sfida di scala più grande rimane la seguente: la maggior parte dei nostri giocatori gioca sui nostri mondi annunciati di recente (il 90% dei giocatori sono presenti nei nostri 10 mondi più grandi). A volte colpiamo ancora i limiti verticali della nostra strategia di scaling.
Nonostante l’età del gioco, recentemente abbiamo registrato un mondo polacco con così tanti giocatori attivi che durante le ore di punta abbiamo dovuto gestire oltre 120 attacchi di giocatori che arrivavano ogni secondo. La logica per il calcolo dei risultati di un attacco può essere abbastanza complicata, quindi scalare per gestire questo throughput è stato molto impegnativo per noi. Nel mondo di Tribal Wars è inoltre estremamente importante che ogni azione sia elaborata nell’ordine corretto, il che significa che non è facile gestire ogni attività in parallelo. Ad esempio, se 20 giocatori in una tribù hanno inviato 50 attacchi al villaggio di un giocatore, dobbiamo assicurarci che il risultato di ogni attacco sia coerente con l’uscita dell’attacco precedente, al millisecondo. In generale abbiamo notato che i giocatori dei mondi polacchi sono solitamente più attivi e quindi dobbiamo adeguare la capacità del server per adattarla a loro.
Una delle maggiori modernizzazioni che abbiamo effettuato è stata quella di introdurre socket.io per consentire le notifiche dal nostro backend ai giocatori. Non siamo stati più costretti a raccogliere nuovi dati o aspettare che il browser del giocatore si aggiorni per assicurarsi che siano disponibili dati nuovi. Al momento eseguiamo 4 istanze del nostro server per ogni mondo, con giocatori distribuiti a caso in ogni istanza. Se l’istanza A deve parlare con l’istanza B (ad esempio quando un giocatore sta digitando a un altro giocatore tramite il sistema di chat), redis viene utilizzato come relè per il messaggio. Spediamo oltre 150k messaggi ogni minuto tramite socket.io.
Dopo aver aggiornato il nostro back-end a PHP7 abbiamo constatato una riduzione complessiva pari a circa il 40% del tempo impiegato. Nonostante tutti i miglioramenti effettuati, diverse parti più datate della logica del gioco sono ancora contenute in funzioni diverse. Ci sono ancora poche parti del gioco in cui il codice non è stato toccato dal commit iniziale a Subversion (sì, lo usiamo ancora).
A causa della ridotta dimensione del nostro team abbiamo compreso che in molti casi è meglio non toccare qualcosa se non si è rotto e non abbiamo bisogno di apportare modifiche maggiori ai sistemi circostanti. In altri casi, abbiamo smontato intere funzioni ricreandole da zero.
Il sistema di posta in Tribal Wars è una delle funzionalità più datate e per circa 10 anni è rimasta una combinazione tra chiamate di funzione con logica del database mescolata ai controller. Per lungo tempo abbiamo lavorato a tutto ciò apportando piccoli cambiamenti. Quando la chat è stata implementata nel gioco l’idea era che il backend dovesse essere lo stesso per la chat e la messaggistica in-game. In questo modo si sarebbe potuta utilizzare la chat live sul browser parlando con un giocatore sul cellulare che l’avrebbe visualizzata nell’elenco delle mail. Grazie a un refactoring totale del sistema di messaggistica potremmo facilmente creare un ponte tra il nostro socket.io server e il nostro backend, per consentire una conversazione senza interruzioni tra giocatori o gruppi di giocatori.
Grazie alla natura ibrida delle nostre applicazioni mobili possiamo introdurre nuove funzionalità senza richiedere alcun tipo di aggiornamento dal lato client. Introduciamo regolarmente nel gioco nuovi eventi temporanei e, grazie alle straordinarie funzionalità dell’HTML5, possiamo offrire una interessante capacità di risposta tramite il webview incorporato.
Si potrebbero trovare difficoltà nei luoghi più imprevisti quando si lavora su un progetto così datato. Mentre Tribal Wars è cresciuta, si è sviluppata la scrittura di script personalizzati in JavaScript e i giocatori hanno cominciato a scrivere agli helper, strumenti utili e che hanno funzionato nel contesto del gioco, un sistema rudimentale di addon. Purtroppo molti di questi script erano serviti esternamente a causa di una connessione non sicura e smettevano di funzionare non appena il gioco veniva caricato in modo sicuro. Per eliminare il problema abbiamo dovuto sviluppare ed eseguire un proxy inverso temporaneo per recuparere il contenuto di questi script esterni servendoli su https tramite i nostri server. Quindi abbiamo iniziato a mettere in guardia i nostril giocatori ogni volta che ciò accadeva.
Le estensioni create dal giocatore rappresentano ancora una parte importante di Tribal Wars, dove possiamo provare sempre e assicurarci che i nostri cambiamenti non interferiscano con le creazioni dei giocatori. Negli ultimi anni abbiamo prestato molta attenzione anche alle personalizzazioni più popolari e quando vediamo un chiaro caso di funzionalità che fornirà un’esperienza migliore per quanto concerne la base del giocatore, ci impegniamo per implementarla autonomamente.
Insegnamenti tratti dal lavoro su un prodotto legacy
Dopo aver lavorato personalmente a Tribal Wars per circa 10 anni come sviluppatore funzionale (backend, frontend, mobile e server base), uno tra gli insegnamenti più importanti che ho appreso è che non sempre devi ricostruire il vecchio codice per poter aggiungere miglioramenti a un prodotto legacy. Ci sono stati molti momenti di ridefinizione dove avremmo potuto stoppare ciò che stavamo facendo per concentrarci solo sul miglioramento della qualità dei codici e potenzialmente spendere mesi su qualcosa che non avrebbe portato alcun vero valore ai nostri giocatori.
Penso anche che lavorare su un codebase legacy non significa che si dovrebbe avere paura di provare nuove tecnologie. Non abbiate paura di testare esperimenti più piccoli come implementare le notifiche del browser. Provate a impegnarvi in progetti più grandi, come la creazione del nostro back-end di nodejs. Abbracciate nuove tecnologie come HTTP2 prima possibile.
È meglio avere qualcosa che funzioni piuttosto di avere qualcosa di perfetto. Si può rilasciare velocemente e ricevere commenti per futuri miglioramenti anziché lavorare all’infinito su qualcosa che potrebbe avere un’implementazione perfetta del back-end, ma che è odiata dai tuoi clienti.
Aggiorna il tuo ambiente non appena puoi e utilizza le nuove funzionalità disponibili per portare cose nuove e fresche ai tuoi utenti finali. Assicurati di comprendere tutti i componenti della tua piattaforma. Non metterti in un angolo curando solo una specialità (ad esempio, lo sviluppo di frontend) quando si dovrebbe avere almeno una conoscenza di base di come funziona il backend, cos’è l’infrastruttura del server, ecc.
E, infine, sii orgoglioso di ciò su cui lavori e non utilizzare l’età di un prodotto come scusa per prendere scorciatoie o ricorrere a una qualità inferiore.