Performanță I/O pe server: Node vs. PHP vs. Java vs. Go
Publicat: 2022-03-11Înțelegerea modelului de intrare/ieșire (I/O) al aplicației dvs. poate însemna diferența dintre o aplicație care se ocupă de sarcina la care este supusă și una care se sifonează în fața cazurilor de utilizare din lumea reală. Poate că, deși aplicația dvs. este mică și nu deservește sarcini mari, poate conta mult mai puțin. Dar, pe măsură ce încărcarea de trafic a aplicației dvs. crește, lucrul cu modelul I/O greșit vă poate duce într-o lume a durerii.
Și, ca în majoritatea situațiilor în care sunt posibile abordări multiple, nu este doar o chestiune de care dintre ele este mai bună, este o chestiune de înțelegere a compromisurilor. 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, vom discuta despre modul în care diferitele limbi își modelează I/O, avantajele și dezavantajele fiecărui model și vom încheia cu câteva benchmark-uri rudimentare. Dacă sunteți îngrijorat de performanța I/O a următoarei aplicații web, acest articol este pentru dvs.
Elemente de bază I/O: o actualizare rapidă
Pentru a înțelege factorii implicați în I/O, trebuie mai întâi să revizuim conceptele la nivel de sistem de operare. Deși este puțin probabil că va trebui să se ocupe de multe dintre aceste concepte în mod direct, le tratați în mod indirect prin mediul de rulare al aplicației dvs. tot timpul. Și detaliile contează.
Apeluri de sistem
În primul rând, avem apeluri de sistem, care pot fi descrise după cum urmează:
- Programul dumneavoastră (în „terrenul utilizatorului”, după 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. Specificul modului în care este implementat variază între sistemele de operare, dar conceptul de bază este același. Va exista o instrucțiune specifică care transferă controlul de la programul tău către nucleu (ca un apel de funcție, dar cu un sos special special pentru a face față acestei situații). În general, apelurile de sistem se blochează, ceea ce înseamnă că programul tău așteaptă ca nucleul să revină la codul tău.
- Nucleul efectuează operația I/O de bază pe dispozitivul fizic în cauză (disc, placă de rețea etc.) și răspunde la apelul de sistem. În lumea reală, kernel-ul ar putea fi nevoit să facă o serie de lucruri pentru a vă îndeplini cererea, inclusiv așteptarea ca dispozitivul să fie gata, actualizarea stării sale interne etc., dar în calitate de dezvoltator de aplicații, nu vă pasă de asta. Asta e treaba nucleului.
Apeluri cu blocare vs. neblocare
Acum, tocmai am spus mai sus că apelurile de sistem se blochează și asta este adevărat într-un sens general. Cu toate acestea, unele apeluri sunt clasificate ca „neblocare”, ceea ce înseamnă că nucleul preia cererea dvs., o pune în coadă sau în buffer undeva și apoi revine imediat fără a aștepta ca I/O să apară. Așa că se „blochează” doar pentru o perioadă foarte scurtă de timp, suficient de lungă pentru a pune cererea în coadă.
Câteva exemple (de apeluri de sistem Linux) ar putea ajuta la clarificare: - read()
este un apel de blocare - î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 revine 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 pe care să ascultați, adăugați/eliminați handlere din acel grup și apoi blocați până când există activitate. Acest lucru vă permite să controlați eficient un număr mare de operațiuni de I/O cu un singur fir, dar mă depășesc. Acest lucru este grozav dacă aveți nevoie de funcționalitate, dar după cum puteți vedea, este cu siguranță mai complex de utilizat.
Este important să înțelegeți aici ordinul de mărime a diferenței de timp. Dacă un nucleu al procesorului rulează la 3GHz, fără a intra în optimizări pe care le poate face CPU, acesta efectuează 3 miliarde de cicluri pe secundă (sau 3 cicluri pe nanosecundă). Un apel de sistem fără blocare poate dura de ordinul a 10 secunde de cicluri pentru a se finaliza - sau „relativ câteva nanosecunde”. Un apel care blochează informațiile primite prin rețea poate dura mult mai mult - să spunem, de exemplu, 200 de milisecunde (1/5 de secundă). Și să presupunem, de exemplu, apelul de non-blocare a durat 20 de nanosecunde, iar apelul de blocare a durat 200.000.000 de nanosecunde. Procesul dvs. a așteptat de 10 milioane de ori mai mult pentru apelul de blocare.
Nucleul oferă mijloacele de a face atât blocarea I/O („citiți din această conexiune de rețea și dați-mi datele”), cât și neblocare I/O („spuneți-mi când oricare dintre aceste conexiuni de rețea are date noi”). Și care mecanism este utilizat va bloca procesul de apelare pentru perioade de timp dramatic diferite.
Programare
Al treilea lucru care este esențial de urmărit este ceea ce se întâmplă atunci când aveți o mulțime de fire sau procese care încep să se blocheze.
Pentru scopurile noastre, nu există o diferență mare între un fir și proces. În viața reală, cea mai vizibilă diferență legată de performanță este că, deoarece firele de execuție împărtășesc aceeași memorie, iar procesele au fiecare propriul spațiu de memorie, realizarea de procese separate tinde să ocupe mult mai multă memorie. Dar când vorbim despre programare, ceea ce se rezumă într-adevăr la o listă de lucruri (deopotrivă fire și procese) de care fiecare are nevoie pentru a obține 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 rulați, trebuie să împărțiți timpul astfel încât fiecare să primească partea sa, fiecare nucleu rulând pentru o perioadă scurtă de timp și apoi trecând la următorul thread. Acest lucru se face printr-o „comutație de context”, făcând CPU-ul să treacă de la rularea unui fir/proces la următorul.
Aceste comutatoare de context au un cost asociat cu ele - 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 de implementare, viteza/arhitectura procesorului, memoria cache a procesorului etc.
Și cu cât mai multe fire (sau procese), cu atât mai multă schimbare de context. Când vorbim despre mii de fire și sute de nanosecunde pentru fiecare, lucrurile pot deveni foarte încet.
Cu toate acestea, apelurile neblocante, în esență, spun nucleului „sunați-mă doar când aveți date sau evenimente noi la una dintre aceste conexiuni”. Aceste apeluri neblocante sunt concepute pentru a gestiona eficient sarcini mari I/O și pentru a reduce schimbarea contextului.
Cu mine până acum? Pentru că acum vine partea distractivă: să ne uităm la ce fac unele limbi populare cu aceste instrumente și să tragem câteva concluzii despre compromisurile dintre ușurința de utilizare și performanță... și alte informații interesante.
Ca o notă, în timp ce exemplele prezentate în acest articol sunt triviale (și parțiale, cu doar biții relevanți afișați); accesul la baza de date, sistemele externe de stocare în cache (memcache, etc.) și orice lucru care necesită I/O va ajunge să efectueze 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 „blocare” (PHP, Java), cererea HTTP și scrierea răspunsului sunt ele însele de blocare a apelurilor: Din nou, mai multe I/O ascunse în sistem cu problemele de performanță asociate. să ia în considerare.
Există o mulțime de factori care intervin în alegerea unui limbaj de programare pentru un proiect. Există chiar o mulțime de factori atunci când iei în considerare doar performanța. Dar, dacă sunteți îngrijorat de faptul că programul dvs. va fi constrâns în primul rând de I/O, dacă performanța I/O este făcută sau întreruptă pentru proiectul dvs., acestea sunt lucruri pe care trebuie să le știți.
Abordarea „Păstrați-l simplu”: PHP
În anii '90, mulți oameni purtau pantofi Converse și scriau scenarii CGI în Perl. Apoi a apărut PHP și, oricât de mult le place unora să se dezvolte, a făcut ca pagini web dinamice să fie mult mai ușoară.
Modelul folosit de PHP este destul de simplu. Există câteva variante ale acestuia, dar serverul tău PHP mediu arată astfel:
O solicitare HTTP vine din browserul unui utilizator și lovește 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 proceselor este, relativ vorbind, lentă). Apache apelează PHP și îi spune să ruleze fișierul .php
corespunzător de pe disc. Codul PHP execută și blochează apelurile I/O. Apelați file_get_contents()
în PHP și sub capotă face apeluri sistem read()
și așteaptă rezultatele.
Și, desigur, codul real este pur și simplu încorporat chiar în pagina ta, iar operațiunile se blochează:
<?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 acesta se integrează cu sistemul, este așa:
Destul de simplu: un proces per cerere. Apelurile I/O sunt doar blocate. Avantaj? Este simplu și funcționează. Dezavantaj? Loviți-l cu 20.000 de clienți simultan și serverul dvs. va izbucni în flăcări. Această abordare nu se scalează bine, deoarece instrumentele furnizate de nucleu pentru a face față cu volum mare I/O (epoll, etc.) nu sunt utilizate. Și, pentru a adăuga insultă, rularea unui proces separat pentru fiecare solicitare tinde să folosească o mulțime de resurse de sistem, în special memoria, care este adesea primul lucru de care rămâneți fără într-un scenariu ca acesta.
Notă: Abordarea folosită pentru Ruby este foarte asemănătoare cu cea a PHP și, într-un mod larg, general, ondulat, acestea pot fi considerate la fel pentru scopurile noastre.
Abordarea cu mai multe fire: Java
Așa că apare Java, chiar în momentul în care ți-ai cumpărat primul nume de domeniu și a fost grozav să spui la întâmplare „punct com” după o propoziție. Și Java are multithreading încorporat în limbaj, care (mai ales pentru momentul în care a fost creat) este destul de grozav.
Majoritatea serverelor web Java funcționează prin inițierea unui nou thread de execuție pentru fiecare cerere care vine și apoi în acest thread apelând în cele din urmă funcția pe care ați scris-o dumneavoastră, în calitate de dezvoltator de aplicație.
Efectuarea I/O într-un Servlet Java tinde să arate ceva de genul:
public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // blocking file I/O InputStream fileIs = new FileInputStream("/path/to/file"); // blocking network I/O URLConnection urlConnection = (new URL("http://example.com/example-microservice")).openConnection(); InputStream netIs = urlConnection.getInputStream(); // some more blocking network I/O out.println("..."); }
Deoarece metoda noastră doGet
de mai sus corespunde unei cereri și este rulată în propriul thread, î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 stocate în cache etc. între fire, deoarece acestea pot accesa memoria celuilalt, dar impactul asupra modului în care interacționează cu programul este aproape identic cu ceea ce se face în PHP. exemplu anterior. Fiecare cerere primește un fir nou și diferitele operațiuni I/O se blochează în interiorul acelui fir până când cererea este tratată complet. Firele sunt reunite pentru a minimiza costul creării și distrugerii lor, dar totuși, mii de conexiuni înseamnă mii de fire, ceea ce este rău pentru planificator.
O etapă importantă este că, în versiunea 1.4, Java (și o actualizare semnificativă din nou în 1.7) a câștigat capacitatea de a efectua apeluri I/O neblocante. Majoritatea aplicațiilor, web și altele, nu îl 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 funcționează în continuare așa cum este descris mai sus.
Java ne apropie și cu siguranță are unele funcționalități bune pentru I/O, dar încă 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 pusă în mișcare. pământul cu multe mii de fire de blocare.
I/O fără blocare ca cetățean de primă clasă: Nod
Copilul popular din bloc atunci când vine vorba de I/O mai bune este Node.js. Oricui a avut chiar și cea mai scurtă introducere în Node i s-a spus că este „neblocator” și că gestionează eficient I/O. Și acest lucru este adevărat în sens general. Dar diavolul este în detalii și mijloacele prin care s-a realizat această vrăjitorie contează când vine vorba de performanță.
În esență, schimbarea de paradigmă pe care o implementează Node este aceea că, în loc să spună „scrieți codul 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 trebuie să faceți ceva care implică I/O, faceți cererea și oferiți o funcție de apel invers pe care Node o va apela când se va termina.

Codul tipic de nod pentru efectuarea unei operații I/O într-o solicitare este 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 apel invers aici. Primul este apelat când începe o solicitare, iar al doilea este apelat atunci când datele fișierului sunt disponibile.
Ceea ce face acest lucru este, practic, să ofere lui Node posibilitatea de a gestiona eficient I/O-urile între aceste apeluri inverse. Un scenariu în care ar fi și mai relevant este în cazul î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: porniți apelul la baza de date și dați lui Node o funcție de apel invers. efectuează operațiunile I/O separat folosind apeluri neblocante și apoi invocă funcția de apel invers atunci când datele solicitate sunt disponibile. Acest mecanism de a pune în coadă apelurile I/O și de a lăsa Node să se ocupe de ele și apoi de a obține un apel invers se numește „bucla de evenimente”. Și funcționează destul de bine.
Există totuși o captură la acest model. Sub capotă, motivul are mult mai mult de-a face cu modul în care motorul JavaScript V8 (motorul JS al Chrome, care este folosit de Node) este implementat 1 decât orice altceva. Codul JS pe care îl scrieți rulează într-un singur fir. Gândește-te la asta pentru o clipă. Înseamnă că, în timp ce I/O se realizează folosind tehnici eficiente de non-blocare, JS-ul tău care face operațiuni legate de CPU rulează într-un singur fir, fiecare bucată de cod blocând-o pe următoarea. Un exemplu comun de unde ar putea apărea acest lucru este trecerea în buclă peste înregistrările bazei de date pentru a le procesa într-un fel înainte de a le trimite către client. Iată un exemplu care arată cum funcționează:
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 gestionează eficient I/O-ul, bucla for
din exemplul de mai sus folosește cicluri CPU în interiorul singurului și singurul tău fir principal. Aceasta înseamnă că, dacă aveți 10.000 de conexiuni, acea buclă ar putea aduce întreaga aplicație la accesare cu crawlere, în funcție de cât timp durează. Fiecare solicitare trebuie să partajeze câte o porțiune de timp, pe rând, în firul dvs. principal.
Premisa pe care se bazează întregul concept este că operațiunile I/O sunt partea cea mai lentă, de aceea este cel mai important să le gestionați eficient, chiar dacă înseamnă să faceți alte procesă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ă scrieți o grămadă de apeluri imbricate și unii susțin că face codul mult mai greu de urmărit. Nu este neobișnuit să vezi apeluri inversate imbricate patru, cinci sau chiar mai multe niveluri adânc în codul Node.
Ne-am întors din nou la compromisuri. Modelul Node funcționează bine dacă problema principală 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 puteți introduce un cod care consumă mult CPU și aduce fiecare conexiune la crawlere dacă nu sunteți atent.
În mod natural fără blocare: du-te
Înainte de a intra în secțiunea Go, este potrivit pentru mine să dezvălui că sunt un fanboy Go. L-am folosit pentru multe proiecte și sunt 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ă vedem cum se ocupă de I/O. O caracteristică cheie a limbajului Go este că conține propriul său programator. În loc ca fiecare fir de execuție să corespundă unui singur fir de operare, funcționează cu conceptul de „goroutine”. Iar runtime-ul Go poate atribui o rutină unui fir de sistem de operare și să-l execute sau să o suspende și să nu fie asociată cu un fir de execuție a sistemului de operare, în funcție de ceea ce face acea rutină. Fiecare solicitare care vine de la serverul HTTP Go este tratată într-o Goroutine separată.
Diagrama cum 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 cererii de a scrie/citi/conecta/etc., pune în stare de adormire goroutina curentă, cu informațiile pentru a trezi goroutine înapoi. când se pot lua măsuri suplimentare.
De fapt, timpul de execuție Go face ceva nu foarte diferit de ceea ce face Node, cu excepția faptului că mecanismul de apel invers este încorporat în implementarea apelului I/O și interacționează automat cu planificatorul. De asemenea, nu suferă de restricția de a trebui să ruleze tot codul dvs. de gestionare în același fir, Go va mapa automat Goroutinele dvs. la câte fire de operare pe care le consideră adecvate, pe baza logicii din planificatorul său. Rezultatul este un cod ca acesta:
func ServeHTTP(w http.ResponseWriter, r *http.Request) { // the underlying network call here is non-blocking rows, 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 non-blocante sub capotă.
În cele mai multe cazuri, acesta ajunge să fie „cel mai bun din ambele lumi”. I/O fără blocare este folosit pentru toate lucrurile importante, dar codul dvs. pare că blochează și, prin urmare, tinde să fie mai simplu de înțeles și de întreținut. Interacțiunea dintre programatorul Go și programatorul OS se ocupă de restul. Nu este o magie completă și, dacă construiți un sistem mare, merită să acordați timp pentru a înțelege mai multe detalii despre cum 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 defecte, dar, în general, modul în care gestionează I/O nu se numără printre ele.
Minciuni, minciuni blestemate și repere
Este dificil de dat momente exacte cu privire la schimbarea contextului implicată cu aceste modele diferite. De asemenea, aș putea argumenta că îți este mai puțin util. Deci, în schimb, vă voi oferi câteva benchmark-uri de bază care compară performanța generală a serverului HTTP a acestor medii de server. Rețineți că o mulțime de factori sunt implicați în performanța întregii căi de solicitare/răspuns HTTP end-to-end, iar numerele prezentate aici sunt doar câteva mostre pe care le-am pus împreună pentru a oferi o comparație de bază.
Pentru fiecare dintre aceste medii, am scris codul adecvat pentru a-l citi într-un fișier de 64k cu octeți aleatori, am rulat un hash SHA-256 pe el de N de ori (N fiind specificat în șirul de interogare al URL-ului, de exemplu, .../test.php?n=100
) și tipăriți hashul rezultat în hex. Am ales aceasta deoarece este o modalitate foarte simplă de a rula aceleași benchmark-uri cu unele I/O consistente și o modalitate controlată de a crește utilizarea procesorului.
Consultați aceste note de referință pentru mai multe detalii despre mediile utilizate.
În primul rând, să ne uităm la câteva exemple de concurență scăzută. Rularea a 2000 de iterații cu 300 de solicitări simultane și doar un hash per solicitare (N=1) ne oferă acest lucru:
Este greu să tragem o concluzie doar din acest singur grafic, dar acest lucru mi se pare că, la acest volum de conexiune și calcul, vedem ori mai mult de-a face cu execuția generală a limbilor în sine, cu atât mai mult încât I/O. Rețineți că limbile care sunt considerate „limbaje de scripting” (tastare liberă, interpretare dinamică) funcționează cel mai lentă.
Dar ce se întâmplă dacă creștem N la 1000, tot cu 300 de solicitări simultane - aceeași încărcare, dar de 100 de ori mai multe iterații hash (încărcare semnificativ mai mare a CPU):
Dintr-o dată, performanța Nodului scade semnificativ, deoarece operațiunile care consumă intens CPU din fiecare cerere se blochează reciproc. Și destul de interesant, performanța PHP devine mult mai bună (față de celelalte) și bate Java în acest test. (Este 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 facem 1000 de iterații hash acum).
Acum să încercăm 5000 de conexiuni simultane (cu N=1) - sau cât de aproape de asta am putut. Din păcate, pentru majoritatea acestor medii, rata de eșec nu a fost nesemnificativă. Pentru acest grafic, ne vom uita la numărul total de solicitări pe secundă. Cu cât mai mare, cu atât mai bine :
Și imaginea arată destul de diferit. Este o presupunere, dar se pare că la un volum mare de conexiune, supraîncărcarea per conexiune implicată în generarea de noi procese și memoria suplimentară asociată cu aceasta în PHP+Apache pare să devină un factor dominant și reduce performanța PHP. În mod clar, Go este câștigătorul aici, urmat de Java, Node și, în final, PHP.
În timp ce factorii implicați în debitul dvs. general sunt mulți și variază, de asemenea, foarte mult de la o aplicație la alta, cu cât înțelegeți mai mult despre curajul a ceea ce se întâmplă sub capotă și compromisurile implicate, cu atât veți fi mai bine.
În concluzie
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 la scară largă care fac multe I/O au evoluat cu ele.
Pentru a fi corect, atât PHP, cât și Java, în ciuda descrierilor din acest articol, au implementări de I/O non-blocante disponibile pentru utilizare în aplicațiile web. Dar acestea nu sunt la fel de obișnuite ca abordările descrise mai sus și ar trebui să se țină seama de costurile operaționale aferente întreținerii serverelor care utilizează astfel de abordări. Ca să nu mai vorbim că codul tău trebuie să fie structurat într-un mod care să funcționeze cu astfel de medii; aplicația dvs. web PHP sau Java „normală” nu va rula de obicei 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 în utilizare, obținem următorul lucru:
Limba | Fire vs. Procese | I/O non-blocante | Ușurință în utilizare |
---|---|---|---|
PHP | Procese | Nu | |
Java | Fire | Disponibil | Necesită apeluri inverse |
Node.js | Fire | da | Necesită apeluri inverse |
Merge | Fire (Goroutines) | da | Nu sunt necesare apeluri inverse |
Thread-urile vor fi, în general, mult mai eficiente în memorie decât procesele, deoarece împart același spațiu de memorie, în timp ce procesele nu. Combinând acest lucru cu factorii legați de I/O neblocante, putem vedea că cel puțin cu factorii luați în considerare mai sus, pe măsură ce trecem în jos în listă, configurația generală în ceea ce privește I/O se îmbunătățește. Deci, 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ă construiți aplicația dvs. este strâns legată de familiaritatea pe care o are echipa dvs. cu acel mediu și de productivitatea generală pe care o puteți obține cu acesta. Deci, s-ar putea să nu aibă sens ca fiecare echipă să se scufunde și să înceapă să dezvolte aplicații și servicii web în Node sau Go. Într-adevăr, găsirea dezvoltatorilor sau familiaritatea echipei tale interne este adesea citată ca principalul motiv pentru a nu folosi un alt limbaj și/sau mediu. Acestea fiind spuse, vremurile s-au schimbat în ultimii cincisprezece ani și ceva, foarte mult.
Sperăm că cele de mai sus vă ajută să pictați o imagine mai clară a ceea ce se întâmplă sub capotă și vă oferă câteva idei despre cum să faceți față scalabilității din lumea reală pentru aplicația dvs. Intrare și ieșire fericită!