Performanța I/O pe partea serverului: Node vs. PHP vs. Java vs. Go

oct. 22, 2021
admin

Înțelegerea modelului de intrare/ieșire (I/O) al aplicației dvs. poate face diferența între o aplicație care face față sarcinii la care este supusă și una care se prăbușește în fața cazurilor de utilizare din lumea reală. Poate că în timp ce aplicația dvs. este mică și nu servește sarcini mari, acest lucru poate conta mult mai puțin. Dar pe măsură ce sarcina de trafic a aplicației dvs. crește, lucrul cu un model de I/O greșit vă poate duce într-o lume a răului.

Și ca în aproape orice situație în care sunt posibile mai multe abordări, nu este vorba doar de care dintre ele este mai bună, ci și de înțelegerea compromisurilor. Haideți să facem o plimbare prin peisajul I/O și să vedem ce putem spiona.

În acest articol, vom compara Node, Java, Go și PHP cu Apache, discutând despre modul în care diferitele limbaje își modelează I/O, avantajele și dezavantajele fiecărui model și vom încheia cu câteva repere rudimentare. Dacă sunteți preocupat de performanța I/O a următoarei dumneavoastră aplicații web, acest articol este pentru dumneavoastră.

I/O Basics: O reîmprospătare rapidă

Pentru a înțelege factorii implicați în I/O, trebuie mai întâi să trecem în revistă conceptele de la nivelul sistemului de operare. Deși este puțin probabil să aveți de-a face cu multe dintre aceste concepte în mod direct, aveți de-a face cu ele în mod indirect prin intermediul mediului de execuție al aplicației dumneavoastră tot timpul. Iar detaliile contează.

Apeluri de sistem

În primul rând, avem apelurile de sistem, care pot fi descrise după cum urmează:

  • Programul dumneavoastră (în „user land”, cum se spune) trebuie să ceară nucleului sistemului de operare să efectueze o operație I/O în numele său.
  • Un „syscall” este mijlocul prin care programul dumneavoastră cere nucleului să facă ceva. Particularitățile modului în care este implementat acest lucru variază de la un sistem de operare la altul, dar conceptul de bază este același. Va exista o instrucțiune specifică care transferă controlul de la programul dvs. către kernel (ca un apel de funcție, dar cu un sos special special pentru a face față acestei situații). În general, syscall-urile sunt blocante, ceea ce înseamnă că programul dumneavoastră așteaptă ca kernelul să se întoarcă la codul dumneavoastră.
  • Kernelul efectuează operațiunea de I/O subiacentă pe dispozitivul fizic în cauză (disc, placă de rețea, etc.) și răspunde la syscall. În lumea reală, este posibil ca nucleul să trebuiască să facă o serie de lucruri pentru a vă îndeplini cererea, inclusiv să aștepte ca dispozitivul să fie gata, să își actualizeze starea internă etc., dar, în calitate de dezvoltator de aplicații, nu vă interesează acest lucru. Aceasta este treaba nucleului.

Apeluri cu blocare vs. apeluri fără blocare

Acum, tocmai am spus mai sus că apelurile syscall sunt blocante, iar acest lucru este adevărat în sens general. Cu toate acestea, unele apeluri sunt categorisite ca fiind „non-blocante”, ceea ce înseamnă că kernelul preia cererea dumneavoastră, o pune în coadă sau în buffer undeva și apoi se întoarce imediat fără a aștepta să aibă loc I/O-ul efectiv. Deci, el „blochează” doar pentru o perioadă foarte scurtă de timp, doar cât timp este necesar pentru a pune în coadă cererea dumneavoastră.

Câteva exemple (de syscall-uri Linux) ar putea ajuta la clarificare: – read() este un apel blocant – îi treceți un handle care spune ce fișier și un buffer de unde să livreze datele pe care le citește, iar apelul se întoarce când datele sunt acolo. Rețineți că acest lucru are avantajul de a fi frumos și simplu.- epoll_create(), epoll_ctl() și epoll_wait() sunt apeluri care, respectiv, vă permit să creați un grup de handle-uri pe care să ascultați, să adăugați/eliminați gestionari din acel grup și apoi să blocați până când există activitate. Acest lucru vă permite să controlați în mod eficient un număr mare de operațiuni de I/O cu un singur fir, dar mă grăbesc. Acest lucru este grozav dacă aveți nevoie de această funcționalitate, dar, după cum puteți vedea, este cu siguranță mai complex de utilizat.

Este important să înțelegeți ordinul de mărime al diferenței de timp aici. Dacă un nucleu de procesor funcționează la 3GHz, fără a intra în optimizările pe care le poate face procesorul, acesta efectuează 3 miliarde de cicluri pe secundă (sau 3 cicluri pe nanosecundă). Un apel de sistem fără blocaj ar putea dura de ordinul a zeci de cicluri pentru a se finaliza – sau „câteva nanosecunde relativ”. Un apel care se blochează în așteptarea informațiilor primite prin rețea ar putea dura mult mai mult timp – să spunem, de exemplu, 200 de milisecunde (1/5 de secundă). Și să spunem, de exemplu, că apelul care nu se blochează a durat 20 de nanosecunde, iar apelul care se blochează a durat 200.000.000 de nanosecunde. Procesul dvs. tocmai a așteptat de 10 milioane de ori mai mult pentru apelul blocant.

Kernelul oferă mijloacele necesare pentru a face atât I/O blocante („citește de la această conexiune de rețea și dă-mi datele”), cât și I/O ne-blocante („spune-mi când oricare dintre aceste conexiuni de rețea are date noi”). Iar mecanismul folosit va bloca procesul apelant pentru perioade de timp dramatic de diferite.

Programare

Al treilea lucru critic de urmărit este ce se întâmplă atunci când aveți o mulțime de fire de execuție sau procese care încep să blocheze.

Pentru scopurile noastre, nu există o diferență uriașă între un fir de execuție și un proces. În viața reală, cea mai vizibilă diferență legată de performanță este că, din moment ce firele de execuție împart aceeași memorie, iar procesele au fiecare propriul spațiu de memorie, realizarea unor procese separate tinde să ocupe mult mai multă memorie. Dar atunci când vorbim despre programare, totul se rezumă la o listă de lucruri (fire de execuție și procese deopotrivă) care trebuie să primească fiecare o porțiune de timp de execuție pe nucleele CPU disponibile. Dacă aveți 300 de fire de execuție și 8 nuclee pe care să le executați, trebuie să împărțiți timpul astfel încât fiecare să-și primească partea sa, fiecare nucleu funcționând pentru o perioadă scurtă de timp și apoi trecând la următorul fir. Acest lucru se face prin intermediul unei „comutări de context”, făcând CPU să treacă de la rularea unui fir/proces la următorul.

Aceste comutări de context au un cost asociat – durează ceva timp. În unele cazuri rapide, poate fi mai puțin de 100 de nanosecunde, dar nu este neobișnuit să dureze 1000 de nanosecunde sau mai mult, în funcție de detaliile implementării, de viteza/arhitectura procesorului, de memoria cache a CPU, etc.

Și cu cât sunt mai multe fire (sau procese), cu atât mai multe comutări de context. Când vorbim de mii de fire de execuție și sute de nanosecunde pentru fiecare, lucrurile pot deveni foarte lente.

Cu toate acestea, apelurile non-blocking îi spun în esență nucleului „cheamă-mă doar atunci când ai niște date sau evenimente noi pe una dintre oricare dintre aceste conexiuni”. Aceste apeluri non-blocante sunt concepute pentru a gestiona eficient sarcinile mari de I/O și pentru a reduce comutarea contextului.

Cu mine până acum? Pentru că acum vine partea distractivă: Să ne uităm la ceea ce fac unele limbaje populare cu aceste instrumente și să tragem niște concluzii despre compromisurile dintre ușurința de utilizare și performanță… și alte mărunțișuri interesante.

Ca o notă, în timp ce exemplele prezentate în acest articol sunt triviale (și parțiale, fiind prezentate doar părțile relevante); accesul la baze de date, sistemele de cache externe (memcache, et. all) și orice altceva care necesită I/O va sfârși prin a efectua un fel de apel I/O sub capotă care va avea același efect ca și exemplele simple prezentate. De asemenea, pentru scenariile în care I/O este descris ca fiind „blocant” (PHP, Java), solicitările HTTP și răspunsurile de citire și scriere sunt ele însele apeluri blocante: Din nou, mai multe I/O ascunse în sistem, cu problemele de performanță aferente de care trebuie să se țină cont.

Există o mulțime de factori care intră în alegerea unui limbaj de programare pentru un proiect. Există chiar o mulțime de factori atunci când se ia în considerare doar performanța. Dar, dacă vă îngrijorează faptul că programul dumneavoastră va fi constrâns în primul rând de I/O, dacă performanța I/O este decisivă pentru proiectul dumneavoastră, acestea sunt lucruri pe care trebuie să le știți.

Abordarea „Keep It Simple”: PHP

În anii ’90, o mulțime de oameni purtau pantofi Converse și scriau scripturi CGI în Perl. Apoi a apărut PHP și, oricât de mult le place unora să îl cârpească, a făcut ca realizarea de pagini web dinamice să fie mult mai ușoară.

Modelul pe care îl folosește PHP este destul de simplu. Există câteva variații ale acestuia, dar serverul PHP obișnuit arată astfel:

O cerere HTTP vine de la browserul unui utilizator și ajunge la serverul dvs. web Apache. Apache creează un proces separat pentru fiecare cerere, cu unele optimizări pentru a le reutiliza, pentru a minimiza câte trebuie să facă (crearea de procese este, relativ vorbind, lentă).Apache apelează PHP și îi spune să ruleze fișierul .php corespunzător de pe disc.Codul PHP se execută și face apeluri I/O blocante. Voi apelați file_get_contents() în PHP și, sub capotă, acesta face read() apeluri de sistem și așteaptă rezultatele.

Și, bineînțeles, codul propriu-zis este pur și simplu încorporat direct în pagina voastră, iar operațiunile sunt blocante:

<?php// blocking file I/O$file_data = file_get_contents('/path/to/file.dat');// blocking network I/O$curl = curl_init('http://example.com/example-microservice');$result = curl_exec($curl);// some more blocking network I/O$result = $db->query('SELECT id, data FROM examples ORDER BY id DESC limit 100');?>

În ceea ce privește modul în care se integrează cu sistemul, este cam așa:

Prea simplu: un proces pe cerere. Apelurile I/O doar se blochează. Avantajul? Este simplu și funcționează. Dezavantaj? Lovește-l cu 20.000 de clienți simultan și serverul tău va lua foc. Această abordare nu se adaptează bine, deoarece instrumentele furnizate de kernel pentru a face față volumului mare de I/O (epoll etc.) nu sunt folosite. Și pentru a adăuga o insultă la prejudiciu, rularea unui proces separat pentru fiecare cerere tinde să folosească o mulțime de resurse de sistem, în special memorie, care este adesea primul lucru pe care îl rămâneți fără într-un astfel de scenariu.

Nota: Abordarea folosită pentru Ruby este foarte asemănătoare cu cea din PHP și, într-un mod larg, general, cu mâna întinsă, ele pot fi considerate la fel pentru scopurile noastre.

The Multithreaded Approach: Java

Așa că apare Java, chiar în perioada în care v-ați cumpărat primul nume de domeniu și era cool să spuneți la întâmplare „dot com” după o propoziție. Iar Java are multithreading încorporat în limbaj, ceea ce (mai ales pentru momentul în care a fost creat) este destul de grozav.

Majoritatea serverelor web Java funcționează prin pornirea unui nou fir de execuție pentru fiecare cerere care intră și apoi, în acest fir, apelând în cele din urmă funcția pe care tu, în calitate de dezvoltator al aplicației, ai scris-o.

Executarea I/O într-un Java Servlet tinde să arate cam așa:

public void doGet(HttpServletRequest request,HttpServletResponse response) throws ServletException, IOException{// blocking file I/OInputStream fileIs = new FileInputStream("/path/to/file");// blocking network I/OURLConnection urlConnection = (new URL("http://example.com/example-microservice")).openConnection();InputStream netIs = urlConnection.getInputStream();// some more blocking network I/Oout.println("...");}

Din moment ce metoda noastră doGet de mai sus corespunde unei cereri și este executată în propriul fir, în loc de un proces separat pentru fiecare cerere care necesită propria memorie, avem un fir separat. Acest lucru are câteva avantaje frumoase, cum ar fi posibilitatea de a partaja starea, datele din memoria cache etc. între fire, deoarece acestea pot accesa memoria celuilalt, dar impactul asupra modului în care interacționează cu programul este în continuare aproape identic cu ceea ce se face în exemplul PHP anterior. Fiecare cerere primește un nou fir de execuție, iar diferitele operațiuni de I/O se blochează în interiorul acelui fir de execuție până când cererea este complet gestionată. Firele sunt grupate pentru a minimiza costul creării și distrugerii lor, dar, cu toate acestea, mii de conexiuni înseamnă mii de fire, ceea ce este rău pentru planificator.

O etapă importantă este faptul că în versiunea 1.4 Java (și un upgrade semnificativ din nou în 1.7) a obținut capacitatea de a face apeluri I/O fără blocare. Majoritatea aplicațiilor, web și nu numai, nu o folosesc, dar cel puțin este disponibilă. Unele servere web Java încearcă să profite de acest lucru în diferite moduri; cu toate acestea, marea majoritate a aplicațiilor Java implementate încă funcționează așa cum s-a descris mai sus.

Java ne apropie și cu siguranță are o funcționalitate bună pentru I/O, dar tot nu rezolvă cu adevărat problema a ceea ce se întâmplă atunci când aveți o aplicație puternic legată de I/O, care este lovită de mai multe mii de fire de execuție care blochează.

Non-blocking I/O as a First Class Citizen: Node

Prietenul popular din cartier când vine vorba de un I/O mai bun este Node.js. Oricui i s-a făcut chiar și cea mai scurtă introducere în Node i s-a spus că este „non-blocant” și că gestionează I/O în mod eficient. Și acest lucru este adevărat într-un sens general. Dar diavolul se află în detalii și mijloacele prin care a fost realizată această vrăjitorie contează atunci când vine vorba de performanță.

În esență, schimbarea de paradigmă pe care Node o implementează este că, în loc să spună, în esență, „scrieți codul dvs. aici pentru a gestiona cererea”, ei spun în schimb „scrieți codul aici pentru a începe să gestionați cererea”. De fiecare dată când aveți nevoie să faceți ceva care implică I/O, faceți cererea și dați o funcție de callback pe care Node o va apela atunci când a terminat.

Codul tipic Node pentru a face o operație I/O într-o cerere arată astfel:

http.createServer(function(request, response) {fs.readFile('/path/to/file', 'utf8', function(err, data) {response.end(data);});});

După cum puteți vedea, există două funcții de callback aici. Prima este apelată atunci când începe o cerere, iar a doua este apelată atunci când datele fișierului sunt disponibile.

Ceea ce face acest lucru este, practic, să ofere lui Node o oportunitate de a gestiona eficient I/O între aceste callback-uri. Un scenariu în care ar fi și mai relevant este cel în care efectuați un apel la baza de date în Node, dar nu mă voi deranja cu exemplul, deoarece este exact același principiu: începeți apelul la baza de date și îi dați lui Node o funcție de apelare, acesta efectuează operațiunile de I/O separat folosind apeluri care nu blochează și apoi invocă funcția dvs. de apelare atunci când datele pe care le-ați cerut sunt disponibile. Acest mecanism de a pune la coadă apelurile de I/O și de a lăsa Node să se ocupe de ele, iar apoi de a primi un callback se numește „Event Loop”. Și funcționează destul de bine.

Există totuși o capcană la acest model. Sub capotă, motivul are mult mai mult de a face cu modul în care este implementat motorul JavaScript V8 (motorul JS al Chrome care este folosit de Node) 1 decât orice altceva. Codul JS pe care îl scrieți se execută în întregime într-un singur fir de execuție. Gândiți-vă la asta pentru o clipă. Înseamnă că, în timp ce I/O este efectuat folosind tehnici eficiente de neblocare, can JS-ul dvs. care face operațiuni legate de CPU rulează într-un singur fir, fiecare bucată de cod blocând-o pe următoarea. Un exemplu obișnuit în care acest lucru ar putea apărea este bucla peste înregistrările din baza de date pentru a le procesa într-un fel sau altul înainte de a le trimite către client. Iată un exemplu care arată cum funcționează acest lucru:

var handler = function(request, response) {connection.query('SELECT ...', function (err, rows) {if (err) { throw err };for (var i = 0; i < rows.length; i++) {// do processing on each row}response.end(...); // write out the results})};

În timp ce Node se ocupă eficient de I/O, acea buclă for din exemplul de mai sus folosește cicluri CPU în interiorul singurului și unicului dvs. fir principal. Acest lucru înseamnă că, dacă aveți 10.000 de conexiuni, acea buclă ar putea aduce întreaga dvs. aplicație la un pas greoi, în funcție de cât de mult durează. Fiecare solicitare trebuie să împartă o felie de timp, una câte una, în firul dvs. principal.

Primăria pe care se bazează întregul concept este că operațiile de I/O sunt partea cea mai lentă, prin urmare este cel mai important să le gestionați eficient, chiar dacă asta înseamnă să faceți alte prelucrări în serie. Acest lucru este adevărat în unele cazuri, dar nu în toate.

Celălalt punct este că, și deși aceasta este doar o opinie, poate fi destul de obositor să scrii o grămadă de callback-uri imbricate și unii susțin că face codul semnificativ mai greu de urmărit. Nu este neobișnuit să vezi callback-uri imbricate pe patru, cinci sau chiar mai multe niveluri de adâncime în codul Node.

Ne-am întors din nou la compromisuri. Modelul Node funcționează bine dacă principala dvs. problemă de performanță este I/O. Cu toate acestea, călcâiul lui Ahile este că puteți intra într-o funcție care gestionează o solicitare HTTP și să introduceți cod intensiv pentru CPU și să aduceți fiecare conexiune la un nivel scăzut dacă nu sunteți atenți.

Naturally Non-blocking: Go

Înainte de a intra în secțiunea pentru Go, se cuvine să dezvălui că sunt un fan al Go. L-am folosit pentru multe proiecte și sunt în mod deschis un susținător al avantajelor sale de productivitate și le văd în munca mea atunci când îl folosesc.

Acestea fiind spuse, să ne uităm la modul în care se ocupă de I/O. O caracteristică cheie a limbajului Go este faptul că acesta conține propriul programator. În loc ca fiecare fir de execuție să corespundă unui singur fir al sistemului de operare, acesta lucrează cu conceptul de „goroutine”. Iar timpul de execuție Go poate să atribuie o goroutine unui fir al sistemului de operare și să o execute, sau să o suspende și să nu fie asociată cu un fir al sistemului de operare, în funcție de ceea ce face acea goroutine. Fiecare solicitare care vine de la serverul HTTP Go este gestionată într-o goroutine separată.

Diagrama modului în care funcționează planificatorul arată astfel:

Sub capotă, acest lucru este implementat de diferite puncte din timpul de execuție Go care implementează apelul I/O prin efectuarea solicitării de scriere/citire/conectare/etc, pun în adormire goroutine-ul curent, cu informația de a trezi din nou goroutine-ul atunci când pot fi întreprinse alte acțiuni.

De fapt, timpul de execuție Go face ceva nu foarte diferit de ceea ce face Node, cu excepția faptului că mecanismul de apelare este integrat în implementarea apelului I/O și interacționează cu planificatorul în mod automat. De asemenea, nu suferă de restricția de a trebui ca tot codul de manipulare să ruleze în același fir de execuție, Go va mapa automat Goroutines la câte fire de execuție ale sistemului de operare consideră adecvate pe baza logicii din planificatorul său. Rezultatul este un cod de genul acesta:

func ServeHTTP(w http.ResponseWriter, r *http.Request) {// the underlying network call here is non-blockingrows, err := db.Query("SELECT ...")for _, row := range rows {// do something with the rows,// each request in its own goroutine}w.Write(...) // write the response, also non-blocking}

După cum puteți vedea mai sus, structura de bază a codului a ceea ce facem seamănă cu cea a abordărilor mai simpliste și, totuși, realizează I/O fără blocaj sub capotă.

În cele mai multe cazuri, acest lucru sfârșește prin a fi „cel mai bun din ambele lumi”. I/O non-blocant este folosit pentru toate lucrurile importante, dar codul dvs. arată ca și cum ar fi blocant și astfel tinde să fie mai simplu de înțeles și de întreținut. Interacțiunea dintre planificatorul Go și planificatorul sistemului de operare se ocupă de restul. Nu este o magie completă și, dacă construiți un sistem mare, merită să investiți timp pentru a înțelege mai multe detalii despre modul în care funcționează; dar, în același timp, mediul pe care îl obțineți „out-of-the-box” funcționează și se scalează destul de bine.

Go poate avea defectele sale, dar, în general, modul în care gestionează I/O nu se numără printre ele.

Lies, Damned Lies and Benchmarks

Este dificil să oferiți timpi exacți cu privire la comutarea de context implicată de aceste diverse modele. Aș putea spune, de asemenea, că este mai puțin util pentru dvs. Așa că, în schimb, vă voi oferi câteva repere de bază care compară performanța generală a serverului HTTP a acestor medii de servere. Țineți cont de faptul că o mulțime de factori sunt implicați în performanța întregului traseu de cerere/răspuns HTTP de la un capăt la altul, iar cifrele prezentate aici sunt doar câteva mostre pe care le-am adunat pentru a oferi o comparație de bază.

Pentru fiecare dintre aceste medii, am scris codul corespunzător pentru a citi un fișier de 64k cu octeți aleatori, am rulat un hash SHA-256 pe acesta de N ori (N fiind specificat în șirul de interogare al URL-ului, de exemplu, .../test.php?n=100) și am imprimat hash-ul rezultat în hexagonal. Am ales acest lucru pentru că este o modalitate foarte simplă de a rula aceleași teste de referință cu unele I/O consistente și o modalitate controlată de a crește utilizarea CPU.

Vezi aceste note de referință pentru mai multe detalii despre mediile utilizate.

În primul rând, să ne uităm la câteva exemple cu concurență redusă. Rularea a 2000 de iterații cu 300 de cereri concurente și un singur hash pe cerere (N=1) ne dă următoarele:

Timpurile sunt numărul mediu de milisecunde pentru a finaliza o cerere în toate cererile concurente. Mai mic este mai bine.

Este greu de tras o concluzie doar din acest grafic, dar mie mi se pare că, la acest volum de conexiuni și calcul, vedem timpi care țin mai mult de execuția generală a limbajelor în sine, mult mai mult decât de I/O. Rețineți că limbajele care sunt considerate „limbaje de scripting” (tastare liberă, interpretare dinamică) se comportă cel mai lent.

Dar ce se întâmplă dacă mărim N la 1000, tot cu 300 de cereri concurente – aceeași sarcină, dar de 100 de ori mai multe iterații hash (semnificativ mai multă sarcină CPU):

Timpurile sunt numărul mediu de milisecunde pentru a finaliza o cerere între toate cererile concurente. Mai mic este mai bine.

Dintr-o dată, performanța nodului scade semnificativ, deoarece operațiile cu utilizare intensivă a CPU din fiecare cerere se blochează reciproc. Și, în mod interesant, performanța PHP devine mult mai bună (în raport cu celelalte) și bate Java în acest test. (Este demn de remarcat faptul că în PHP implementarea SHA-256 este scrisă în C și calea de execuție petrece mult mai mult timp în acea buclă, deoarece acum facem 1000 de iterații hash).

Acum să încercăm 5000 de conexiuni concurente (cu N=1) – sau cât de aproape de asta am putut ajunge. Din păcate, pentru cele mai multe dintre aceste medii, rata de eșec nu a fost nesemnificativă. Pentru acest grafic, ne vom uita la numărul total de cereri pe secundă. Cu cât mai mare, cu atât mai bine:

Numărul total de cereri pe secundă. Mai mare este mai bine.

Și imaginea arată destul de diferit. Este o presupunere, dar se pare că, la un volum mare de conexiuni, cheltuielile generale per conexiune implicate de generarea de noi procese și de memoria suplimentară asociată acestora în PHP+Apache par să devină un factor dominant și să arunce în aer performanța PHP. În mod clar, Go este câștigătorul aici, urmat de Java, Node și, în cele din urmă, de PHP.

În timp ce factorii implicați în randamentul general sunt mulți și, de asemenea, variază foarte mult de la o aplicație la alta, cu cât înțelegeți mai multe despre ceea ce se întâmplă sub capotă și despre compromisurile implicate, cu atât vă va fi mai bine.

În rezumat

Cu toate cele de mai sus, este destul de clar că, pe măsură ce limbajele au evoluat, soluțiile pentru a face față aplicațiilor pe scară largă care fac multe I/O au evoluat odată cu ele.

Pentru a fi corecți, atât PHP cât și Java, în ciuda descrierilor din acest articol, au implementări de I/O fără blocaj disponibile pentru utilizare în aplicațiile web. Dar acestea nu sunt la fel de comune ca abordările descrise mai sus și ar trebui să se țină cont de costurile operaționale aferente de întreținere a serverelor care utilizează astfel de abordări. Ca să nu mai vorbim de faptul că codul dvs. trebuie să fie structurat într-un mod care să funcționeze cu astfel de medii; aplicația dvs. web „normală” PHP sau Java, de obicei, nu va rula fără modificări semnificative într-un astfel de mediu.

Ca o comparație, dacă luăm în considerare câțiva factori semnificativi care afectează performanța, precum și ușurința de utilizare, obținem următoarele:

.

Limbaj Threads vs. Procese Nu.blocarea I/O Facilitate de utilizare
PHP Procese Nu
Java Threads Available Requires Callbacks
Node.js Threads Yes Requires Callbacks
Go Threads (Goroutines) Yes Yes Nu sunt necesare callback-uri

În general, firele vor fi mult mai eficiente din punct de vedere al memoriei decât procesele, deoarece ele împart același spațiu de memorie, în timp ce procesele nu o fac. Combinând acest lucru cu factorii legați de I/O fără blocaj, putem vedea că, cel puțin în cazul factorilor luați în considerare mai sus, pe măsură ce ne deplasăm în josul listei, configurația generală în ceea ce privește I/O se îmbunătățește. Așadar, dacă ar trebui să aleg un câștigător în concursul de mai sus, acesta ar fi cu siguranță Go.

Chiar și așa, în practică, alegerea unui mediu în care să vă construiți aplicația este strâns legată de familiaritatea pe care echipa dvs. o are cu respectivul mediu și de productivitatea generală pe care o puteți obține cu acesta. Așadar, s-ar putea să nu aibă sens pentru fiecare echipă să se arunce pur și simplu și să înceapă să dezvolte aplicații și servicii web în Node sau Go. Într-adevăr, găsirea de dezvoltatori sau familiaritatea echipei dvs. interne este adesea citată ca fiind principalul motiv pentru a nu utiliza un limbaj și/sau un mediu diferit. Acestea fiind spuse, vremurile s-au schimbat în ultimii cincisprezece ani sau cam așa ceva, foarte mult.

Sperăm că cele de mai sus ajută la conturarea unei imagini mai clare a ceea ce se întâmplă sub capotă și vă oferă câteva idei despre cum să abordați scalabilitatea în lumea reală pentru aplicația dumneavoastră. Intrare și ieșire fericite!

.

Lasă un răspuns

Adresa ta de email nu va fi publicată.