Monitorer une JVM en local

MonitoringLe JDK de Sun/Oracle propose plusieurs outils et APIs pour monitorer une jvm locale et ceci, depuis le JDK 6, sans avoir besoin de configurer cette jvm de manière particulière (enfin, ca c’est dans les cas simples 🙂 nous verrons qu’il faut parfois ajouter une petite option)

Les outils du JDK

Le JDK propose deux utilitaires avec une interface graphique : JConsole et JVisualVM. Le premier permettant principalement d’accéder facilement aux MBeans JMX exposés par la JVM. Le deuxième étant beaucoup plus complet, en particulier si vous utilisez les plugins en téléchargement optionnel.

Il propose aussi des outils en ligne de commande :

  • jps (liste des process java)
  • jstat (statistique sur le jvm en particulier sur l’état de la mémoire)
  • jstack (pour demander un thread dump), jmap (pour demander un heap dump)

Tous ces outils sont présents dans le répertoire bin du JDK.

Ces outils sont soumis à quelques restrictions :

  • certaines sont liées à des questions de sécurité. En particulier, ils ne sont souvent utilisables que par l’utilisateur qui exécute les jvms à monitorer
  • d’autres restrictions sont moins évidentes ; elles sont en partie décrites ici : http://visualvm.java.net/troubleshooting.html

Sous windows, je rencontre actuellement des problèmes lorsque je me connecte à un serveur à distance (via Remote Desktop) : généralement, il faut toujours au moins que je me connecte en mode admin (mstsc.exe /admin (ou /console dans les anciennes versions) ) pour que les outils de monitoring puisse fonctionner. Mais cela ne suffit pas toujours …

Accès programmatiques

Ces outils sont très intéressants, mais il est parfois nécessaire d’accéder à des métriques particulières d’une jvm ou de l’application que la jvm héberge. Ou ils peuvent tout simplement ne pas fonctionner dans certains cas particuliers.

Dans la suite de ce post, je vais me focaliser sur les différentes façons d’accéder au serveur jmx des jvms en local sans avoir à configurer l’accès distant : qui peut se révéler complexe si l’on veut que cet accès reste sécurisé ( tout est dédrit ici : http://docs.oracle.com/javase/7/docs/technotes/guides/management/agent.html )

L’API Attach

L’utilisation de l’API Attach pour se connecter à un serveur jmx en local est décrit sur la même page web que l’accès distant :

http://docs.oracle.com/javase/7/docs/technotes/guides/management/agent.html#gdhkz

Cette api permet essentiellement :

  • de découvrir les jvms disponibles
  • récupérer quelques  informations sur ces jvms (propriétés système et propriétés des agents chargés)
  • charger un agent à l’intérieur de la JVM cible

Cette dernière fonctionnalité permet en particulier de charger l’agent JMX si il n’est pas déjà lancé. On peut lancer l’agent jmx au démarrage de la jvm via l’option -Dcom.sun.management.jmxremote. A partir du JDK 6, c’est grâce à l’API Attach, que cette option n’est souvent plus nécessaire : l’agent peut être chargé à la demande comme montré dans l’exemple ci-dessous.

(ce programme nécessite que le jar tools.jar présent dans le répertoire lib du JDK soit dans le classpath)

 package jmx; import java.io.BufferedReader; import java.io.InputStreamReader; import java.io.File; import java.lang.management.ManagementFactory; import java.lang.management.RuntimeMXBean; import java.util.List; import javax.management.MBeanServerConnection; import javax.management.remote.JMXConnector; import javax.management.remote.JMXConnectorFactory; import javax.management.remote.JMXServiceURL; import com.sun.tools.attach.VirtualMachine; import com.sun.tools.attach.VirtualMachineDescriptor; public class JmxAttachAPI { public static void main(String[] args) throws Exception { new JmxAttachAPI().dotIt(); } static final String CONNECTOR_ADDRESS = "com.sun.management.jmxremote.localConnectorAddress"; private void dotIt() throws Exception { System.out.println("Available local vms : "); List vmdList = VirtualMachine.list(); for(VirtualMachineDescriptor vmd : vmdList) { System.out.printf("%s : %s%n", vmd.id() , vmd.displayName()); } // Choose one vm : System.out.println(); BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in)); String pid = ""; while(pid.length() == 0) { System.out.println("vm id ? "); pid = stdin.readLine(); } System.out.println(); System.out.printf("Connecting to jvm : %s%n",pid); VirtualMachine vm = VirtualMachine.attach(pid); String connectorAddress = null; try { // get the connector address connectorAddress = vm.getAgentProperties().getProperty(CONNECTOR_ADDRESS); // no connector address, so we start the JMX agent if (connectorAddress == null) { System.out.println("Agent not Started, loading it ..."); String agent = vm.getSystemProperties().getProperty("java.home") + File.separator + "lib" + File.separator + "management-agent.jar"; vm.loadAgent(agent); // agent is started, get the connector address connectorAddress = vm.getAgentProperties().getProperty(CONNECTOR_ADDRESS); } else { System.out.println("JMX Agent already started !"); } } finally { vm.detach(); } System.out.println(); System.out.printf("Connecting to jmx server with connectorAddress : %s%n",connectorAddress); // establish connection to connector server JMXServiceURL url = new JMXServiceURL(connectorAddress); JMXConnector connector = JMXConnectorFactory.connect(url); MBeanServerConnection con = connector.getMBeanServerConnection(); RuntimeMXBean runtime = ManagementFactory.newPlatformMXBeanProxy( con, ManagementFactory.RUNTIME_MXBEAN_NAME, RuntimeMXBean.class); System.out.printf("Extracted classpath : %s%n",runtime.getClassPath()); } }

Et voici un exemple de son exécution :

Available local vms : 8372 : org.apache.catalina.startup.Bootstrap start 2108 : jmx.JmxAttachAPI vm id ? 8372 Connecting to jvm : 8372 Agent not Started, loading it Connecting to jmx server with connectorAddress : service:jmx:rmi://127.0.0.1/stub/rO0ABXNyAC5qYXZheC5tYW5hZ2VtZW50LnJlbW90ZS5ybWkuUk1JU2VydmVySW1wbF9TdHViAAAAAAAAAAICAAB4cgAaamF2YS5ybWkuc2VydmVyLlJlbW90ZVN0dWLp/tzJi+FlGgIAAHhyABxqYXZhLnJtaS5zZXJ2ZXIuUmVtb3RlT2JqZWN002G0kQxhMx4DAAB4cHc6AAtVbmljYXN0UmVmMgAADzE2OS4yNTQuMTQxLjIwNAAA1Qkf/eH4sQVkBOeL7SsAAAE1TnnPy4ABAHg= Extracted classpath : C:\products\apache-tomcat-6.0.32\bin\bootstrap.jar

Après avoir listé les jvms disponibles, ce programme s’attache à la jvm sélectionné et regarde si l’agent jmx est déjà chargé (en cherchant la propriété com.sun.management.jmxremote.localConnectorAddress)

Si il ne la trouve pas, il demande à la jvm de charger cet agent (l’agent est dans le jar management-agent.jar présent dans le répertoire jre/lib/ de la jvm cible)

Notez que la JConsole fait exactement la même chose : elle l’indique d’ailleurs dans la fenêtre de connexion en affichant “Note : The management agent will be enabled on this process” sous la liste des process disponibles lorsque le process sélectionné n’a pas encore l’agent JMX chargé

Une fois l’adresse jmx locale récupérée, l’API Attach a terminé son travail. L’api JMX prend ensuite le relai.

Ici on se contente d’extraire le classpath de la JVM cible. Mais l’ensemble des MBeans JMX de la JVM sont maintenant accessibles : que ce soit les MBeans de la JVM ou les MBeans que l’application aurait exposé dans le serveur JMX de la JVM

L’API MonitoredHost

Dans certains cas, l’API Attach ne fonctionne pas … (lorsque la JVM est exécutée par un service windows en particulier ; du moins dans l’environnement où je travaille actuellement)

JConsole utilise une 2ème API pour lister les jvms et extraire des infos : sun.jvmstat.monitor.MonitoredHost. Il utilise aussi une autre api bas niveau sun.management.ConnectorAddressLink : pour récupérer l’adresse JMX local

La limitation principale de cette api est qu’elle n’est pas capable d’activer l’agent JMX s’il ne l’est pas déjà. Il faut donc l’avoir activer explicitement via l’option -Dcom.sun.management.jmxremote (même avec un jdk 6 ou plus)

Sous réserve que l’agent jmx soit activé, on peut donc accéder au serveur JMX de cette façon :

(ce programme, très fortement inspiré du code source de la JConsole, nécessite aussi que le jar tools.jar soit dans le classpath)

 package jmx; import java.io.BufferedReader; import java.io.InputStreamReader; import java.lang.management.ManagementFactory; import java.lang.management.RuntimeMXBean; import java.util.HashMap; import java.util.Map; import java.util.Set; import javax.management.MBeanServerConnection; import javax.management.remote.JMXConnector; import javax.management.remote.JMXConnectorFactory; import javax.management.remote.JMXServiceURL; import sun.jvmstat.monitor.HostIdentifier; import sun.jvmstat.monitor.MonitoredHost; import sun.jvmstat.monitor.MonitoredVm; import sun.jvmstat.monitor.MonitoredVmUtil; import sun.jvmstat.monitor.VmIdentifier; import sun.management.ConnectorAddressLink; public class JmxMonitoredHost { public static void main(String[] args) throws Exception { new JmxMonitoredHost().doIt(); } private void doIt() throws Exception { MonitoredHost localMonitoredHost = MonitoredHost.getMonitoredHost(new HostIdentifier((String)null)); Set activeVms = localMonitoredHost.activeVms(); Map pidToAddress = new HashMap(); System.out.println("Available local vms : "); for(Integer vmId : activeVms) { String pid = vmId.toString(); try { MonitoredVm localMonitoredVm = localMonitoredHost.getMonitoredVm(new VmIdentifier(pid)); String commandLine = MonitoredVmUtil.commandLine(localMonitoredVm); boolean attachable = MonitoredVmUtil.isAttachable(localMonitoredVm); String connectorAddress = ConnectorAddressLink.importFrom(vmId.intValue()); System.out.printf("%5s %30s %4s%n",pid,commandLine,attachable); pidToAddress.put(pid,connectorAddress); localMonitoredVm.detach(); } catch (Exception e) { e.printStackTrace(); } } // Choose one vm : System.out.println(); BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in)); String pid = ""; while(pid.length() == 0) { System.out.println("vm id ? "); pid = stdin.readLine(); } // get the connector address String connectorAddress = pidToAddress.get(pid); System.out.println(); System.out.printf("Connecting to jmx server with connectorAddress : %s%n",connectorAddress); // establish connection to connector server JMXServiceURL url = new JMXServiceURL(connectorAddress); JMXConnector connector = JMXConnectorFactory.connect(url); MBeanServerConnection con = connector.getMBeanServerConnection(); RuntimeMXBean runtime = ManagementFactory.newPlatformMXBeanProxy( con, ManagementFactory.RUNTIME_MXBEAN_NAME, RuntimeMXBean.class); System.out.printf("Extracted classpath : %s%n",runtime.getClassPath()); } }

L’API LocalVirtualMachine de la JConsole

La JConsole a implémenté sa propre api de découverte au dessus des apis décrites ci-dessus : sun.tools.jconsole.LocalVirtualMachine ( On pourra trouvé son code source ici par exemple : http://javasourcecode.org/html/open-source/jdk/jdk-6u23/sun/tools/jconsole/LocalVirtualMachine.java.html)

Plutôt que d’utiliser ces deux Apis directement, il est donc plus simple d’utiliser LocalVirtualMachine

Cela nous donne ceci :

( ce programme nécessite tools.jar ET jconsole.jar dans la classpath pour fonctionner ) :

 package jmx; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.lang.management.ManagementFactory; import java.lang.management.RuntimeMXBean; import java.util.Map; import javax.management.MBeanServerConnection; import javax.management.remote.JMXConnector; import javax.management.remote.JMXConnectorFactory; import javax.management.remote.JMXServiceURL; import sun.tools.jconsole.LocalVirtualMachine; public class JmxJConsole { public static void main(String[] args) throws Exception { new JmxJConsole().doIt(); } private void doIt() throws Exception { Map allVirtualMachines = LocalVirtualMachine.getAllVirtualMachines(); System.out.println("Available local vms : "); for(Integer vmId : allVirtualMachines.keySet()) { LocalVirtualMachine vm = allVirtualMachines.get(vmId); System.out.printf("%10s %50s %s%n",vmId,vm.displayName(),vm.isAttachable()); } // Choose one vm : System.out.println(); BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in)); String pid = ""; while(pid.length() == 0) { System.out.println("vm id ? "); pid = stdin.readLine(); } // get the connector address LocalVirtualMachine vm = allVirtualMachines.get(new Integer(pid)); // load jmx agent if necessary/possible : vm.startManagementAgent(); String connectorAddress = vm.connectorAddress(); System.out.println(); System.out.printf("Connecting to jmx server with connectorAddress : %s%n",connectorAddress); // establish connection to connector server JMXServiceURL url = new JMXServiceURL(connectorAddress); JMXConnector connector = JMXConnectorFactory.connect(url); MBeanServerConnection con = connector.getMBeanServerConnection(); RuntimeMXBean runtime = ManagementFactory.newPlatformMXBeanProxy( con, ManagementFactory.RUNTIME_MXBEAN_NAME, RuntimeMXBean.class); System.out.printf("Extracted classpath : %s%n",runtime.getClassPath()); } }

Conclusion

Avec ce dernier code, vous devriez être paré pour vous connecter en local aussi bien que JConsole et extraire vos propres métriques ou lancer des opérations de diagnostique (au hasard : lancer un heap dump) via les Apis JMX de la jvm

Toutefois même la JConsole n’arrive pas tout le temps à se connecter ou à voir une jvm local.

En cas de soucis, voici au moins trois pré-requis qui m’ont été nécessaire récemment :

  • exécuter JConsole (ou votre code custom) avec le même utilisateur que la JVM cible
  • si vous vous connecté en remote desktop sur un serveur Windows : utiliser le mode admin : mstsc.exe /admin
  • activer explicitement l’agent jmx sur votre jvm cible (via -Dcom.sun.management.jmxremote)
Author image
Fort de plus de 20 ans d'expérience autour de l'écosystème JavaEE, Fabien accompagne ses clients dans la conception et le cadrage de leurs projets.