Archive for ottobre 20, 2006

Un MDB singleton in un cluster di JBoss AS 4.x

Finisce anche questa settimana… contraddistinta dal’imperversare dell’influenza in ufficio (…proprio ora sono di ritorno dalla visita medica). Entro questa settimana mi ero prefissato di tirar su un singleton MDB in un ambiente JBoss clusterizzato e, nonostante le notti insonni per il naso completamente chiuso, alla fine ci sono riuscito.

JBoss Messaging è un JMS Provider ad altissime prestazioni, è una completa riscrittura di JBossMQ (il JMS Provider di default in JBoss AS 4.x) e sarà il JMS Provider di default in JBoss AS 5.x. Purtroppo, alla sua attuale versione, 1.0.1.GA, JBoss Messaging non offre supporto al clustering e dovrò pertanto utilizzare JBossMQ, riservandomi la possibilità di switchare a JBoss Messaging quando verrà rilasciata la versione 1.0.2. Il passaggio non dovrebbe riservare grosse criticità in quanto JBoss Messaging implementa JMS 1.1 ed è compatibile con JMS 1.0.2b, pertanto il codice JMS scritto per JBossMQ può girare senza alcuna modifica in JBoss Messaging… ma questa è qualcosa che potrò confermare solo al momento opportuno.

Prima di addentrarmi nella configurazione JMS, ho preferito focalizzare bene le mie conoscenze sulla gestione del cluster da parte di JBoss.

JBoss offre pieno supporto al clustering, permettendo così la distribuzione del carico su diversi servers. Il componente singolo del cluster è detto nodo (un’istanza di JBoss) e più nodi possono essere raggruppati insieme definendo la stessa “partition” name nel run.

Per es.

run.sh -c MyApp -b 192.168.0.10 -Djboss.partition.name=myPartition
run.sh -c MyApp -b 192.168.0.11 -Djboss.partition.name=myPartition

La maggior parte dei servizi esposti da JBoss AS (JNDI, EJB, RMI, JBoss Remoting, ecc.) richiede che il client ottenga (tramite lookup e download) un oggetto stub (o proxy). Lo stub è generato dal server ed implementa le interfacce di business del servizio. A questo punto il client effettua chiamate “locali” sull’oggetto stub. In un ambiente clusterizzato, lo stub generato dal server è anche un interceptor (proxy) che sa come indirizzare le chiamate ai nodi del cluster (conosce gli indirizzi ip dei nodi disponibili, l’algoritmo per la distribuzione del carico e sa come rimediare ad una chimata fallita). Ogni volta che viene richiesto un servizio viene aggiornato lo stub interceptor alle ultime modifiche nella composizione del cluster.

Lookup Servizio con una richiesta non-HTTP

Quando i servizi richiesti viaggiano su HTTP (per es. WebService) al client non è richiesto alcun oggetto stub, il ruolo dell’interceptor viene effettuato da un Load-balancer.

Lookup Servizio con una richiesta HTTP

Il servizio di JNDI è vitale per un application server e, in un ambiente clusterizzato di JBoss, è l’HA-JNDI (High Availability JNDI) il servizio che gestisce questo contesto nel cluster. L’albero di contesto JNDI di un cluster è sempre disponibile finchè c’è almeno un nodo nel cluster. A sua volta, ogni nodo del cluster, mantiene un proprio contesto JNDI (locale). Dal lato server, ogni new InitialContext() crea un contesto JNDI locale (per es. le EJB homes). Nel caso di una lookup remota, invece, se l’oggetto richiesto è disponibile nel contesto JNDI clusterizzato allora viene ritornato, altrimenti viene effettuata una lookup nel contesto JNDI locale. Se anche quest’ultima non ritorna l’oggetto richiesto, allora l’HA-JNDI chiede a tutti i nodi del cluster se nella loro JNDI locale è presente l’oggetto richiesto. Se anche questa lookup fallisce viene restituita una NameNotFoundException. Pertanto, un client JNDI deve essere a conoscenza dell’HA-JNDI cluster e come parametro PROVIDER_URL per la lookup dovrà passare la lista degli ip dei nodi del cluster con la porta HA-JNDI (1100).

Per esempio:

Properties p = new Properties();
p.put(Context.INITIAL_CONTEXT_FACTORY, "org.jnp.interfaces.NamingContextFactory");
p.put(Context.URL_PKG_PREFIXES, "jboss.naming:org.jnp.interfaces");
p.put(Context.PROVIDER_URL, "192.168.0.10:1100,192.168.0.11:1100");
return new InitialContext(p);

Tutta questa pappardella mi è servita per arrivare al punto cruciale: a partire dalla versione 3.2.4, JBoss AS supporta l’high availability JMS (HA-JMS) nella configurazione “all”, implementato come servizio singleton clusterizzato in modalità fail-over (se il nodo master diventa irraggiungibile, allora verrà nominato un nuovo master su cui verrà deploiato l’HA-JMS).
Per poter utilizzare l’HA-JMS occorre configurare i servizi JMS in modo identico su ogni nodo del cluster (nella directory $JBOSS_SERVER/deploy-hasingleton/jms). Di default, tutti i servizi JMS vengono persistiti su Hypersonic (hsqldb), un database java che risiede in memoria, il quale è appropriato in fase di sviluppo e di test, ma non è assolutamente adatto in un ambiente di produzione. Infatti in caso di avvio con servizi JMS di default, si riceverebbe un warning sul log di console:

JBoss Messaging Warning: DataSource connection transaction isolation should be READ_COMMITTED, but it is currently NONE.
Using an isolation level less strict than READ_COMMITTED may lead to data consistency problems.
Using an isolation level more strict than READ_COMMITTED may lead to deadlock.

La prima cosa da fare è quindi settare un db esterno per la persistenza. Dal momento che i servizi JMS utilizzano il datasource di Default (DefaultDS) le strade che si possono percorrere sono 2:

  • creo un datasource di default che punta la mio db esterno
  • creo un datasource con un mio JNDI name e mi ricordo di modificare anche i files xml dei servizi JMS in modo che utilizzino il mio datasource.

Anche se l’approccio KISS rimane sempre il mio preferito, ho optato per la seconda soluzione, se non altro per avere una visuale più pulita (ammesso che questa possa essere una motivazione valida).

Creo quindi il file del datasource con un mio JNDI Name, che punta ad un database di Oracle10g.

myapp-jms-oracle-ds.xml

<datasources>
    <local-tx-datasource>
        <jndi-name>MyOracleDS</jndi-name>
        <connection-url>jdbc:oracle:thin:@192.168.0.15:1521:XE</connection-url>
        <driver-class>oracle.jdbc.driver.OracleDriver</driver-class>
        <user-name>myuser</user-name>
        <password>myuser</password>
        <exception-sorter-class-name>org.jboss.resource.adapter.jdbc.vendor.OracleExceptionSorter</exception-sorter-class-name>
        <metadata>
            <type-mapping>Oracle9i</type-mapping>
        </metadata>
    </local-tx-datasource>
</datasources>

Elimino il file hsqldb-jdbc2-service.xml e creo il file per la configurazione dei servizi di persistenza e cache configurato ad hoc per un db oracle (per altri db, i files di configurazione sono disponibili in $JBOSS_HOME/doc/examples/jms).

myapp-jms-oracle-jdbc2-service.xml (il file può avere qualsiasi nome che rispetti il pattern ..*-service.xml)

<server>
    <mbean code="org.jboss.mq.server.jmx.DestinationManager" name="jboss.mq:service=DestinationManager">
        <depends optional-attribute-name="MessageCache">jboss.mq:service=MessageCache</depends>
        <depends optional-attribute-name="PersistenceManager">jboss.mq:service=PersistenceManager</depends>
        <depends optional-attribute-name="StateManager">jboss.mq:service=StateManager</depends>
    </mbean>    

    <mbean code="org.jboss.mq.server.MessageCache" name="jboss.mq:service=MessageCache">
        <attribute name="HighMemoryMark">50</attribute>
        <attribute name="MaxMemoryMark">60</attribute>
        <attribute name="CacheStore">jboss.mq:service=PersistenceManager</attribute>
    </mbean>    

    <mbean code="org.jboss.mq.pm.jdbc2.PersistenceManager" name="jboss.mq:service=PersistenceManager">
        <depends optional-attribute-name="ConnectionManager">jboss.jca:service=DataSourceBinding,name=MyOracleDS</depends>
        <attribute name="SqlProperties">
            INSERT_EMPTY_BLOB = INSERT INTO JMS_MESSAGES (MESSAGEID, DESTINATION, MESSAGEBLOB, TXID, TXOP) VALUES(?,?,EMPTY_BLOB(),?,?)
            LOCK_EMPTY_BLOB = SELECT MESSAGEID, MESSAGEBLOB FROM JMS_MESSAGES WHERE MESSAGEID = ? AND DESTINATION = ? FOR UPDATE
            BLOB_TYPE=BINARYSTREAM_BLOB
            INSERT_TX = INSERT INTO JMS_TRANSACTIONS (TXID) values(?)
            INSERT_MESSAGE = INSERT INTO JMS_MESSAGES (MESSAGEID, DESTINATION, MESSAGEBLOB, TXID, TXOP) VALUES(?,?,?,?,?)
            SELECT_ALL_UNCOMMITED_TXS = SELECT TXID FROM JMS_TRANSACTIONS
            SELECT_MAX_TX = SELECT MAX(TXID) FROM (SELECT MAX(TXID) AS TXID FROM JMS_TRANSACTIONS UNION SELECT MAX(TXID) AS TXID FROM JMS_MESSAGES)
            DELETE_ALL_TX = DELETE FROM JMS_TRANSACTIONS
            SELECT_MESSAGES_IN_DEST = SELECT MESSAGEID, MESSAGEBLOB FROM JMS_MESSAGES WHERE DESTINATION=?
            SELECT_MESSAGE_KEYS_IN_DEST = SELECT MESSAGEID FROM JMS_MESSAGES WHERE DESTINATION=?
            SELECT_MESSAGE = SELECT MESSAGEID, MESSAGEBLOB FROM JMS_MESSAGES WHERE MESSAGEID=? AND DESTINATION=?
            MARK_MESSAGE = UPDATE JMS_MESSAGES SET TXID=?, TXOP=? WHERE MESSAGEID=? AND DESTINATION=?
            UPDATE_MESSAGE = UPDATE JMS_MESSAGES SET MESSAGEBLOB=? WHERE MESSAGEID=? AND DESTINATION=?
            UPDATE_MARKED_MESSAGES = UPDATE JMS_MESSAGES SET TXID=?, TXOP=? WHERE TXOP=?
            UPDATE_MARKED_MESSAGES_WITH_TX = UPDATE JMS_MESSAGES SET TXID=?, TXOP=? WHERE TXOP=? AND TXID=?
            DELETE_MARKED_MESSAGES_WITH_TX = DELETE FROM JMS_MESSAGES MESS WHERE TXOP=:1 AND EXISTS (SELECT TXID FROM JMS_TRANSACTIONS TX WHERE TX.TXID = MESS.TXID)
            DELETE_TX = DELETE FROM JMS_TRANSACTIONS WHERE TXID = ?
            DELETE_MARKED_MESSAGES = DELETE FROM JMS_MESSAGES WHERE TXID=? AND TXOP=?
            DELETE_TEMPORARY_MESSAGES = DELETE FROM JMS_MESSAGES WHERE TXOP='T'
            DELETE_MESSAGE = DELETE FROM JMS_MESSAGES WHERE MESSAGEID=? AND DESTINATION=?
            CREATE_MESSAGE_TABLE = CREATE TABLE JMS_MESSAGES ( MESSAGEID INTEGER NOT NULL, DESTINATION VARCHAR(255) NOT NULL, TXID INTEGER, TXOP CHAR(1),
            MESSAGEBLOB BLOB, PRIMARY KEY (MESSAGEID, DESTINATION) )
            CREATE_IDX_MESSAGE_TXOP_TXID = CREATE INDEX JMS_MESSAGES_TXOP_TXID ON JMS_MESSAGES (TXOP, TXID)
            CREATE_IDX_MESSAGE_DESTINATION = CREATE INDEX JMS_MESSAGES_DESTINATION ON JMS_MESSAGES (DESTINATION)
            CREATE_TX_TABLE = CREATE TABLE JMS_TRANSACTIONS ( TXID INTEGER, PRIMARY KEY (TXID) )
            CREATE_TABLES_ON_STARTUP = TRUE
        </attribute>
        <!-- Uncomment to override the transaction timeout for recovery per queue/subscription, in seconds -->
        <!--attribute name="RecoveryTimeout">0</attribute-->
        <!-- The number of blobs to load at once during message recovery -->
        <attribute name="RecoverMessagesChunk">0</attribute>
    </mbean>    

</server>

…ciò che è importante da modificare è il parametro di binding al datasource scelto (ammesso che si abbia optato per la seconda soluzione). Sempre nella stessa directory è disponibile anche il file hsqldb-jdbc-state-service.xml, che potrebbe indurre a rimuoverlo, in linea con quanto è stato fatto con hsqldb-jdbc2-service.xml. In realtà hsqldb-jdbc-state-service.xml ha un nome abbastanza infelice, infatti la configurazione è adatta a qualsiasi db SQL92 compliant (per pulizia lo rinominerei in myapp-jms-jdbc-state-service.xml). L’unica cosa da modificare è il puntamento al giusto JNDI name, che è settato a DefaultDS. Occorre infine creare il file per la configurazione della coda/e.

myapp-jms-destination-service.xml:

<server>
    <mbean code="org.jboss.mq.server.jmx.Queue" name="jboss.mq.destination:service=Queue,name=MyQueue">
        <depends optional-attribute-name="DestinationManager">jboss.mq:service=DestinationManager</depends>
        <depends optional-attribute-name="SecurityManager">jboss.mq:service=SecurityManager</depends>
        <attribute name="MessageCounterHistoryDayLimit">-1</attribute>
        <attribute name="SecurityConf">
            <security>
                <role name="guest" read="true" write="true"/>
                <role name="publisher" read="true" write="true" create="false"/>
                <role name="noacc" read="false" write="false" create="false"/>
            </security>
        </attribute>
    </mbean>
</server>

Ho preparato un task ant per effettuare il deploy dei servizi JMS personalizzati:

<target name="install-myapp-jms">
    <copy todir="${jboss.server}/deploy-hasingleton/jms" overwrite="yes">
        <fileset file="component/jms/etc/myapp-jms-oracle-ds.xml"/>
        <fileset file="component/jms/etc/myapp-jms-destinations-service.xml"/>
        <fileset file="component/jms/etc/myapp-jms-oracle-jdbc2-service.xml"/>
        <fileset file="component/jms/etc/myapp-jms-jdbc-state-service.xml"/>
    </copy>
    <delete file="${jboss.server}/deploy-hasingleton/jms/hsqldb-jdbc2-service.xml" failonerror="false"/>
</target>

Una volta tirati su i servizi JMS personalizzati, occorre deploiare un solo MDB per tutto il cluster. Creo quindi un MDB semplice che all’arrivo di un messaggio me lo printa sul log.

MyMDB.java:

package test;    

import org.apache.log4j.Logger;
import javax.ejb.MessageDrivenBean;
import javax.ejb.EJBException;
import javax.ejb.MessageDrivenContext;
import javax.jms.MessageListener;
import javax.jms.Message;    

public class MyMDB implements MessageDrivenBean, MessageListener {    

    private static final Logger logWriter = Logger.getLogger(MyMDB.class);    

    /**
     * Message driven context
     */
    private MessageDrivenContext messageDrivenContext;    

    public InboundRisultatiBean() {}    

    public void onMessage(Message message) {
        logWriter.debug("---- Called onMessage() ----");
        logWriter.debug("Messaggio: " + message);
        logWriter.debug("---- Exit onMessage() ----");
    }    

    public void ejbRemove() throws EJBException {}    

    public void setMessageDrivenContext(MessageDrivenContext messageDrivenContext) throws EJBException {
        this.messageDrivenContext = messageDrivenContext;
    }    

    public void ejbCreate() {}    

}

Setto la transazione a Required solo per il metodo onMessage().

ejb-jar.xml:

<ejb-jar>
    <enterprise-beans>
        <message-driven>
            <description>Singleton MDB, MyMDB</description>
            <ejb-name>MyMDB</ejb-name>
            <ejb-class>test.MyMDB</ejb-class>
            <transaction-type>Container</transaction-type>
            <acknowledge-mode>Auto-acknowledge</acknowledge-mode>
            <message-driven-destination>
                <destination-type>javax.jms.Queue</destination-type>
            </message-driven-destination>
        </message-driven>
    </enterprise-beans>        

    <assembly-descriptor>
        <container-transaction>
            <method>
                <ejb-name>MyMDB</ejb-name>
                <method-name>onMessage</method-name>
            </method>
            <trans-attribute>Required</trans-attribute>
        </container-transaction>
    </assembly-descriptor>    

</ejb-jar>

Infine setto in jboss.xml il bind dell’MDB alla mia coda e soprattutto scelgo come configuration-name “Singleton Message Driven Bean”, una configurazione già disponibile in JBoss AS senza dover specificare altri parametri nel tag <container-configurations>:

<jboss>
    <enterprise-beans>
        <message-driven>
            <ejb-name>MyMDB</ejb-name>
            <configuration-name>Singleton Message Driven Bean</configuration-name>
            <destination-jndi-name>queue/GTSRisultatiQueue</destination-jndi-name>
        </message-driven>
    </enterprise-beans>
</jboss>

Impacchetto il tutto in un jar e deploio su tutti i nodi nella directory $JBOSS_SERVER/deploy.hasingleton. Se il deploy è effettuato a caldo (hot-deploy), quando tutti i servizi JMS sono stati startati non si dovrebbero ricevere errori, se invece si avvia JBoss con l’mdb già presente in directory si dovrebbe ricevere un errore di JNDI name non trovata (si tratta di una JNDI name dei servizi JMS, per es. CachedConnectionManager, DefaultJMSProvider…).Tutto ciò avviene, in quanto, JBoss tenta di deploiare il jar (e quindi di attaccare l’mdb alla coda) quando ancora i servizi JMS non sono stati avviati.
Una delle soluzioni trovate in rete (che come al solito a me non ha funzionato), è quella di settare a true il parametro RecursiveSearch nel file $JBOSS_SERVER/conf/jboss-service.xml. Con riferimento invece alle dipendenze dei vari servizi di JBoss, ho modificato il file ejb-deployer.xml in $JBOSS_SERVER/deploy aggiungendo la dipendenza a ServerSessionPoolMBean (ultimo servizio JMS ad essere avviato):

<depends>jboss.mq:service=ServerSessionPoolMBean,name=StdJMSPool</depends>

Grazie a quest’ultimo ritocco tutti i tasselli dovrebbero essersi incastrati perfettamente… per verificarlo basta la prova del nove. Ho messo in cluster 3 nodi jboss, deploiando singolarmente il mio mdb. L’mdb è stato creato solo sul nodo su cui ho effettuato per primo il deploy. Ho provato ad inviare con uno stress test una serie di messaggi sulla coda con un client esterno a cui ho fornito la lista di provider url HA-JNDI. Tutti i msg sono stati letti in sequenza (senza concorrenza, essendoci un solo mdb) dall’unico nodo su cui era stato deploiato l’mdb. Infine con mia grande soddisfazione, ho buttato giù il nodo master dell’mdb e automaticamente il cluster ha scelto un nuovo nodo su cui creare l’mdb.

ottobre 20, 2006 at 7:45 PM 16 commenti


Calendario

ottobre: 2006
L M M G V S D
 1
2345678
9101112131415
16171819202122
23242526272829
3031  

Posts by Month

Posts by Category