Scaling Play! la mii de cereri concurente
Publicat: 2022-03-11Dezvoltatorii Web Scala nu reușesc adesea să ia în considerare consecințele miilor de utilizatori care accesează aplicațiile noastre în același timp. Poate pentru că ne place să prototipăm rapid; poate pentru că testarea unor astfel de scenarii este pur și simplu dificilă .
Indiferent, voi argumenta că ignorarea scalabilității nu este atât de rău pe cât pare - dacă utilizați setul adecvat de instrumente și urmați bunele practici de dezvoltare.
Lojinha și jocul! Cadru
Cu ceva timp în urmă, am început un proiect numit Lojinha (care se traduce prin „magazin mic” în portugheză), încercarea mea de a construi un site de licitație. (Apropo, acest proiect este open source). Motivațiile mele au fost următoarele:
- Îmi doream foarte mult să vând niște lucruri vechi pe care nu le mai folosesc.
- Nu-mi plac site-urile tradiționale de licitații, mai ales cele pe care le avem aici, în Brazilia.
- Am vrut să mă „joc” cu Play! Cadrul 2 (joc de cuvinte).
Deci, evident, așa cum am menționat mai sus, am decis să folosesc Play! Cadru. Nu am un număr exact de cât timp a durat să fie construit, dar cu siguranță nu a trecut mult până când mi-am pus site-ul în funcțiune cu sistemul simplu implementat la http://lojinha.jcranky.com. De fapt, am petrecut cel puțin jumătate din timpul de dezvoltare pe design, care folosește Twitter Bootstrap (rețineți: nu sunt designer...).
Paragraful de mai sus ar trebui să clarifice cel puțin un lucru: nu m-am îngrijorat prea mult de performanță, dacă chiar deloc, atunci când am creat Lojinha.
Și exact acesta este punctul meu de vedere: există putere în utilizarea instrumentelor potrivite - instrumente care vă mențin pe drumul cel bun, instrumente care vă încurajează să urmați cele mai bune practici de dezvoltare prin însăși construcția lor.
În acest caz, acele instrumente sunt Play! Framework și limbajul Scala, cu Akka făcând câteva „apariții ca oaspeți”.
Lasă-mă să-ți arăt ce vreau să spun.
Imuabilitate și stocare în cache
În general, este de acord că reducerea la minimum a mutabilității este o bună practică. Pe scurt, mutabilitatea face mai greu să raționezi despre codul tău, mai ales când încerci să introduci orice paralelism sau concurență.
Piesa! Cadrul Scala vă face să utilizați imuabilitatea o bună parte a timpului, la fel și limbajul Scala în sine. De exemplu, rezultatul generat de un controler este imuabil. Uneori ați putea considera această imuabilitate „deranjantă” sau „enervantă”, dar aceste „bune practici” sunt „bune” dintr-un motiv.
În acest caz, imuabilitatea controlerului a fost absolut crucială când am decis în sfârșit să rulez niște teste de performanță: am descoperit un blocaj și, pentru a o remedia, pur și simplu am stocat în cache acest răspuns imuabil.
Prin stocarea în cache , mă refer la salvarea obiectului răspuns și la servirea unei instanțe identice, așa cum este, oricăror clienți noi. Acest lucru eliberează serverul de a fi nevoit să recalculeze rezultatul din nou. Nu ar fi posibil să se ofere același răspuns mai multor clienți dacă acest rezultat ar fi mutabil.
Dezavantajul: pentru o perioadă scurtă (timp de expirare a cache-ului), clienții pot primi informații învechite. Aceasta este doar o problemă în scenariile în care aveți absolut nevoie ca clientul să acceseze cele mai recente date, fără toleranță pentru întârziere.
Pentru referință, iată codul Scala pentru încărcarea paginii de start cu o listă de produse, fără cache:
def index = Action { implicit request => Ok(html.index(body = html.body(Items.itemsHigherBids(itemDAO.all(false))), menu = mainMenu)) }
Acum, adăugând memoria cache:
def index = Cached("index", 5) { Action { implicit request => Ok(html.index(body = html.body(Items.itemsHigherBids(itemDAO.all(false))), menu = mainMenu)) } }
Destul de simplu, nu-i așa? Aici, „index” este cheia care trebuie utilizată în sistemul cache și 5 este timpul de expirare, în secunde.
Pentru a testa efectul acestei modificări, am rulat câteva teste JMeter (incluse în depozitul GitHub) la nivel local. Înainte de a adăuga memoria cache, am atins un debit de aproximativ 180 de solicitări pe secundă. După stocarea în cache, debitul a crescut la 800 de solicitări pe secundă. Aceasta este o îmbunătățire de peste 4 ori pentru mai puțin de două linii de cod.

Consum de memorie
Un alt domeniu în care instrumentele Scala potrivite pot face o mare diferență este consumul de memorie. Iată, din nou, Joacă! te împinge în direcția corectă (scalabilă). În lumea Java, pentru o aplicație web „normală” scrisă cu API-ul servlet (adică aproape orice cadru Java sau Scala de acolo), este foarte tentant să puneți o mulțime de nedorit în sesiunea utilizatorului, deoarece API-ul oferă ușor de utilizat metode de apel care vă permit să faceți acest lucru:
session.setAttribute("attrName", attrValue);
Deoarece este atât de ușor să adăugați informații la sesiunea utilizatorului, acestea sunt adesea abuzate. În consecință, riscul de a utiliza prea multă memorie fără un motiv întemeiat este la fel de mare.
Cu Play! framework, aceasta nu este o opțiune - cadrul pur și simplu nu are un spațiu de sesiune pe partea serverului. Piesa! sesiunea cadru de utilizator este păstrată într-un cookie de browser și trebuie să trăiești cu el. Aceasta înseamnă că spațiul de sesiune este limitat în dimensiune și tip: puteți stoca doar șiruri. Dacă trebuie să stocați obiecte, va trebui să utilizați mecanismul de stocare în cache despre care am discutat anterior. De exemplu, este posibil să doriți să stocați adresa de e-mail sau numele de utilizator al utilizatorului curent în sesiune, dar va trebui să utilizați memoria cache dacă trebuie să stocați un întreg obiect utilizator din modelul dvs. de domeniu.
Din nou, acest lucru ar putea părea o durere la început, dar într-adevăr, Joacă! vă menține pe drumul cel bun, forțându-vă să luați în considerare cu atenție utilizarea memoriei, ceea ce produce cod de primă trecere care este practic pregătit pentru cluster - mai ales având în vedere că nu există nicio sesiune pe partea serverului care ar trebui să fie propagată în clusterul dvs., făcând viață infinit mai ușor.
Suport asincron
Următorul în această joacă! revizuire a cadrului, vom examina modul în care Play! strălucește și în suportul asincron (cronic). Și dincolo de caracteristicile sale native, Play! vă permite să încorporați Akka, un instrument puternic pentru procesarea asincronă.
Deși Lojinha nu profită încă din plin de Akka, simpla sa integrare cu Play! a făcut foarte ușor să:
- Programați un serviciu de e-mail asincron.
- Procesați ofertele pentru diverse produse concomitent.
Pe scurt, Akka este o implementare a modelului de actor făcut celebru de Erlang. Dacă nu sunteți familiarizat cu Akka Actor Model, imaginați-vă doar ca pe o unitate mică care comunică doar prin mesaje.
Pentru a trimite un e-mail asincron, mai întâi creez mesajul și actorul potrivit. Apoi, tot ce trebuie să fac este ceva de genul:
EMail.actor ! BidToppedMessage(item.name, itemUrl, bidderEmail)
Logica de trimitere a e-mail-ului este implementată în interiorul actorului, iar mesajul îi spune actorului ce e-mail dorim să trimitem. Acest lucru se face într-o schemă fire-and-forget, ceea ce înseamnă că linia de mai sus trimite cererea și apoi continuă să execute tot ce avem după aceea (adică, nu se blochează).
Pentru mai multe informații despre Async nativ Play!, aruncați o privire la documentația oficială.
Concluzie
În rezumat: Am dezvoltat rapid o aplicație mică, Lojinha, capabilă să se extindă foarte bine. Când am întâmpinat probleme sau am descoperit blocaje, remediile au fost rapide și ușoare, cu mult merit datorită instrumentelor pe care le-am folosit (Play!, Scala, Akka și așa mai departe), care m-au împins să urmez cele mai bune practici în ceea ce privește eficiența și scalabilitate. Cu puțină preocupare pentru performanță, am reușit să mă adaptez la mii de solicitări simultane.
Când dezvoltați următoarea aplicație, luați în considerare instrumentele cu atenție.