Sådan skaleres din Node.js-server ved hjælp af klyngedannelse

Skalerbarhed er et populært emne inden for teknologi, og hvert programmeringssprog eller enhver ramme giver sin egen måde at håndtere store belastninger på trafik på.

I dag skal vi se et let og ligetil eksempel om Node.js-klyngedannelse. Dette er en programmeringsteknik, der hjælper dig med at parallelisere din kode og fremskynde ydeevnen.

“En enkelt forekomst af Node.js kører i en enkelt tråd. For at drage fordel af multi-core systemer vil brugeren nogle gange starte en klynge af Node.js-processer for at håndtere belastningen. ”

- Node.js-dokumentation

Vi opretter en simpel webserver ved hjælp af Koa, som virkelig ligner Express med hensyn til brug.

Det komplette eksempel er tilgængeligt i dette Github-arkiv.

Hvad vi skal bygge

Vi bygger en simpel webserver, der fungerer som følger:

  1. Vores server modtager en POSTanmodning, vi foregiver at brugeren sender os et billede.
  2. Vi kopierer et billede fra filsystemet til en midlertidig mappe.
  3. Vi vender det lodret ved hjælp af Jimp, et billedbehandlingsbibliotek til Node.js.
  4. Vi gemmer det i filsystemet.
  5. Vi sletter det, og vi sender et svar til brugeren.

Selvfølgelig er dette ikke en rigtig verdensapplikation, men er temmelig tæt på en. Vi vil bare måle fordelene ved at bruge klyngedannelse.

Opsætning af projektet

Jeg bruger yarntil at installere mine afhængigheder og initialisere mit projekt:

Da Node.js er enkelt gevind, hvis vores webserver går ned, forbliver den nede, indtil en anden proces genstarter den. Så vi installerer for evigt, en simpel dæmon, der genstarter vores webserver, hvis den nogensinde går ned.

Vi installerer også Jimp, Koa og Koa Router.

Kom godt i gang med Koa

Dette er den mappestruktur, vi skal oprette:

Vi har en srcmappe, der indeholder to JavaScript-filer: cluster.jsog standard.js.

Den første er den fil, hvor vi eksperimenterer med clustermodulet. Den anden er en simpel Koa-server, der fungerer uden klyngedannelse.

I modulebiblioteket opretter vi to filer: job.jsog log.js.

job.jsudfører billedmanipuleringsarbejdet. log.jslogger hver begivenhed, der opstår under denne proces.

Log-modulet

Logmodul vil være en simpel funktion, der tager et argument og skriver det til stdout(ligner console.log).

Det tilføjer også det aktuelle tidsstempel i begyndelsen af ​​loggen. Dette giver os mulighed for at kontrollere, hvornår en proces startede, og til at måle dens ydeevne.

Jobmodulet

Jeg skal være ærlig, dette er ikke et smukt og superoptimeret script. Det er bare et let job, der giver os mulighed for at stresse vores maskine.

Koa Webserver

Vi opretter en meget enkel webserver. Det vil svare på to ruter med to forskellige HTTP-metoder.

Vi kan udføre en GET-anmodning den //localhost:3000/. Koa vil svare med en simpel tekst, der viser os den aktuelle PID (proces-id).

Den anden rute accepterer kun POST-anmodninger på /flipstien og udfører det job, vi lige har oprettet.

Vi opretter også en simpel middleware, der sætter en X-Response-Timeheader. Dette giver os mulighed for at måle ydeevnen.

Store! Vi kan nu starte vores serverindtastning node ./src/standard.jsog teste vores ruter.

Problemet

Lad os bruge min maskine som server:

  • Macbook Pro 15-tommer 2016
  • 2,7 GHz Intel Core i7
  • 16 GB RAM

Hvis jeg fremsætter en POST-anmodning, sender scriptet ovenfor mig et svar på ~ 3800 millisekunder. Ikke så slemt, da det billede, jeg arbejder på i øjeblikket, er omkring 6,7 MB.

Jeg kan prøve at komme med flere anmodninger, men svartiden falder ikke for meget. Dette skyldes, at anmodningerne udføres sekventielt.

Så hvad ville der ske, hvis jeg forsøgte at stille 10, 100, 1000 samtidige anmodninger?

Jeg lavede et simpelt Elixir-script, der udfører flere samtidige HTTP-anmodninger:

Jeg valgte Elixir, fordi det er virkelig nemt at oprette parallelle processer, men du kan bruge hvad du foretrækker!

Test af ti samtidige anmodninger - uden klyngedannelse

Som du kan se, skaber vi ti samtidige processer fra vores iex (en Elixir REPL).

Node.js-serveren kopierer straks vores billede og begynder at vende det.

Det første svar logges efter 16 sekunder og det sidste efter 40 sekunder.

En sådan dramatisk performance falder! Med kun 10 samtidige anmodningervi reducerede webserverens ydeevne med 950%!

Introduktion til klyngedannelse

Kan du huske, hvad jeg nævnte i begyndelsen af ​​artiklen?

For at drage fordel af multi-core-systemer vil brugeren nogle gange starte en klynge af Node.js-processer for at håndtere belastningen.

Afhængigt af hvilken server vi kører vores Koa-applikation, kan vi have et andet antal kerner.

Hver kerne er ansvarlig for håndtering af lasten individuelt. Dybest set vil hver HTTP-anmodning blive opfyldt af en enkelt kerne.

Så for eksempel - min maskine, der har otte kerner, vil håndtere otte samtidige anmodninger.

Vi kan nu tælle, hvor mange CPU'er vi har takket være osmodulet:

Den cpus()metode vil returnere et array af objekter, der beskriver vores CPU'er. Vi kan binde dens længde til en konstant, der kaldes numWorkers, for det er antallet af arbejdere, som vi vil bruge.

Vi er nu klar til at kræve clustermodulet.

Vi har nu brug for en måde at opdele vores hovedproces i Nforskellige processer.

Vi kalder vores hovedproces masterog de andre processer workers.

Node.js- clustermodulet tilbyder en metode kaldet isMaster. Det returnerer en boolsk værdi, der fortæller os, om den nuværende proces styres af en arbejdstager eller mester:

Store. Den gyldne regel her er, at vi ikke ønsker at betjene vores Koa-applikation under masterprocessen.

Vi ønsker at oprette en Koa-applikation for hver arbejdstager, så når en anmodning kommer ind, vil den første gratis arbejdstager tage sig af den.

Den cluster.fork()metode vil passe vores formål:

Ok, i starten kan det være lidt vanskeligt.

Som du kan se i scriptet ovenfor, hvis vores script er blevet udført af masterprocessen, erklærer vi en konstant kaldet workers. Dette opretter en medarbejder for hver kerne i vores CPU og gemmer alle oplysninger om dem.

Hvis du føler dig usikker på den vedtagne syntaks, er brugen af […Array(x)].map()det samme som:

Jeg foretrækker bare at bruge uforanderlige værdier, mens jeg udvikler en app med høj samtidighed.

Tilføjer Koa

Som vi sagde før, vil vi ikke betjene vores Koa-applikation under masterprocessen.

Lad os kopiere vores Koa-appstruktur til elseerklæringen, så vi vil være sikre på, at den bliver betjent af en arbejdstager:

Som du kan se, tilføjede vi også et par begivenhedslyttere i isMastererklæringen:

Den første vil fortælle os, at der er skabt en ny arbejdstager. Den anden opretter en ny medarbejder, når en anden medarbejder går ned.

På den måde vil masterprocessen kun være ansvarlig for at skabe nye medarbejdere og orkestrere dem. Hver arbejdstager vil tjene en instans af Koa, som vil være tilgængelig i :3000havnen.

Test af ti samtidige anmodninger - med klyngedannelse

Som du kan se, fik vi vores første svar efter cirka 10 sekunder og det sidste efter cirka 14 sekunder. Det er en fantastisk forbedring i forhold til den foregående svartid på 40 sekunder!

Vi fremsatte ti samtidige anmodninger, og Koa-serveren tog otte af dem med det samme. Når den første arbejdstager har sendt sit svar til klienten, tog den en af ​​de resterende anmodninger og behandlede den!

Konklusion

Node.js har en fantastisk kapacitet til at håndtere høje belastninger, men det ville ikke være klogt at stoppe en anmodning, før serveren er færdig med sin proces.

Faktisk kan Node.js webservere kun håndtere tusindvis af samtidige anmodninger, hvis du straks sender et svar til klienten.

En bedste praksis ville være at tilføje en pub / sub messaging interface ved hjælp af Redis eller et hvilket som helst andet fantastisk værktøj. Når klienten sender en anmodning, starter serveren en realtidskommunikation med andre tjenester. Dette tager ansvar for dyre job.

Belastningsafbalancere vil også hjælpe meget med at opdele høje trafikbelastninger.

Endnu en gang giver teknologien os uendelige muligheder, og vi er sikker på at finde den rigtige løsning til at skalere vores applikation til uendelig og videre!