Sådan køres 1000'erne af AWS Lambda-funktioner hvert minut

I de sidste par måneder har jeg hørt flere mennesker stille mere eller mindre det samme spørgsmål:

Hvordan kører jeg tusinder af AWS Lambda-funktioner parallelt?

For nylig fandt jeg et lignende spørgsmål her på Reddit, og i stedet for at svare på tråden besluttede jeg at tage lidt tid og skrive denne artikel, som beskriver, hvordan jeg gjorde det på et af mine projekter i fortiden, med nogle eksempler og kode.

En brugssag

Lad os løse problemet med Reddit-indlægget, jeg nævnte ovenfor. I stillingen byggede de en "oppetidssundhedskontrol" -tjeneste, der anmoder om et antal websteder. Tjenesten kontrollerer responskoden og udløser en advarsel i tilfælde af problemer; for eksempel høj latenstid eller 5xx responskode. Lad os også sige, at vores ikke-eksisterende service har 1.000 klienter, og hver af dem har 10 kontroller, der er konfigureret til at udløse hvert minut.

Hvad betyder det? At vores "applikation" forventes at foretage 10.000 opkald til eksterne API'er, vente på et svar, gemme resultater (responskode, måske svarorgan). Og det skal gøre dette hvert minut.

Hvis du ikke ser en udfordring her, lad os overveje en enkel løsning. Hvis du går på den "traditionelle" måde at programmere på, skaber vi nogle arbejdstagere, der foretager disse opkald. Vi udløser dem ved hjælp af en cronjob eller noget lignende. Problemet er, at hvert af disse opkald kan tage nogen tid, fra et par millisekunder til mange sekunder. Fordi det ikke vil være let at genbruge en arbejdstager til flere opkald (vi er ikke sikre på, om vi er i stand til at afslutte et opkald på 60 sekunder), bliver vi nødt til at designe et system med 10.000 arbejdere til at håndtere vores belastning samtidigt .

Foreslået løsning nr. 1

Lad os se på min foreslåede løsning. Der er en "ny og dekorativ" måde at køre mange parallelle funktioner på samme tid uden at skulle eje infrastrukturen. Det kaldes "Serverless". Lad os prøve at bruge AWS Lambda og se, om det kan håndtere 10.000 arbejdstagere.

Men hvordan skal vi begynde (eller “påkalde” talende i AWS-slang) 10.000 Lambdas hvert minut? Nå, lad os skrive en simpel pythonkode for at udløse alle de funktioner, vi har brug for, og lad os lade som om det er en rigtig app og videregive dem nogle "URL-adresser" til at behandle.

Lad os også oprette en anden funktion kaldet testLambdaWorker - vores arbejdstagerfunktion, som i en reel applikation ville være ansvarlig for at fremsætte en "sundhedsanmodning". Vi simulerer et "virkeligt liv" -scenarie og foregiver, at vores arbejdstager gør noget, så lad os sige 0,5 sekunder i gennemsnit. Lad os antage, at det skal være nok til at håndtere alt, hvad vi skal gøre i arbejdstagerfunktionen, herunder ”gemme resultater” i databasen.

Da vi bliver nødt til at køre vores "invoker" hvert minut og gentage den igen og igen, indstiller vi en eksekveringsgrænse på 60 sekunder. For at sikre, at vi kører så hurtigt som muligt, lad os allokere den maksimale tilgængelige hukommelse til "invoker" - 3008MB (jo mere hukommelse du tildeler, jo mere CPU modtager du). Lad os køre det!

Skærmbillede af AWS Lambda Console. Vi var i stand til kun at påkalde 1049 funktioner på 60 sekunder

Det er ikke nøjagtigt, hvad vi forventede. Vi var i stand til kun at udløse 1049 “arbejdere” på 60 sekunder. Det er ikke engang tæt på, hvad vi har brug for - vi forventede at planlægge 10.000 funktioner, men Lambda var kun i stand til at påkalde 1049 funktioner. For at afklare - når vi påkalder Lambda-funktion med InvocationType = Begivenhed, vi ikke faktisk venter på, at en funktion skal afsluttes, venter vi kun på AWS for at bekræfte, at de har planlagt et job.

Bemærk også, at selvom vi tildelte 3008 MB hukommelse til vores funktion, brugte Lambda kun 63 MB, så vi var ikke bundet af CPU og / eller hukommelse. Skønt vi tildelte 128 MB, ville vores funktion være endnu langsommere. Lad os tænke på en anden, mere skalerbar løsning.

Foreslået løsning nr. 2

I gennemsnit kunne vi påkalde 17,5 lambda-funktioner hvert sekund. Vi kan prøve at bruge et "Fanout" -mønster, så i stedet for at påkalde dem alle i en enkelt løkke, kan vi oprette 100 batches af 100 URL'er (10.000 i alt, hvad vi har brug for) og have en mellemliggende lambda-funktion, der vil udløse individuelle arbejdstagere for hver URL.

“Fanout” -mønster til planlægning af Lambda-funktioner

Det kan være lettere at forstå dette, hvis vi ser på visualisering til venstre. Vi har en "hovedfunktion", der læser en konfiguration af alle opgaver, som vi skal udføre. Det opdeler dem i batches på 100 URL'er og påkalder “trigger” Lambda-funktioner, hvorved en batch overføres til hver af dem. I vores tilfælde bliver vi nødt til at påkalde det 100 gange med 100 webadresser hver. "Triggerer" -funktion modtager en batch af webadresser og påkalder "arbejder" -funktion for hver af dem. Da hver af “triggerne” kun skal påkalde op til 100 “arbejdere”, skulle det tage 5-10 sekunder, baseret på vores tidligere erfaringer med løsning nr. 1. Hver arbejdstager er ansvarlig for kun at håndtere 1 URL, så den blokerer ikke for andre processer. Lad os prøve at sammensætte alt sammen og se, hvordan vores løsning fungerer nu. Lad os se på den endelige version af vores løsning, som også er tilgængelig som en essens på github:

Vi forsøger at optimere omkostningerne nu, så "Triggerer" -funktionen er konfigureret med kun 256 MB hukommelse og 60'ers grænse, det samme som "hoved" -funktion. Da "Arbejder" -funktion udfører en netværksanmodning og venter på et svar - hukommelse og CPU vil ikke være en begrænsende faktor her. Da vi også har en masse "arbejdstagere", der kører hvert minut, lad os give minimum tilgængelig hukommelse, 128 MB til denne funktion. Nu kan vi starte den vigtigste lambda-funktion, gå til CloudWatch og se, hvor mange kald vi har på "arbejder". Efter et par sekunder kan vi se, at "arbejder" blev påberåbt 10.000 gange. Lad os "fremstille" dette eksempel og konfigurere det til at køre hvert minut. Til dette kan vi oprette en CloudWatch-regel for at påkalde vores "hoved" -funktion en gang om minuttet.

En CloudWatch-begivenhed, der udløser vores Lambda-funktion hvert minut

Efter en time kontrollerede vi CloudWatch for vores "arbejder" -funktion igen:

Vi kan se, at der var 50.000 påkaldelser på hvert datapunkt (det er grupperet med 5 minutter på denne visning, så 10.000 hvert minut). Den gennemsnitlige varighed var ~ 513ms, og den maksimale varighed undertiden toppede 1,8 sekunder uden fejl. Vi kan også se, at vi normalt får 50-100 throttlinger / 5 min. Da vi påberåber os "Event" -type Lambda-funktioner - AWSs standardadfærd er at prøve igen, når det bliver smurt, og vi kan se, at vi ikke får nogen DeadLetterErrors eller andre fejl, så det er vellykket. For produktionsarbejdsbyrde bør vi naturligvis enten starte "arbejder" -funktioner langsommere (så ikke alle "arbejdere" påberåbes på samme tid), eller vi kan anmode om en forhøjelse af vores grænser for Lambda-samtid, som er indstillet til 1000 for nye konti som standard. Hvis du leverer en gyldig brugssag, er AWS Support normalt glad for at øge dine grænser.

Pris

Og endelig, lad os kontrollere, hvor meget denne løsning vil koste os. Vi skal betale $ 0.000000208 / 100ms / 128MB eksekveringstid for vores arbejdstagere. Da vi har brug for 10.000 (kald) * 60 (minutter) * 24 (timer) * 30 (dage) = 432.000.000 kald / måned. Hver påkaldelse vil vare ~ 600 ms, så:

Varighed: $ 0.000000208 * 432.000.000 * 6 = $ 539 / måned
Anmodninger: $ 0.0000002 * 432.000.000 = $ 86.40 / måned
Vores omkostninger ved at køre medarbejdere vil være $ 625 / måned i AWS Lambda-regningen.

Vi bliver også nødt til at tage højde for "udløsere" -omkostninger (1 hovedfunktion og 100 faktiske udløsere): 101 (kald) * 60 (minutter) * 24 (timer) * 30 (dage) = 4.363.200 kald, og hver vil tage cirka 10 sekunder at gennemføre (vi multiplicerer med 2 for varighedens omkostninger, da vi leverer 256 MB hukommelse i stedet for 128):

Varighed: $ 0.000000208 * 4.363.200 * 100 * 2 = $ 181 / måned
Anmodninger: $ 0.0000002 * 4.362.200 = $ 0.87 / måned
Vores omkostninger til "triggerere" vil være $ 182 / måned i AWS Lambda-regningen.

Samlet pris - lidt over $ 800 / måned.

Afsluttende tanker

Naturligvis er dette kun en af ​​de mulige løsninger. Du kunne sandsynligvis opnå de samme resultater ved hjælp af flere tråde på en enkelt maskine, men efter min mening vil det gøre løsningen lidt mere kompliceret. Det er også muligt, at du alligevel vil være tæt på at ramme grænsen for en enkelt EC2-instans - enten netværk, CPU eller hukommelse eller en kombination af dem. Og for $ 800 pr. Måned har du kun råd til et par anstændigt store tilfælde. Du skal også sørge for, at dine forekomster er sunde (autoskaleringsgruppe hjælper dig med det).

Jeg antyder ikke, at du ikke kan løse denne brugssag uden AWS Lambda. Jeg prøver bare at vise, at det er virkelig let at implementere en løsning ved hjælp af Lambda på en meget skalerbar måde. Ved hjælp af AWS Lambdas kan du have ren kode, der udføres på hundreder eller endda tusinder af maskiner parallelt, og denne løsning kan let skalere 10x uden ændringer af koden.