Tilbake

Luke 24

Tore NedretvedtSanta hat

Fra 0 til 60k CPU-er på 90 sekunder med AWS Lambda og Step Functions

Av Tore Nedretvedt

Julenissen er i ferd med å dra ut med pakkene, men oppdager i siste liten at han har mistet reiseruten sin. Uten denne vil han ikke være i stand til å levere alle pakkene i tide. Han må beregne ruten på nytt, og til dette trenger han enorm datakraft, ettersom han må få gjennomført beregningen så kjapt som mulig av en rute som gjør reisetiden så kort som mulig. Julenissen trenger å få satt opp noe raskt, for han har ingen tid å miste. Han ser til skyen og serverless.

Hva er AWS Lambda

AWS Lambda er en serverless skytjeneste hos Amazon Web Services som lar utviklere kjøre kode uten å måtte sette opp eller drifte servere. Man trenger bare å laste opp koden, og så sørger AWS automatisk for alt som trengs for å kjøre den: skalering, tilgjengelighet osv. Det gjør at utviklere kan fokusere på forretningslogikk i stedet for infrastruktur. AWS Lambda støtter flere runtimes slik som Node.js, Python, Java, Go, Rust m.m. Man betaler bare for den tiden koden kjører.

Parallellitet med Lambda

Flere instanser av samme lambda funksjon kan kjøre samtidig, og hver instans kan ha opp til 6 (virtuelle) CPU-er og 10GB minne. Minne kan justeres mellom 128MB til 10GB og antall CPU-er bestemmes automatisk basert på minne, fra 1 hel CPU ved 1 769 MB, til 6 CPU-er fra og med 8 846 MB.

En enkelt funksjon kan skalere opp til 1 000 instanser umiddelbart, deretter kan den skalere med 1 000 ytterligere instanser hvert 10. sekund. Typisk har man en begrensning (“soft-limit”) på 1 000 samtidige kjørende lambda instanser. Dette tallet gjelder for hele AWS kontoen, et isolert miljø som tilhører en selv hvor man kan spinne opp egne ressurser.

Eksempel:

  • Funksjon A kjører med 600 samtidige instanser

  • Funksjon B kjører med 400 samtidige instanser

Da ligger man akkurat på grensen på 1 000.

Denne grensen er mest for å beskytte seg selv. Man kan imidlertid øke den til f.eks. 10 000 ved å sende en forespørsel til AWS (quota request). Da kan man oppnå 10 000 samtidige kjørende lambda instanser med totalt 6 x 10 000 = 60 000 CPU-er på rundt 90 sekunder, som vi vil vise senere. Denne massive parallelliteten åpner opp for å kunne fullføre en oppgave i løpet av sekunder eller minutter i stedet for timer eller dager. For å skvise mest mulig ut av CPU-en og samtidig være minne-effektiv, er noen gode kandidater Rust eller Go. Man må selvsagt kjøre multitrådet for å få utbytte av flere CPU-er.

Merk at hvis man ikke utfører CPU-intensive operasjoner, men trenger likevel massiv parallellitet - f.eks. for å utføre lasttesting av en web-applikasjon med titusener av HTTP-klienter - så kan man sette et lavere tall for minne, siden CPU-antallet bestemmes proporsjonalt med minnet. Dette kan redusere kostnaden betraktelig. Det er mange scenarioer hvor man kan klare seg med 128MB (ikke med alle språk/runtimes nødvendigvis). En lambda funksjon med høyeste minne-innstilling vil være 80 ganger dyrere å kjøre enn en med laveste minne-innstilling (10240MB/128MB=80). Imidlertid kan lite minne gå utover nettverksytelsen til lambda funksjonen, så her må man prøve seg litt frem og se hvor mye man trenger.

Men hvordan klarer man å starte 10 000 lambda funksjoner (nesten) samtidig? Det krever jo at man har en trigger-mekanisme som skalerer minst like bra. Det er her Step Functions kommer inn i bildet.

Step Functions Distributed Map

AWS Step Functions er en tjeneste fra AWS som lar deg bygge arbeidsflyter (workflows) for å koordinere ulike steg som må utføres i en bestemt rekkefølge, og den støtter både sekvensiell og parallell utførelse. Det er en slags tilstandsmaskin hvor hvert steg er en tilstand som styrer flyten videre til en annen tilstand basert på input den får fra forrige tilstand. Et steg i flyten kan være en helt annen arbeidsflyt. Man kan f.eks. ha en parent-flow med mange child flows. Det er flere typer tilstander i en flyt, som støttes, og en av disse er Map. Ved å konfigurere Map-tilstanden til å bruke distributed mode for høy parallellitet, og sette child-flow sin eksekveringstype til Express, kan man oppnå 10 000 samtidig kjørende child-flows. Hver instans av child-flow'en vil prosessere ett element fra input og vi kan konfigurere den til å kalle lambda med denne input'en.

I Step Functions Workflow Studio kan vi tegne opp flyter, og i dette tilfellet er flyten ganske enkel, visuelt sett. Normalt har en tilstandsmaskin langt flere tilstander og overganger, men i vårt tilfelle bruker vi den bare for å starte lambda:

Underliggende definisjon av en slik tilstandsmaskin kan se slik ut i Amazon State Language (ASL). Her er det en del mer detaljer enn hva man kan se ut i fra den visuelle representasjonen:

{
  "Comment": "Massive Parallel State Machine",
  "StartAt": "Map",
  "States": {
    "Map": {
      "Type": "Map",
      "ItemProcessor": {
        "ProcessorConfig": {
          "Mode": "DISTRIBUTED",
          "ExecutionType": "EXPRESS"
        },
        "StartAt": "ProcessItemFunction",
        "States": {
          "Process Item": {
            "Type": "Task",
            "Resource": "arn:aws:states:::lambda:invoke",
            "Output": "{% $states.result.Payload %}",
            "Arguments": {
              "FunctionName": "arn:aws:lambda:eu-north-1:999888777666:function:ProcessItemFunction:$LATEST",
              "Payload": "{% $states.input %}"
            },
            "Retry": [
              {
                "ErrorEquals": [
                  "Lambda.ServiceException",
                  "Lambda.AWSLambdaException",
                  "Lambda.SdkClientException",
                  "Lambda.TooManyRequestsException"
                ],
                "IntervalSeconds": 10,
                "MaxAttempts": 10,
                "BackoffRate": 1,
                "JitterStrategy": "FULL"
              }
            ],
            "End": true
          }
        }
      },
      "Label": "Map",
      "MaxConcurrency": 10000,
      "End": true,
      "Items": "{% $states.input.items %}"
    }
  },
  "QueryLanguage": "JSONata"
}

Vi kan se her at vi har konfigurert Map tilstanden med flere retries (MaxAttempts = 10). Den vil nemlig forsøke å kalle lambda funksjonen 10 000 ganger umiddelbart (MaxConcurrency = 10 000), men funksjonen vil bare kunne håndtere 1 000 forespørsler umiddelbart. Etter 10 sekunder vil funksjonen kunne ta i mot 1 000 nye forespørsler, men fortsatt vil 8 000 forespørsler måtte vente, osv. Etter 90 sekunder vil alle elementene bli plukket opp av lambda funksjonen.

Vi kan starte tilstandsmaskinen med følgende (forenklet) input

{
  "items": [
    {"id":"0000"},
    {"id":"0001"},
    {"id":"0002"},
    ...
    {"id":"9999"}
  ]
}    

Hvert element i listen vil blir prosessert av en separat child-flow/lambda instans. Input som sendes til lambda funksjonen vil inneholde en unik id. Dette kan være en referanse til dataene som ligger lagret et annet sted, og som lambda funksjonen må hente inn i minnet for prosessering.

Bygg, Deploy og Kjør

Med infrastruktur som kode, nærmere bestemt AWS SAM, kan man definere Step Functions og Lambda i en YAML-fil, flyten til tilstandsmaskinen i en ASL json-fil, samt koden til Lambda'en i en kildekodefil. Så kan man med SAM CLI bygge og deploy'e med to kommandoer

sam build
sam deploy

Dette vil opprette en CloudFormation stack i AWS med alle ressursene.

Til slutt kan man starte en kjøring av tilstandsmaskinen med kommandoen

aws stepfunctions start-execution \
  --state-machine-arn <STATE_MACHINE_ARN> \
  --input file://input.json

Begrensninger ⚠️

En AWS lambda funksjon kan kjøre i maksimalt 15 minutter. Men en express workflow i Step Functions kan bare kjøre i 5 minutter. Så her må man passe på å stykke opp arbeidet i små nok biter til at hver lambda instans kan bli ferdig med prosesseringen i løpet av 5 minutter.

Merk også at denne type parallellprosessering egner seg helst for problemer som er "embarrassingly parallel", hvor problemet kan deles opp i mange små delproblemer som hver for seg kan løses isolert, med minimalt eller ingen kommunikasjon mellom, i dette tilfellet, lambda funksjonene. Hver lambda instans kan skrive resultatet sitt til f.eks. en database, og så kan en dedikert lambda funksjon til slutt iterere over delresultatene og sette sammen et sluttresultat (som konseptet "map-reduce").

Kan man skrelle vekk de 90 sekundene? 🕐

I teorien, ja, med noe økt kompleksitet i konfigurasjon av Step Functions maskinen. Man kan ha 10 parallelle Map-tilstander som hver for seg bruker en unik lambda funksjon, og hver Map-tilstand settes opp med MaxConcurrency = 1 000. En unik funksjon A kan nemlig skalere opp til 1 000 samtidige instanser, mens en annen lambda funksjon B også kan skalere opp til 1 000, samtidig. Da kan man ha 10 lambda funksjoner som tilsammen umiddelbart kan skalere opp til 10 000.

Advarsel 💲💲

Ikke kjør 10 000 samtidige lambda instanser med 10GB minne "bare for å teste". Hvis du kjører disse i 5 minutter vil det koste deg over 5000 kroner. Med 128MB koster det "bare" i overkant av 60 kr. Disse tingene må selvsagt forsvares med at det gir forretningsmessig verdi. Dette må ikke tolkes som at "lambda er dyrt" i enhver sammenheng. Vi snakker her om en ekstrem bruk av lambda og da vil det selvsagt koste.

Forøvrig er kostnaden for bruk av Step Functions maskinen neglisjerbar her i forhold til lambda.

Med lambda kan man velge mellom arm64 og x86_64 CPU-arkitektur, førstnevnte er nyere og vil kunne gi en kostnadsbesparelse på rundt 20%, men da må man ikke være avhengig av x86-binaries.

Konklusjon

Bruk av lambda til massiv parallellitet gir rask tilgang til mye prosesseringskraft og det tar relativt kort tid å sette opp hvis man er kjent med tjenester som Lambda og Step functions. Det er imidlertid viktig å ha et forhold til kostnaden ved dette. Det kan bli veldig dyrt hvis man skal kjøre dette kontinuerlig, men bruksområdet som er tenkt her er situasjoner der man raskt trenger tilgang på mye prosesseringskraft i et kortere tidsrom. Poenget er at tjenester som Lambda og Step Functions gir mulighet for å operere i en massiv skala kjapt når man trenger det, og koster ingenting når man ikke bruker det.

Og julenissen? Han fikk beregnet ruten sin på imponerende kort tid og dro av gårde i god tro på å få levert alle pakkene i løpet av julaften. Og han trenger heller ikke å bekymre seg for å ha glemt å stenge ned et svindyrt HPC-cluster i skyen…

God jul!

Kilder

Lær mer om Lambda, Step Functions og AWS serverless på serverlessland.com

ForrigeNeste