Introduzione
Il
controllo del corretto comportamento di un’applicazione è sempre
stata un’esigenza molto sentita che ha motivato lo sviluppo di strumenti
di monitoraggio adeguati. In particolare, alcune applicazioni che svolgono
azioni critiche necessitano di veri e propri controllori che sorveglino
online la corretta evoluzione del programma, ed eventualmente intraprendano,
di fronte a situazioni d’allarme, opportune azioni correttive.
Nella
fattispecie, questo articolo si propone di monitorare le risorse utilizzate
da una Java Virtual Machine. Per risorse si devono intendere quelle entità,
logiche o fisiche, essenziali all’esecuzione di un programma. Le risorse
più critiche sono quelle che si scontrano coi limiti fisici dell’hardware
sottostante, e sono tipicamente il consumo di CPU, la disponibilità
di memoria fisica e la banda delle risorse di comunicazione (disco o rete).
Il problema del
denial-of-service
Con
denial-of-service si deve intendere l’incapacità, da parte di un’applicazione,
di soddisfare le specifiche secondo le prestazioni attese. Questo problema
affligge soprattutto le applicazioni di tipo server-side, soprattutto se
con caratteristiche di estensibilità. Tali applicazioni possono
eseguire dei moduli di codice ricevuti direttamente dalla rete, cosicché
il loro carico di lavoro non è noto a priori e può risultare
altamente variabile. Il rischio è che ovviamente alcuni moduli monopolizzino
alcune risorse del server, penalizzando gli altri utenti e il funzionamento
globale del sistema. Si noti che questo tipo di problema è a valle
del problema della sicurezza, e ne risulta indipendente.
Java
è una tecnologia senz’altro potente e diffusa per lo sviluppo di
applicazioni distribuite, tuttavia essa non possiede né un modello
né un meccanismo per il controllo delle risorse. Per la sua filosofia
di “write once, run anywhere”, la JVM è una macchina astratta, e
come tale nessuna delle sue API fa riferimento al consumo di memoria degli
oggetti, o del consumo di CPU dei thread, o della misura del traffico sui
canali di comunicazione. Per quanto detto prima, lo stesso Security Manager
è uno strumento inadatto al controllo delle risorse, in quanto attuare
un controllo degli accessi alle risorse non significa garantire un alto
livello di performance.
Uscire da Java:
JVMPI
JVMPI
(JVM Profiler Interface) è un’interfaccia, non ancora standard,
compresa nel JDK 1.2. Essa definisce un protocollo di comunicazione tra
la JVM e un Profiler Agent, ovvero un modulo di codice preposto all’ascolto
di “eventi” generati dalla JVM stessa. Il Profiler Agent si presenta come
una DLL (Dynamic Link Library, libreria a collegamento dinamico), scritta
dal programmatore e caricata dal processo java. I suoi compiti sono di
ricevere gli eventi dalla JVM, raccogliere le statistiche volute e fornirle
ad un processo esterno (o, perché no?, di nuovo alla JVM), che è
il monitor vero e proprio.
Il
codice del Profiler Agent va scritto in C/C++, utilizzando le dichiarazioni
contenute nel file <javadir>/include/jvmpi.h. I programmatori “pure-Java”
incontreranno molte difficoltà: dovranno sporcarsi le mani coi puntatori,
oppure chiedere aiuto ad un Vero Programmatore (vedi sezione MokaIdiots).
Alcuni risultati faranno impallidire alcuni puristi Java, che li riterranno
delle azioni illegali, ma non devono sorprendere i programmatori C.
Raccogliere gli
eventi
Per
scrivere il codice di un Profiler Agent basta seguire i seguenti passi:
-
inizializzare
l’interfaccia JVMPI;
-
scrivere
una propria funzione di ricezione degli eventi;
-
comunicare
alla JVM gli eventi che si desidera ricevere.
La prima
azione è semplice: al caricamento della DLL basta ottenere un riferimento
alla struttura dati JVMPI_Interface:
/*
file mio_profiler.cpp */
#include
<jvmpi.h>
//riferimento
all’interfaccia
static
JVMPI_Interface *jvmpi;
//
inizializzazione
jvmpi=jvm->GetEnv((void**)&jvmpi,JVMPI_VERSION_1);
A questo
punto bisogna scrivere una funzione di ricezione degli eventi, che deve
avere prototipo void ( ) (JVMPI_Event *), ad esempio:
void
mia_funzione(JVMPI_Event *event)
{
if(event.event_type==JVMPI_EVENT_THREAD_START)
printf(“E’ stato creato un nuovo thread\n”);
}
...
jvmpi->NotifyEvent=mia_funzione;
/*adesso questo puntatore richiama la funzione definita dall’utente*/
Come
ultima operazione bisogna abilitare gli eventi di interesse (a default
sono tutti disabilitati):
jvmpi->EnableEvent(JVMPI_EVENT_THREAD_START,NULL);
jvmpi->EnableEvent(JVMPI_EVENT_CLASS_LOAD,NULL);
jvmpi->EnableEvent(JVMPI_EVENT_METHOD_ENTRY2,NULL);
jvmpi->EnableEvent(JVMPI_EVENT_OBJECT_ALLOC,NULL);
jvmpi->EnableEvent(JVMPI_EVENT_OBJECT_FREE,NULL);
...
Una
volta compilata e caricata la libreria, il sistema è pronto per
funzionare. Ogni volta che un thread Java eseguirà un’azione di
interesse, la JVM sospenderà l’esecuzione del thread, genererà
un evento corrispondente e passerà il controllo alla DLL: l’entry
point sarà ovviamente la funzione mia_funzione e l’argomento passato
sarà l’evento in questione. Una volta terminato il corpo della funzione,
il controllo ritornerà al thread chiamante che riprenderà
normalmente la sua esecuzione. Le azioni contenute in mia_funzione sono
totalmente arbitrarie, e dipendono solo dalla fantasia del programmatore.
E’ chiaro che, statisticamente, molti thread eseguiranno il codice di mia_funzione
contemporaneamente e concorrentemente; eventuali accessi a strutture dati
comuni vanno dunque sincronizzate (utilizzando opportuni costrutti C forniti
dalla stessa JVMPI).
Che
cosa contiene l’argomento di tipo JVMPI_Event? Esattamente contiene:
-
un riferimento
al thread Java che ha provocato l’evento (mediante un puntatore ad una
struttura JNIEnv, la stessa utilizzata dall’interfaccia JNI);
-
una costante
simbolica che identifica il tipo di evento;
-
un campo
variabile che contiene informazioni specifiche per quel tipo di evento.
Senza
scendere nei dettagli, i 36 tipi di evento predefiniti riescono ad indicare:
-
la nascita/morte
di un thread;
-
il caricamento
di una classe (compresi nomi e riferimenti di campi e metodi);
-
l’allocazione/deallocazione
di un oggetto (compresi i byte occupati in memoria);
-
la chiamata
di un metodo;
-
la sospensione
su un monitor Java;
-
l’attività
del Garbage Collector;
-
vedi [1]
Si ricorda
inoltre che tutte queste informazioni fanno capo allo specifico thread
Java.
Altre potenzialità
Il
fatto che il Profiler Agent sia scritto in C/C++ offre grandi possibilità
ai programmatori più smaliziati. Ad esempio, ai fini del monitoraggio
della rete, si potrebbe tener traccia delle chiamate ai metodi writeXxxx
su una socket, per avere un’idea del traffico in uscita imputabile ai singoli
thread. Analizzando il codice sorgente delle API Java si può notare
che il metodo Socket.getOutputStream() restituisce un oggetto di tipo SocketOutputStream,
non visibile fuori dal package java.net; i programmatori Java lo trattano
infatti come un generico oggetto OutputStream. Inoltre, i metodi SocketOutputStream.writeXxxx()
richiamano tutti, al loro interno, il metodo privato e nativo socketWrite,
che trasferisce da 1 a 1024 byte.
Un
Profiler Agent non ha alcuna difficoltà a localizzare classi e metodi
privati, di cui un programmatore Java non sa (e non deve sapere) nemmeno
l’esistenza. Il Profiler Agent controllerà quindi tutte le classi
caricate, finché non troverà la SocketOutputStream e memorizzerà
l’identificatore del metodo socketWrite. Ogni volta che viene eseguito
un metodo, verificherà che non si tratti del metodo socketWrite:
in tal caso si saprà che il thread chiamante sta scrivendo dati
su una socket TCP (di cui si possono ottenere tutti i dettagli).
E il gioco è
fatto!
Va
ricordato che il Profiler Agent riesce a “spiare” qualsiasi codice Java:
infatti l’applicazione Java da monitorare è totalmente inconsapevole
del monitor, né può proteggersi dalle sue indiscrezioni.
Sempre più
giù: JNI
L’interfaccia
JVMPI è molto potente, ma si ferma ancora al livello di astrazione
della macchina virtuale Java.
JNI
(Java Native Interface) consente di scrivere metodi Java in codice nativo
(C/C++ compilato per la specifica piattaforma), e di caricarlo sottoforma
di DLL. La vera potenzialità sta nel fatto che tale codice può
avere accesso agli oggetti Java, gestire eccezioni Java, etc, e al contempo
superare le barriere espressive delle API.
Ai
fini del monitoraggio, combinando JVMPI e JNI si possono ottenere misurazioni
di performance direttamente dal sistema operativo sottostante.
Ad
esempio, se si assume che ogni thread Java sia mappato su un thread di
sistema (ipotesi vera per Win32, Solaris e Linux, ma non per altri sistemi
operativi), è possibile misurare il consumo di CPU per ogni thread
(con tecniche diversificate: via registro per WindowsNT, via directory
/proc per Solaris e Linux), o ancora il consumo di memoria fisica e i page-fault
dell’intero processo java. Anche qui basta avere un po’ di fantasia, e
un po’ di pazienza nel ricercare le primitive C per interrogare il sistema...
Più avanti viene dato un esempio di questa tecnica.
Svantaggi
Utilizzare
JVMPI e JNI risulta una scelta obbligata per la risoluzione del monitoraggio,
d’altro canto ciò significa uscire da Java e perdere la portabilità
del codice. Infatti è chiaro che una DLL ha un formato specifico
per ciascun sistema operativo. Se si vuole preservare la portabilità
del codice sarà dunque necessario ricompilare lo stesso codice C/C++
per più piattaforme, se non addirittura sviluppare più versioni
della stessa libreria se si utilizzano primitive specifiche.
Un esempio di
monitor
Ho
realizzato una piccola libreria che contiene alcune primitive di monitoraggio,
che ovviamente utilizzano JVMPI e JNI. In questo caso le informazioni raccolte
dal Profiler Agent sono restituite, via JNI, ad un thread posto sulla stessa
macchina virtuale. Quindi, tra i vari thread, il monitor vede anche sè
stesso.
Il
monitor conta il numero di eventi che la JVM gli fornisce, suddividendoli
per thread chiamante. In figura 5 si vede come, per ogni thread attivo,
vengano riportati il numero di classi caricate, il numero di oggetti allocati,
la memoria da essi occupata, il numero di attese su monitor Java, il numero
di metodi invocati e, tra questi, il numero di operazioni in lettura e
scrittura su socket TCP e UDP. Si noti la presenza di alcuni thread “di
sistema” molto onerosi di risorse.
In
figura 6 si può vedere un monitor di processi: esso non sfrutta
JVMPI ma soltanto JNI. Sono state implementate due DLL, una per WindowsNT
e una per Solaris, con medesima interfaccia ma che utilizzano primitive
di sistema completamente diverse. Il risultato è comunque lo stesso:
per ogni processo il monitor misura, ad intervalli di tempo regolari, il
consumo di CPU e l’occupazione di memoria fisica. Il consumo di CPU è
anche suddiviso rispetto ai singoli thread che compongono il processo.
Si noti come java occupi la bellezza di 17M di memoria fisica per una banale
finestra.
Bibliografia
Le
guide di riferimento a JVMPI e JNI:
[1]
Sun, “Java Virtual Machine Profiler Interface”, http://web2.java.sun.com/products/jdk/1.2/
docs/guide/jvmpi, 1999
[2]
Rob Gordon, “Essential JNI: Java Native Interface”, Prentice Hall PTR,
1998
Un
bellissimo programma, completo di sorgente C++, per chi è curioso
di vedere come lavora JVMPI:
[3]
Gary Pennington, Rob Watson, “jProf: A Java Profiler”, http://www.garyshouse.freeserve.co.uk/jProf/jProf.html,
1999
Mini
Biografia
Luca
Armani nasce a Trento il 25/6/1975. Si laurea a in Ingegneria Informatica
presso l’Università di Bologna quest’anno. Attualmente lavora come
Web Administrator presso la Minosse srl di Bologna. Oltre ai computer ama
il basket, Schulz, i REM e i Placebo.
|