Microservizi con Serverless Functions? E’ un’ottima idea purchè si possa cambiare

Questo articolo è la traduzione dell’originale in inglese pubblicato su InfoQ.

“Dove mettiamo questo (Micro)servizio? E’ meglio una Serverless Function oppure un Container? O magari un Virtual Machine (VM) dedicata?”. Quando si progetta una nuova soluzione “pronta per il cloud”, queste sono domande che gli architetti prima o poi si pongono.

Come spesso accade, per questo tipo di domande c’è solo una risposta valida: “dipende”. Non solo “dipende”, ma “può anche cambiare”. La soluzione ottimale presa in un certo momento può diventare fortemente sub-ottimale nell’arco di pochi mesi. Al lancio di un nuovo prodotto o servizio può essere conveniente optare per un modello basato su Serverless Functions dal momento che sono veloci da mettere in piedi e richiedono un basso investimento iniziale. In più, il modello puramente “pay per use” è molto interessante quando spesso non si hanno sicurezze sui carichi che una applicazione dovrà sostenere. Più tardi però, quando si iniziano a conoscere i carichi e si è in grado di prevederli, può essere economicamente conveniente, molto conveniente, spostarsi su modelli di esecuzione tradizionali basati su Container o server dedicati.

Quando progettiamo e costruiamo un nuovo sistema è pertanto importante mantenere la flessibilità di poter cambiare il modello di esecuzione dei vari (Micro)servizi al più basso costo possibile. Essere sulla giusta infrastruttura in ogni momento può comportare grandi risparmi sulle nostre fatture Cloud.

Serverless Functions

Le Serverless Functions (altrimenti conosciute come FaaS, Functions as a Service) sono unità logiche di elaborazione che vengono caricate ed eseguite in risposta ad eventi appositamente configurati, quali per esempio richieste Http in entrata oppure messaggi ricevuti in topic Kafka. Quando l’esecuzione si completa queste funzioni spariscono, almeno da un punto di vista logico, ed il loro costo scende a zero.

FaaS che risponde ad eventi Http: esecuzioni parallele multiple e modello“pay per use”

Tutti i più importanti Cloud provider hanno una offerta FaaS (AWS Lambda, Azure Functions, Google Functions, Oracle Cloud Functions) ed un modello FaaS può anche essere reso disponibile on premise con frameworks come Apache OpenWhisk. Ci sono limitazioni in termini di risorse (su AWS per esempio si possono avere al massimo 10GB di memoria e 15 minuti di tempo di elaborazione), ma in generale sono in grado di rispondere bene alla grande maggioranza di casi d’uso coperti dalle moderne applicazioni.

La granularità delle Serverless Functions può variare. Da una singola responsabilità molto focalizzata, per esempio il generare un SMS alla conferma di un ordine, ad un intero Microservizio che può girare fianco a fianco ad altri Microservizi eseguiti tramite Containers oppure su VM dedicate (Sam Newman spiega nel dettaglio la relazione fra FaaS e Microservizi in questa interessante presentazione).

Applicazione di Front End che si basa su Microservizi che utilizzano diversi modelli di esecuzione

Vantaggi del modello FaaS

Quando le Funzioni Serverless non lavorano non costano (modello “Pay per use”). Se una Funzione Serverless è invoca contemporaneamente da 100 client, 100 istanze di essa sono create quasi istantaneamente (almeno nella maggior parte dei casi). Ancora una volta, quando hanno finito di servire la loro richiesta si “spengono” automaticamente e non costano nulla. L’approvvigionamento e la gestione dell’infrastruttura, l’alta affidabilità (almeno fino ad un certo livello), la scalabilità (da 0 ai limiti definiti da chi usa FaaS) sono forniti come parte del servizio da team di specialisti che lavorano dietro le quinte.

Serverless Functions significano “Elasticità on steroids” e “Focus su ciò che è differenziante per il business”

Il modello FaaS pertanto offre due grandi vantaggi:

  • Elasticità on steroids: possibilità di scalare verso l’alto e verso il basso (fino a zero risorse) in maniera del tutto automatica e costi legati esclusivamente all’effettivo utilizzo
  • Focus su ciò che è differenziante per il business: poter concentrare tutte le forze sullo sviluppo delle applicazioni più critiche, senza dover disperdere energie prezioso in campi complessi come la gestione delle moderne infrastrutture che il modello FaaS offre come una commodity.

Elasticità e costi

Un “buon servizio” significa, fra le altre cose, un tempo di risposta buono in tutte le circostanze. In tutte le circostanze significa sia quando l’applicazione serve un carico normale sia quando deve servire un picco di richieste.

Un “nuovo servizio” spesso deve uscire sul mercato in fretta, con il minor investimento iniziale possibile, e deve essere un “buon servizio” sin dall’inizio.

Quando vogliamo lanciare un nuovo servizio, un modello FaaS è probabilmente la scelta migliore. Le Funzioni Serverless possono essere messe in piedi rapidamente e minimizzano il lavoro che si deve dedicare alle infrastrutture. Il modello “pay per use” significa nessun investimento iniziale. La capacità di scalare garantisce gli stessi tempi di risposta sotto condizioni di carico differenti.

Se, dopo qualche tempo, il carico si stabilizza oppure se ne capisce l’andamento e si è in grado di predirlo, allora la storia può cambiare, allora un modello più tradizionale basato su risorse dedicate, siano esse cluster Kubernetes o VMs, può diventare economicamente molto più conveniente che un modello FaaS.

Profili di carico differenti possono comportare variazioni molto significative nei costi Cloud quando si confronta un modello FaaS con soluzioni basate su risorse dedicate.

Ma quanto può essere la differenza in costi? Al solito dipende dallo specifico scenario, ma ciò che è certo è che la scelta sbagliata può avere impatti molto grossi sulle fatture del Cloud a fine mese.

Serverless Functions possono FAR RISPARMIARE MOLTO ma posso anche FAR SPENDERE MOLTO

Stimare i costi del Cloud è diventata ormai una una scienza di per sè. Possiamo tuttavia farci un’idea dei potenziali benefici di un modello sull’altro utilizzando il listino prezzi AWS (a Febbraio 2021).

L’applicazione usata come esempio. Consideriamo una applicazione che riceva 3.000.000 di richieste al mese. Ciascuna richiesta viene processato in 500 ms da una Lambda configurata con 4 MB di memoria (la CPU viene assegnata automaticamente in base alla memoria).

Il modello FaaS è “pay per use”. Quindi, indipendentemente dalla curva di carico, sia essa con picchi o piatta, il costo per mese è fisso: 100.60 USD.

Dall’altro lato, se consideriamo un modello basato su VMs dedicate le cose sono molto diverse ed i costi dipendono fortemente dalla curva di carico, ovviamente con l’assunzione che vogliamo fornire lo stesso tempo di risposta sotto tutte le condizioni di carico.

Scenario con picchi di carico. In presenza di curve di carico caratterizzate da picchi, per garantire il tempo di risposta sotto ogni condizione dobbiamo dimensionare l’infrastruttura alle condizioni di picco. Se al picco abbiamo 10 richieste al secondo (cosa possibile per una applicazione con 3.000.000 di richieste al mese, basta che queste siano concentrate in alcune ore del giorno oppure in alcuni giorni del mese), è possibile che si abbia bisogno di una VM (AWS EC2) con 8 CPU e 32MB di memoria per garantire gli stessi tempi di risposta della Lambda. In questo caso i costi mensili salirebbero a 197.22 USD (risparmi anche significativi possono essere ottenuti con impegni pluriennali, ma ciò riduce di molto la flessibilità finanziaria che il Cloud può garantire). I costi sono sostanzialmente raddoppiati. La differenza potrebbe cambiare con una gestione dinamica delle risorse, in questo caso la VM, ma ciò richiederebbe che l’andamento del carico sia prevedibile ed in ogni caso aumenterebbe la complessità della soluzione e quindi il suo costo.

Scenario con un carico piatto. Se il carico è sostanzialmente costante nel tempo, allora ci troviamo in una situazione differente. In questo caso per sostenere il carico può bastare una macchina molto più piccola. Probabilmente una VM con 2 CPU e 8 MB di memoria può essere sufficiente ed i costi mensili in questo caso scenderebbero a 31.73 USD, meno di un terzo del costo della Lambda.

Un business case realistico è molto più complesso e richiede una analisi dettagliata. Tuttavia, solo guardando a questi scenari semplificati appare chiaro che un modello FaaS può risultare molto conveniente in determinati scenari ma può diventare una insopportabile zavorra se le condizioni al contorno cambiano. Pertanto è importante poter cambiare il modello di deployment a seconda delle circostanze.

La domanda a questo punto diventa: ma come possiamo garantirci questa flessibilità? quanto viene a costare questa libertà d’azione?

Anatomia del codice di una applicazione moderna

Quando si utilizzano pratiche di sviluppo moderne, di solito la base di codice di una applicazione finisce per essere suddivisa in aree logiche

  • Logica applicativa. Il codice (di solito scritto in linguaggi come Java, Typescript o Go) che fa fare all’applicazione quello che deve fare.
  • DevSecOps (CI/CD). Di solito sono script e file di configurazione che automatizzano le fasi di Build, Test, Security Checks e Deployment dell’applicazione

Logica applicativa

E’ possibile suddividere ulteriormente la logica applicativa di un servizio di back end in parti con specifiche responsabilità

  • Business Logic. E’ la parte di codice che realizza il comportamento del servizio, espresso in forma di API logiche (funzioni o metodi) che tipicamente si aspettano alcuni dati in entrata, magari un Json, e ritornano dei dati come risultato. Questo codice non dipende in alcun modo dalla “meccanica” legata all’effettivo ambiente su cui viene eseguito, sia esso un Container, una Serverless Function oppure un Application Server. Quando questa logica è eseguita in un Container, può essere invocata da strati quali Spring Boot (se si usa Java), Express (se si usa Node) o Gorilla (se si usa Go). Quando viene eseguita in una Serverless Function, saranno i meccanismi FaaS specifici del Cloud Provider ad invocare le API logiche.
  • Codice dipendente dal Deployment. E’ la parte di codice che gestisce le meccaniche dell’ambiente di esecuzione specifico scelto in fase di deployment. Se ci sono più modelli di deployment, ci devono essere differenti implementazioni di questa parte, ma solo di questa parte. Nel caso di Container, questa è la parte dove si concentrano le dipendenze dai vari Spring Boot, Express o Gorilla. Nel caso di FaaS questa è la parte che gestisce le meccaniche specifiche del Cloud Provider (AWS Lambda, Azure functions e Google Cloud functions hanno le proprie librerie proprietarie utilizzate per invocare la Business Logic).
Separazione fra Business Logic (comune) e codice dipendente dal Deployment per dare flessibilità nel poter scegliere fra diversi modelli di Deployment

Per garantire flessibilità nella strategia di Deployment al minor costo possibile, è necessario mantenere queste due parti nettamente separate, il che significa:

  • è il “Codice dipendente dal Deployment” che importa i moduli della “Business Logic”
  • la “Business Logic” non importa alcun modulo/pacchetto/libreria che dipenda dallo specifico runtime

Seguendo queste due semplici regole, siamo in grado di massimizzare la quantità di codice condiviso per tutti i modelli di Deployment, in altre parole l’area della “Business Logic”, minimizzando pertanto il costo di spostamento da un modello all’atro.

E’ impossibile in astratto stimare il peso relativo di queste due parti. Analizzando, a titolo di esempio, una applicazione un semplice gioco interattivo che può essere eseguita sia come Serverless Function su AWS Lambda che come Container (nell’esempio il deployment viene fatto su Google Application Engine), si vede che il “Codice dipendente dal Deployment” pesa circa il 6% di tutta la base di codice (circa 7.200 righe), che significa che il 94% del codice è lo stesso indipendentemente che il servizio giri su Lambda o Container.

DevSecOps (CI/CD)

Questa è la parte del codice responsabile per automatizzare il build, test (inclusivo dei gateway di sicurezza) e deploy di una applicazione.

Le fasi di build e deploy, per loro natura, sono fortemente dipendenti dall’effettivo ambiente di esecuzione scelto per l’esecuzione del servizio. Se scegliamo i Container, allora la fase di build probabilmente utilizzerà tools Docker. Se optiamo per un modello FaaS, una fase di build staccata dal deployment di fatto non esiste, ma esistono comandi per caricare il codice sulla piattaforma FaaS scelta e configurare i punti di ingresso applicativi da richiamare quando una Serverless Function è invocata. Ugualmente vi possono essere check di sicurezza che sono specifici per uno specifico modello di esecuzione.

Ma c’è un’altro importante beneficio aggiuntivo che possiamo ottenere se manteniamo una chiara separazione fra “Business Logic” e “Codice dipendente dal Deployment”. Tale beneficio riguarda la fase, anzi le fasi, di test. Se queste due parti rimangono nettamente separate, allora i nostri Unit Test ma, cosa ancora più importante, i nostri Integration Test possono essere fatti eseguire in maniera indipendente dal modello di Deployment finale che andremo a scegliere. Ciò ci permette pertanto di scegliere, per queste fasi di test, la configurazione più semplice (di solito un Container) ed eseguire le varie suite di test con questa configurazione. I test quindi possono essere fatti girare anche sulla macchina dello sviluppatore, incrementando di molto la velocità con cui possono essere eseguiti.

C’è ancora necessità di eseguire passi di test dopo che l’effettivo Deployment è completato, ma il grosso del lavoro di test, ovvero il test della correttezza della “Business Logic”, può essere svolto in modo indipendente dal modello finale scelto, migliorando di molto la produttività degli sviluppatori.

Come mantenere la separazione fra “Business Logic” e “Codice dipendente dal Deployment”

Se vogliamo rimanere flessibili nella scelta del modello di Deployment da utilizzare, dobbiamo mantenere chiari i confini fra le parti di codice che dipendono dalla piattaforma di esecuzione e quelle che ne sono logicamente indipendenti. Dobbiamo disegnare, fin dall’inizio, le nostre applicazioni per minimizzare le prime e massimizzare le seconde e dobbiamo assicurarci che questa netta divisione sia mantenuta durante tutto il ciclo di vita dell’applicazione, magari tramite regolari sessioni di review del disegno e del codice.

Split of concerns among parts of a modern application code base

Conclusioni

Le Serverless Functions rappresentano una opzione molto interessante per le moderne architetture. Esse offrono il miglior time-to-market, il costo di ingresso più basso, la miglior elasticità. Per questi motivi sono l’opzione migliore quando si deve portare in fretta un prodotto sul mercato oppure quando si devono affrontare curve di carico estremamente variabili.

Ma non tutte le situazioni sono simili. Le cose inoltre possono cambiare nel corso del tempo. I carichi possono essere stabili oppure diventare prevedibili. In questi casi un modello FaaS può risultare molto più costoso di un modello tradizionale basato su risorse dedicate.

Pertanto è importante avere la possibilità di cambiare il modello di Deployment di una applicazione al più basso costo possibile. Ciò può essere ottenuto mantenendo una chiara separazione fra ciò che è logica di business, per natura indipendente dalla piattaforma su cui è eseguita, e ciò che dipende dall’effettivo modello di Deployment scelto.

Seguire questa semplice linea guida architetturale ed assicurarsi, tramite costanti peer-review, che sia rispettata sono la chiave per mantenere questa flessibilità.

A man with passion for code and for some strange things that sometimes happen in IT organizations. Views and thoughts here are my own.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store