Cloud PaaS: l’architettura di Google App Engine

Google App Engine

Google App Engine è la soluzione Platform as a Service (PaaS) di Google per lo sviluppo e l’hosting di applicazioni web nei propri server lanciata nell’aprile del 2008. Le applicazioni sono concepite per servire una moltitudine di utenti simultaneamente, senza incorrere in un decadimento complessivo delle prestazioni. Per garantire tale proprietà viene assicurata elevata scalabilità, allocando dinamicamente le risorse sulla base del numero di richieste in un determinato istante. In definitiva si può affermare che le potenzialità di Google App Engine rispetto alle offerte di altri provider PaaS si identificano nello sviluppo e hosting di applicazioni web altamente scalabili.

Il servizio non prevede investimenti iniziali o abbonamenti a lungo termine, ma implementa il principio pay-per-use attraverso il pagamento delle sole risorse infrastrutturali richieste, come CPU, Storage e banda in ingresso/uscita.

Le applicazioni destinate all’infrastruttura scalabile di Google vengono sviluppate in locale attraverso i software development kit (SDK) relativi ai diversi linguaggi supportati (Go, Java, Python). Il SDK è corredato di tool a linea di comando per consentire un utilizzo indipendente, oppure è possibile integrare il kit con l’ambiente di sviluppo di interesse. In particolare, per gli sviluppatori Java esiste un plugin per l’IDE Eclipse che include tutto il necessario, superando la necessità di utilizzare il SDK separatamente. Ogni SDK (o plugin) include un web server per lo sviluppo che consente l’esecuzione in locale delle applicazioni web ed emula di conseguenza tutti gli aspetti e servizi che concernono il futuro ambiente reale in cui tali applicativi dovranno risiedere, quali il runtime environment, il Datastore, l’URL fetch service e il Mail service. Il server locale può essere mantenuto in esecuzione durante lo sviluppo, in quanto monitora attivamente i file sorgenti aggiornandosi ad ogni caricamento della pagina. Il Datastore emulato crea automaticamente gli indici per le query utilizzate dall’applicativo. Tali indici vengono utilizzati in seguito anche da App Engine quando il programma viene trasferito nell’infrastruttura di Google.

Architettura di Google App Engine

App Engine architettura

Le applicazioni sviluppate con AppEngine sono destinate ad esaudire le richieste provenienti dagli utilizzatori tramite web. Nello specifico, un client contatta l’applicazione effettuando una richiesta HTTP, solitamente attraverso un browser. Un load balancer convoglia la richiesta ad un particolare server di confine (frontend), il quale identifica la risorsa di interesse in base all’ID, al dominio ed a un eventuale sottodominio indicati nell’indirizzo URL, nella forma <subDomain>.<application-ID>.<domain>. Ad ogni applicazione è assegnato gratuitamente un dominio del tipo appspot.com eventualmente modificabile.

In seguito viene analizzata la risorsa di interesse per determinare le attività operative seguenti. Se l’URL identifica un file statico, come un’immagine, la richiesta viene esaudita direttamente. I file statici identificano infatti le risorse che non coinvolgono codice applicativo e pertanto vengono trattati da Google App Engine utilizzando dei server appositamente dedicati, ottimizzati per la gestione di tale tipologia di risorsa. L’URL può identificare un request handler, ossia una porzione di codice relativa ad un’applicazione. In tal caso la richiesta è trasmessa ai server applicativi, in cui viene stabilito su quale particolare server fisico o virtuale deve essere creata un’istanza dell’applicazione in funzione dei tempi stimati di gestione. In particolare viene invocata un’istanza dell’applicazione utilizzando il contenuto della richiesta HTTP e viene atteso il completamento, restituendo i dati relativi al risultato al client. Si consideri tuttavia che esiste l’eventualità che una istanza adeguata alla gestione della richiesta corrente sia già attiva; in tal caso la richiesta viene accodata. Il server si occupa inoltre di gestire le risorse per l’applicativo (CPU, memoria) e controlla che il consumo non ecceda le specifiche. Se l’URL non identifica nessuna risorsa nota viene notificato al client l’errore HTTP 404.

I frontend server possono essere configurati per gestire l’autenticazione e l’autorizzazione dei client. In particolare possono essere implementate delle restrizioni sugli indirizzi URL in funzione della categoria di utente (utente generico, amministratore) o del dominio di appartenenza. Inoltre gestiscono le risposte per l’utente, effettuando eventualmente delle compressioni dei dati se richiesto. I programmi che risiedono nei server applicativi si avvalgono sia di diversi componenti essenziali, come il Datastore e la Memcache per la funzionalità di Storage, che di servizi ausiliari come URL Fetch Service o Mail Service. Tali aspetti verranno approfonditi nelle relative sezioni. I diversi tipi di server descritti sono governati complessivamente da un’entità che può essere denotata come “app master”. La sua funzione principale è l’aggiornamento delle applicazioni in concomitanza con gli incrementi di versione stabiliti dallo sviluppatore. Tutte le richieste che precedono la definizione di una nuova versione standard vengono tuttavia esaudite in base alle specifiche della richiesta, in quanto l’aggiornamento si verifica gradualmente e per domini di fallimento, per non inficiare la validità dell’applicazione.

Nella prospettiva del sistema le diverse richieste risultano identiche e senza alcuna ingerenza, pertanto non esiste una nozione esplicita di stato per l’ambiente di esecuzione. Le applicazioni di conseguenza devono essere architettate per conformarsi a tale principio secondo un design di tipo loose-coupling dei componenti e utilizzando opportuni meccanismi per il mantenimento di una nozione di stato (memcache, datastore). La scalabilità rappresenta infatti un aspetto critico per le applicazioni cloud in generale e per Google App Engine in particolare, il quale consente di distribuire il carico complessivo relativo all’applicazione su diversi server scalabili dinamicamente, senza avere la certezza che la medesima richiesta da parte dello stesso utente in un tempo anche molto ristretto sia gestita dallo stesso nodo. Per consentire la distribuzione delle richieste ed evitare nel contempo interferenze tra diverse applicazioni eventualmente residenti sullo stesso server fisico, ogni programma risiede all’interno di un ambiente di esecuzione “sandbox” isolato che si inserisce tra il sistema operativo e le applicazioni stesse. L’ambiente limita il dominio delle funzionalità consentite proibendo alcune operazioni, tra le quali le piu significative risultano essere:

Scrittura nel filesystem. Le applicazioni devono utilizzare App Engine Datastore per memorizzare dati persistenti. La lettura è invece consentita in quanto non costituisce un’operazione pericolosa.

Apertura di socket o accesso diretto ad altri host. Le applicazioni utilizzano le API di App Engine fetch service per comunicare con gli altri host attraverso richieste HTTP o HTTPS rispettivamente nelle porte 80 e 443.

Creazione di un processo o thread. Le richieste devono essere gestite in pochi secondi e attraverso un unico processo. In caso contrario si verifica la terminazione forzata per sovraccarico dei server, in quanto potrebbero verificarsi fenomeni indesiderati come quelli causati da una fork bomb. Inoltre si consideri che le applicazioni sviluppate secondo un approccio multi-threading potrebbero risultare difficilmente scalabili generando problemi di inconsistenza.

– Effettuazione di chiamate di sistema non previste.

Google App Engine consente la scelta di diversi ambienti di esecuzione in relazione al linguaggio e alle tecnologie che si intendono adottare per lo sviluppo dell’applicazione. Nello specifico, è possibile utilizzare gli ambienti Java, Python e Go. Sviluppando applicazioni in Java ad esempio, l’ambiente è costituito dal Java Runtime Environment (JRE) e dalle relative librerie, mentre le restrizioni vengono implementate direttamente nella Java Virtual Machine (JVM). Un programma può utilizzare funzionalità di libreria soltanto se queste non violano i vincoli imposti. In realtà lo sviluppo è consentito con tutti i linguaggi compatibili con la JVM, quali PHP, Ruby(JRuby) e JavaScript. In aggiunta il runtime environment limita la quantità di risorse utilizzabili da una singola richiesta, quali CPU e memoria. L’utilizzo di un server per ogni singola richiesta è una soluzione altamente scalabile ma nel contempo penalizzante in termini di tempo, in quanto è necessario prevedere l’esecuzione di un numero considerevole di istanze.

Per mitigare tale problematica AppEngine prevede il mantenimento dell’applicazione nella memoria del server il più a lungo possibile in funzione dell’attività in un determinato istante. Quando è necessario liberare risorse per gestire nuove richieste viene eliminata dalla memoria l’istanza meno recente. L’ambiente di esecuzione è in ogni caso caricato preventivamente, pertanto richieste relative ad applicazioni che non sono già in memoria hanno come conseguenza la sola creazione di una nuova istanza. Google App Engine prevede comunque il supporto per lo sviluppo di applicazioni multi-tenant, attraverso l’utilizzo di opportune API chiamate namespaces API. La tecnologia consente di utilizzare la medesima istanza dell’applicazione e la stessa base di dati per gestire le richieste di più utenti o organizzazioni. E’ sufficiente impostare diversi namespace per l’applicazione e assegnare il medesimo namespace ai tenant per i quali si vuole abilitare la condivisione.

Datastore

Le applicazioni possono avere la necessità di memorizzare dati durante l’esaudimento delle richieste per diversi motivi, ad esempio per rendere le informazioni disponibili per le esigenze future. L’utilizzo di un singolo database consente di mantenere una rappresentazione canonica dei dati e offre la garanzia di fornire sempre informazioni aggiornate ai suoi utilizzatori. Tuttavia non incontra le necessità di un sistema altamente scalabile, in quanto è limitato dal numero massimo di accessi simultanei.

Google App Engine si appoggia al repository distribuito di Google noto come BigTable per la memorizzazione dei dati, utilizzando un modello di rappresentazione molto simile a quello relativo ai database ad oggetti, differente sotto diversi aspetti dal modello di database relazionale largamente utilizzato. BigTable è concepito per ottimizzare la scalabilità orizzontale, ed è infatti utilizzato da Google per elargire le proprie applicazioni e servizi anche al di fuori del contesto di Google App Engine. Utilizzando un classico database relazionale al contrario sarebbe necessaria una qualche forma di condivisione di memoria (caching) per gestire richieste dirette a diversi server. BigTable è utilizzato, nel contesto di Google App Engine, attraverso delle opportune API che complessivamente assumono la denominazione di Datastore.

Google Big Table

Utilizzando il datastore le informazioni vengono memorizzate attraverso l’astrazione di entità. Ogni entità appartiene ad un certo tipo che ne definisce la categoria e possiede un set di proprietà caratterizzate da un nome e da un valore appartenente ad uno dei tipi di dato primitivi (es. int, oat). In prima istanza potrebbe risultare ragionevole una similitudine con il modello relazionale, associando le entità alle righe delle tabelle e le proprietà alle colonne. Tuttavia esistono alcuni importanti elementi distintivi del modello Datastore:

– Entità dello stesso tipo non hanno necessariamente le stesse proprietà.

– Le entità possono avere una molteplicità di valori per una singola proprietà.

– Possono esistere proprietà con lo stesso nome ma con diverso tipo di valori.

– Ogni entità possiede una chiave univoca separata dal modello dei dati (non è una proprietà).

Le entità sono definite schemaless, in quanto la loro struttura non è definita dal modello, ma viene stabilita dall’applicazione responsabile della manipolazione dei dati nell’instante in cui esse vengono create. Ad esempio le entità possono avere un numero arbitrario di proprietà e di diverso tipo. Il Datastore può essere interrogato per ottenere un insieme di entità del medesimo tipo, ordinate o filtrate sulla base dei valori delle relative proprietà. Le query possono anche interessare unicamente le chiavi, ad esempio per effettuare rapide ricerche. Operativamente le interrogazioni sono drasticamente differenti rispetto a quanto avviene per il modello relazionale. Il Datastore mantiene una tabella chiamata indice per ogni tipologia di query che deve essere eseguita. L’indice definisce una prima forma di discriminazione in quanto contiene i risultati potenziali, individuati sulla base del tipo di query e delle proprietà e operatori relativi a filtri e ordinamento.

Diversamente dall’approccio real-time del modello relazionale, App Engine deve conoscere a priori gli indici relativi alle query che saranno trattate dall’applicazione. Esiste in tal senso un file di configurazione chiamato datastore-indexes.xml. Quando la query viene eseguita attribuendo i valori reali, viene individuato in prima istanza l’indice corrispondente. In seguito la tabella viene scansionata restituendo le entità che verificano le condizioni specificate. Gli indici vengono solitamente creati automaticamente analizzando le query dell’applicazione, ma è anche possibile una configurazione di tipo manuale. Ogni modifica del Datastore, come l’inserimento di nuove entità o la modifica di quelle esistenti, provoca un aggiornamento istantaneo degli indici. L’approccio descritto consente di eseguire le query molto rapidamente, in quanto deve essere scandita una sola semplice tabella. Tuttavia comporta un decadimento delle prestazioni per un eventuale aggiornamento del Datastore, in quanto devono essere istantaneamente aggiornati tutti gli indici interessati.

In linea generale il Datastore supporta unicamente le query le cui prestazioni scalano in funzione della dimensione del result-set. In termini pratici significa che le performance di una query che restituisce in una certa condizione cento entità sono le medesime di un risultato di mille o un milione di entità. Una ulteriore limitazione per la tipologia di query realizzabili è costituita dall’utilizzo degli indici descritti. Alcune operazioni fondamentali del modello relazionale come la join sono proibite in quanto difficilmente concretizzabili in un contesto distribuito, poiché sarebbe necessario reperire informazioni da diversi nodi per ogni singola interrogazione. I dati devono essere mantenuti consistenti anche a fronte di accessi simultanei da parte di diversi utenti. Google App Engine possiede gli strumenti per assicurare che gli aggiornamenti relativi alle entita del Datastore avvengano nella forma di transazioni, ossia le diverse azioni che costituiscono la modifica vengono interamente completate senza errori come un’unica operazione atomica. In caso contrario il Datastore viene riportato nel suo stato originale precedente all’aggiornamento. Inoltre viene assicurato il livello di isolamento piu elevato di tipo Serializable, nel quale tutte le transazioni sono eseguite in serie, una dopo l’altra e senza sovrapposizioni. Una applicazione può aggiornare entità multiple con una singola transazione atomica, ma deve specificare l’identità delle singole entità interessate nell’istante della loro creazione, attraverso la nozione di gruppi di entità (entity groups).

Se l’applicazione utilizza le Datastore API per un aggiornamento, il controllo non viene restituito fino a quando la transazione non viene completata e sono stati aggiornati gli indici in caso di successo. Se si verifica concorrenza sulle medesima entità, il tentativo di transazione viene ripetuto diverse volte prima di dichiarare una condizione di errore. In particolare viene utilizzata la metodologia optimistic concurrency control, la quale prevede che le transazioni non blocchino le risorse che interessano l’aggiornamento, veri cando pero che i dati non siano stati modificati da altre transazioni prima di effettuare il commit. La concorrenza non interessa le operazioni di lettura, le quali riferiscono sempre lo stato stabile piu recente.

Google App Engine prevede due tipologie fondamentali di Datastore:

High Replication Datastore (HRD). Rappresenta l’opzione di default per le applicazioni. Consente la replicazione automatica dei dati su diversi datacenter localizzati in differenti domini di fallimento per garantire elevata affidabilità dei dati e di conseguenza elevata disponibilità delle applicazioni che ne fanno uso. Le operazioni di lettura e scrittura risultano valide anche durante i periodi di downtime pianificati.

Master/Slave Datastore. Consente la replicazione asincrona dei dati su diversi data center, ma è possibile che i dati utente siano temporaneamente non disponibili in quanto in un determinato istante solamente un datastore master possiede l’autorità per la scrittura. A livello di programmazione e in riferimento al linguaggio Java il Datastore è gestito attraverso due modalità fondamentali:

Utilizzando le API low-level proprietarie di Google App Engine. Si vuole evidenziare come questo aspetto rappresenti una prima forma di vincolo tecnologico, in quanto limita l’esistenza delle applicazioni sviluppate alla piattaforma GAE e quindi la relativa portabilità.

Utilizzando i framework Java Persistence API (JPA) o Java Data Objects(JDO). Tali framework definiscono delle interfacce standard per l’interazione con i database a prescindere dal tipo specifico, pertanto rappresentano una valida alternativa per garantire maggiore portabilità alle applicazioni sviluppate. Un primo esempio di servizio è costituito dalle funzionalità offerte dal Datastore. L’applicazione utilizza delle opportune API che consentono l’accesso al sistema di memorizzazione dati, il quale è totalmente disaccoppiato dall’ambiente di esecuzione. Tale caratteristica rappresenta inoltre una prima forma di disaccoppiamento dei componenti.

Altre tipologie di servizi previsti da Google App Engine

Memory cache o memcache. Il servizio rende disponibile una memoria cache distribuita e condivisa dalle istanze per la memorizzazione o il recupero dei dati con prestazioni molto piu elevate rispetto al Datastore, attraverso uno schema di tipo chiave-valore. I dati risiedono in memoria principale, pertanto un fallimento dei server provoca la cancellazione della relativa cache. L’utilizzo tradizionale prevede che le applicazioni verifichino la possibilità di soddisfare le proprie query utilizzando i dati presenti all’interno della memcache per ricevere una risposta immediata. In caso contrario viene interrogato il datastore.

URL Fetch Service. Il servizio consente di accedere altre risorse web, come web services o dati, attraverso delle richieste HTTP. I server remoti potrebbero impiegare molto tempo per la risposta, pertanto il servizio prevede che la comunicazione avvenga in background, ma entro i limiti di esecuzione dell’handler complessivo per la gestione della richiesta dell’utente.

Mail service. Il servizio consente di inviare o ricevere dei messaggi email utilizzando l’infrastruttura di Google. Per quanto concerne l’invio, il mittente dei messaggi può essere l’utente che esegue la richiesta o l’applicazione stessa. Se il programma è invece configurato per la ricezione di email, i messaggi diretti all’indirizzo URL dell’applicazione vengono intercettati da Mail service per essere tradotti nella forma di una richiesta HTTP.

Image manipulation. Il servizio offre la possibilità di manipolare le immagini attraverso l’utilizzo di opportune API. Alcune delle operazioni consentite sono ad esempio la rotazione e il ridimensionamento di immagini JPEG o PNG.

Fonti:

  • Essential App Engine: Building High-Performance Java Apps with Google App Engine – Adriaan De Jonge
  • Developing with Google App Engine – Eugene Ciurana
  • Cloud Computing: modelli, piattaforme e sviluppo di un caso applicativo. Tesi di laurea triennale di Yuri Ricci.