Jul i den gamle serverless: Emneord med sky

Illustration: Bigstock/Mikeledray
Vi får omsat omfangsrig kunstig intelligens-kode til tre små klasser, der kan køre serverless - men den var ikke gået uden hjælp fra en venlig sjæl.

Freden begynder så småt at indfinde sig på Version2’s kode-redaktion. Det lille mødelokale på fjerde sal er fyldt til loftet med gaver, og it-supporten går rundt med nissehuer på hovedet.

Men som i alle gode historier skulle der overvindes nogle forhindringer på vejen.

I sidste kapitel af den tilsyneladende uendelige historie om Version2’s emneords-gætteri med kunstig intelligens, gik vi serverless, for at slippe for server-vedligeholdelse, bash-scripts og eksistentiel angst for sikkerhedshuller.

Læs også: V2’s emneords-spåkone går serverless

Vi fik et ‘hallo verden’-program op at køre i Amazons sky, efter at have installeret en hulens bunke kommando-linje-programmer. Denne gang skal vi have vores kunstige intelligens syet ind i de rammer, som serverless - kaldet Lambda - tilbyder.

Som vi så sidst, har vi allerede fået en masse fordele. Som nævnt dengang skal jeg ikke længere vedligeholde en Linux-server, og http-forbindelsen klarer serverless-miljøet for mig, og det samme gælder certifikater til SSL.

I sin tid skrev jeg en klasse i Java, Tester, som indlæser de afledte ordhyppigheder, eller estimater, der benyttes til at beregne, om et emneord skal accepteres eller forkastes, på baggrund af ordene i en spritny artikel på Version2.

Læs også: V2 amok i skygge-it: Bookmarklet-SSL-server på netværket med hjemmelavet certifikat

Jeg er ganske grøn i forhold til Amazons serverless, så jeg gør det, der virker nemmest: Ser, om jeg ikke kan fuske lidt med ‘hallo verden’-appen, så den producerer emneords-bud.

Det gør jeg med venlig hjælp fra cloud-konsulent Henrik Oddershede, der fik sat mig i sving med serverless i den sidste artikel. Henrik har faktisk lavet et eksempel, der virker, men jeg skal jo finde ud af det på egen hånd, ellers er det lidt snyd i forhold til fortællingen - det synes jeg da, i det mindste.

Apps uden filsystem

Jeg bruger dog noget af Henriks kode, det må jeg tilstå. Det drejer sig netop om tabellerne med estimater, der fylder 126 megabyte. På den gamle server indlæste jeg dem som en fil, på helt almindelig vis. Men her i serverless er der ikke noget filsystem, så vi skal gøre noget andet.

Amazons serverless-api kan læse objekter fra storage-systemet S3, så jeg uploader estimaterne til min ‘bucket’, som det hedder, med et klik på en knap i Amazons web-konsol. Det kan næppe blive nemmere.

Estimat-filen kan nemt indlæses i serverless-app’en på følgende vis - og det er altså Henrik, der har skrevet koden - jeg vil ikke pynte mig med lånte fjer. Jeg har dog ændret lidt i koden til mit formål.

public static DiskTraener.Estimater getEstimater() 
    throws IOException, ClassNotFoundException {
  System.out.println("Læser estimat-fil fra s3");
  AmazonS3 s3 = AmazonS3ClientBuilder.standard().build();
  S3Object objectInputStream = s3.getObject(
    "taniasnyebucket", "estimater.ser");
  ObjectInputStream in = new ObjectInputStream(
    new BufferedInputStream(objectInputStream.getObjectContent()));
  DiskTraener.Estimater estimater = 
    (DiskTraener.Estimater) in.readObject();
  in.close();
  System.out.println("Læste estimat-fil fra s3");
  return estimater;
}

Det foregår i Tester-klassen, der finder frem til de rigtige emneord - eller laver ‘inferens’, som det fint hedder på kunstig intelligens-sprog.

Det vil ikke

Men det virker dæleme ikke. Den vil ikke compile.

December-vejret er tungt og gråt her på Kalvebod Brygge i København, og jeg føler mig ikke så intelligent denne dag. Jeg ringer til min livlinje.

»Hej Henrik! Den vil ikke compile!«

»Har du tjekket, at det er den rigtige package,« lyder det venlige svar fra Henrik.

Øh nej, det har jeg vist ikke, der står package helloworld i toppen af App.java-kildefilen, og ikke package v2ml.

Total pinlig begynderfejl. Det er også det dumme vejrs skyld. Sådan ser jeg på det.

Jeg ringer af og får rettet min copy-paste-fejl.

Efter at have rettet et par andre småfejl, compiler min lille app med den gamle Tester-kode. Amazons serverless-miljø sætter unittest op som standard, når ‘hello world’-skelettet genereres. Testen udføres, når der buildes med kommandoen sam build. Det er smart, for jeg kan teste, at det virker, med assert-sætningen

assertTrue(content.contains("sundheds-it"));

Læs også: Sådan skriver du unit tests

Henrik har hardcodet en test-streng som input i sin kode, som ser sådan ud: sundhed ambulance hospital sikkerhed og den skal blandt andre returnere emneordet 'sundheds-it', som er det jeg tester i assert-kaldet ovenfor.

Unittesten passerer - det virker! Jeg laver en triumferende springdans bag mit skrivebord.

Lokal succes

Men det er altså kun succes på min lokale maskine. Skyen venter lidt endnu.

Jeg har et lille problem. Min kode benytter faciliteter fra Java 9, og Amazons serverless-miljø kører Java 8. Henrik har i sit eksempel løst det ved - hold nu fast - at dekompilere en class-fil, fra det tidligere Version2-eksempel, hvor kildekoden ikke var med, og rettet min kode til Java 8. Temmelig imponerende.

Nu skete der det, i disse dage, som der står i evangeliet, at Amazon tilføjede Java 11 til sit serverless-miljø. Det løser alle problemer.

Jeg prøver at oprette et nyt Java 11-projekt med

sam init --runtime java11 -n v2mlv2
Usage: sam init [OPTIONS]
Try "sam init -h" for help.

der svarer:
Error: Invalid value for "-r" / "--runtime": invalid choice: java11.

– så den gik ikke.

Jeg må spørge sagkundskaben igen. Denne gang sender jeg en mail - jeg synes ikke jeg kan forstyrre Henrik hele tiden med mine dumme spørgsmål.

Han forklarer, at jeg skal opdatere SAM-kommandoen:

brew tap aws/tap
brew upgrade aws-sam-cli

Jeg tester versionen:

sam --version
SAM CLI, version 0.37.0

Og det er den nyeste. »Men du behøver ikke at starte et nyt projekt for at skifte til java11. Du kan nøjes med at redigere din eksisterende template.yaml,« skriver Henrik i sin mail.

Template.yaml er en konfigurationsfil. Den ligger i roden af projektmappen. Inde i den finder jeg punktet Runtime, der er sat til java8. Det ændrer jeg til java11:

Resources:
  HelloWorldFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: HelloWorldFunction
      Handler: v2ml.App::handleRequest
      Runtime: java11

Henrik fortæller mig også, hvor jeg kan finde logfilen for funktionen. Det gøres ved at logge ind på AWS-konsollen. Under punktet ‘Find Services’ skriver jeg ‘lambda’ og trykker på det øverste resultat, der fører mig til en liste over mine funktioner.

Jeg har kun den ene, og den klikker jeg på. Så klikker jeg på fanen ‘Monitoring’ og derefter på knappen ‘View logs in CloudWatch’. Nu kommer logfilen frem, hvor jeg for eksempel kan se exceptions, hvis funktionen crasher. Hvis jeg skriver med System.out.println, er det også her, meddelelserne bliver printet.

Emneord med sky

Nu builder jeg projektet igen, og pakker og udruller. Den kompilerer fint, men crasher lige på stedet, når jeg kalder funktionen via browseren. Jeg ved godt, hvad det skyldes. Det er det der med hukommelsen, som drillede så meget, da jeg kørte programmet på sky-instansen, hvor der kun var 1 gigabyte til rådighed.

Læs også: Version2 bliver Devops-hajer i skygge-skyen (sådan da)

I Amazons serverless kan man allokere op til 3 gigabyte, uanset om det er gratis eller betalt. Lige nu står hukommelsen på 512 MB i template.yaml. Det ændrer jeg til 2048:

Resources:
  HelloWorldFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: HelloWorldFunction
      Handler: v2ml.App::handleRequest
      Runtime: java11
      MemorySize: 2048

Endnu en gang build, package og deploy, og så ind i browseren.

Illustration: Version2

Så kører funktionen - herligt, herligt. Men der er endnu et punkt på dagsorden. For nemheds skyld har Henrik hardcodet den test-streng, vi så tidligere, som inddata, og som giver de ovenstående emneord som resultat. Det skal lige rigges til, så inddata kommer fra en post-request, fra den webudvidelse, der syer gættemaskinen sammen med vores cms.

Læs også: Værktøjskasse: Sådan koder du en browser-udvidelse i en ruf

Når jeg sender en forespørgsel via post, med curl-kommandoen i bash:

curl -s -d "java java java java" -X  POST https://x8d598jn85.execute-api.us-east-1.amazonaws.com/Prod/hello/

– får jeg en ‘autentifikationsfejl.’ Det skyldes at min funktion er sat op til at bruge GET i HTTP-kaldet, så det ændrer jeg til POST i template.yaml:

Resources:
  HelloWorldFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: HelloWorldFunction
      Handler: v2ml.App::handleRequest
      Runtime: java11
      MemorySize: 2048
      Environment:
        Variables:
          PARAM1: VALUE
      Events:
        HelloWorld:
          Type: Api
          Properties:
            Path: /hello
            Method: post

Men det driller mig i en overraskende grad. Amazons api er nemlig ikke så godt forklaret, og det tager lidt tid at finde ud af, at når jeg sender et POST-kald, får jeg en Map som input i min funktion, og det skal vist forestille en slags interimistisk JSON-repræsentation. Her kan jeg pille kaldet ud på denne her lidt knudrede facon:

String body = (String) ((Map) input).get("body");

Det giver ikke nogle skønhedspoint, men lige nu skal jeg bare have det op og køre.

Varm ping til kold start

Det virker fint, når jeg kører min curl-kommando fra før. Men første kald tager en håndfuld sekunder. Det er det såkaldte cold start-problem, som skyldes, at runtimen lige skal komme i omdrejninger.

Der er et lille problem (nu igen,) og det består i, at hvis der går 15-20 minutter mellem kald til funktionen, pilles den ud af hukommelsen i Amazons serverless-system. Så mange kald får funktionen heller ikke i løbet af en dag, så jeg kan altså ende i en situation, hvor hvert kald tager måske 5 sekunder. Den går inte, Granberg.

Løsningen er ganske simpelt at ‘pinge’ - kalde funktionen med jævne mellemrum. Først bruger jeg bare en kommando i bash, der kører på min egen pc, men…

Helt ærligt, jeg har lavet ninja-hacks nok, siden jeg startede på det her skygge-it-projekt i… marts sidste år?

Læs også: Java-algoritme gætter Version2's emneord med en træfsikkerhed på 98 pct.

Hold da op, hvor tiden løber.

Nu må jeg dæleme bruge mere robuste løsninger. Slut med gaffa-tape, elastikker og Egon Olsen-planer.

Amazons Cloudwatch-tjeneste, der også viste Lambda-funktionens logfil tidligere, kan lige det jeg vil - pinge funktionen hver 5. minut. Jeg går ind i webkonsollen, finder Cloudwatch, og det tager ikke lang tid at sætte et ping op. Cloudwatch er også gratis at anvende, på mit niveau - indtil de gratis glæder udløber til april næste år. Den tid, den sorg.

Sidste punkt på dagsordenen er at tilrette url’en i vores webudvidelse, så den peger på vores Lambda-funktion. Jeg genindlæser webudvidelsen og smutter ind i en Version2-artikel, klikker på emneordsknappen, og så kommer de gule forslag frem i dialogen, som med sky-serveren.

Så nåede vi til vejs ende - julen er reddet på kode-redaktionen. 🎅

Hva' koster'et?

Men hvad koster det? Amazons gratis tilbud i serverless hedder maksimum 1 million kald om måneden og 400.000 gigabyte-sekunder. De 1 million kald er ikke noget problem - det er 32000 om dagen, og så mange artikler skriver vi heller ikke, heller ikke selvom vi pinger funktionen hver 5. minut. Men hvad er et gigabyte-sekund?

Det er mængden af RAM, vi har allokeret, ganget med den tid, et kald tager. Ifølge loggen, som vi kiggede på tidligere, tager et typisk kald efter opvarmning af runtimen 28 millisekunder. Vi allokerede 2 gigabyte i vores template.yaml-konfigurationsfil, så det giver 2 x 0,028, eller 0,056 gigabytesekunder. Og vi har altså 400.000 gratis stykker, hver måned, så længe det varer.

Når det gratis tilbud udløber, er prisen 0,20 USD pr. 1 million forespørgsler og 0,000016667 USD for hvert gigabyte-sekund. Med 2048 megabyte RAM giver det 0,000003333 USD pr. 100 millisekunder, og vores tager som sagt omkring 28 millisekunder at udføre. Så det er langt billigere, end da vi brugte en server-instans.

Læs også: Cloud-konsulent: Pas på håndjern i serverless

Efter megen bøvl endte fortællingen godt til sidst, som lovet - og mange tak til Henrik Oddershede, jeg havde ikke fundet ud af det hele uden hans hjælp.

Kode-redaktionen ønsker alle læsere en god jul og så videre. I det nye år er ambitionen at kigge på deep learning og neurale netværk til emneordene. Vi ses på den anden side.

Tips og korrekturforslag til denne historie sendes til tip@version2.dk
Følg forløbet
Kommentarer (2)
sortSortér kommentarer
  • Ældste først
  • Nyeste først
  • Bedste først
#1 Frithiof Andreas Jensen

Amazons api er nemlig ikke så godt forklaret,

Helt enig!

Hvis man har en lambda-service, som kan ses af internettet, er det nok en god ide at sätte enten en specifik 'user' eller 'role' (hvad er mon bedst!?) som 'ejer' servicen og konfigurere rettighederne for denne temmeligt minimalistisk.

Jeg har bare ikke fundet et ordentligt eksempel på det, hverken på AWS eller på nettet!

PS -

AWS 'Hello World' eksemplerne fra AWS, samt 'nettet' körer med credentials for den ‘Identity and Access Management’-bruger som AWS-CLI'et avender og det er nok ikke heelt ideelt ude i den vilde verden.

Måske burde man også have noget 'rate-limiting' som blokerer servicen hvis den kaldes mere end det forventede. Det ville väre sörgeligt hvis julehandlen begränses af NMAP-scans som har dränet kreditkortet!

Så vidt jeg kan läse på AWS så kan man naturligvis göre alt dette, men det er så langt jeg er kommet med det :)

  • 2
  • 0
#2 Sune Marcher

Nu må jeg dæleme bruge mere robuste løsninger. Slut med gaffa-tape, elastikker og Egon Olsen-planer.

At "pinge" din lambda er nu altså stadig en gaffertape løsning :-)

Tag et kig på Quarkus – det er et projekt der gør det nemt at precompile Java-programmer(*) med Graal... du får ekstremt meget hurtigere opstartstid, lavere memory-forbrug, og hurtigere eksekvering (teoretisk set er JIT-kompilering mere effektiv, men lambda-funktioner når typisk ikke at køre lang nok tid til at få effektiv hotspot-analyse, så precompiled er en kæmpe fordel her).

Projektet er efterhånden ved at være produktionsklart, det er nemt at gå til, og mens du udvikler er det også ret behageligt, da det har hot reload.

(*): ikke kun Java, men JVM generelt. Personligt skal der ret gode argumenter til for jeg ville vælge Java over Kotlin...

  • 0
  • 0
Log ind eller Opret konto for at kommentere