Tutoriel pour apprendre à monitorer une machine virtuelle Java en local

| Fabien Arrault | Ippon Technologies |

Le JDK de Sun/Oracle propose plusieurs outils et API pour monitorer une JVM (Machine Virtuelle Java) locale et ceci, depuis le JDK 6, sans avoir besoin de configurer cette JVM de manière particulière (enfin, cela c'est dans les cas simples, nous verrons qu'il faut parfois ajouter une petite option).

Pour réagir au contenu de cet article, un espace de dialogue vous est proposé sur le forum Commentez .

- Titre : Tutoriel pour apprendre à monitorer une machine virtuelle Java en local

- Auteurs : Fabien Arrault, Ippon Technologies

- Parution : 11 décembre 2013

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2013 Fabien Arrault. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Les outils du JDK

- II. Accès programmatiques

- II-A. L'API Attach

- III. L'API MonitoredHost

- IV. L'API LocalVirtualMachine de la JConsole

- V. Conclusion

- VI. Remerciements

I - 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 JVM à 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 puissent fonctionner. Mais cela ne suffit pas toujours…

II - 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 billet, je vais me focaliser sur les différentes façons d'accéder au serveur jmx des JVM 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écrit ici : http://docs.oracle.com/javase/7/docs/technotes/guides/management/agent.html).

II-A - L'API Attach

L'utilisation de l'API Attach pour se connecter à un serveur jmx en local est décrite 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 JVM disponibles ;

- de récupérer quelques informations sur ces JVM (propriétés système et propriétés des agents chargés) ;

- de charger un agent à l'intérieur de la JVM cible.

Cette dernière fonctionnalité permet en particulier de charger l'agent JMX s'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. À 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.

1. package jmx;

3. import java.io.BufferedReader;

4. import java.io.InputStreamReader;

5. import java.io.File;

6. import java.lang.management.ManagementFactory;

7. import java.lang.management.RuntimeMXBean;

8. import java.util.List;

10. import javax.management.MBeanServerConnection;

11. import javax.management.remote.JMXConnector;

12. import javax.management.remote.JMXConnectorFactory;

13. import javax.management.remote.JMXServiceURL;

15. import com.sun.tools.attach.VirtualMachine;

16. import com.sun.tools.attach.VirtualMachineDescriptor;

18. public class JmxAttachAPI {

19. public static void main(String[] args) throws Exception {

20. new JmxAttachAPI().dotIt();

21. }

23. static final String CONNECTOR_ADDRESS = "com.sun.management.jmxremote.localConnectorAddress";

25. private void dotIt() throws Exception {

27. System.out.println("Available local vms : ");

28. List vmdList = VirtualMachine.list();

29. for(VirtualMachineDescriptor vmd : vmdList) {

30. System.out.printf("%s : %s%n", vmd.id() , vmd.displayName());

31. }

33. // Choose one vm :

34. System.out.println();

35. BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in));

36. String pid = "";

37. while(pid.length() == 0)  {

38. System.out.println("vm id ? ");

39. pid = stdin.readLine();

40. }

42. System.out.println();

43. System.out.printf("Connecting to JVM : %s%n",pid);

44. VirtualMachine vm = VirtualMachine.attach(pid);

45. String connectorAddress = null;

46. try {

47. // get the connector address

48. connectorAddress = vm.getAgentProperties().getProperty(CONNECTOR_ADDRESS);

50. // no connector address, so we start the JMX agent

51. if (connectorAddress == null) {

52. System.out.println("Agent not Started, loading it ...");

53. String agent = vm.getSystemProperties().getProperty("java.home") +

54. File.separator + "lib" + File.separator + "management-agent.jar";

55. vm.loadAgent(agent);

57. // agent is started, get the connector address

58. connectorAddress =

59. vm.getAgentProperties().getProperty(CONNECTOR_ADDRESS);

60. } else {

61. System.out.println("JMX Agent already started !");

62. }

63. } finally {

64. vm.detach();

65. }

67. System.out.println();

68. System.out.printf("Connecting to jmx server with connectorAddress : %s%n",connectorAddress);

70. // establish connection to connector server

71. JMXServiceURL url = new JMXServiceURL(connectorAddress);

72. JMXConnector connector = JMXConnectorFactory.connect(url);

74. MBeanServerConnection con = connector.getMBeanServerConnection();

76. RuntimeMXBean runtime = ManagementFactory.newPlatformMXBeanProxy(

77. con, ManagementFactory.RUNTIME_MXBEAN_NAME, RuntimeMXBean.class);

78. System.out.printf("Extracted classpath : %s%n",runtime.getClassPath());

80. }

81. }

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 JVM disponibles, ce programme s'attache à la JVM sélectionnée et regarde si l'agent JMX est déjà chargé (en cherchant la propriété com.sun.management.jmxremote.localConnectorAddress).

S'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.

III - 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 deuxième API pour lister les JVM 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 locale.

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 activé 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.

1. package jmx;

2. import java.io.BufferedReader;

3. import java.io.InputStreamReader;

4. import java.lang.management.ManagementFactory;

5. import java.lang.management.RuntimeMXBean;

6. import java.util.HashMap;

7. import java.util.Map;

8. import java.util.Set;

10. import javax.management.MBeanServerConnection;

11. import javax.management.remote.JMXConnector;

12. import javax.management.remote.JMXConnectorFactory;

13. import javax.management.remote.JMXServiceURL;

15. import sun.JVMstat.monitor.HostIdentifier;

16. import sun.JVMstat.monitor.MonitoredHost;

17. import sun.JVMstat.monitor.MonitoredVm;

18. import sun.JVMstat.monitor.MonitoredVmUtil;

19. import sun.JVMstat.monitor.VmIdentifier;

20. import sun.management.ConnectorAddressLink;

22. public class JmxMonitoredHost {

24. public static void main(String[] args) throws Exception {

25. new JmxMonitoredHost().doIt();

26. }

28. private void doIt() throws Exception {

29. MonitoredHost localMonitoredHost = MonitoredHost.getMonitoredHost(new HostIdentifier((String)null));

30. Set activeVms = localMonitoredHost.activeVms();

32. Map pidToAddress = new HashMap();

34. System.out.println("Available local vms : ");

35. for(Integer vmId : activeVms) {

36. String pid = vmId.toString();

37. try {

38. MonitoredVm localMonitoredVm = localMonitoredHost.getMonitoredVm(new VmIdentifier(pid));

40. String commandLine = MonitoredVmUtil.commandLine(localMonitoredVm);

41. boolean attachable = MonitoredVmUtil.isAttachable(localMonitoredVm);

42. String connectorAddress = ConnectorAddressLink.importFrom(vmId.intValue());

44. System.out.printf("%5s %30s %4s%n",pid,commandLine,attachable);

45. pidToAddress.put(pid,connectorAddress);

47. localMonitoredVm.detach();

48. } catch (Exception e) {

49. e.printStackTrace();

50. }

51. }

53. // Choose one vm :

54. System.out.println();

55. BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in));

56. String pid = "";

57. while(pid.length() == 0)  {

58. System.out.println("vm id ? ");

59. pid = stdin.readLine();

60. }

62. // get the connector address

63. String connectorAddress = pidToAddress.get(pid);

64. System.out.println();

65. System.out.printf("Connecting to jmx server with connectorAddress : %s%n",connectorAddress);

67. // establish connection to connector server

68. JMXServiceURL url = new JMXServiceURL(connectorAddress);

69. JMXConnector connector = JMXConnectorFactory.connect(url);

71. MBeanServerConnection con = connector.getMBeanServerConnection();

73. RuntimeMXBean runtime = ManagementFactory.newPlatformMXBeanProxy(

74. con, ManagementFactory.RUNTIME_MXBEAN_NAME, RuntimeMXBean.class);

75. System.out.printf("Extracted classpath : %s%n",runtime.getClassPath());

76. }

77. }

IV - L'API LocalVirtualMachine de la JConsole

La JConsole a implémenté sa propre API de découverte au-dessus des API décrites ci-dessus : sun.tools.jconsole.LocalVirtualMachine (on pourra trouver 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 API 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.

1. package jmx;

3. import java.io.BufferedReader;

4. import java.io.IOException;

5. import java.io.InputStreamReader;

6. import java.lang.management.ManagementFactory;

7. import java.lang.management.RuntimeMXBean;

8. import java.util.Map;

10. import javax.management.MBeanServerConnection;

11. import javax.management.remote.JMXConnector;

12. import javax.management.remote.JMXConnectorFactory;

13. import javax.management.remote.JMXServiceURL;

15. import sun.tools.jconsole.LocalVirtualMachine;

17. public class JmxJConsole {

19. public static void main(String[] args) throws Exception {

20. new JmxJConsole().doIt();

21. }

23. private void doIt() throws Exception {

25. Map allVirtualMachines = LocalVirtualMachine.getAllVirtualMachines();

27. System.out.println("Available local vms : ");

28. for(Integer vmId : allVirtualMachines.keySet()) {

29. LocalVirtualMachine vm = allVirtualMachines.get(vmId);

30. System.out.printf("%10s %50s %s%n",vmId,vm.displayName(),vm.isAttachable());

31. }

33. // Choose one vm :

34. System.out.println();

35. BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in));

36. String pid = "";

37. while(pid.length() == 0)  {

38. System.out.println("vm id ? ");

39. pid = stdin.readLine();

40. }

42. // get the connector address

43. LocalVirtualMachine vm = allVirtualMachines.get(new Integer(pid));

44. // load jmx agent if necessary/possible :

45. vm.startManagementAgent();

46. String connectorAddress = vm.connectorAddress();

48. System.out.println();

49. System.out.printf("Connecting to jmx server with connectorAddress : %s%n",connectorAddress);

51. // establish connection to connector server

52. JMXServiceURL url = new JMXServiceURL(connectorAddress);

53. JMXConnector connector = JMXConnectorFactory.connect(url);

55. MBeanServerConnection con = connector.getMBeanServerConnection();

57. RuntimeMXBean runtime = ManagementFactory.newPlatformMXBeanProxy(

58. con, ManagementFactory.RUNTIME_MXBEAN_NAME, RuntimeMXBean.class);

59. System.out.printf("Extracted classpath : %s%n",runtime.getClassPath());

61. }

62. }

V - 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 diagnostic (au hasard : lancer un heap dump) via les API JMX de la JVM.

Toutefois même la JConsole n'arrive pas tout le temps à se connecter ou à voir une JVM locale.

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

- exécutez JConsole (ou votre code custom) avec le même utilisateur que la JVM cible ;

- si vous vous connectez en remote desktop sur un serveur Windows : utilisez le mode admin : mstsc.exe /admin ;

- activez explicitement l'agent jmx sur votre JVM cible (via -Dcom.sun.management.jmxremote).

VI - Remerciements

Cet article a été publié avec l'aimable autorisation de Fabien Arrault. L'article original peut être vu sur le blog/site de Ippon.

Nous tenons à remercier Claude Leloup pour sa relecture orthographique attentive de cet article et Mickael Baron et mlny84 pour la mise au gabarit.