Server-side I/O Performance: Node vs. PHP vs. Java vs. Go

okt 22, 2021
admin

Inzicht in het Input/Output (I/O) model van uw applicatie kan het verschil betekenen tussen een applicatie die de belasting aankan waaraan hij wordt blootgesteld, en een die afbrokkelt bij gebruik in de praktijk. Als uw toepassing klein is en geen hoge belasting kent, maakt het misschien veel minder uit. Maar als de belasting van uw applicatie toeneemt, kan het werken met het verkeerde I/O model u in een wereld van pijn brengen.

En zoals in bijna elke situatie waar meerdere benaderingen mogelijk zijn, is het niet alleen een kwestie van welke beter is, het is een kwestie van het begrijpen van de afwegingen. Laten we een wandeling maken door het I/O landschap en zien wat we kunnen zien.

In dit artikel zullen we Node, Java, Go, en PHP vergelijken met Apache, bespreken hoe de verschillende talen hun I/O modelleren, de voor- en nadelen van elk model, en afsluiten met enkele rudimentaire benchmarks. Als je je zorgen maakt over de I/O prestaties van je volgende web applicatie, dan is dit artikel iets voor jou.

I/O Basics: Een snelle opfrisser

Om de factoren te begrijpen die een rol spelen bij I/O, moeten we eerst de concepten op het niveau van het besturingssysteem bekijken. Hoewel het onwaarschijnlijk is dat je direct met veel van deze concepten te maken krijgt, heb je er indirect mee te maken via de runtime omgeving van je applicatie. En de details doen er toe.

System Calls

Ten eerste hebben we system calls, die als volgt kunnen worden beschreven:

  • Uw programma (in “gebruikersland,” zoals ze zeggen) moet de kernel van het besturingssysteem vragen om namens hem een I/O operatie uit te voeren.
  • Een “syscall” is het middel waarmee uw programma de kernel vraagt om iets te doen. De details van hoe dit wordt geïmplementeerd variëren tussen besturingssystemen, maar het basisconcept is hetzelfde. Er zal een specifieke instructie zijn die de controle van uw programma overbrengt naar de kernel (zoals een functie-aanroep, maar met een speciaal sausje om met deze situatie om te gaan). In het algemeen zijn syscalls blokkerend, wat betekent dat je programma wacht tot de kernel terugkeert naar je code.
  • De kernel voert de onderliggende I/O operatie uit op het fysieke apparaat in kwestie (schijf, netwerkkaart, etc.) en antwoordt op de syscall. In de echte wereld moet de kernel misschien een aantal dingen doen om aan je verzoek te voldoen, zoals wachten tot het apparaat klaar is, het bijwerken van de interne status, enzovoort, maar als applicatie-ontwikkelaar kan dat je niet schelen. Dat is de taak van de kernel.

Blocking vs. Non-blocking Calls

Nou, ik heb net hierboven gezegd dat syscalls blocking zijn, en dat is waar in algemene zin. Sommige aanroepen worden echter gecategoriseerd als “niet-blokkeerbaar”, wat betekent dat de kernel uw verzoek aanneemt, het ergens in een wachtrij of buffer plaatst, en dan onmiddellijk terugkeert zonder te wachten tot de eigenlijke I/O plaatsvindt. Dus het “blokkeert” slechts voor een zeer korte periode, net lang genoeg om je verzoek te enqueue.

Enkele voorbeelden (van Linux syscalls) kunnen helpen verduidelijken:- read() is een blokkerende aanroep – je geeft het een handle door die zegt welk bestand en een buffer van waar de gegevens die het leest geleverd moeten worden, en de aanroep keert terug als de gegevens er zijn. epoll_create(), epoll_ctl() en epoll_wait() zijn aanroepen waarmee je respectievelijk een groep handles kunt aanmaken om op te luisteren, handlers aan die groep kunt toevoegen/verwijderen en vervolgens kunt blokkeren totdat er activiteit is. Dit stelt je in staat om efficiënt een groot aantal I/O operaties te controleren met een enkele thread, maar ik loop op de zaken vooruit. Dit is geweldig als je de functionaliteit nodig hebt, maar zoals je kunt zien is het zeker complexer om te gebruiken.

Het is belangrijk om de orde van grootte van het verschil in timing hier te begrijpen. Als een CPU core op 3GHz draait, zonder in te gaan op optimalisaties die de CPU kan doen, voert hij 3 miljard cycli per seconde uit (of 3 cycli per nanoseconde). Een niet-blokkerende systeemaanroep kan er ongeveer 10 cycli over doen om te voltooien – of “een relatief paar nanoseconden”. Een aanroep die blokkeert voor informatie die via het netwerk wordt ontvangen, kan veel langer duren – laten we zeggen bijvoorbeeld 200 milliseconden (1/5 van een seconde). En laten we bijvoorbeeld zeggen dat de niet-blokkerende aanroep 20 nanoseconden duurde, en de blokkerende aanroep 200.000.000 nanoseconden. Uw proces heeft zojuist 10 miljoen keer langer gewacht op de blokkerende oproep.

De kernel biedt de mogelijkheid om zowel blokkerende I/O uit te voeren (“lees van deze netwerkverbinding en geef me de gegevens”) als niet-blokkerende I/O (“vertel me wanneer een van deze netwerkverbindingen nieuwe gegevens heeft”). En welk mechanisme wordt gebruikt zal het aanroepende proces voor dramatisch verschillende lengtes van tijd blokkeren.

Scheduling

Het derde ding dat kritisch is om te volgen is wat er gebeurt als je veel threads of processen hebt die beginnen te blokkeren.

Voor onze doeleinden is er geen groot verschil tussen een thread en een proces. In het echte leven is het meest merkbare prestatiegerelateerde verschil dat, omdat threads hetzelfde geheugen delen, en processen elk hun eigen geheugenruimte hebben, het maken van afzonderlijke processen de neiging heeft veel meer geheugen in beslag te nemen. Maar als we het over plannen hebben, komt het eigenlijk neer op een lijst van dingen (zowel threads als processen) die elk een stukje uitvoeringstijd moeten krijgen op de beschikbare CPU cores. Als je 300 threads hebt draaien en 8 cores om ze op te draaien, dan moet je de tijd zo verdelen dat elke thread zijn deel krijgt, waarbij elke core een korte tijd draait en dan doorgaat naar de volgende thread. Dit wordt gedaan door een “context switch”, waarbij de CPU overschakelt van de ene thread/proces naar de volgende.

Deze context switches hebben een kostenplaatje – ze kosten wat tijd. In sommige snelle gevallen kan dat minder dan 100 nanoseconden zijn, maar het is niet ongewoon dat het 1000 nanoseconden of langer duurt, afhankelijk van de implementatiedetails, processorsnelheid/architectuur, CPU-cache, enz.

En hoe meer threads (of processen), hoe meer context switching.

En hoe meer threads (of processen), hoe meer er van context gewisseld moet worden. Als we het hebben over duizenden threads, en honderden nanoseconden voor elk, kan het erg traag worden.

Hoewel, niet-blokkerende aanroepen vertellen de kernel in essentie “roep me alleen aan als je nieuwe gegevens of gebeurtenissen hebt op een van deze verbindingen.” Deze niet-blokkerende aanroepen zijn ontworpen om efficiënt om te gaan met grote I/O ladingen en context switching te verminderen.

Begrijp je me tot nu toe? Want nu komt het leuke gedeelte: Laten we eens kijken naar wat een aantal populaire talen doen met deze tools en wat conclusies trekken over de afwegingen tussen gebruiksgemak en prestaties … en andere interessante weetjes.

Aanmerking: terwijl de voorbeelden in dit artikel triviaal zijn (en gedeeltelijk, met alleen de relevante bits getoond); databasetoegang, externe caching systemen (memcache, et. all) en alles wat I/O vereist, gaat uiteindelijk een soort I/O-aanroep onder de motorkap uitvoeren die hetzelfde effect zal hebben als de eenvoudige voorbeelden getoond. Ook voor de scenario’s waar de I/O wordt beschreven als “blokkerend” (PHP, Java), zijn het HTTP verzoek en antwoord lezen en schrijven zelf blokkerend: Nogmaals, meer I/O verborgen in het systeem met de bijbehorende prestatieproblemen om rekening mee te houden.

Er zijn veel factoren die meespelen bij het kiezen van een programmeertaal voor een project. Er zijn zelfs een heleboel factoren als je alleen naar de prestaties kijkt. Maar als u zich zorgen maakt dat uw programma voornamelijk door I/O zal worden beperkt, als I/O-prestatie bepalend is voor uw project, dan zijn dit dingen die u moet weten.

De “Keep It Simple”-aanpak: PHP

Terug in de jaren 90, droegen veel mensen Converse schoenen en schreven CGI scripts in Perl. Toen kwam PHP en, hoe graag sommige mensen het ook afkraken, het maakte het maken van dynamische webpagina’s veel eenvoudiger.

Het model dat PHP gebruikt is vrij eenvoudig. Er zijn enkele variaties, maar de gemiddelde PHP-server ziet er als volgt uit:

Een HTTP-verzoek komt binnen van de browser van een gebruiker en komt bij de Apache-webserver terecht. Apache maakt een apart proces aan voor elk verzoek, met wat optimalisaties om ze te hergebruiken om het aantal te minimaliseren (processen aanmaken is, relatief gezien, traag).Apache roept PHP op en vertelt het om het juiste .php bestand op de schijf uit te voeren.PHP code wordt uitgevoerd en doet blokkerende I/O aanroepen. Je roept file_get_contents() aan in PHP en onder de motorkap doet het read() syscalls en wacht op de resultaten.

En natuurlijk is de eigenlijke code gewoon ingebed in je pagina, en de operaties zijn blokkerend:

<?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');?>

In termen van hoe dit integreert met het systeem, is het als volgt:

Vrij eenvoudig: een proces per aanvraag. I/O-aanvragen worden gewoon geblokkeerd. Voordeel? Het is eenvoudig en het werkt. Nadeel? Als je er 20.000 gelijktijdige clients op zet, barst je server in vlammen uit. Deze aanpak is niet goed schaalbaar omdat de hulpmiddelen die de kernel biedt voor het omgaan met hoog volume I/O (epoll, enz.) niet worden gebruikt. En om het nog erger te maken, het draaien van een apart proces voor elk verzoek heeft de neiging om veel systeembronnen te gebruiken, vooral geheugen, dat vaak het eerste is wat opraakt in een scenario als dit.

Note: De aanpak die wordt gebruikt voor Ruby lijkt erg op die van PHP, en in een brede, algemene, hand-golvende manier kunnen ze voor onze doeleinden als hetzelfde worden beschouwd.

De Multithreaded Benadering: Java

Dus Java komt langs, precies rond de tijd dat je je eerste domeinnaam kocht en het cool was om gewoon willekeurig “dot com” te zeggen na een zin. En Java heeft multithreading ingebouwd in de taal, wat (zeker voor toen het werd gemaakt) behoorlijk geweldig is.

De meeste Java webservers werken door een nieuwe thread van uitvoering te starten voor elk verzoek dat binnenkomt en dan in deze thread uiteindelijk de functie aan te roepen die jij, als de applicatie-ontwikkelaar, hebt geschreven.

Het uitvoeren van I/O in een Java Servlet ziet er ongeveer zo uit:

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("...");}

Omdat onze doGet methode hierboven overeenkomt met een verzoek en wordt uitgevoerd in zijn eigen thread, in plaats van een apart proces voor elk verzoek dat zijn eigen geheugen nodig heeft, hebben we een aparte thread. Dit heeft een aantal leuke voordelen, zoals het kunnen delen van state, cached data, etc. tussen threads omdat ze elkaars geheugen kunnen benaderen, maar de impact op de interactie met het schema is nog steeds bijna identiek aan wat er eerder in het PHP voorbeeld is gedaan. Elk verzoek krijgt een nieuwe thread en de verschillende I/O operaties blokkeren in die thread tot het verzoek volledig is afgehandeld. Threads worden gepoold om de kosten van het aanmaken en vernietigen ervan te minimaliseren, maar toch, duizenden verbindingen betekent duizenden threads en dat is slecht voor de scheduler.

Een belangrijke mijlpaal is dat Java in versie 1.4 (en weer een belangrijke upgrade in 1.7) de mogelijkheid kreeg om niet-blokkerende I/O aanroepen te doen. De meeste toepassingen, web en anderszins, gebruiken het niet, maar het is tenminste beschikbaar. Sommige Java webservers proberen hier op verschillende manieren gebruik van te maken; de overgrote meerderheid van geïmplementeerde Java applicaties werkt echter nog steeds zoals hierboven beschreven.

Java brengt ons dichterbij en heeft zeker een aantal goede out-of-the-box functionaliteiten voor I/O, maar het lost nog steeds niet echt het probleem op van wat er gebeurt als je een zwaar I/O-gebonden applicatie hebt die de grond in wordt gestampt met vele duizenden blokkerende threads.

Niet-blokkerende I/O als een eerste-klas burger: Node

Het populairste kind op het blok als het gaat om betere I/O is Node.js. Iedereen die ook maar een korte introductie tot Node heeft gehad, is verteld dat het “non-blocking” is en dat het efficiënt met I/O omgaat. En dit is waar in algemene zin. Maar de duivel zit in de details en de manier waarop deze tovenarij is bereikt is van belang als het gaat om prestaties.

In wezen is de paradigmaverschuiving die Node implementeert dat in plaats van in wezen te zeggen “schrijf je code hier om het verzoek af te handelen”, ze in plaats daarvan zeggen “schrijf code hier om te beginnen met het verwerken van het verzoek.” Elke keer dat je iets moet doen dat I / O met zich meebrengt, maak je het verzoek en geef je een callback-functie die Node zal aanroepen wanneer het klaar is.

Typische Node-code voor het doen van een I / O-bewerking in een verzoek gaat als volgt:

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

Zoals je kunt zien, zijn er twee callback-functies hier. De eerste wordt aangeroepen wanneer een verzoek start, en de tweede wordt aangeroepen wanneer de bestandsgegevens beschikbaar zijn.

Wat dit doet is in feite Node een kans geven om de I/O efficiënt af te handelen tussen deze callbacks in. Een scenario waar het nog relevanter zou zijn is waar je een database call in Node doet, maar ik zal je niet lastig vallen met het voorbeeld omdat het precies hetzelfde principe is: Je start de database call, en geeft Node een callback functie, het voert de I/O operaties apart uit met behulp van non-blocking calls en roept dan je callback functie aan wanneer de data waar je om vroeg beschikbaar is. Dit mechanisme van het in de wachtrij plaatsen van I/O oproepen en het door Node laten afhandelen en dan een callback krijgen wordt de “Event Loop” genoemd. En het werkt behoorlijk goed.

Er zit echter een addertje onder het gras bij dit model. Onder de motorkap heeft de reden hiervoor veel meer te maken met hoe de V8 JavaScript-engine (de JS-engine van Chrome die door Node wordt gebruikt) is geïmplementeerd 1 dan met iets anders. De JS-code die u schrijft, wordt allemaal in een enkele thread uitgevoerd. Denk daar even over na. Het betekent dat terwijl I/O wordt uitgevoerd met behulp van efficiënte niet-blokkerende technieken, uw JS-kan die CPU-gebonden bewerkingen uitvoert in een enkele thread draait, waarbij elk brok code de volgende blokkeert. Een veel voorkomend voorbeeld van waar dit zou kunnen komen is looping over database records om ze te verwerken op een bepaalde manier alvorens ze uit te voeren naar de client. Hier is een voorbeeld dat laat zien hoe dat werkt:

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})};

Hoewel Node de I/O efficiënt afhandelt, gebruikt die for lus in het bovenstaande voorbeeld CPU cycli in je enige hoofddraad. Dit betekent dat als je 10.000 verbindingen hebt, die lus je hele applicatie tot stilstand kan brengen, afhankelijk van hoe lang het duurt. Elk verzoek moet een deel van de tijd delen, een voor een, in je hoofddraad.

De vooronderstelling waar dit hele concept op is gebaseerd is dat de I/O operaties het langzaamste deel zijn, dus is het het belangrijkst om die efficiënt af te handelen, zelfs als dat betekent dat je andere verwerkingen serieel moet doen. Dit is waar in sommige gevallen, maar niet in alle.

Het andere punt is dat, en hoewel dit slechts een mening is, het behoorlijk vermoeiend kan zijn om een hoop geneste callbacks te schrijven en sommigen beweren dat het de code aanzienlijk moeilijker te volgen maakt. Het is niet ongewoon om callbacks te zien die vier, vijf, of zelfs meer niveaus diep in Node code genest zijn.

We zijn weer terug bij de afwegingen. Het Node model werkt goed als je belangrijkste prestatie probleem I/O is. Maar de achilleshiel is dat je in een functie die een HTTP-verzoek behandelt, CPU-intensieve code kunt stoppen en elke verbinding tot stilstand kunt brengen als je niet oppast.

Natuurlijk niet-blokkerend: Go

Voordat ik inga op de sectie voor Go, is het gepast voor mij om te onthullen dat ik een Go fanboy ben. Ik heb het gebruikt voor veel projecten en ik ben openlijk een voorstander van de productiviteitsvoordelen, en ik zie ze in mijn werk als ik het gebruik.

Dat gezegd hebbende, laten we eens kijken hoe het omgaat met I/O. Een belangrijk kenmerk van de Go taal is dat het zijn eigen scheduler bevat. In plaats van dat elke uitvoeringsdraad overeenkomt met een enkele OS-draad, werkt het met het concept van “goroutines.” En de Go runtime kan een goroutine toewijzen aan een OS thread en deze laten uitvoeren, of opschorten en niet laten associëren met een OS thread, afhankelijk van wat die goroutine aan het doen is. Elk verzoek dat binnenkomt van Go’s HTTP-server wordt afgehandeld in een aparte goroutine.

Het schema van hoe de planner werkt ziet er als volgt uit:

Onder de motorkap wordt dit geïmplementeerd door verschillende punten in de Go runtime die de I/O-oproep implementeren door het verzoek om te schrijven/lezen/verbinden/etc. te doen, de huidige goroutine in slaapstand zetten, met de informatie om de goroutine weer wakker te maken wanneer verdere actie kan worden ondernomen.

In feite doet de Go runtime iets dat niet verschrikkelijk veel verschilt van wat Node doet, behalve dat het callback mechanisme is ingebouwd in de implementatie van de I/O oproep en automatisch interageert met de planner. Het heeft ook geen last van de beperking dat al je handler code in dezelfde thread moet draaien, Go zal automatisch je Goroutines toewijzen aan zoveel OS threads als het geschikt acht op basis van de logica in zijn scheduler. Het resultaat is code als deze:

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}

Zoals u hierboven kunt zien, lijkt de basis code structuur van wat we doen op die van de meer simplistische benaderingen, en toch bereikt het niet-blokkerende I/O onder de motorkap.

In de meeste gevallen, is dit uiteindelijk “het beste van beide werelden.” Niet-blokkerende I/O wordt gebruikt voor alle belangrijke dingen, maar je code ziet eruit alsof het blokkeert en is dus meestal eenvoudiger te begrijpen en te onderhouden. De interactie tussen de Go scheduler en de OS scheduler regelt de rest. Het is geen complete magie, en als je een groot systeem bouwt, is het de moeite waard om de tijd te nemen om meer details te begrijpen over hoe het werkt; maar tegelijkertijd werkt en schaalt de omgeving die je “out-of-the-box” krijgt vrij goed.

Go mag dan zijn fouten hebben, maar over het algemeen is de manier waarop het I/O afhandelt daar niet een van.

Lies, Damned Lies and Benchmarks

Het is moeilijk om exacte timings te geven over de context switching die betrokken is bij deze verschillende modellen. Ik zou ook kunnen aanvoeren dat het minder nuttig voor u is. Dus in plaats daarvan geef ik je een aantal basis benchmarks die de algemene HTTP server prestaties van deze server omgevingen vergelijken. Bedenk dat veel factoren een rol spelen bij de prestaties van het gehele end-to-end HTTP verzoek/antwoord pad, en de getallen die hier worden gepresenteerd zijn slechts enkele voorbeelden die ik samenstelde om een basis vergelijking te geven.

Voor elk van deze omgevingen, schreef ik de juiste code om een 64k bestand met willekeurige bytes in te lezen, er N aantal keren een SHA-256 hash op uit te voeren (N wordt gespecificeerd in de URL’s query string, b.v. .../test.php?n=100) en de resulterende hash in hex af te drukken. Ik koos hiervoor omdat het een zeer eenvoudige manier is om dezelfde benchmarks te draaien met wat consistente I/O en een gecontroleerde manier om CPU gebruik te verhogen.

Zie deze benchmark notities voor wat meer detail over de gebruikte omgevingen.

Laten we eerst eens kijken naar een paar voorbeelden met lage concurrency. Het uitvoeren van 2000 iteraties met 300 gelijktijdige verzoeken en slechts één hash per verzoek (N=1) levert het volgende op:

De tijden zijn het gemiddelde aantal milliseconden om een verzoek te voltooien over alle gelijktijdige verzoeken. Lager is beter.

Het is moeilijk om een conclusie te trekken uit deze ene grafiek, maar het lijkt mij dat we bij dit volume van verbindingen en berekeningen tijden zien die meer te maken hebben met de algemene uitvoering van de talen zelf, veel meer dan met de I/O. Merk op dat de talen die als “scripttalen” worden beschouwd (losse types, dynamische interpretatie) het traagst presteren.

Maar wat gebeurt er als we N verhogen tot 1000, nog steeds met 300 gelijktijdige verzoeken – dezelfde belasting, maar 100x meer hash iteraties (aanzienlijk meer CPU belasting):

De tijden zijn het gemiddelde aantal milliseconden om een verzoek te voltooien over alle gelijktijdige verzoeken. Lager is beter.

Opeens daalt de Node-prestatie aanzienlijk, omdat de CPU-intensieve bewerkingen in elk verzoek elkaar blokkeren. En interessant genoeg wordt de prestatie van PHP veel beter (ten opzichte van de anderen) en verslaat Java in deze test. (Het is de moeite waard om op te merken dat in PHP de SHA-256 implementatie in C is geschreven en het executiepad veel meer tijd in die lus doorbrengt, omdat we nu 1000 hash iteraties doen).

Nu gaan we 5000 gelijktijdige verbindingen proberen (met N=1) – of zo dicht mogelijk daarbij als ik kon komen. Helaas, voor de meeste van deze omgevingen, was het faalpercentage niet onaanzienlijk. Voor deze grafiek kijken we naar het totaal aantal verzoeken per seconde. Hoe hoger, hoe beter:

Totaal aantal verzoeken per seconde. Hoger is beter.

En het plaatje ziet er heel anders uit. Het is een gok, maar het lijkt erop dat bij een hoog verbindingsvolume de overhead per verbinding die gepaard gaat met het spawnen van nieuwe processen en het extra geheugen dat daarmee gepaard gaat in PHP+Apache een dominante factor lijkt te worden en de prestaties van PHP in de war schopt. Het is duidelijk dat Go hier de winnaar is, gevolgd door Java, Node en tot slot PHP.

Hoewel de factoren die betrokken zijn bij je totale doorvoer veel zijn en ook sterk variëren van applicatie tot applicatie, hoe meer je begrijpt van wat er onder de motorkap gebeurt en de afwegingen die daarbij een rol spelen, hoe beter je af bent.

In samenvatting

Met al het bovenstaande is het vrij duidelijk dat naarmate talen zijn geëvolueerd, de oplossingen voor het omgaan met grootschalige toepassingen die veel I/O doen, mee zijn geëvolueerd.

Om eerlijk te zijn, zowel PHP als Java, ondanks de beschrijvingen in dit artikel, hebben implementaties van non-blocking I/O beschikbaar voor gebruik in web applicaties. Maar deze zijn niet zo gebruikelijk als de hierboven beschreven benaderingen, en er moet rekening worden gehouden met de operationele overhead van het onderhouden van servers die dergelijke benaderingen gebruiken. Om nog maar te zwijgen van het feit dat uw code moet worden gestructureerd op een manier die werkt met dergelijke omgevingen; uw “normale” PHP of Java webapplicatie zal gewoonlijk niet draaien zonder aanzienlijke aanpassingen in een dergelijke omgeving.

Als vergelijking, als we een paar belangrijke factoren in overweging nemen die zowel de prestaties als het gebruiksgemak beïnvloeden, krijgen we dit:

Taal Threads vs. Processen Niet-blokkerende I/O Gebruiksgemak
PHP Processen Nee
Java Threads Available Requires Callbacks
Node.js Threads Ja Requires Callbacks
Go Threads (Goroutines) Ja Geen Callbacks nodig

Threads zijn over het algemeen veel geheugenefficiënter dan processen, omdat ze dezelfde geheugenruimte delen, terwijl processen dat niet doen. Als we dat combineren met de factoren die betrekking hebben op niet-blokkerende I/O, kunnen we zien dat ten minste met de factoren die hierboven zijn bekeken, naarmate we lager op de lijst komen, de algemene opzet met betrekking tot I/O verbetert. Dus als ik een winnaar zou moeten aanwijzen in de bovenstaande wedstrijd, dan zou het zeker Go zijn.

Hoe dan ook, in de praktijk hangt het kiezen van een omgeving om je applicatie in te bouwen nauw samen met de bekendheid die je team heeft met die omgeving, en de algemene productiviteit die je ermee kunt bereiken. Het is dus misschien niet voor elk team zinvol om er gewoon in te duiken en webapplicaties en -diensten te gaan ontwikkelen in Node of Go. Sterker nog, het vinden van ontwikkelaars of de vertrouwdheid van je in-house team wordt vaak genoemd als de belangrijkste reden om geen andere taal en/of omgeving te gebruiken. Dat gezegd hebbende, de tijden zijn veranderd in de afgelopen vijftien jaar of zo, veel.

Hopelijk helpt het bovenstaande een duidelijker beeld te schetsen van wat er gebeurt onder de motorkap en geeft het je wat ideeën over hoe om te gaan met de echte wereld schaalbaarheid voor uw applicatie. Veel plezier bij het invoeren en uitvoeren!

Geef een antwoord

Het e-mailadres wordt niet gepubliceerd.