MokaByte Numero  43  - Luglio Agosto 2000
Il problema del monitoraggio in Java
di 
Luca Armani
Monitorare una applicazione Java per sapere quanto consuma è una cosa molto utile

In questo articolo vengono affrontate le tematiche del monitoraggio in ambiente Java, e di come sia possibile sviluppare degli strumenti idonei alla misurazione dello stato delle risorse della macchina fisica sottostante. Nonostante ciò possa essere considerata un’involuzione rispetto al processo di astrazione fortemente voluto dai progettisti Java (che in effetti hanno definito una macchina “virtuale” sopra la macchina “reale”), questa esigenza è fortemente sentita per applicazioni che non possono prescindere da un degrado delle prestazioni

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.
 

Chi volesse mettersi in contatto con la redazione può farlo scrivendo a mokainfo@mokabyte.it