Monitorare Tomcat tramite JMX dietro un firewall

Riprendiamo (dopo una pausa di più di un anno dall’ultima puntata) la serie “Me lo appunto qui almeno non me lo dimentico“.

Monitorare il carico delle applicazioni web che avete progettato e/o utilizzate all’interno dell’architettura distribuita che vi permette di offrire un determinato servizio, è fondamentale per una serie infinita di ragioni che esulano dagli scopi di questo articolo e che sono sicuro potete anche solo lontanamente immaginare.

Se l’applicazione web in questione è in Java ed utilizzate l’ottimo Apache Tomcat come application server, questo è fattibile abilitando un agente JMX remoto che vi permetterà di interrogare Tomcat per sapere in un determinato istante dati di natura statistica (es. quante volte è stata interrogata un’applicazione, quante volte è andata in errore ecc ecc) che vi permetteranno di tenere sempre sotto controllo il vostro sistema piuttosto che analizzare le prestazioni delle varie applicazioni per provare ad ottimizzarle.

Questo è fattibile modificando poche righe nei file di configurazione di Tomcat, che come ben specificato nella documentazione ufficiale e che per completezza vi riporto.

Nello specifico dovrete aggiungere i seguenti parametri alla variabile CATALINA_OPTS

-Dcom.sun.management.jmxremote \
-Dcom.sun.management.jmxremote.port=%my.jmx.port% \
-Dcom.sun.management.jmxremote.ssl=false \
-Dcom.sun.management.jmxremote.authenticate=false

(sostituite %my.jmx.port% con un numero maggiore o uguale di 1024 vostro piacimento) nel file $CATALINA_BASE/conf/tomcat.conf se non necessitate limitare l’accesso tramite username e password. In caso contrario i parametri da aggiungere alla medesima variabile saranno:

-Dcom.sun.management.jmxremote.authenticate=true \
-Dcom.sun.management.jmxremote.password.file=../conf/jmxremote.password \
-Dcom.sun.management.jmxremote.access.file=../conf/jmxremote.access \

e dovrete andare a modificare (o creare se non esiste) il file $CATALINA_BASE/conf/jmxremote.access, specificando i nomi degli utenti che potranno usare l’agente JMX remoto e i loro permessi come segue:

monitorRole readonly
controlRole readwrite

Quindi nel file $CATALINA_BASE/conf/jmxremote.password dovrete andare a specificare le password degli utenti che avranno accesso al servizio. Ad esempio:

monitorRole tomcat
controlRole tomcat

Se nulla vi separa dalla macchina dove risiedono le applicazioni che volete monitorare, tutto sarà pronto per funzionare. Se invece tra voi (o Cacti piuttosto che Munin o Nagios), c’è un firewall le cose si complicano.

In prima battuta dovrete andare a configurare il firewall in modo tale da permettere la comunicazione tra il server dove c’è Apache Tomcat e la vostra macchina (o il server dove avete installato il Cacti/Munin/Nagios di turno) in modo tale da permettere connessioni in entrata sul server applicativo sulla porta specificata in fase di configurazione.

Purtroppo però, come specificato in una piccola postilla nella documentazione ufficiale di Tomcat, le note dolenti non sono finite. Infatti leggendo quanto segue:

Note: The JSR 160 JMX-Adaptor opens a second data channel on a random port. That is a problem when you have a local firewall installed.

risulta evidente che vi toccherà lavorare di fino per evitare soluzioni poco ortodosse come quella di aprire tutte le porte tra le due macchine in questione.

La soluzione è scriversi un agente JMX in casa che utilizzi 2 porte ben definite per permettere di monitorare Tomcat, come invece non fa l’agente JMX che viene distribuito di default assieme a Tomcat stesso.

Se andate di fretta, potete provare ad utilizzare da Daniel Fuchs, ovvero:

/\*
 \* CustomAgent.java
 \*
 \* Copyright 2007 Sun Microsystems, Inc.  All Rights Reserved.
 \*
 \* Redistribution and use in source and binary forms, with or without
 \* modification, are permitted provided that the following conditions
 \* are met:
 \*
 \*   - Redistributions of source code must retain the above copyright
 \*     notice, this list of conditions and the following disclaimer.
 \*
 \*   - Redistributions in binary form must reproduce the above copyright
 \*     notice, this list of conditions and the following disclaimer in the
 \*     documentation and/or other materials provided with the distribution.
 \*
 \*   - Neither the name of Sun Microsystems nor the names of its
 \*     contributors may be used to endorse or promote products derived
 \*     from this software without specific prior written permission.
 \*
 \* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
 \* IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 \* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 \* PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 \* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 \* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 \* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 \* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 \* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 \* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 \* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 \* 
 \* Created on Jul 25, 2007, 11:42:49 AM
 \* 
 \*/

package example.rmi.agent;

import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.net.InetAddress;
import java.rmi.registry.LocateRegistry;
import java.util.HashMap;
import javax.management.MBeanServer;
import javax.management.remote.JMXConnectorServer;
import javax.management.remote.JMXConnectorServerFactory;
import javax.management.remote.JMXServiceURL;

/\*\*
 \* This CustomAgent will start an RMI COnnector Server using only
 \* port "example.rmi.agent.port".
 \*
 \* @author dfuchs
 \*/
public class CustomAgent {

    private CustomAgent() { }

    public static void premain(String agentArgs) 
	throws IOException {

        // Ensure cryptographically strong random number generator used
        // to choose the object number - see java.rmi.server.ObjID
        //
        System.setProperty("java.rmi.server.randomIDs", "true");

        // Start an RMI registry on port specified by example.rmi.agent.port
        // (default 3000).
        //
        final int port= Integer.parseInt(
                System.getProperty("example.rmi.agent.port","3000"));
        System.out.println("Create RMI registry on port "+port);
        LocateRegistry.createRegistry(port);

        // Retrieve the PlatformMBeanServer.
        //
        System.out.println("Get the platform's MBean server");
        MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();

        // Environment map.
        //
        System.out.println("Initialize the environment map");
        HashMap env = new HashMap();
        
        // This where we would enable security - left out of this
        // for the sake of the example....
        //

        // Create an RMI connector server.
        //
        // As specified in the JMXServiceURL the RMIServer stub will be
        // registered in the RMI registry running in the local host on
        // port 3000 with the name "jmxrmi". This is the same name the
        // out-of-the-box management agent uses to register the RMIServer
        // stub too.
        //
        // The port specified in "service:jmx:rmi://"+hostname+":"+port
        // is the second port, where RMI connection objects will be exported.
        // Here we use the same port as that we choose for the RMI registry. 
        // The port for the RMI registry is specified in the second part
        // of the URL, in "rmi://"+hostname+":"+port
        //
        System.out.println("Create an RMI connector server");
        final String hostname = InetAddress.getLocalHost().getHostName();
        JMXServiceURL url =
            new JMXServiceURL("service:jmx:rmi://"+hostname+
            ":"+port+"/jndi/rmi://"+hostname+":"+port+"/jmxrmi");
        
        // Now create the server from the JMXServiceURL
        //
        JMXConnectorServer cs =
            JMXConnectorServerFactory.newJMXConnectorServer(url, env, mbs);

        // Start the RMI connector server.
        //
        System.out.println("Start the RMI connector server on port "+port);
        cs.start();
    }
}

Compilatelo creando un jar specificando l’attributo Premain-Class nel suo manifest. Tutto questo è fattibile tramite Ant utilizzando un buildfile siffatto:

    <!-- Builds dist.agent.jar -->    
    <target name="-build-agent-jar"
        description="build an agent jar that can be used with -javaagent ">
        <jar basedir="${build.classes.dir}" 
            jarfile="${dist.agent.jar}">
                <manifest>
                    <attribute name="Premain-Class" value="example.rmi.agent.CustomAgent"/>
                </manifest>
        </jar>
        <echo>To use this application with agent try:</echo>
        <echo>java -Dexample.rmi.port=3000 -javaagent:${dist.agent.jar} -classpath <application-classpath> <application-main-class></echo>
    </target>

A questo punto non vi rimane che aggiungere i seguenti parametri:

 -Dexample.rmi.agent.port=<port> -javaagent:<agent.jar>

alla variabile CATALINA_OPTS del file $CATALINA_BASE/conf/tomcat.conf al posto di quelli specificati all’inizio (ovviamente indicando la porta che volete utilizzare al posto di port ed il percorso del vostro jar al posto di agent.jar).

Non vi ammorbo a questo punto ulteriormente su come realizzare un custom agent che permetta l’autenticazione degli utenti abilitanti ad utilizzare il servizio JMX remote, rimandandovi direttamente alla fonte.

Un pensiero riguardo “Monitorare Tomcat tramite JMX dietro un firewall

  1. ciao molto interessante come articolo ma avevo in mente di farti 2 domande, la prima e’ capire il motivo nell’avere 2 ruoli (read and write) per JMX, perche si hanno 2 ruoli? l’altra domanda e oltre a jmx esistono validi concorrenti da usare con tomcat?
    grazie

Rispondi