Una delle fasi maggiormente delicate nello sviluppo delle applicazioni riguarda la pianificazione delle attività, al fine di stimare le risorse necessarie (umane e temporali) nonché i costi che saranno affrontati durante lo sviluppo. Il project manager, utilizzando una tecnica denominata Work Breakdown Structure , definisce una struttura analitica del progetto elencando tutte le diverse attività e assegnando a ciascuna di esse il nome del responsabile e il tempo previsto per il completamento dell’attività stessa. Tra le diverse attività, è fondamentale tenere in considerazione lo sforzo necessario alla preparazione dell’ambiente infrastrutturale su cui l’applicazione dovrà essere eseguita, che comprende l’installazione e la configurazione dell’ambiente middleware (database, Web server, application server), l’installazione delle necessarie dipendenze richieste dall’applicazione nonché l’esecuzione dei test finalizzati alla verifica della compatibilità di tutte le parti. Queste attività dovranno essere eseguite su tutti gli ambienti infrastrutturali che si prevede di utilizzare. Pertanto se è previsto un ambiente di sviluppo, un ambiente di test e un ambiente di produzione, ciascuno step dovrà essere ripetuto tre volte: uno su ciascuno degli ambienti su cui l’applicazione dovrà essere eseguita. Diventa quindi evidente come all’aumentare della complessità dell’applicazione, anche il processo di preparazione dell’ambiente potrebbe diventare insostenibile. Idealmente, sarebbe preferibile “riutilizzare” la medesima configurazione su tutti gli ambienti infrastrutturali al fine di contenere tempi e costi.
L’utilizzo dei virtualizzatori basati sul container permette di raggiungere questo ambizioso obiettivo: ridurre i tempi di sviluppo, di deployment e di testing di una applicazione su differenti sistemi. Attualmente sono presenti sul panorama Open Source diversi container per applicazioni. Meritano sicuramente di essere citati Docker e OpenVZ . Il container invece di cui vogliamo parlare in questo articolo è Singularity , un container ottimizzato per applicazioni HPC, ovvero tutte quelle applicazioni di fascia alta che ricorrono al calcolo parallelo per l’elaborazione di dati in campo scientifico e che possono fornire prestazioni molto elevate dell’ordine dei PetaFLOPS. Prima di mostrare come installare, configurare e utilizzare Singularity su un’applicazione HPC, è necessario comprendere (o ripassare) cosa si intende con il termine virtualizzazione, quali sono le diverse tecniche e i benefici derivanti dall’utilizzo di una tecnologia di virtualizzazione.
Tipi di virtualizzazioni
Con il termine virtualizzazione si intende descrivere la separazione delle risorse utilizzate da un servizio dal sottostante livello che ospita il servizio stesso. Oggi, la tecnica di virtualizzazione può essere applicata a differenti livelli dell’infrastruttura IT: rete, storage, server, sistema operativo, database e applicazioni. Originariamente, la virtualizzazione è stata utilizzata per introdurre un livello di astrazione tra l’hardware, il sistema operativo e le applicazioni che girano su di esso. Questo tipo di virtualizzazione è denominata Full Virtualization , e prevede la presenza di un Virtual Machine Monitor (VMM), detto anche Hypervisor , che rende invisibile al sistema operativo ospitato tutte le risorse fisiche presenti nel sistema ospitante.
L’architettura del modello di virtualizzazione di tipo Full
Il Virtual Machine Monitor viene eseguito sul sistema ospitante come qualsiasi altra applicazione. Le risorse fisiche della macchina (dischi, schede di rete, RAM ecc.) sono controllate direttamente dal VMM. È possibile eseguire differenti sistemi in parallelo sul sistema ospitante grazie al partizionamento dell’hardware in uno o più unità logiche chiamate Virtual Machine .
Differenti macchine virtuali in esecuzione su un singolo host fisico
La comunicazione del sistema operativo virtuale (ospitato) con i dispositivi virtuali avviene generalmente attraverso specifici driver della macchina virtuale. Il vantaggio di questo approccio è che è molto semplice da realizzare. Ad esempio, attraverso VirtualBox l’utente ha la possibilità di installare più sistemi operativi sul proprio sistema fisico. Altro vantaggio di questo approccio è che le macchine virtuali sono isolate le une dalle altre, in questo modo un problema su una macchina virtuale non ha effetti sulle altre macchine virtuali. Per contro offre performance ridotte, fino al 30 per cento in meno rispetto a quando il sistema operativo viene eseguito direttamente sull’hardware (e quindi senza virtualizzazione).
Un secondo approccio è denominato OS-Layer Virtualization , spesso conosciuto come Single Kernel Image (SKI) o Container-Based Virtualization .
Il modello di virtualizzazione Container-Based
Questo modello di virtualizzazione permette di eseguire più istanze dello stesso sistema operativo in parallelo. Ciò significa che non è l’hardware ad essere virtualizzato, ma direttamente il sistema operativo. Il layer di virtualizzazione viene quindi eseguito come se questo fosse un’applicazione sul sistema operativo ospitante. Il sistema ospite viene chiamato container. Ogni container è isolato dagli altri attraverso la virtualizzazione dei namespace. A differenza dell’approccio Full Virtualization, il Container-Based Virtualization riduce la distanza tra l’applicazione finale e l’hardware fisico, semplificando in tal modo l’amministrazione del sistema in quanto permette all’amministratore di assegnare specifiche risorse a ciascun container (ad esempio una CPU garantita oppure una specifica quantità di memoria).
L’architettura di un modello di virtualizzazione basata sul container
Dal punto di vista dell’efficienza offre maggiori performance, poiché c’è solo un sistema operativo che si occupa di gestire gli accessi all’hardware. Come viene mostrato nella figura precedente, il container si interfaccia con il Container Daemon in esecuzione sul sistema operativo della macchina fisica che, interagendo con il sistema operativo fisico, accede direttamente all’hardware. Altro aspetto che rende questo approccio particolarmente attraente è che favorisce il workflow di sviluppo, testing e messa in produzione: gli sviluppatori possono in questo modo sviluppare la propria applicazione in un container dedicato allo sviluppo e ai test.
Quando l’applicazione è pronta, viene creato un container di produzione contenente l’applicazione. Per contro presenta un grosso inconveniente: poiché la macchina virtuale usa lo stesso kernel del sistema operativo reale, il sistema operativo ospite deve necessariamente essere lo stesso del sistema operativo sottostante (quindi non è possibile eseguire Windows su una macchina GNU/Linux).
Il terzo approccio è chiamato Hardware-Level Virtualization . Questo modello di virtualizzazione è particolarmente usato nel mercato dei server grazie all’elevato isolamento delle macchine virtuali e alle performance. Il Virtual Machine Monitor viene eseguito direttamente sull’hardware fisico, e controlla e sincronizza gli accessi all’hardware da parte dei sistemi operativi ospiti.
Hardware-Level Virtualization
Nel modello denominato Para Virtualization , il sistema operativo ospite viene invece modificato in modo da operare in un ambiente virtuale, prendendo quindi consapevolezza di essere in esecuzione su un ambiente virtuale. Viene quindi inserito un layer abbastanza snello che funge da interfaccia tra l’hardware fisico e il sistema operativo guest modificato. Il sistema di virtualizzazione pertanto espone una libreria di chiamate ( Virtual Hardware API ) che implementa una semplice astrazione delle periferiche. L’interazione con i dispositivi in un ambiente para-virtualizzato è simile a quella dell’approccio Full Virtualization e questo garantisce bassi overhead ma un crash del sistema di paravirtualizzazione porterebbe in crash anche tutte le macchine virtuali. La Para-Virtualization è il modello di virtualizzazione adottato da Xen.
Il modello Para-Virtualizzato
Nel modello denominato Application Virtualization invece viene generato un livello di astrazione tra l’applicazione e il sistema operativo. In questo modo l’utente può eseguire un’applicazione sul proprio server senza dover necessariamente installare l’intero applicativo. Le applicazioni vengono quindi eseguite in un piccolo ambiente virtuale contenente solo le risorse di cui necessita l’applicazione per potere essere eseguita.
Un’applicazione virtuale nel modello denominato Application Virtualization
Un altro approccio, denominato Resource Virtualization permette di “nascondere” al sistema operativo la complessità delle risorse che dovrà utilizzare. Tipici esempi di Resource Virtualization riguardano l’aggregazione di molte componenti in un singolo resource pool (ad esempio, tanti dischi dedicati allo storage possono essere aggregati in modo da essere visti come una singola componente), più computer e le relative risorse possono essere combinati in un unico super computer con un enorme quantità di risorse, una singola risorsa può essere partizionata in un numero maggiore di risorse dello stesso tipo.
Singularity: container per HPC
Singularity è dunque un Container-Based Virtualizator, particolarmente ottimizzato per le applicazioni HPC in grado di facilitare il porting dell’applicazione su differenti cluster di esecuzione. Fornisce a ricercatori e sviluppatori la possibilità di impacchettare in un unico prodotto l’intero workflow computazionale (ovvero uno o più applicazioni con le loro dipendenze) ed eseguirlo in tutti gli ambienti che supportano Singularity.
Supponiamo ad esempio di lavorare su un’applicazione che deve essere eseguita su un cluster HPC. Ovviamente, in un primo momento, prima che il sistema venga mandato in esecuzione, dobbiamo configurare l’ambiente di test, installare le necessarie librerie, verificare il corretto funzionamento dell’ambiente e cosi via. Quando il prodotto è stato completato, lo stesso processo di configurazione deve essere ripetuto per l’ambiente di produzione, e se dopo qualche tempo il cluster HPC viene sostituito da un sistema più performante, è nuovamente necessario rieseguire l’intero processo di configurazione e test del nuovo ambiente. Con l’utilizzo del container, tutto questo viene superato, perché è sufficiente copiare l’immagine ed eseguirla sul nuovo cluster. Essendo un virtualizzatore Container-Based, Singularity non richiede l’immagine di un sistema operativo completo: nel container infatti verrà installato solo ciò che serve per il programma da eseguire. Dal punto di vista dell’utente, il container diventa invisibile. L’utente può infatti interagire con i programmi interni al container con la stessa facilità con cui interagisce con i programmi ad esso esterni; proprio come se il container non ci fosse. Per raggiungere questo obiettivo, Singularity fa uso di una netta separazione dello spazio dei nomi (namespace). In questo modo è possibile eseguire all’interno del container anche applicazioni X window, purché in fase di creazione del container siano stati installate tutte le necessarie dipendenze.
L’architettura di Singularity
L’architettura di Singularity è abbastanza semplice: un launcer, eseguito dall’utente, carica il container ed esegue le applicazioni in esso contenute, come se queste fossero presenti sul sistema operativo ospitante. Il container può essere immaginato come un classico pacchetto che, oltre all’applicazione, contiene anche le dipendenze di cui l’applicazione necessità e i dati che devono essere elaborati. Come vedremo, la creazione del container avviene attraverso la definizione di un file di specifiche che descrive le dipendenze necessarie all’applicazione e tutte quelle informazioni che Singularity deve usare per poter eseguire correttamente l’applicazione.
Modello architetturale di Singularity
Una volta creato, il container può essere gestito come un file tradizionale: può essere cancellato, spostato o copiato in un cluster differente in cui eseguire l’applicazione senza dover procedere alla configurazione del nuovo sistema ospitante. In questo modo gli sviluppatori distribuiscono non solo il codice sorgente ma l’intero ambiente di esecuzione. Il container può inoltre essere eseguito come un tradizionale applicativo o uno script sul computer. Un’altra interessante caratteristica di Singularity è la sua elevata affidabilità in materia di sicurezza: Singularity infatti non consente di sostituire, all’interno del container, il proprio utente con uno più privilegiato. Per eseguire all’interno del container un comando con i privilegi di root è necessario che all’esterno del container l’utente sia root. Questa è una delle principali caratteristiche che distingue Singularity da altri container come ad esempio Docker.
Singularity e MPI
Singularity è stato progettato per funzionare con qualsiasi tipo di applicazione ma gli sviluppatori, durante la progettazione del sistema, hanno sempre tenuto in considerazione le esigenze di ricercatori e sviluppatori di applicazioni scientifiche che assorbono moltissime risorse dei cluster ad alte performance. Singularity, infatti, è in grado di eseguire correttamente anche le applicazioni parallele scritte utilizzando MPI quale standard di comunicazione tra i processi in esecuzione. Singularity offre la completa compatibilità con OpenMPI , un’implementazione delle specifiche MPI ( Message Passing Interface ) utilizzata per lo sviluppo di applicazioni per sistemi a memoria distribuita. L’utente, tramite la propria shell, esegue il comando mpirun , il quale chiama il processo di gestione ORTED il quale a sua volta lancia il container Singularity richiesto dal comando mpirun. Singularity istanzia il container, quindi lancia l’applicazione MPI all’interno del container. L’applicazione carica le librerie OpenMPI che si connettono al processo ORTED tramite le Interfacce di gestione del processo (Process management interface, PMI). A questo punto il processo all’interno del container viene eseguito normalmente all’interno del container stesso.
Installiamo Singularity
Cloniamo la repository git di Singularity con i seguenti comandi dal terminale:
cd /home/lxm
git clone -b master https://github.com/gmkurtzer/singularity.git
poi configuriamo, compiliamo e installiamo:
cd singularity
sh./autogen.sh
./configure --prefix=/usr/local/singularity
make
make install
export $HOME_SING=/usr/local/singularity
export PATH=$PATH:$HOME_SING/bin
Al termine della compilazione, nella home del progetto noteremo la presenza della directory bin nella quale è presente il file binario singularity : è un wrapper per l’esecuzione di un comando o di uno script sul container. Se si esegue il comando singularity senza alcun parametro, vengono elencate le diverse opzioni e i sotto-comandi che possono essere passati al wrapper.
Creazione dell’immagine
A questo punto possiamo procedere con la creazione dell’immagine, ovvero del file fisico che conterrà il container, l’applicazione e le relative dipendenze. Per la creazione dell’immagine utilizziamo il comando singularity con il parametro create :
cd /home/lxm/singularity
singularity create centos.img
Com’è possibile notare, abbiamo creato un’immagine, denominata centos.img . Il messaggio di output ci avvisa che il file creato ha una certa dimensione. Possiamo averne conferma attraverso il comando ls :
ls -lh centos.img
che nel nostro caso restituisce in output:
-rwxr-xr-x 1 root root 1,1G giu 4 00:00 centos.img*
Un’analisi più approfondita, tuttavia, ci mostra come la dimensione realmente occupata dal file è appena di 33 MB:
# du -sh centos.img
33M centos.img
La dimensione dell’immagine creata è abbastanza piccola, ma tenderà a crescere con l’installazione di file, dati e programmi. Possiamo quindi procedere con il setup e il bootstrap dell’immagine creata. A tale scopo è necessario definire un file di configurazione in modo da specificare quale sistema operativo e quali pacchetti devono essere installati all’interno dell’immagine. Il file di configurazione contiene un elenco di attributi chiave-valore che descrivono le azioni che il container dovrà compiere durante la fase di bootstrap e ogniqualvolta viene acceduto.
Di seguito mostriamo due file di configurazione, il primo utilizzato per creare un’immagine di un sistema Centos 7:
# vi centos.def
RELEASE=7
DistType "redhat"
MirrorURL "http://mirror.centos.org/centos-${RELEASE}/${RELEASE}/os/\x86_64"
Setup
Bootstrap
InstallPkgs yum vim-minimal python procps-ng
RunScript "echo \"Hello World\""
Cleanup
Il secondo, invece, per creare un’immagine con Debian Jessie:
# vi debian.def
VERSION=jessie
DistType "debian"
MirrorURL "http://ftp.au.debian.org/debian/"
Setup
Bootstrap
InstallPkgs yum install vim-minimal python procps-ng
RunScript "echo \"Hello World\""
Cleanup
In entrambi i file, la direttiva DistType specifica la distribuzione che deve essere installata nel container; la direttiva MirrorURL specifica invece l’URL dal quale devono essere recuperati i binari da installare nel container. I due comandi successivi, Setup e Bootstrap , creano l’immagine e avviano il container utilizzando le direttive sotto indicate; il comando InstallPkgs permette di installare le dipendenze necessarie al corretto funzionamento dell’applicazione che si desidera eseguire all’interno del container; infine, il comando RunScript esegue un comando specificato quando il container viene avviato.
Possiamo quindi procedere con il bootstrap del container:
singularity bootstrap centos.img centos.def
Questo comando deve essere lanciato con i privilegi di root dall’esterno del container in modo che si possa agire all’interno del container con i medesimi privilegi. Al termine della procedura di bootstrap possiamo esaminare le dimensioni dell’immagine, che ovviamente saranno aumentate rispetto al controllo precedente:
# du -sh centos.img
293M centos.img
Accesso al container ed esecuzione di comandi
Il container a questo punto è pronto e l’utente può accedervi:
singularity shell centos.img
La prima cosa che notiamo dopo aver effettuato l’accesso al container è che il prompt della shell è cambiato, segnalando che stiamo lavorando all’interno del container. A questo punto, tutti i comandi che lanceremo nella shell saranno eseguiti all’interno del container. Ad esempio, il comando top mostrerà i processi in esecuzione all’interno del container.
Se esaminiamo la struttura delle directory all’interno del container, ci accorgiamo che alcune directory del server fisico sono visibili e accessibili anche all’interno del container, compatibilmente con i privilegi assegnati all’utente connesso al server fisico. Tuttavia alcune directory, ad esempio /bin , sono differenti. Per esserne sicuri, contiamo il numero di file presenti nella directory /bin del container e quelli presenti nella medesima posizione del sistema operativo ospitante: noteremo che sono differenti. Come risulta evidente, dal punto di vista del sistema operativo il container viene gestito come un vero e proprio file. Questo rappresenta un’importante caratteristica offerta da Singularity, in quanto assegnando opportunamente le corrette ACL è possibile definire i permessi di accesso degli utenti al container. In questo modo, ad esempio, si può consentire l’accesso in lettura e scrittura ad alcuni utenti, limitando ad altri la possibilità di scrivere sul container.
Configuriamo il container per l’applicazione HPC
Il container, una volta creato, assume una dimensione minimale, in quanto all’interno di esso vengono inserite, oltre alle librerie necessarie al corretto funzionamento del container stesso, tutti i pacchetti indicati tramite la direttiva InstalPkgs del file di configurazione. Tutte le altre dipendenze di cui la nostra applicazione HPC dovesse necessitare, devono essere installate a mano, attraverso ad esempio l’utility Yum oppure compilando a mano i sorgenti. Poiché l’applicazione che vogliamo eseguire sul container usa le librerie OpenMPI per implementare lo scambio di messaggi tra i diversi processi in esecuzione sull’infrastruttura di calcolo, è necessario istallare queste librerie all’interno del container. Per prima cosa è necessario rendere scrivibile l’immagine, in quanto, per motivi di sicurezza, il container viene creato in sola lettura:
singularity shell -w centos.img
Procediamo quindi con l’installazione e la configurazione di OpenMPI all’interno del container:
# singularity shell -w centos.img
Singularity/centos.img> yum install openmpi
Singularity/centos.img> yum install openmpi-devel
Singularity/centos.img> export PATH=$PATH:/usr/lib64/openmpi/bin
Anziché realizzare da zero l’applicativo MPI, abbiamo preferito usare il codice di un programma demo, denominato ring_c.c , distribuito assieme a OpenMPI. Il programma è molto semplice: istanzia il numero di processi indicati dall’utente e simula una comunicazione ad anello. Ciascun processo istanziato viene identificato univocamente attraverso un ID che inizia da zero.
Il primo processo, quello con ID zero, crea un pacchetto contenente un intero pari a 10, quindi lo invia al successivo processo (quello con ID 1), questo lo riceve, decrementa l’intero e lo invia al successivo. Il giro continua fino a quando il messaggio non ha raggiunto valore 0. A questo punto tutti i processi completano la loro esecuzione e il programma termina. La compilazione del programma avviene utilizzando l’utility mpicc :
Singularity/centos.img> mpicc ring_c.c -o ring
Mentre l’esecuzione del binario avviene tramite mpirun :
Singularity/centos.img> mpirun -np 4 ring
Così facendo abbiamo eseguito il comando mpirun chiedendo a OpenMPI di instanziare 4 processi che comunicheranno tra loro.
Performance ed overhead
I progettisti delle applicazioni scientifiche di fascia enterprise sono particolarmente attenti alle prestazioni delle proprie applicazioni, tanto da misurare i tempi attraverso specifici strumenti di profilazione. L’utilizzo di qualsiasi strumento di virtualizzazione, come nel caso di Singularity, introduce inevitabilmente un overhead aggiuntivo che si traduce nella perdita, seppure minima, delle performance. Il team di sviluppo di Singularity ha cercato di quantificare, attraverso un approccio sperimentale, l’impatto del container a livello di performance, confrontando la latenza di un’applicazione eseguita all’interno di Singularity e della stessa applicazione eseguita direttamente sull’host.
Conclusioni
Giunto al momento in cui scriviamo alla release 2.2, Singularity possiede tutte le caratteristiche per essere considerato un prodotto di punta nel panorama dei virtualizzatori basati su container. Da una parte l’alta efficienza consente agli sviluppatori delle applicazioni scientifiche l’utilizzo del prodotto anche in ambito enterprise e di ricerca, dall’altra la sua elevata portabilità garantisce la cosiddetta “Mobility of Compute”, permettendo di ridurre costi e tempi necessari alla preparazione dell’ambiente infrastrutturale su cui l’applicazione dovrà essere eseguita.
Nei nostri laboratori, abbiamo creato un container su una distro GNU/Linux, installando all’interno di esso un’applicazione HPC. Abbiamo quindi trasferito il container in un ambiente Debian notando come l’applicazione installata nel container ha continuato a funzionare senza dover configurare nulla sul sistema target.