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

okt 22, 2021
admin

Forståelse af input/output-modellen (I/O) for din applikation kan betyde forskellen mellem en applikation, der klarer den belastning, den udsættes for, og en applikation, der knækker sammen i forbindelse med virkelige brugssituationer i den virkelige verden. Måske mens din applikation er lille og ikke tjener høje belastninger, kan det betyde langt mindre. Men når trafikbelastningen i din applikation stiger, kan det at arbejde med den forkerte I/O-model få dig ud i en verden af smerte.

Og som i de fleste andre situationer, hvor flere tilgange er mulige, er det ikke bare et spørgsmål om, hvilken der er bedst, det er et spørgsmål om at forstå kompromiserne. Lad os tage en tur gennem I/O-landskabet og se, hvad vi kan spotte.

I denne artikel vil vi sammenligne Node, Java, Go og PHP med Apache, diskutere, hvordan de forskellige sprog modellerer deres I/O, fordele og ulemper ved hver model, og afslutte med nogle rudimentære benchmarks. Hvis du er bekymret for I/O-ydelsen i din næste webapplikation, er denne artikel for dig.

I/O Basics: En hurtig genopfriskning

For at forstå de faktorer, der er involveret i I/O, skal vi først gennemgå begreberne nede på styresystemniveau. Selv om det er usandsynligt, at du skal beskæftige dig direkte med mange af disse begreber, beskæftiger du dig hele tiden indirekte med dem gennem dit programs kørselstidsmiljø. Og detaljerne har betydning.

Systemkald

For det første har vi systemkald, som kan beskrives således:

  • Dit program (i “brugerland”, som man siger) skal bede operativsystemets kerne om at udføre en I/O-operation på dets vegne.
  • Et “syscall” er det middel, hvormed dit program beder kernen om at gøre noget. De nærmere detaljer om, hvordan dette er implementeret, varierer fra operativsystem til operativsystem, men det grundlæggende koncept er det samme. Der vil være en eller anden specifik instruktion, der overfører kontrol fra dit program til kernen (ligesom et funktionskald, men med en særlig sovs specielt til at håndtere denne situation). Generelt er syscalls blokerende, hvilket betyder, at dit program venter på, at kernen vender tilbage til din kode.
  • Kernen udfører den underliggende I/O-operation på den pågældende fysiske enhed (disk, netværkskort osv.) og svarer på syscall’et. I den virkelige verden skal kernen måske gøre en række ting for at opfylde din anmodning, herunder vente på, at enheden er klar, opdatere sin interne tilstand osv. men som programudvikler er du ligeglad med det. Det er kernens opgave.

Blokkerende vs. ikke-blokkerende kald

Nu sagde jeg lige ovenfor, at syscalls er blokkerende, og det er sandt i en generel forstand. Nogle kald er imidlertid kategoriseret som “non-blocking”, hvilket betyder, at kernen tager din anmodning, lægger den i køen eller bufferen et sted og derefter straks vender tilbage uden at vente på, at den faktiske I/O finder sted. Så den “blokerer” kun i et meget kort tidsrum, lige længe nok til at sætte din anmodning i kø.

Nogle eksempler (på Linux-syscalls) kan måske hjælpe med at afklare: – read() er et blokerende kald – du giver den et håndtag, der siger hvilken fil og en buffer, hvor den skal levere de data, den læser, og kaldet vender tilbage, når dataene er der. Bemærk, at dette har den fordel, at det er pænt og enkelt. – epoll_create(), epoll_ctl() og epoll_wait() er kald, der henholdsvis lader dig oprette en gruppe af handles til at lytte på, tilføje/fjern handlers fra denne gruppe og derefter blokere, indtil der er aktivitet. Dette giver dig mulighed for effektivt at styre et stort antal I/O-operationer med en enkelt tråd, men jeg går for vidt. Dette er fantastisk, hvis du har brug for funktionaliteten, men som du kan se, er det helt sikkert mere komplekst at bruge.

Det er vigtigt at forstå størrelsesordenen af forskellen i timing her. Hvis en CPU-kerne kører med 3 GHz, uden at komme ind på de optimeringer, som CPU’en kan foretage, udfører den 3 milliarder cyklusser i sekundet (eller 3 cyklusser pr. nanosekund). Et ikke-blokerende systemopkald kan tage i størrelsesordenen 10’er af cyklusser at gennemføre – eller “et relativt få nanosekunder”. Et kald, der blokerer for oplysninger, der modtages over nettet, kan tage meget længere tid – lad os sige f.eks. 200 millisekunder (1/5 af et sekund). Og lad os f.eks. sige, at det ikke-blokerende opkald tog 20 nanosekunder, og at det blokerende opkald tog 200.000.000 nanosekunder. Din proces har lige ventet 10 millioner gange længere på det blokerende kald.

Kernen giver mulighed for at udføre både blokerende I/O (“læs fra denne netværksforbindelse og giv mig dataene”) og ikke-blokkerende I/O (“fortæl mig, når en af disse netværksforbindelser har nye data”). Og hvilken mekanisme der anvendes, vil blokere den kaldende proces i dramatisk forskellige tidsrum.

Scheduling

Den tredje ting, der er afgørende at følge, er, hvad der sker, når du har mange tråde eller processer, der begynder at blokere.

For vores formål er der ikke den store forskel på en tråd og en proces. I det virkelige liv er den mest mærkbare ydelsesrelaterede forskel, at eftersom tråde deler den samme hukommelse, og processer har hver deres egen hukommelsesplads, har det en tendens til at lave separate processer, der optager meget mere hukommelse. Men når vi taler om planlægning, er det i virkeligheden en liste over ting (både tråde og processer), som hver især skal have en del af deres eksekveringstid på de tilgængelige CPU-kerner. Hvis du har 300 tråde kørende og 8 kerner at køre dem på, er du nødt til at dele tiden op, så hver enkelt får sin andel, hvor hver kerne kører i en kort periode og derefter går videre til den næste tråd. Dette gøres ved hjælp af et “kontekstskifte”, hvor CPU’en skifter fra at køre en tråd/proces til den næste.

Disse kontekstskifter har en omkostning forbundet med dem – de tager noget tid. I nogle hurtige tilfælde kan det være mindre end 100 nanosekunder, men det er ikke ualmindeligt, at det kan tage 1000 nanosekunder eller længere tid, afhængigt af implementeringsdetaljerne, processorens hastighed/arkitektur, CPU-cache osv.

Og jo flere tråde (eller processer), jo flere kontekstskift. Når vi taler om tusindvis af tråde og hundredvis af nanosekunder for hver enkelt, kan tingene blive meget langsomme.

Men ikke-blokerende kald fortæller i det væsentlige kernen “kun kalde mig, når du har nogle nye data eller en ny hændelse på en af disse forbindelser”. Disse non-blocking calls er designet til effektivt at håndtere store I/O-belastninger og reducere kontekstskifte.

Er du med mig indtil videre? For nu kommer den sjove del: Lad os se på, hvad nogle populære sprog gør med disse værktøjer og drage nogle konklusioner om afvejningen mellem brugervenlighed og ydeevne … og andre interessante ting.

Som en note, mens eksemplerne vist i denne artikel er trivielle (og delvise, med kun de relevante bits vist); databaseadgang, eksterne caching-systemer (memcache, et. all) og alt, der kræver I/O, vil ende med at udføre en form for I/O-kald under motorhjelmen, som vil have samme effekt som de enkle eksempler vist. I de scenarier, hvor I/O beskrives som “blokerende” (PHP, Java), er HTTP-forespørgsler og -svarslæsninger og -skrivninger også selv blokerende kald: Igen er der mere I/O skjult i systemet med de dertil hørende ydelsesproblemer, der skal tages i betragtning.

Der er mange faktorer, der spiller ind på valget af programmeringssprog til et projekt. Der er endda mange faktorer, når man kun tager hensyn til ydeevnen. Men hvis du er bekymret for, at dit program primært vil blive begrænset af I/O, hvis I/O-ydelsen er afgørende for dit projekt, er dette ting, du skal vide.

Den “Keep It Simple”-tilgang: PHP

Tilbage i 90’erne var der mange mennesker, der gik i Converse-sko og skrev CGI-scripts i Perl. Så kom PHP, og uanset hvor meget nogle mennesker kan lide at svine det til, gjorde det det det meget nemmere at lave dynamiske websider.

Modellen, som PHP bruger, er ret enkel. Der er nogle variationer, men en gennemsnitlig PHP-server ser således ud:

En HTTP-forespørgsel kommer ind fra en brugers browser og rammer din Apache-webserver. Apache opretter en separat proces for hver anmodning, med nogle optimeringer til at genbruge dem for at minimere, hvor mange den skal lave (det er relativt set langsomt at oprette processer).Apache kalder PHP og fortæller den, at den skal køre den relevante .php fil på disken.PHP-koden udføres og foretager blokerende I/O-kald. Du kalder file_get_contents() i PHP, og under motorhjelmen laver den read() syscalls og venter på resultaterne.

Og selvfølgelig er selve koden simpelthen indlejret direkte i din side, og operationerne er blokerende:

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

Med hensyn til hvordan dette integreres med systemet, er det sådan her:

Ganske simpelt: én proces pr. anmodning. I/O-opkald blokerer bare. Fordel? Det er simpelt, og det virker. Ulempe? Hvis du rammer den med 20.000 klienter samtidig, vil din server bryde i brand. Denne fremgangsmåde skalerer ikke godt, fordi de værktøjer, som kernen har til rådighed til håndtering af store mængder I/O (epoll osv.), ikke bliver brugt. Og for at føje spot til skade har det at køre en separat proces for hver forespørgsel tendens til at bruge en masse systemressourcer, især hukommelse, som ofte er det første, man løber tør for i et scenario som dette.

Note: Den tilgang, der anvendes til Ruby, ligner meget den, der anvendes til PHP, og på en bred, generel, håndfast måde kan de betragtes som det samme til vores formål.

Den multitrådede tilgang: Java

Så kommer Java, lige omkring det tidspunkt, hvor du købte dit første domænenavn, og det var cool at sige tilfældigt “dot com” efter en sætning. Og Java har multithreading indbygget i sproget, hvilket (især for dengang det blev skabt) er ret fantastisk.

De fleste Java-webservere fungerer ved at starte en ny udførelsestråd for hver anmodning, der kommer ind, og så i denne tråd til sidst kalde den funktion, som du som applikationsudvikler har skrevet.

Opførsel af I/O i en Java-servlet plejer at se nogenlunde sådan ud:

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

Da vores doGet-metode ovenfor svarer til én anmodning og køres i sin egen tråd, har vi i stedet for en separat proces for hver anmodning, som kræver sin egen hukommelse, en separat tråd. Dette har nogle gode fordele, som f.eks. at kunne dele tilstand, cachede data osv. mellem tråde, fordi de kan få adgang til hinandens hukommelse, men virkningen på hvordan det interagerer med skemaet er stadig næsten identisk med det, der gøres i PHP-eksemplet tidligere. Hver forespørgsel får en ny tråd, og de forskellige I/O-operationer blokeres i denne tråd, indtil forespørgslen er fuldt håndteret. Tråde bliver puljet for at minimere omkostningerne ved at oprette og ødelægge dem, men tusindvis af forbindelser betyder stadig tusindvis af tråde, hvilket er dårligt for scheduleren.

En vigtig milepæl er, at Java i version 1.4 (og en betydelig opgradering igen i 1.7) fik mulighed for at lave ikke-blokkerende I/O-opkald. De fleste applikationer, web og andre, bruger det ikke, men i det mindste er det tilgængeligt. Nogle Java-webservere forsøger at udnytte dette på forskellige måder; men langt de fleste implementerede Java-applikationer fungerer stadig som beskrevet ovenfor.

Java bringer os tættere på og har helt sikkert nogle gode out-of-the-box-funktioner til I/O, men det løser stadig ikke rigtig problemet med, hvad der sker, når man har en stærkt I/O-bunden applikation, der bliver banket i jorden med mange tusinde blokerende tråde.

Non-blocking I/O som førsteklasses borger: Node

Den populære dreng på blokken, når det kommer til bedre I/O, er Node.js. Enhver, der har fået bare den korteste introduktion til Node, har fået at vide, at det er “non-blocking”, og at det håndterer I/O effektivt. Og det er sandt i en generel forstand. Men djævelen er i detaljerne, og de midler, hvormed denne heksekunst blev opnået, betyder noget, når det kommer til ydeevne.

Et paradigmeskift, som Node implementerer, er i det væsentlige, at i stedet for i det væsentlige at sige “skriv din kode her for at håndtere anmodningen”, siger de i stedet “skriv kode her for at begynde at håndtere anmodningen”. Hver gang du skal gøre noget, der involverer I/O, laver du anmodningen og giver en callback-funktion, som Node kalder, når den er færdig.

Typisk Node-kode til at udføre en I/O-operation i en anmodning går sådan her:

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

Som du kan se, er der to callback-funktioner her. Den første bliver kaldt, når en anmodning starter, og den anden bliver kaldt, når fildataene er tilgængelige.

Det, dette gør, er grundlæggende at give Node en mulighed for at håndtere I/O effektivt mellem disse callbacks. Et scenarie, hvor det ville være endnu mere relevant, er, hvor du foretager et databasekald i Node, men jeg vil ikke gide det eksempel, fordi det er nøjagtig det samme princip: Du starter databasekaldet og giver Node en callback-funktion, den udfører I/O-operationerne separat ved hjælp af non-blocking kald og påkalder derefter din callback-funktion, når de data, du bad om, er tilgængelige. Denne mekanisme med at sætte I/O-opkald i kø og lade Node håndtere det og derefter få et callback kaldes “Event Loop”. Og den fungerer ret godt.

Der er dog en hage ved denne model. Under motorhjelmen har grunden til det meget mere at gøre med, hvordan V8 JavaScript-motoren (Chromes JS-motor, der bruges af Node) er implementeret 1 end noget andet. Den JS-kode, som du skriver, kører alt sammen i en enkelt tråd. Tænk over det et øjeblik. Det betyder, at mens I/O udføres ved hjælp af effektive ikke-blokkerende teknikker, kører din JS-kan, der udfører CPU-bundne operationer, i en enkelt tråd, hvor hver kodeklump blokerer den næste. Et almindeligt eksempel på, hvor dette kan forekomme, er looping over databaseposter for at behandle dem på en eller anden måde, før de udgives til klienten. Her er et eksempel, der viser, hvordan det fungerer:

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

Selv om Node håndterer I/O effektivt, bruger for-løkken i eksemplet ovenfor CPU-cyklusser i din eneste hovedtråd. Det betyder, at hvis du har 10.000 forbindelser, kan denne løkke få hele din applikation til at gå i stå, afhængigt af hvor lang tid den tager. Hver anmodning skal dele en del af tiden, en ad gangen, i din hovedtråd.

Den præmis, som hele dette koncept er baseret på, er, at I/O-operationerne er den langsomste del, og derfor er det vigtigst at håndtere dem effektivt, selv om det betyder, at du skal udføre anden behandling serielt. Dette er sandt i nogle tilfælde, men ikke i alle.

Det andet punkt er, og selvom det kun er en mening, at det kan være ret trættende at skrive en masse nested callbacks, og nogle hævder, at det gør koden betydeligt sværere at følge med i. Det er ikke ualmindeligt at se callbacks nested fire, fem eller endnu flere niveauer dybt inde i Node-kode.

Vi er tilbage til kompromiserne igen. Node-modellen fungerer godt, hvis dit primære ydelsesproblem er I/O. Dens akilleshæl er imidlertid, at du kan gå ind i en funktion, der håndterer en HTTP-forespørgsel, og lægge CPU-intensiv kode ind og bringe hver eneste forbindelse til at gå i stå, hvis du ikke er forsigtig.

Naturligvis ikke-blockerende: Go

Hvor jeg kommer ind på afsnittet om Go, er det passende for mig at afsløre, at jeg er en Go-fanboy. Jeg har brugt det til mange projekter, og jeg er åbenlyst tilhænger af dets produktivitetsfordele, og jeg kan se dem i mit arbejde, når jeg bruger det.

Det sagt, så lad os se på, hvordan det håndterer I/O. En vigtig egenskab ved Go-sproget er, at det indeholder sin egen scheduler. I stedet for at hver udførelsestråd svarer til en enkelt OS-tråd, arbejder det med begrebet “goroutiner”. Og Go-køringstiden kan tildele en goroutine til en OS-tråd og få den til at udføre eller suspendere den og få den til ikke at være forbundet med en OS-tråd, baseret på hvad den pågældende goroutine laver. Hver anmodning, der kommer ind fra Go’s HTTP-server, håndteres i en separat goroutine.

Diagrammet over, hvordan scheduleren fungerer, ser således ud:

Under motorhjelmen implementeres dette af forskellige punkter i Go-køretiden, der implementerer I/O-kaldet ved at foretage anmodningen om at skrive/læse/forbinde/etc., sætter den aktuelle goroutine i dvale, med oplysninger om at vække goroutinen igen, når der kan foretages yderligere handling.

I realiteten gør Go-køretiden noget, der ikke er så frygtelig ulig det, som Node gør, bortset fra at callback-mekanismen er indbygget i implementeringen af I/O-kaldet og interagerer med scheduleren automatisk. Det lider heller ikke under den begrænsning, at al din handler-kode skal køre i den samme tråd, Go vil automatisk mappe dine Goroutines til så mange OS-tråde, som den finder passende, baseret på logikken i sin scheduler. Resultatet er kode som denne:

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}

Som du kan se ovenfor, ligner den grundlæggende kodestruktur for det, vi gør, de mere forenklede tilgange, og alligevel opnår vi ikke-blokkerende I/O under motorhjelmen.

I de fleste tilfælde ender dette med at være “det bedste af begge verdener”. Ikke-blokerende I/O bruges til alle de vigtige ting, men din kode ser ud som om den er blokerende og har derfor en tendens til at være enklere at forstå og vedligeholde. Interaktionen mellem Go-planlæggeren og OS-planlæggeren klarer resten. Det er ikke komplet magi, og hvis du bygger et stort system, er det værd at bruge tid på at forstå mere detaljeret, hvordan det fungerer; men samtidig fungerer og skalerer det miljø, du får “out-of-the-box” ganske godt.

Go kan have sine fejl, men generelt set er den måde, den håndterer I/O på, ikke blandt dem.

Lies, Damned Lies and Benchmarks

Det er svært at give nøjagtige tidsangivelser på den kontekstskifte, der er involveret i disse forskellige modeller. Jeg kunne også argumentere for, at det er mindre nyttigt for dig. Så i stedet vil jeg give dig nogle grundlæggende benchmarks, der sammenligner den samlede HTTP-serverydelse for disse servermiljøer. Husk på, at der er mange faktorer involveret i ydeevnen for hele end-to-end HTTP-forespørgsels-/responsstien, og de tal, der præsenteres her, er blot nogle eksempler, som jeg har sammensat for at give en grundlæggende sammenligning.

For hvert af disse miljøer skrev jeg den relevante kode til at læse en 64k-fil med tilfældige bytes, kørte en SHA-256-hash på den N antal gange (N er angivet i URL’ens forespørgselsstreng, f.eks. .../test.php?n=100) og udskrev den resulterende hash i hex. Jeg valgte dette, fordi det er en meget enkel måde at køre de samme benchmarks på med nogle konsekvente I/O og en kontrolleret måde at øge CPU-forbruget på.

Se disse benchmark-noter for at få lidt flere detaljer om de anvendte miljøer.

Først skal vi se på nogle eksempler med lav samtidighed. Hvis vi kører 2000 iterationer med 300 samtidige anmodninger og kun én hash pr. anmodning (N=1), får vi dette:

Tider er det gennemsnitlige antal millisekunder til at gennemføre en anmodning på tværs af alle samtidige anmodninger. Lavere er bedre.

Det er svært at drage en konklusion ud fra denne ene graf, men for mig ser det ud til, at vi ved denne forbindelses- og beregningsmængde ser tider, der mere har at gøre med den generelle udførelse af selve sprogene, meget mere end med I/O. Bemærk, at de sprog, der betragtes som “scripting-sprog” (løs typning, dynamisk fortolkning) udfører langsommest.

Men hvad sker der, hvis vi øger N til 1000, stadig med 300 samtidige anmodninger – samme belastning, men 100x flere hash-iterationer (betydeligt mere CPU-belastning):

Tiderne er det gennemsnitlige antal millisekunder til at gennemføre en anmodning på tværs af alle samtidige anmodninger. Lavere er bedre.

Pludselig falder nodeydelsen betydeligt, fordi de CPU-intensive operationer i hver anmodning blokerer for hinanden. Og interessant nok bliver PHP’s ydeevne meget bedre (i forhold til de andre) og slår Java i denne test. (Det er værd at bemærke, at i PHP er SHA-256-implementeringen skrevet i C, og udførelsesvejen bruger meget mere tid i denne løkke, da vi nu laver 1000 hash-iterationer).

Nu skal vi prøve 5000 samtidige forbindelser (med N=1) – eller så tæt på det, som jeg kunne komme. Desværre var fejlfrekvensen for de fleste af disse miljøer ikke ubetydelig. I dette diagram vil vi se på det samlede antal anmodninger pr. sekund. Jo højere jo bedre:

Totalt antal anmodninger pr. sekund. Højere er bedre.

Og billedet ser helt anderledes ud. Det er et gæt, men det ser ud til, at ved høj forbindelsesmængde ser det ud til, at det overhead pr. forbindelse, der er forbundet med at spawne nye processer og den ekstra hukommelse, der er forbundet med det i PHP+Apache, synes at blive en dominerende faktor og tømmer PHP’s ydeevne. Det er klart, at Go er vinderen her, efterfulgt af Java, Node og til sidst PHP.

Mens de faktorer, der er involveret i dit samlede gennemløb, er mange og også varierer meget fra applikation til applikation, vil du være bedre stillet, jo mere du forstår, hvad der foregår under motorhjelmen og de kompromiser, der er involveret, jo bedre stillet vil du være.

Sammenfattende

Med alt det ovenstående er det ret tydeligt, at efterhånden som sprogene har udviklet sig, har løsningerne til håndtering af store applikationer, der laver mange I/O, udviklet sig med det.

For at være fair har både PHP og Java, på trods af beskrivelserne i denne artikel, implementeringer af non-blocking I/O til rådighed til brug i webapplikationer. Men disse er ikke så almindelige som de ovenfor beskrevne fremgangsmåder, og der skal tages hensyn til det medfølgende driftsomkostninger ved at vedligeholde servere, der anvender sådanne fremgangsmåder. For ikke at nævne, at din kode skal være struktureret på en måde, der fungerer med sådanne miljøer; din “normale” PHP- eller Java-webapplikation vil normalt ikke kunne køre uden betydelige ændringer i et sådant miljø.

Som sammenligning, hvis vi overvejer et par væsentlige faktorer, der påvirker både ydeevne og brugervenlighed, får vi dette:

Sprog Threads vs. Processer Non-blokerende I/O Brugskomfort
PHP Processer Nej
Java Threads Available Kræver callbacks
Node.js Threads Ja Kræver Callbacks
Threads (Goroutines) Ja Ingen callbacks er nødvendige

Threads vil generelt være meget mere hukommelseseffektive end processer, da de deler det samme hukommelsesrum, mens processer ikke gør det. Hvis vi kombinerer dette med de faktorer, der er relateret til non-blocking I/O, kan vi se, at i det mindste med de faktorer, der er taget i betragtning ovenfor, forbedres den generelle opsætning i forhold til I/O, efterhånden som vi bevæger os nedad på listen. Så hvis jeg skulle vælge en vinder i ovenstående konkurrence, ville det helt sikkert være Go.

Men i praksis hænger valget af et miljø, som man skal bygge sin applikation i, tæt sammen med den fortrolighed, som ens team har med det pågældende miljø, og den generelle produktivitet, man kan opnå med det. Så det giver måske ikke mening for alle hold at kaste sig ud i det og begynde at udvikle webapplikationer og tjenester i Node eller Go. Faktisk nævnes det ofte som hovedårsag til ikke at bruge et andet sprog og/eller miljø, at man ikke kan finde udviklere eller at ens interne team ikke er fortroligt med det. Når det er sagt, har tiderne ændret sig meget i løbet af de sidste femten år eller deromkring.

Håber ovenstående hjælper med at tegne et klarere billede af, hvad der sker under motorhjelmen, og giver dig nogle idéer til, hvordan du kan håndtere den reelle skalerbarhed for din applikation. God ind- og udskrivning!

Skriv et svar

Din e-mailadresse vil ikke blive publiceret.