Utilisez JMX pour agir ou superviser votre daemon Java

Le contexte

L’exécution en daemon d’un process Java est simple, mais comment superviser ce daemon ? Comment vérifier qu’il tourne bien ? qu’il n’utilise pas trop de mémoire ? que les ressources (connections JDBC, etc.) sont OK ? et comment agir depuis une ligne de commande avec ce daemon ?

JMX – Définition

La solution est native dans java, mais souvent inexploitée et méconnue. Il s’agit de JMX.
JMX pour Java Management Extensions. Comme le dit Wikipédia, c’est une API Java permettant de gérer le fonctionnement d’une application en cours d’exécution. JMX utilise une connexion TCP sur un port défini et se connecte ainsi à la JVM.

C’est exactement ce qu’il nous faut ! Mais attention, se lancer tête baissée dans JMX peut être complexe.

Il existe une solution simple surtout si votre application est basé sur Spring

Coté Java Virtual Machine …

Tout d’abord, assurez vous de lancer votre JVM avec les options JMX tel que:

java -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=8085 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false  com.alinto.MonDaemon

Le point important (et modifiable) de cette ligne de commande, est le port tcp, ici on utilise le port 8085.

Pour s’assurer que le daemon est lancé avec JMX, rien de plus simple ! Voici le main de mon daemon :

package com.alinto.jmxdemo;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class MyDaemon
{
	private static final Logger	LOG	= LoggerFactory.getLogger( MyDaemon.class );

	/**
	 * Main method
	 * Starting up Spring framework context and start DaemonBean
	 *
	 * @param args
	 * @throws Exception
	 */
	public static void main( String[] args ) throws Exception
	{
		long startTime = System.currentTimeMillis();
		if ( System.getProperty( "com.sun.management.jmxremote" ) != null )
		{
			LOG.info( "JMX enabled." );
		}
		else
		{
			System.err.println( "JMX disabled. Please launch JVM with:  \n      -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=8085 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false\n\nto enable JMX.\n\n" );
		}
		ApplicationContext context = new ClassPathXmlApplicationContext( "context.xml" );
		DaemonBean daemonBean = context.getBean( DaemonBean.class );
		daemonBean.startup();
		LOG.info( "Started in : " + ( System.currentTimeMillis() - startTime ) + "ms. Server Ready !" );
	}

}

On vérifie simplement que la propriété com.sun.management.jmxremote est non nulle.

Préparer Spring Framework

Dans l’un de vos fichier spring (context.server.app.xml dans mon cas) il suffit de copier/coller le bout de conf suivant :

   <!-- JMX stuff. Creates an MBeanServer. -->
	<bean id="mbeanServer" class="org.springframework.jmx.support.MBeanServerFactoryBean">
		<property name="locateExistingServerIfPossible" value="true" />
	</bean>

	<!-- JMX stuff. Creates an MBeanServer. -->
	<bean id="exporter" class="org.springframework.jmx.export.MBeanExporter">
		<property name="autodetect" value="true" />
		<property name="namingStrategy" ref="namingStrategy" />
		<property name="assembler" ref="assembler" />
		<property name="server" ref="mbeanServer" />
	</bean>

	<bean id="attributeSource"
		class="org.springframework.jmx.export.annotation.AnnotationJmxAttributeSource" />

	<bean id="assembler"
		class="org.springframework.jmx.export.assembler.MetadataMBeanInfoAssembler">
		<property name="attributeSource" ref="attributeSource" />
	</bean>

	<bean id="namingStrategy"
		class="org.springframework.jmx.export.naming.MetadataNamingStrategy">
		<property name="attributeSource" ref="attributeSource" />
	</bean>

Bon, rien de sorcier ici, ce n’est que de la conf à copier/coller 😉

Coté Code ! (enfin)

Coté code, c’est on ne peut plus simple. Grâce aux annotations, c’est super rapide !
Identifier votre bean Spring à annoter (du moins le 1er)
une fois que c’est fait, ajouter cette annotation devant la ligne « public class … » comme l’exemple ci-dessous le montre:

/**
 * Daemon Bean
 * Main Spring bean
 * Starts and manage the daemon thread
 *
 * expose JMX methods to control the daemon
 * @author charles
 * @date 22 juin 2012
 */
@Component
@ManagedResource(objectName = "DaemonBean:name=DaemonBean", description = "Main daemon service")
public class DaemonBean

Il est important de respecter la syntaxe de l’objectName du type « nomService:name=nomService » car c’est un peu « normalisé » dirons nous… Concernant la description, c’est libre, et ça peut être utile pour vos utilisateurs.

La classe c’est fait, maintenant, les méthodes et les attributs

Comme pour la classe, on utilise les annotations:

   /**
	 * JMX Operation example
	 * Check service status get a short code : OK or FAILED
	 * @return OK or FAILED
	*/
	@ManagedOperation(description = "Check service status get a short code : OK or FAILED")
	public String checkServices()
	{
		LOG.info( "checkServices called" );
		// Check service here...
		this.nbOfCalls++;
		return "OK: nb of Calls=" + this.nbOfCalls;
	}

L’annotation « @ManagedOperation » permet d’identifier une méthode comme « accessible via JMX ». Ici ma méthode me permettra depuis
un check Nagios de vérifier que tout est pour dans mon Daemon.

Pour un attribut, c’est aussi simple.
Poser une annotation « @ManagerAttribute » devant votre getter par exemple, et le tour est joué!

	/**
	 * JMX Attribute example
	 * @return
	 */
	@ManagedAttribute(description = "Number of call to checkService")
	public int getNbOfCall()
	{
		LOG.info( "Number of call to checkService=" + this.nbOfCalls );
		return this.nbOfCalls;
	}

Comment appeler nos méthodes ?

Coté code, tout est ok, il nous faut donc tester et appeler nos méthodes depuis JMX. Pour cela, il suffit de lancer notre code (depuis votre eclipse ou IDE favoris par exemple) et ensuite lancer « jconsole ».
jconsole est la console JMX livrée avec le JDK. Il se situe dans le dossier bin de java. Depuis un terminal (ou une ligne de commande) tapez simplement « jconsole ».

Au lancement de jconsole, la fenêtre suivante apparaît. Choisissez le bon process Java ou tapez dans le champ « Remote Process » : « localhost:8085 ».

Une fois connecté, vous pouvez naviguer dans les différents onglets de jconsole, et allez à l’onglet « MBeans ».
Vous retrouverez ici notre « DaemonBean », avec ses attributs et ses méthodes.

Et dans la partie « Operations », vous pourrez lancer les différentes méthodes :

Pour agir depuis la ligne de commande

Il y a différentes méthodes, je vous propose ici d’utiliser jmxsh.

Depuis cet outil en ligne de commande, vous aurez accès à un mode « navigateur » en mode texte, qui vous permettra de visiter les différents beans exposés via jmx

$ ./jmxsh -h localhost -p 8085
jmxsh v1.0, Tue Jan 22 17:23:12 CET 2008

Type 'help' for help.  Give the option '-?' to any command
for usage help.

Starting up in shell mode.
%

Tapez simplement sur entrez pour découvrir les MBeans :

Entering browse mode.
====================================================

 Available Domains:

       1. DaemonBean
       2. JMImplementation
       3. com.sun.management
       4. java.lang
       5. java.util.logging

  SERVER: service:jmx:rmi:///jndi/rmi://localhost:8085/jmxrmi

====================================================
Select a domain:

Puis tapez « 1 » (qui correspond au domaine de notre « DaemonBean ») :

Select a domain: 1
====================================================

 Available MBeans:

       1. DaemonBean:name=DaemonBean

  SERVER: service:jmx:rmi:///jndi/rmi://localhost:8085/jmxrmi
  DOMAIN: DaemonBean

====================================================
Select an mbean:

Puis tapez encore « 1 » (qui correspond à notre « DaemonBean ») :

Select an mbean: 1
====================================================

 Attribute List:

       1. -r- int         NbOfCall

 Operation List:

       2. void        shutdown()
       3. String      checkServices()
       4. int         getNbOfCall()

  SERVER: service:jmx:rmi:///jndi/rmi://localhost:8085/jmxrmi
  DOMAIN: DaemonBean
  MBEAN:  DaemonBean:name=DaemonBean

====================================================
Select an attribute or operation:

Tapez « 3 » pour lancer la méthode « checkServices() » :

Select an attribute or operation:  3
=====================================================

   Invoking Operation checkServices

Result: OK: nb of Calls=1
Press enter to continue.

Une fois que vous avez compris le concept, vous pouvez créer une commande pour jmxsh avec un fichier tcl.

# launch checkServices
set java_obj [jmx_invoke -n -m DaemonBean:name=DaemonBean checkServices]
set res [$java_obj toString]
puts $res

puis l’appeler avec la commande :

$./jmxsh -h localhost -p 8085 chechServices.tcl
OK: nb of Calls=2

Conclusion

Vous n’aurez plus d’excuses maintenant pour créer un daemon « manageable » qui vous donne tous les éléments pour assurer son bon fonctionnement 🙂

Vous pouvez télécharger les sources du projet :
AlintoJMXDemo

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *