Ridimensionamento Gioca! a migliaia di richieste simultanee
Pubblicato: 2022-03-11Gli sviluppatori Web di Scala spesso non considerano le conseguenze di migliaia di utenti che accedono contemporaneamente alle nostre applicazioni. Forse è perché amiamo creare rapidamente prototipi; forse è perché testare tali scenari è semplicemente difficile .
Indipendentemente da ciò, sosterrò che ignorare la scalabilità non è così male come sembra, se si utilizza il set appropriato di strumenti e si seguono buone pratiche di sviluppo.
Lojinha e il gioco! Struttura
Qualche tempo fa, ho avviato un progetto chiamato Lojinha (che si traduce in "piccolo negozio" in portoghese), il mio tentativo di costruire un sito di aste. (A proposito, questo progetto è open source). Le mie motivazioni erano le seguenti:
- Volevo davvero vendere delle vecchie cose che non uso più.
- Non mi piacciono i siti di aste tradizionali, specialmente quelli che abbiamo quaggiù in Brasile.
- Volevo “giocare” con il Play! Quadro 2 (gioco di parole).
Quindi ovviamente, come accennato in precedenza, ho deciso di utilizzare Play! Struttura. Non ho un conteggio esatto di quanto tempo ci è voluto per costruire, ma di certo non passò molto tempo prima che il mio sito fosse attivo e funzionante con il semplice sistema distribuito su http://lojinha.jcranky.com. In realtà, ho dedicato almeno la metà del tempo di sviluppo al design, che utilizza Twitter Bootstrap (ricorda: non sono un designer...).
Il paragrafo sopra dovrebbe chiarire almeno una cosa: non mi sono preoccupato troppo delle prestazioni, se non del tutto durante la creazione di Lojinha.
E questo è esattamente il mio punto: c'è potere nell'usare gli strumenti giusti, strumenti che ti tengono sulla strada giusta, strumenti che ti incoraggiano a seguire le migliori pratiche di sviluppo dalla loro stessa costruzione.
In questo caso, quegli strumenti sono Play! Framework e la lingua di Scala, con Akka che fa alcune "apparizioni come ospiti".
Lascia che ti mostri cosa intendo.
Immutabilità e memorizzazione nella cache
È generalmente accettato che ridurre al minimo la mutabilità sia una buona pratica. In breve, la mutabilità rende più difficile ragionare sul codice, specialmente quando si tenta di introdurre parallelismo o concorrenza.
Il gioco! Il framework Scala ti fa usare l'immutabilità una buona parte del tempo, così come lo stesso linguaggio Scala. Ad esempio, il risultato generato da un controller è immutabile. A volte potresti considerare questa immutabilità "fastidiosa" o "fastidiosa", ma queste "buone pratiche" sono "buone" per una ragione.
In questo caso, l'immutabilità del controller è stata assolutamente cruciale quando alla fine ho deciso di eseguire alcuni test delle prestazioni: ho scoperto un collo di bottiglia e, per risolverlo, ho semplicemente memorizzato nella cache questa risposta immutabile.
Per caching , intendo salvare l'oggetto risposta e servire un'istanza identica, così com'è, a qualsiasi nuovo client. Questo libera il server dal dover ricalcolare il risultato da capo. Non sarebbe possibile fornire la stessa risposta a più client se questo risultato fosse mutevole.
Lo svantaggio: per un breve periodo (il tempo di scadenza della cache), i client possono ricevere informazioni obsolete. Questo è solo un problema negli scenari in cui è assolutamente necessario che il client acceda ai dati più recenti, senza tolleranza per il ritardo.
Per riferimento, ecco il codice Scala per caricare la pagina iniziale con un elenco di prodotti, senza memorizzare nella cache:
def index = Action { implicit request => Ok(html.index(body = html.body(Items.itemsHigherBids(itemDAO.all(false))), menu = mainMenu)) }
Ora, aggiungendo la cache:
def index = Cached("index", 5) { Action { implicit request => Ok(html.index(body = html.body(Items.itemsHigherBids(itemDAO.all(false))), menu = mainMenu)) } }
Abbastanza semplice, non è vero? Qui, "indice" è la chiave da utilizzare nel sistema di cache e 5 è il tempo di scadenza, in secondi.
Per testare l'effetto di questa modifica, ho eseguito alcuni test JMeter (inclusi nel repository GitHub) in locale. Prima di aggiungere la cache, ho raggiunto un throughput di circa 180 richieste al secondo. Dopo la memorizzazione nella cache, il throughput è salito a 800 richieste al secondo. Questo è un miglioramento di oltre 4 volte per meno di due righe di codice.

Consumo di memoria
Un'altra area in cui i giusti strumenti Scala possono fare una grande differenza è il consumo di memoria. Ecco, di nuovo, Gioca! ti spinge nella giusta direzione (scalabile). Nel mondo Java, per una "normale" applicazione web scritta con l'API servlet (ovvero, quasi tutti i framework Java o Scala là fuori), è molto allettante mettere un sacco di spazzatura nella sessione utente perché l'API offre facilità di metodi di chiamata che ti consentono di farlo:
session.setAttribute("attrName", attrValue);
Poiché è così facile aggiungere informazioni alla sessione dell'utente, viene spesso abusato. Di conseguenza, il rischio di consumare troppa memoria forse senza una buona ragione è altrettanto alto.
Con il gioco! framework, questa non è un'opzione: il framework semplicemente non ha uno spazio di sessione lato server. Il gioco! la sessione utente del framework è conservata in un cookie del browser e devi conviverci. Ciò significa che lo spazio della sessione è limitato per dimensioni e tipo: puoi memorizzare solo stringhe. Se devi archiviare oggetti, dovrai utilizzare il meccanismo di memorizzazione nella cache di cui abbiamo discusso prima. Ad esempio, potresti voler memorizzare l'indirizzo e-mail o il nome utente dell'utente corrente nella sessione, ma dovrai utilizzare la cache se devi memorizzare un intero oggetto utente dal tuo modello di dominio.
Anche in questo caso, all'inizio potrebbe sembrare una seccatura, ma in verità, Play! ti tiene sulla strada giusta, costringendoti a considerare attentamente l'utilizzo della memoria, che produce codice di primo passaggio praticamente pronto per il cluster, soprattutto dato che non esiste una sessione lato server che dovrebbe essere propagata in tutto il cluster, rendendo la vita infinitamente più facile.
Supporto asincrono
Il prossimo in questo gioco! revisione del quadro, esamineremo come Play! brilla anche nel supporto asincrono (orario). E al di là delle sue caratteristiche native, Play! ti consente di incorporare Akka, un potente strumento per l'elaborazione asincrona.
Anche se Lojinha non sfrutta ancora appieno Akka, la sua semplice integrazione con Play! ha reso davvero facile:
- Pianifica un servizio di posta elettronica asincrono.
- Elabora offerte per vari prodotti contemporaneamente.
In breve, Akka è un'implementazione dell'Actor Model reso famoso da Erlang. Se non hai familiarità con l'Akka Actor Model, immaginalo come una piccola unità che comunica solo tramite messaggi.
Per inviare un'e-mail in modo asincrono, creo prima il messaggio e l'attore corretti. Quindi, tutto ciò che devo fare è qualcosa del tipo:
EMail.actor ! BidToppedMessage(item.name, itemUrl, bidderEmail)
La logica di invio delle e-mail è implementata all'interno dell'attore e il messaggio dice all'attore quale e-mail vorremmo inviare. Questo viene fatto in uno schema fire-and-forget, il che significa che la riga sopra invia la richiesta e quindi continua a eseguire tutto ciò che abbiamo dopo (cioè, non si blocca).
Per ulteriori informazioni sull'Async nativo di Play!, dai un'occhiata alla documentazione ufficiale.
Conclusione
Riassumendo: ho sviluppato rapidamente una piccola applicazione, Lojinha, in grado di scalare verso l'alto e verso il basso molto bene. Quando incontravo problemi o scoprivo colli di bottiglia, le correzioni erano facili e veloci, con molto merito dovuto agli strumenti che utilizzavo (Play!, Scala, Akka e così via), che mi hanno spinto a seguire le migliori pratiche in termini di efficienza e scalabilità. Con poca preoccupazione per le prestazioni, sono stato in grado di scalare a migliaia di richieste simultanee.
Quando sviluppi la tua prossima applicazione, considera attentamente i tuoi strumenti.