Untitled Document
   
 
Progettazione e misurazione delle performance di applicazioni J2EE altamente distribuite
di Frank Teti - traduzione di Mario Giammarco

Introduzione
Agli albori dell'informatica un saggio architetto di sistema disse: "non si può gestire un'applicazione che non si può monitorare". In molti casi documentare il design di una nuova applicazione, lasciamo stare poi documentare come potrebbe essere monitorata, è oltre lo scopo del progetto. Questo articolo descrive un meccanismo riutilizzabile per ottenere le statistiche di performance su applicazioni J2EE altamente distribuite.

Fondamentalmente le applicazioni devono essere preparate per supportare dei livelli di servizio (Service Level Agreement). In alcuni casi i SLA possono avere una granularità fine come, per esempio, dichiarare il tempo medio atteso per realizzare la persistenza dei dati relativi ad una persona. L'architettura mostrata in figura 1 è stata implementata per gestire un modulo d'ordine via Internet comprendente un mezzo per permettere ad un cliente di aggiornare il proprio profilo.


Figura 1 - Una architettura distribuita J2EE

Poiché era previsto alto traffico l'applicazione utilizza le transazioni distribuite EJB, che possono essere aggregate in cluster e pool per ottenere alta disponibilità e scalabilità. Le applicazioni altamente distribuite non devono essere realizzate pensando solamente al punto di vista degli oggetti distribuiti, ma anche dalla prospettiva del bilanciamento del carico e del clustering. L'architettura in figura non mostra i dettagli dell'implementazione del clustering e del bilanciamento del carico.

Questo testo descrive una metodologia di base per i test di performance su un'architettura distribuita, in particolar modo tratta del design e del codice necessario per ottenere una stima di base della velocità di una applicazione distribuita.

 

Architettura distribuita e sincronizzazione del tempo
L'applicazione J2EE in figura si basa su cinque strati: Web, EJB, EAI (Enterprise Application Integration), ERP (Enterprise Resource Planning) e DBMS. Un client da internet invia una richiesta HTTP allo strato Web il quale ritrasmette la richiesta allo strato EJB tramite RMI o IIOP. Lo strato EJB comunica con lo strato EAI "SeeBeyond" utilizzando un MUX e*Way della SeeBeyond. Lo strato EAI comunica con l'applicazione CRM (Customer RelationShip Management) della PeopleSoft all'interno dello strato ERP utilizzando un adattatore della PeopleSoft. Per finire l'applicazione CRM comunica con un DBMS Oracle attraverso SQL*net.

Una delle parti chiave di ogni ambiente distribuito è il time server. La nostra metodologia raccoglie le metriche sul server dove il codice viene eseguito, quindi è importante che tutte le macchine all'interno dell'architettura distribuita J2EE abbiano l'orario di sistema sincronizzato. Nell'ambiente Unix IBM AIX si utilizza il demone xntpd per settare l'ora di ogni macchina. Tale servizio è usato per sincronizzare gli orari con una macchina esterna utilizzzando il protocollo NTP (Network Time Protocol). In genere esiste un computer che funge da time server (sul quale non è necessario che giri Unix o AIX) al quale tutti gli altri computer di una organizzazione si sincronizzano alla partenza (in realtà si sincronizzano anche periodicamente).

Alternativamente una macchina si può sincronizzare con un time server esterno su Internet. Il demone xntpd è configurato spento di default quindi le varie informazioni, come per esempio l'indirizzo IP del time server, vanno configurate nel file "ntp.conf". Poiché la precisione richiesta per le metriche è a livello di secondi o addirittura di millisecondi eventuali discrepanze fra le varie macchine possono condurre a statistiche errate.

 

Specifiche degli oggetti e comportamento della strumentazione
L'architettura J2EE proposta si basa su vari design pattern famosi come per esempio: Business Delegate (conosciuto meglio come Proxy) , Data Access Object, Model View Controller, Session Facade e Transfer Object (conosciuto anche come Value Object). Un "transfer object" è una classe serializzabile che raggruppa attributi simili per formare un valore composto. Nell'architettura il "value object" è utilizzato come tipo di ritorno di un metodo business remoto come appunto negli Enterprise Java Bean. Ottenere attributi multipli attraverso un unico value object in un sola interazione con il server diminuisce il traffico di rete e minimizza la latenza e l'utilizzo delle risorse del server.

La figura 2 descrive l'oggetto PersonValueObject che implementa le interfacce IpersonValueObject e Serializable. Il bean PersonWorkerBean usa un PersonValueObject per contenere gli attributi di una persona e delle temporizzazioni. Gli attributi delle temporizzazioni sono di tipo long e contengono il valore ottenuto da System.currentTimeMillis(). L'oggetto PersonValueObject include anche metodi di accesso per tutti gli attributi.


Figura 2 - diagramma UML del pattern Value Object, noto anche
come Tranfer Obejct
(clicca sull'immagine per ingrandire)

Mentre è una buona pratica OO quella di provvedere dei metodi di accesso invece che manipolare direttamente le variabili d'istanza bisogna tenere conto che accedere ad una variabile attraverso un metodo rallenta l'esecuzione del programma. È utile avere dei metodi di accesso che nascondano l'implementazione di get e set e che permettano alle sottoclassi di cambiare il tipo di sincronizzazione di get e set, ma anche questo comporta dei costi aggiuntivi. I value object possono essere estesi tramite subclassing oppure possono avere associazioni con altri value object. Per esempio se le specifiche richiedono che tutti i value object di una applicazione debbano essere testati per la performance, è raccomandabile effettuare il refactoring dei metodi per il test raggruppandoli in un FrameworkValueObject. In questa specifica alternativa per la figura 2 l'oggetto PersonValueObject estende il FrameworkValueObject e implementa l'interfaccia IpersonValueObject; l'interfaccia IpersonValueObject estende IframeworkValueObject. Comunque le operazioni di IpersonVAlueObject sono le stesse di figura 2 per entrambe le versioni. Il vantaggio di usare questo modello è quello che tutti i value object estendono FrameworkValueObject e quindi possiedono metdodi per effettuare i test. Questa è una relazione molto complessa e, nonostante sia corretta dal punto di vista dell'linguaggio UML, aggiunge comunque un costo all'applicazione.

Per i value object che hanno un'associazione diretta con altri value objcet, queste relazioni sono implementate come variabili d'istanza di classe. UML raccomanda che, per un value object che sia in relazione di aggregazione o di composizione con altri value object, questa relazione sia implementata come una Collection. Una Collection è l'interfaccia radice di tutta la gerarchia delle Collection Java. Una Collection rappresenta un gruppo di oggetti, conosciuti col nome di "elementi". Quest interfaccia è tipicamente usata per manipolare insiemi di oggetti qualora sia richiesta la massima generalità. Per esempio un oggetto PersonValueObject contiene una relazione uno a molti verso una Collection di AddressValueObject. Comunque questa relazione non è inclusa nel diagramma perché non riguarda i test cronometrati, ma è importante conoscerla ugualmente perché si usa nelle applicazioni reali.

 

Il processo di raccolta dei dati strumentali.
L'obiettivo di questo progetto è:

  • di stimare il tempo totale impiegato per trattare e rendere persistente un PersonValueObject all'interno dei componenti middleware;
  • di determinare quali livelli o quali parti sono quelle computazionalmente più costose e che quindi richiedono un'ottimizzazione;
  • di poter fare il log dei test sui tempi di vari livelli in un singolo file di log per semplificare l'analisi del report.

Dopo che all'oggetto valore sono stati aggiunti i metodi per raccogliere i tempi, sono necessarie delle modifiche anche al codice in J2EE e SeeBeyond. È necessario identificare gli oggetti che settano le stime dei tempi nel value object responsabile per il suo livello, e inoltre quale oggetto debba essere responsabile per ottenere le stime e scriverle nel log. La decisione dei posti dove mettere del codice per raccogliere i tempi è stata arbitraria. Si è trovato che i seguenti oggetti dovevano essere modificati

  • PersonWorkerBean del livello Web - che deve diventare responsabile per settare gli attributi dei tempi (cioè T1 e T6) sul PersonValueObject per determinare il tempo totale speso nel trasmettere l'oggetto EJB Session remoto dal livello Web, e per prendere le stime dei tempi dall'oggetto PersonValueObject e scriverle in un singolo file di log.
  • il SessionBean EJB del livello EJB - responsabile per il settaggio delle stime dei tempi (T2 e T5) sul PersonValueObject per determinare il tempo impiegato nel livello EJB per ottenere una connessione a SeeBeyond e per trasformare il PersonValueObject in una stringa da far processare SeeBeyond.
  • PersonDAO (Data Access Object) del livello EJB - responsabile per settare le stime dei tempi (T3 e T4) del PersonValueObject per determinare il tempo totale impiegato nel livello EAI.

 

Limitazioni dell'architettura
L'input per il test dell'applicazione è stato generato usando LoadRunner della Mercury Interactive ma non parleremo di questo perché è oltre gli obiettivi di questo testo. Il tempo di trasferimento dati dal server HTTP al browser non è raccolto dal value object; i tempi impiegati dal download da intranet si calcolano sottraendo il tempo raccolti nel log framework_log4j.log dal PersonValueObject dai tempi ottenuti da LoadRunner.

L'output di log4j.log può essere salvato su file, su un OutputStream, su un java.io.Writer, su un server log4j remoto, su un server remoto syslog Unix o addirittura su un logger di eventi di Windows NT. La performance di log4j è molto buona. Su un AMD Duron a 800mhz col JDK 1.3.1 occorrono 5 nanosecondi per stabilire se un comando di log deve essere effettivamente scritto sul log o no. Anche la fase di scrittura su log è piuttosto veloce, a partire da 21 microsecondi utilizzando la configurazione standard.

È importante far notare che lo scopo di questa metodologia non è quello di fare il profiling della memoria o della cpu, in quanto in tal caso si potrebbe utilizzare JProbe di QuestSoftware oppure OptimizeIt della Borland. Gli unici strumenti che sono specifici per il controllo distribuito della performance in maniera simile a quanto suggerito in questo articolo sono PathWAI Dashboard di Candle Corp e Introscope di Wily Technology.


Figura 3 - diagramma UML sequence: fase di creazione del PersonValue Object
(clicca sull'immagine per ingrandire)


Una esecuzione corretta della applicazione dallo step 1 (createPerson) allo step 23 (PersonWorkerBean) produce nel file Log4j una singola riga che può essere trasformata nella seguente matrice. I tempi sono raccolti in millisecondi. La tabella qui sotto è usata per descrivere il tempo passato per i processi nel livello applicazione, che generalmente è una decisione arbitraria basata sulle specifiche. In effetti varie combinazioni di stime di tempi possono essere realizzate, se il programmatore/analista ha una comprensione avanzata del contesto in cui la stima dei tempi è stata catturata. Per esempio 250 rappresenta il tempo totale consumato manipolando l'oggetto PersonValueObject dal livello Web attraverso il resto dell'applicazione, mentre invece la colonna Incremental Elapsed Time è un tempo stimato per il processo attraverso i livelli.

Dettagli dell'implementazione

Passo 1: Implementare il codice per gestire i tempi nell'interfaccia IPersonValueObject per la classe PersonValueObject (vedi figura 2).

Passo 2: Implementare il codice per la classe PersonValueObject (vedi figura 2).

Passo 3: Implementare il codice per log4j.xml che setta i livelli di priorità per il logging dell'applicazione nel file framework_log4j.log. Log4j necessita un file di configurazione per impostarne il funzionamento. È possibile anche cambiare i livelli di log (DEBUG, INFO, WARN, ecc) dinamicamente.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">

<log4j:configuration
debug="true">
<appender name="TEMP"
class="org.apache.log4j.FileAppender">
<param name="File" value="framework_log4j.log"/>
<layout class="org.apache.log4j.PatternLayout">
<param name="ConversionPattern" value="%d - %m%n"/>
</layout>
</appender>
<category name="proofofconcept">
<priority value="info"
<appender-ref ref="TEMP"/>
</category>
</log4j:configuration>

Passo 4: Implementare codice per PersonWorkerBean (vedi figura 3). Il PersonWorkerBean scrive sul file framework_log4j.log i tempi invocando il metodo statico log4j.info della classe Log4j Logger. Le classi chiave in realtà sono due: la classe PropertyConfigurator e la classe Logger. Il metodo PropertyConfigurator.configure è usato per dire dove si trova il file di configurazione. La classe Logger si usa per settare i messaggi di log, i livelli di log, e per identificare per quali classi dell'applicazione si vuole effettuare il log degli errori. Per estendere la classe Logger si consiglia di racchiuderla con un'altra classe (wrapping) o di crearne un riferimento. È invece sconsigliato estendere Logger col subclassing. Log4j prevede la possibilità di settare dinamicamente i livelli di log o di fornire i livelli tramite un file di configurazione. Settando il livello ad "info" tutti i messaggi di tipo "info" o superiori verranno scritti sul file di log. Nel file di log dovrebbe esserci un unico messaggio di livello info (tranne nel caso che venga lanciata un'eccezione) che riporta le stime dei tempi per tutti i livelli di middleware.

Passo 5: Implementare codice per il PersonServiceSessionBean (vedi figura 3). Il metodo create dell'oggetto PersonSessionBean riceve una copia serializzata del PersonValueObject, setta le stime nel PersonValueObject e chiama createPerson dell'oggetto PersonDAO. Nel modello l'oggetto DAO è usato per ottenere una connessione al SeeBeyondEAI application server, e quindi il PersonValueObject non è reso persistente direttamente nel DBMS. Il punto è che, nella maggior parte delle applicazioni J2EE, il pattern Data Access Object è utilizzato per specificare come rendere i dati persistenti nel DBMS.

Passo 6: Implementare codice per la classe PersonDAO (vedi figura 3). Il metodo createPerson del PersonDAO invia l'oggetto PersonValueObject all'application server SeeBeyond utilizzando il protocollo sincrono MUX e*Way. Per questa implementazione gli attributi di PersonValueObject sono inviati come messaggi di testo perchè il server SeeBeyond supporta solo questi ultimi. Il messaggio di ritorno di SeeBeyond deve essere convertito a PersonValueObject tramite uno StringTokenizer in quanto arriva sotto forma di stringa. Il codice scritto assume che ci siano 3 token in questo formato: "Id^timestamp1^timestamp2". Il codice Id è generato da SeeBeyond e rappresenta la chiave primaria per l'oggetto PersonValueObject creato.

Il server SeeBeyond provvede i "timestamp" per l'integrazione con il CRM della PeopleSoft. SeeBeyond restituisce anche la chiave primaria generata che può essere necessaria per rendere persistenti i value object associati come per esempio AddressValueObject. Questo non si vede dal diagramma sequenza UML perché il tema centrale qui è come raccogliere stime di tempi da livelli disparati.

protected IPersonValueObject createValueObject(
String rs )
{
IPersonValueObject valueObject = null;
try
{
valueObject = getPersonValueObject();
if ( valueObject == null ) // create one
{
valueObject = new PersonValueObject();
}
StringTokenizer st =
new StringTokenizer(rs, "^");
PersonPrimaryKey pk =
new PersonPrimaryKey(st.nextToken());
valueObject.setPrimaryKey( pk);
valueObject.setT3( Long.parseLong(st.nextToken()));
valueObject.setT4( Long.parseLong(st.nextToken()));
}
catch ( Exception exc )
{
printMessage("PersonDAO:createValueObject()-" +
exc );
}

return( valueObject );
}

 

Implicazioni architetturali
Idealmente il miglior design dovrebbe permettere di passare un oggetto ValueObject a tutti i livelli del middleware. SeeBeyond per il livello EAI attualmente possiede un supporto asincrono che utilizza messaggi di testo JMS (java messaging service), ma non quelli di tipo JMS ObjectMessage. La maggior parte degli implementatori preferirebbero i messaggi di tipo ObjectMessage. Per esempio, utilizzando un tipo JMS ObjectMessage, il PersonValueObject non dovrebbe essere convertito in un oggetto String, ma potrebbe essere inviato direttamente. Gli ObjectMessage, che contengono oggetti Java serializzati, specificano meglio al programmatore quali tipi di dati e di attributi stanno ricevendo o spedendo. Col supporto per JMS ObjectMessage, l'oggetto PersonEAI descritto in figura 4 avrebbe potuto essere implementato in SeeBeyond, che avrebbe potuto manipolare direttamente gli oggetti PersonValueObject. Per iprogrammatori ad oggetti distribuiti questo è tutto quello che serve: trasferire oggetti attraverso la rete.


Figura 4 - diagramma UML con vista delle classi che partecipano al
Sequence Diagram: fase di creazione del PersonValue Object
(clicca sull'immagine per ingrandire)

 

 

A proposito dell'autore
Frank è un capo architetto specializzato in design di applicazioni J2EE e implementatore di applicazioni aziendali a tutto campo. Può essere raggiunto a frank_teti@hotmail.com

L'articolo è stato originariamente pubblicato su TheServerSide.com