Nelle puntate precedenti si è visto come progettare una applicazione JavaEE, come stratificare i vari layer applicativi, quali pattern utilizzare e come organizzare le classi in modo da creare software che aderisse ai vincoli di riuso, scomponibilità , semplicità di utilizzo e performance complessive.
Si è visto in particolare come l‘adozione di opportuni pattern di trasporto potesse migliorare drasticamente tempi di risposta di particolari operazioni.
Nonostante il tema di questo articolo, sono fermamente convinto che, a fronte di una applicazione che funziona male (scarse performance, difficoltà di integrazione con lo scenario aziendale, scarsa flessibilità ), il primo punto su cui lavorare sia quello della ingegnerizzazione complessiva (per cui adozione di layer, pattern, e quanto visto nei mesi precedenti).
E‘ altresì vero che, in taluni casi tale approccio può non essere sufficiente, e può essere utile disporre di strumenti alternativi a quelli applicativi, legati alla processo di configurazione della piattaforma di esercizio.
In questo articolo si parlerà di come ottimizzare una applicazione intervenendo sui file di configurazione di deploy dell‘application server, utilizzando gli strumenti che il container ci mette a disposizione, come il lazy-loading, la read-ahead cache e il precaricamento per gruppi di dati.
Già negli articoli precedenti, per motivi di praticità i molti concetti teorici sono stati adattati al caso specifico dell‘application server JBoss.
In questo articolo, dato il taglio prettamente pratico, più che negli articoli precedenti, si è reso necessario presentare i vari argomenti facendo riferimento ad un prodotto ben breciso, il quale, come in passato, sarà JBoss AS. Con le dovute precisazioni ed adattamenti è ovvio che i concetti esposti sono comunque universalmente validi e portabili in altri contesti applicativi.
Le prestazioni e la complessità e prestazioni
In ogni sistema che faccia uso di tecniche di persistenza complesse, tre sono i fattori principali che impattano sulle prestazioni generali:
- Il numero di operazioni eseguite sul sistema di persistenza
- La complessità di ogni singola operazione
- La quantità totale di dati trasferiti in ogni singola operazione.
Le scelte che si possono eseguire per ridurre la complessità sono volte a risolvere gli aspetti legati a questi tre punti. Le tecniche attuative sono nella maggior parte dei casi dettate dal buon senso: il primo punto ci suggerisce che sia peggio eseguire 100 query con risultato di una riga per ritornare 100 righe di una tabella piùttosto che eseguire una sola query da 100 righe complessive.
Questo però va contro il terzo principio, dato che le singole interrogazioni diventano più costose. E‘ quindi necessario per prima cosa conoscere il dominio applicativo e il modello dati al fine di stabilire in modo pragmatico quale sia il male minore fra i due. Come appare evidente, in tutte queste considerazioni non vi è molto di JavaEE, dato che si tratta di raccomandazioni che sono impartite ad un qualsiasi corso introduttivo di database-management.
A fianco di queste valutazioni la specifica EJB, e nel caso specifico JBossCMP, offre degli strumenti che aiutano ad implementare la strategia giusta, spostando in alcuni casi la visione che si può avere del problema.
La Entity Cache
Come noto il motore JBossCMP fa uso della JBoss Entity Cache per memorizzare i dati associati a ciascun entity bean presente nella cache primaria. Tali entity sono memorizzati o perché facenti parte della transazione corrente oppure perché configurati in modo che il container tenga copia in memoria fra più transazioni (tramite le opzioni A e D).
Per ogni bean in cache, il container memorizza sia gli attributi del bean che quelli di relazione con altri bean.
Prerequisito affinche tutto il sistema funzioni con successo è che i dati siano acceduti solamente tramite l‘applicazione EJB: nel caso in cui altre applicazioni al di fuori del container modifichino i dati (compresa la console SQL per modifiche dirette sulle righe delle singole tabelle), si potranno ottenere degradazioni delle prestazioni o peggio inconsistenza dei dati e sovrascrittura da parte del container delle modifiche eseguite dall‘esterno.
JBossCMP ReadAhead Cache
Un ulteriore livello di ottimizzazione è reso possibile in JBossCMP tramite il pre-caricamento dei dati relativi a bean non facenti parte della transazione corrente, ma che si immagina potrebbero essere necessari in un istante successivo.
Tali dati pre-caricati non possono essere inseriti nella cache primaria, dato che in questo spazio sono matenuti dati associati ad una transazione (sui quali quindi viene eseguita una lock sul db) per far si che siano consistenti con lo strato sottostante.
Per questo motivo JBoss si avvale di uno spazio di memorizzazione differente detto ReadAhead Cache: in questo contenitore, con una politica ottimistica, sono precaricati i dati non ancora associati alla transazione in atto. Anche se di fatto molto del lavoro fatto dal container per popolare tale cache è trasparente agli occhi del programmatore, più avanti verranno mostrati alcuni casi concreti in cui tale tecnica può essere guidata dal programmatore o da chi comunque effettua l‘impacchettamento e configurazione della applicazione EJB.
I load groups
Un modo interessante per effettuare il precaricamento dei dati è ortogonale al precaricamente per righe: invece di “indovinare” al tempo t quali righe della tabella potrebbero essere necessario al tempo t+1, si fa l‘assunzione che se servono alcuni campi di un bean, è presumibile che a breve potrebbe essere necessario accedere agli altri campi del bean stesso.
Questa strada può essere molto più precisa perché è il programmatore che conosce il flusso applicativo ed è quindi in grado di organizzare l‘accorpamento delle proprietà secondo le effettive necessità della applicazione nelle varie fasei del workflow.
Un gruppo di caricamento può essere specificato tramite una porzione di XML definito all‘interno del file jboss-cmp-jdbc.xml.
Ad esempio nello script che segue si aggregano alcuni campi del bean Article in un gruppo denominato ‘check‘ (ovvero si definisce esplicitamente quali sono i campi necessari in per eseguire il processo di verifica di un articolo) ed uno html-trans in cui sono aggregati i campi necessari per la trasformazione di un articolo del MokaByte-CMS da formato XML a HTML.
Article ...dati necesari per l‘operazione di controllo di un articolo check name title subtitle abstract dati necessari per l‘operazione di trasformazione HTML di un articolo html-trans name title subtitle abstract ...
Una volta definiti i vari gruppi di caricamento essi potranno essere utilizzati all‘interno delle tecniche di caricamento preventivo come mostrato in seguito.
Caricamento preventivo
Il meccanismo di caricamento preventivo utilizza i vari load-group sulla base della ipotesi che caricare in memoria un campo, ha un costo paragonabile al caricamento di n campi.
Il container esegue tre tipologie di caricamento preventivo in corrispondenza di tre tipi di eventi particolari:
- esecuzione di un metodo finder o una ejbSelect
- caricamento di un entity successivamente alla ricerca
- referenziazione delle relazioni fra enity beans
di seguito sono approfonditi i singoli aspetti di questi tre scenari differenti.
Caricamento preventivo per interrogazioni
Il meccanismo di caricamento di un entity bean prevede il reperimento in prima battuta della chiave primaria del bean stesso. Dalla chiave il container esegue il caricamento dei vari attributi per il completamento dello stato del bean solo al momento dell‘effettivo bisogno.
Per come è definita la specifica EJB, per ogni query che preveda un risultato multiplo (ma il discorso è del tutto equivalente per le ricerche a risposta singola), il container compone una collezione (Set o Collections) di chiavi primarie, le quali sono poi associate ai dati veri e propri solo al momento dell‘effettivo bisogno.
Se questo sistema, detto lazy loading, è pensato per ottimizzare le operazioni di accesso al database, in alcuni casi può portare al risultato esattamente opposto; si consideri Ad esempio il metodo di ricerca:
public interface Article extends EJBLocalHome {public Collection findAll() throws FinderException;}
che viene poi codificato in EJBQL con il seguente script
SELECT OBJECT(o) FROM Article AS o
tradotto infine in SQL nel seguente
SELECT t0_o.ID FROM ARTICLE t0_o
Dove il campo ID è la chiave dell‘entity bean Article.
Tipicamente, dopo la fase di ricerca, una volta ottenuto il set di bean che rispettano una qualche regola, seguono operazioni che accedono ai vari elementi (ArticleBean) ricavati. Nel peggiore dei casi si potrebbe dover ricavare una qualche proprietà per ogni elemento facente parte del set di oggetti ricavati:
// ricava i qualche modo la local home interface del bean ArticleCollection aritcleLocals = articleLocalHome.findAll();for (Iterator i = aritcleLocals.iterator(); i.hasNext();) {ArticleLocal articleLocal = (ArticleLocal) i.next();String title = articleLocal.getTitle();...}
Facendo due conti appare chiaro che, se è in vigore il lazy loading, questo semplice codice implica N+1 operazioni di accesso al database. Se dentro il ciclo for si accede anche ad altre proprietà del bean il numero N aumenterà in modo proporzionale al numero di proprietà .
Per risolvere questo inconveniente si può ricorrere all‘utilizzo dei gruppi di aggregazione visti poco prima. Il meccanismo di read-ahead dei dati infatti permette di ridurre considerevolmente il numero di interrogazioni al database.
Article findAll on-find light-data
In questo caso la lettura read-ahead utilizza il gruppo ‘light-data‘ (che è associato al pacchetto di dati presenti in un DTO Light).
Con questa nuovo accorgimento, rifacendo i conti, da N+1 si passa a 1 sola select sul database.
Inutile ricordare che se dopo una findAll() si volesse accedere ad una propriettà del bean non presente nel gruppo di pre-caricamento, non si otterrebbe nessun vantaggio dalla tecnica appena vista.
Una findAll() è in ogni caso una operazione da evitare, o di cui limitare l‘uso; ricerche per le quali si prevedono ampi set in risposta dovrebbero restituire una collezione di DTO leggeri (come visto negli aricoli precedenti della serie).
E‘ sempre bene lasciare fuori dai gruppi di precaricamento i campi xLOB per la loro intrinseca pesantezza.
Seguire le linee guida definite per la progettazione dei vari DTO (leggeri, pesanti, completi, composti) può essere un buon modo per creare e usare gruppi di caricamento corretti.
I dati precaricati con la strategia dei load-group, verranno memorizzati nella ReadAhead Cache.
Caricamento preventivo per instance loading
Nel momento in cui l‘applicazione tenta di accedere ad una proprietà del bean, il container verificherà la presenza di tale campo nella cache come spiegato nel caso precedente. Se tale proprietà non è presente verrà eseguita una nuova select per reperire tale valore.
In questo momento è possibile dar vita ad un ulteriore step di ottimizzazione, definendo una altra strategia di precaricamento associata questa volta non alla operazione di ricerca ma di loading del bean.
Article ...check
che genera la seguente istruzione SQL
SELECT t0_o.NAME , t0_o.TITLE FROM ARTICLE t0_o WHERE t0_o. ID = ?
In questo caso si definisce una strategia di precaricamento (e quindi di popolamento ulteriore della cache) dei dati in relazione ad una operazione di controllo di un articolo. Si noti la possibilità di modificare l‘XML appena visto introducendo il page-size:
Article on-load 5 check
che produce la seguente select
SELECT t0_o.NAME , t0_o.TITLE FROM ARTICLE t0_o WHERE t0_o. ID = ?OR t0_o. ID = ?OR t0_o. ID = ?OR t0_o. ID = ?OR t0_o. ID = ?OR t0_o. ID = ?
Lo scopo di tale operazione è quello di dar vita ad una qualche forma di paginazione dei dati precaricando non solo gli attributi di un bean che si prevede possano servire in un prossimo futuro, ma anche le righe della tabella (ovvero i bean) che si suppone di dover accedere poco dopo.
Ovviamente in questo caso entrano in gioco considerazioni sull‘effettiva utilità di caricare una riga ‘vicina‘ e soprattutto sul ‘se e come‘ i dati possano in qualche modo essere considerati legati da un legame di continuità e contiguità .
A livello teorico non è possibile fornire ulteriori indicazioni e solo una attenta analisi del dominio specifico potrà dare le risposte cercate.
In linea di massima si consideri il meccanismo di paginazione non come una forma di ottimizzazione ma di prevenzione dal tanto temuto ‘out of memory‘ che si potrebbe verificare in corrispondenza di select ad ampio spettro. Prima di ogni forma di premonizione e ottimizzazione vale la regola d‘oro dei tempi in cui si programmava direttamente in SQL: mai select * senza where.
Caricamento preventivo per relazioni
Il meccanismo di precaricamento può essere messo in atto anche in concomitanza di interrogazioni che coinvolgano relazioni fra bean. Analogamente ai casi appena visti, il caricamento di gruppo permette di precaricare i dati ‘navigando‘ la relazione
Article-has-Writers Article Writers on-find 5
Anche in questo caso è possibile definire la dimensione della paginazione effettuata sulle entità (Writers) puntate dalla entità principale (Article). Ovvio che paginare il numero di oggetti dipendenti ha significato solo nel caso di una relazione uno-a-molti.
L‘impatto dell‘uso delle transazioni
L‘utilizzo delle transazioni ha certamente un forte impatto sulla efficienza delle tecniche di ottimizzazione basate sul pre-caricamento. Un qualsiasi meccanismo di cache (non ultimo quello di JBossCMP) può essere messo in atto con successo solo se le informazioni in memoria possono essere considerate consistenti con la versione salvata nel database o nel sistema di memorizzazione secondario.
All‘interno di una transazione questo è certamente vero (i dati sono isolati per definizione) e quindi l‘utilizzo della cache è certamente consentito e corretto fino alla commit della transazione stessa.
Fuori da una transazione i dati possono essere modificati in ogni momento nel database, cosa che potrebbe in alcuni casi portare a risultati del tutto inattesi.
Per questo motivo, contrariamente a quanto a volte si è portati a credere, il mancato utilizzo delle transazioni può portare a un degrado delle prestazioni.
Bibliografia
‘Architetture e tecniche di progettazione EJB: IV parte: trasmissione dati fra strati con DTO‘ di Giovanni Puliti – MokaByte 103 – Gennaio 2006