Qualche tempo fa ci siamo occupati di un problema molto importante ma spesso sottovalutato in fase di progettazione e realizzazione di una applicazione Web: le performance. Come ben sappiamo un servizio Web o una applicazione in genere deve avere un tempo di risposta adeguato alle esigenze dell'utente e la potenza di calcolo attuale ci da la possibilità di aspettarci dei tempi di esecuzione molto bassi per le elaborazioni più comuni.
Nel suddetto articolo ci siamo occupati della fase di test, anzi, di stress test, dove attraverso uno strumento molto utile, JMeter, abbiamo visto come martellare un server e valutare l'eventuale degradazione delle prestazioni. Quando ci accorgiamo di questo degrado, dunque, quali sono gli accorgimenti da mettere in atto?
Impareremo in questo articolo a dimensionare (in inglese, tuning) una applicazione java enterprise (JEE). I concetti che esporremo sono dei concetti generali; noi vedremo un caso concreto applicato all'application server JBoss 5. Del resto la gestione del tuning di un application server dipende fortemente dall'implementazione dello stesso.
Misurare le performance
Uno strumento utile a valutare le prestazioni è il già citato Jmeter, ma, qualsiasi altro strumento vogliate utilizzare per mettere sotto assedio la vostra applicazione andrà bene. Altri due strumenti sono fondamentali a tal proposito:
- JConsole, con il quale valutare in tempo reale le prestazioni della JVM (in particolare il consumo di memoria) su cui l'application server sta girando
- JMX dalla quale console potrete valutare uno ad uno gli elementi in gioco (connessioni, ejb, etc.) durante un'esecuzione. Quest'ultimo strumento in particolare si può consultare per JBoss accedendo all'interfaccia presente su
http://localhost:8080/jmx-console
(con l'installazione di default)
Tuning Datasource
Il collo di bottiglia di qualsiasi connessione è tipicamente legato alla gestione delle connessioni presso una base dati. Nel mondo "pre-enterprise" la gestione delle connessioni al database non era una cosa standard, quindi, utilizzando i driver JDBC, ci dovevamo preoccupare di creare una nuova connessione ad ogni uso, usarla e chiuderla. Un nuovo utilizzo avrebbe comportato una nuova creazione. Best practice è stata sempre infatti tenere un pool di connessioni ed allocarle alla bisogna.
Il data source di fatto rende standard questa best practice mettendo a disposizione dello sviluppatore una sorgente dati (un database, ma potrebbe essere qualsiasi sorgente) da interrogare. Compito dell'application server creare un pool di connessioni e gestirne il ciclo di vita. Quello che in fase di configurazione dovremo fare sarà dimensionare questo pool per non avere un eccessivo consumo di risorse (ogni oggetto nel pool consuma memoria) né tantomeno trovarsi nella situazione di creare troppe nuove connessioni, o, peggio ancora, di aspettare che una si liberi.
Per JBoss in particolare questa gestione si configura attraverso un file di configurazione da installare nella la cartella deploy
del server in esecuzione (funziona per hot-deploy). In generale i parametri da configurare sono abbastanza standard in qualsiasi application server in quanto la logica astratta deve seguire gli standard JEE.
Le proprietà più importanti sono rappresentate dai tag seguenti (in parentesi i valori di default):
- Min-pool-size (0)
- Max-pool-size(20)
- Blocking-timeout-millis (30000)
- Idle-timeout-minutes(0)
I primi due parametri rappresentano il numero minimo e massimo di connessioni che un pool deve avere. Attraverso questi due parametri riusciamo a dimensionare il tempo di risposta in base al carico atteso. Blocking-timeout-millis
è molto importante perché è il tempo massimo che un thread è disposto ad aspettare in caso di pool completamente occupato. In questo modo evitiamo di creare code eccessivamente lunghe in caso di un pool limitato. Idle-timeout
definisce il tempo che l'application server attende prima di deallocare la connessione dal pool.
Questi semplici parametri base già ci danno la possibilità di ottimizzare lo stream verso la base dati conoscendo il carico atteso. Una buona domanda potrebbe essere: qual è la configurazione per servire alla meglio x
utenti concorrenti? Dipende.
In particolare dipende dal tipo di richieste fatte, dalla durata e dal "peso" delle query. Una configurazione standard dovrebbe essere comunque in grado di servire 100 utenti con correntemente per un sito web. Vediamo un esempio concreto.
//mysql-ds.xml <datasources> <local-tx-datasource> <jndi-name>DS</jndi-name> <connection-url>jdbc:mysql://localhost:3306/dbname</connection-url> <driver-class>com.mysql.jdbc.Driver</driver-class> <user-name>username</user-name> <password>pwd</password> <min-pool-size>5</min-pool-size> <max-pool-size>50</max-pool-size> <idle-timeout-minutes>1</idle-timeout-minutes> </local-tx-datasource> </datasources>
In questo caso concreto abbiamo deciso di settare il pool di connessioni tra 5 e 50 con un timeout massimo di 1 minuto. L'altra proprietà la ereditiamo di default. In questo caso stiamo definendo una local-tx-datasource in alcuni casi potremmo aver bisogno di altri tipi di data source (no-tx, xa, etc.) la cui discussione esula dagli scopi del presente articolo.
Per poter valutare il comportamento dell'applicazione, sotto stress test, dovremmo accedere al pannello JMX citato prima cercando il MBean che si chiama DS (il jndi-name che abbiamo assegnato) e valutare le seguenti proprietà:
- ConnectionCount
- AvailableConnectionCount
- MaxConnectionsInUseCount
- InUseConnectionCount
- ConnectionCreatedCount
- ConnectionDestroyedCount
Il nome delle proprietà è auto esplicativo. Analizzando questi parametri potrete trarre le conseguenze sul dimensionamento del vostro pool. La cosa migliore ovviamente è sempre fare diverse prove e valutare i risultati relativi.
Tuning HTTP Request pool
Il concetto di pool è di fondamentale importanza per l'ottimizzazione di un ambiente enterprise Java. Lo ritroviamo nella discussione del tuning delle richieste http. Una richiesta http è di fatto un thread che si prende in carico la richiesta di un cliente reale, la esegue e da la risposta. Affinchè il processo sia multiutente è ovvio che affidiamo questa logica a dei thread Java. In realtà questa logica è nascosta ai nostri occhi ed è totalmente affidata all'application server, l'unica cosa che possiamo fare è ridefinire i parametri per un uso efficiente in base al carico richiesto.
Il file che dobbiamo modificare è un file globale, quindi modifica la configurazione di tutto l'ambiente di esecuzione. Si tratta del file ../deployer/jboss.sar/server.xml
. Editando il file avrete modo di vedere diversi attributi del tag <Connector protocol="HTTP/1.1" ...>
tra le cuali spiccano:
- maxThreads (200)
- minSpareThreads (4)
- maxSpareThreads (50)
- acceptCount (10)
La configurazione di questo file impatta fortemente le performance relative alle petizioni web. MaxThreads
indica il numero massimo di connessioni contemporanee che il server può accettare, dopodiché crea una coda non più lunga di acceptCount. Se superiamo queste dimensioni il server ci risponderà con un errore 503
. Attraverso il bilanciamento degli spareThreads
facciamo in modo di avere sempre un numero minimo (e massimo) di thread in attesa di richieste.
Tuning web layer
Ci avviciniamo alla parte più vicina allo sviluppatore, la logica applicativa. Per web layer intendiamo il lato web di una applicazione enterprise, quindi, servlet e JSP che si interfacciano con il cliente finale.
Il ciclo di vita della servlet è uno dei punti forti della tecnologia enterprise grazie alla gestione del ciclo di vita fatta dal container. Unica accortezza che dobbiamo seguire è quella relativa alla sincronizzazione delle servlet. Se avete proprio la necessità di eseguire dei blocchi synchronized ricordate sempre di limitare questa operazione a dei singoli blocchi, se proprio non potete evitarla. Assoultamente da evitare è l'implementazione dell'interfaccia SingleThreadModel (tra l'altro deprecata nelle ultime versioni di Java).
Per quanto riguarda gli aspetti di configurazione una buona pratica è quella di dire al web container che il servizio è un servizio di produzione abilitando i seguenti parametri nel file ../deployer/jboss.deployer/web.xml
:
- development
- checkInterval
Impostando development=false
e checkInterval=300
(secondi) o un valore sufficientemente grande, facciamo in modo che il server faccia dei controlli su un eventuale cambio della JSP (servlet) dopo un tempo largo e non ad ogni richiesta.
Tuning application layer
In questo paragrafo vedremo come poter configurare ed ottimizzare la presenza di moduli di logica applicativa EJB (in particolare stateless session bean e message driven bean). Sull'uso di questa tecnologia abbiamo discusso nella guida J2EE ed abbiamo visto come proprio grazie al pooling degli EJB un servizio risenta di grandi benefici. Ma come configurare un pooling di EJB? Anche qui, come per gli altri servizi dell'application server tutto è fortemente dipendente dallo stesso server su cui sono eseguiti.
La specifica EJB3.0 che ha dato un enorme facilitazione in fase di sviluppo ha un po' messo da parte il famoso file di deploy della versione 2 a favore di uno sviluppo annotation-driven (tramite annotazioni). La configurazione in realtà è ancora presente ma preferiamo mostrare come già in fase di sviluppo possa essere effettuato un tuning di EJB 3.0 su application server JBoss 5.
La logica su cui si basa l'esecuzione di un metodo di un EJB è dettata dalla classe org.jboss.ejb3.pool.ThreadLocalPool che viene usata praticamente in tutti gli enterprise Java Bean per il meccanismo di accesso concorrente e pooling. Quello che dovremo fare è quindi ridefinire usando la seguente annotazione:
@Retention(RetentionPolicy.RUNTIME) @Target( {ElementType.TYPE}) public @interface Pool { String value() default PoolDefaults.POOL_IMPLEMENTATION_THREADLOCAL; int maxSize() default PoolDefaults.DEFAULT_POOL_SIZE; long timeout() default Long.MAX_VALUE; }
Quest'annotazione ci permette di indicare la politica di pooling, la dimensione massima del pool ed il timeout. Vediamo un esempio concreto:
import org.jboss.ejb3.annotation.Pool; import org.jboss.ejb3.annotation.defaults.PoolDefaults; @Stateless @Pool(value=PoolDefaults.POOL_IMPLEMENTATION_STRICTMAX,maxSize=100,timeout=1000) @Remote(AnotherBean.class) public class ASessionBean implements AnotherBean{ ... }
Questo particolare bean (ASessionBean
) è un bean di tipo stateless che conterrà un massimo di 100 bean nel pool e con la politica IMPLEMENTATION_STRICTMAX
(meglio usare quella di default per evitare la sincronizzazione). La stessa logica può essere estesa ai bean message-driven con la stessa annotazione. L'unica cosa da fare sarà includere nel classpath di compilazione il package relativo a quelle annotazioni (nella directory jboss/lib
).
La stessa operazione ovviamente può essere fatta utilizzando dei descrittori di deploy senza dover ricompilare un bean già in esecuzione. Essendo l'implementazione di JBoss 5 orientata agli aspetti questi file di configurazione possono risultare molto tediosi e difficili da comprendere, qui segue un esempio di tuning di un MDB.
Definiamo dapprima l'aspetto da applicare:
<?xml version="1.0" encoding="UTF-8"?> <aop xmlns="urn:jboss:aop-beans:1.0"> <domain name="Strictly Pooled Message Driven Bean" extends="Message Driven Bean" inheritBindings="true"> <annotation expr="!class(@org.jboss.ejb3.annotation.Pool)"> @org.jboss.ejb3.annotation.Pool (value="StrictMaxPool", maxSize=50, timeout=10000) </annotation> </domain> </aop>
Praticamente stiamo includendo l'annotazione nel file attraverso il tag annotation, difatti nel descrittore ejb citeremo l'aspetto appena creato:
<?xml version="1.0" encoding="utf-8"?> <jboss xmlns:xs="http://www.jboss.org/j2ee/schema" xs:schemaLocation=http://www.jboss.org/j2ee/schema jboss_5_0.xsd version="5.0"> <enterprise-beans> <message-driven> <ejb-name>TuningExample</ejb-name> <destination-jndi-name>queue/example</destination-jndi-name> <aop-domain-name>Strictly Pooled Message Driven Bean</aop-domain-name> </message-driven> </enterprise-beans> </jboss>
Clustering
Finora abbiamo visto come poter configurare un solo application server per trarne il massimo di performance. Il più delle volte però, seppure ben ottimizzato un solo server di applicazioni non è capace a far fronte alle richieste degli utenti (pensate a un grosso sito web come Google o Amazon). In questi casi possiamo ricorrere alla pratica del clustering che ci permette di scalare un applicazione senza grosse difficoltà. Questa possibilità è da mettere in pratica qualora tutte le ottimizzazioni viste ancora non permettono alla vostra applicazione dei sufficienti tempi di risposta.
Come per il resto delle configurazioni anche quella del clustering dipende dall'implementazione dell'application server, ma di questo aspetto ci occuperemo in un articolo in futuro.