Monitorando e gerenciando sua aplicação Java com JMX

Meu objetivo era escrever sobre o Actuator do Spring Boot, mas enquanto o escrevia lembrei algo importante: boa parte dos desenvolvedores Java não sabem que tem uma ferramenta maravilhosa para monitorar e gerenciar suas aplicações remotamente e que já vêm de "brinde" com a JVM: o JMX. Com ele você pode:

  • Acompanhar qualquer aspecto relativo ao ambiente de execução do seu projeto (incluindo o estado da sua aplicação).
  • Executar ações remotas em seu código: pense em coisas como limpar o cache, remover arquivos temporários, executar alguma ação, enfim, qualquer coisa.
  • Disparar notificações para outros módulos da sua plataforma, o que possibilita a construção de uma estratégia de manutenção e monitoramento pró-ativa.

E tudo isto com recursos que você já tem aí na sua aplicação Java, sem precisar instalar qualquer biblioteca ou framework. Só precisa saber Java (ou qualquer outra linguagem que gere bytecode e interaja com a JVM, como Groovy, Scala, Clojure, JavaScript...).

JMX é uma ferramenta poderosíssima que deve estar no cinto de utilidades de qualquer pessoa que desenvolva aplicações Java.

Meu objetivo é apresentar as funcionalidades que a tecnologia oferece e lhe dar a base para que possa se aprofundar no assunto. Não se preocupe: incluirei alguns links que vão te ajudar a se aprofundar no decorrer do texto.

Naturalmente, começaremos pelo conceitual e, mais à frente, veremos como aplicar na prática esta tecnologia.

O que é JMX?

Antes de descrever o que é JMX é importante fazer outra pergunta.

Que problemas JMX resolve?

Após terminar seu código segue o próximo passo: a implantação. Para uma mentalidade ingênua a história acabaria aí, mas eu e você sabemos que esta não é a verdade, longe disto! Uma vez implantada a sua aplicação você precisa monitorá-la e gerenciá-la. Monitorar, o que é isto? E gerenciar?

Monitorar é vigiar: acompanhar a qualidade de serviço da aplicação que você implantou. Como está o consumo de memória e CPU? Quantas transações foram realizadas com sucesso ou erro em um período pré-determinado? Quantos usuários estão autenticados? Quantas requisições estão em execução neste momento?

Gerenciar é interagir: você precisa executar algumas ações esporadicamente, tais como limpar caches, liberar espaço em disco, finalizar processos, iniciar ações de manutenção, alterar configurações, etc.

Se você está diante da máquina em que sua aplicação está em execução estas ações são relativamente simples. Basta ir a este equipamento e executar o seu trabalho, se for remoto, também é tranquilo, usa-se algum protocolo de comunicação como o SSH e tá tranquilo. Mas e se você precisar, durante a operação, reiniciar sua aplicação e, com isto, penitenciar seus usuários com o sistema fora do ar? Será que é possível executar estas ações sem parar a aplicação? Yeap: e JMX é uma solução para estes problemas.

Mas há mais uma situação: e se você pudesse ser alertado de que problemas estão prestes a ocorrer? Por exemplo: e se você pudesse receber alertas lhe informando que o número de requisições aumentou repentinamente ou que o espaço em disco está prestes a esgotar?

JMX resolve este problema também a partir do disparo de eventos, o que permite a adoção de estratégias pró-ativas de manutenção.

Resumindo, os principais problemas resolvidos pelo JMX são:

  • Oferecer uma ferramenta de monitoramento remoto (ou local) de aplicações.
  • Possibilitar a adoção de estratégias de manutenção pró-ativas.
  • Possibilita a gestão remota de sistemas feitos em Java.

Naturalmente você pode pensar em outros usos criativos pra tecnologia após ler este post, prossigamos.

Finalmente, o que é JMX?

A sigla JMX significa (Java Management Extensions): é o framework (não biblioteca, quer saber a definição de framwork, leia este meu texto) que fornece as estruturas para que possamos resolver os problemas que mencionei acima.

É interessante observar que estamos falando de uma tecnologia bem antiga e bastante estável: aparece pela primeira vez na JSR-3 do Java Community Process e que se tornou parte integral do Java SE a partir da sua versão 5. Temos portanto uma JSR com 22 anos de idade (aprovada em 1998) e 16 de aplicação massiva (lançamento do Java 5 em 2004). Nada mal, o que me faz questionar por que tão pouco usada. Este post talvez ajude a reverter a situação.
(há também a JSR-160 que atualiza a especificação, que você pode conferir aqui)

Um parêntese de prática antes dos conceitos

Neste momento você pensa no JMX como uma ferramenta de monitoramento essencialmente. Acho interessante começarmos vendo-o em ação. Para isto vamos monitorar a aplicação mais simples possível: o código abaixo:

public class Chata {
  public static void main(String args[]) {
     System.out.println("Oi, eu sou chata. Digite algo e eu paro.");
     System.console().readLine();
     System.out.println("Tchau…");
   }
}

Temos o código Java mais tedioso que você pode imaginar, mas um código que pode ser monitorado por JMX, veja como é simples:

javac Chata # compilo o programa
java Chata # executo o programa

E aí você já pode imaginar a empolgante saída no console:

Na sequência executamos um programa que sempre vêm com o JDK, se chama jconsole (há outras opções, mas usarei este pois é o mais simples e que sempre está disponível): gravei um vídeo expondo seu funcionamento.

Observe que seleciono o processo local que quero (Chata) e, a partir dali, conectando-me ao processo por JMX, tenho acesso a TUDO: uso de CPU, memória, threads e, ao final, os managed beans, sobre os quais falaremos mais tarde.

Como podem ver, JMX se aplica a qualquer aplicação Java, ou seja, você pode aplicá-lo em seus projetos agora se quiser. Mas antes vamos para um passeio rápido por um certo edifício.

Um tour por nosso edifício conceitual

Novamente aqui há alguns conceitos cujo conhecimento é essencial para que você possa dominar a tecnologia. Sendo assim vamos subir as escadas do nosso edifício conceitual.

Primeiro andar: instrumentação e gerenciamento

Não é por acaso que escolhi este como nosso ponto de partida. 80% do que você precisa conhecer está aqui e, como verá, são poucos conceitos.

Resource (Recurso)

Na documentação oficial do JMX (aqui e aqui) além de diversos artigos da internet você encontrará este termo: "resource", então é importante ter este conceito bem fixado em sua cabeça. Resource é qualquer coisa que você deseje monitorar, assim como os aspectos que deseja monitorar. Vamos a alguns exemplos:

  • CPU - você quer monitorar o seu uso no momento atual ou a média ao longo de determinado período.
  • Threads - threads no momento atual ou em determinado período.
  • Transações - quantas transações sua aplicação executou em um período ou encontram-se em execução agora.
  • Alguma métrica de negócio do seu projeto - exemplo: quantas vendas realizei hoje?

Resumindo: é aquilo que você quer espiar e os atributos da coisa que você quer espiar.

MBean

Aqui nós temos o núcleo do JMX: essencialmente são as classes que escrevemos para realizar ações de monitoramento e gerenciamento dos nossos sistemas. Mencionarei aqui os mais importantes:

  • Standard - o que veremos aqui
  • MXBeans - referencia apenas um número pré-determinado de tipos, o que permite ser portável em, essencialmente, qualquer plataforma. É útil, portanto, para o público que implementa ferramentas de monitoramento comerciais. Mais detalhes aqui.
  • Dynamic - os demais tipos de beans possuem um número fixo de propriedades e operações, já os dynamic beans não: estes implementam interfaces que permitem crescer ou diminuir o número de propriedades e ações em tempo de execução, o que pode ser muito útil quando, por exemplo, precisamos monitorar listas de objetos. Mais detalhes aqui.

Para o desenvolvedor que não desenvolve frameworks ou ferramentas de monitoramento de longe o MBean mais comum é o padrão (Standard), sobre o qual falaremos aqui. Mas antes de prosseguirmos é importante saber o que este tipo de bean (e na realidade, todos os outros) deve oferecer:

  • Propriedades que expõem dados que iremos monitorar. Estas podem ser do tipo somente leitura ou permitindo escrita.
  • Operações que iremos executar pelo nosso MBean.

Seu primeiro MBean

Então vamos para o nosso primeiro MBean. Para este exemplo resolvi implementar algo real: nosso MBean irá executar as seguintes ações:

  • Listar quantos arquivos há no meu diretório home.
  • Listar quantos diretórios há no meu diretório home.
  • Criar um diretório temporário na pasta home.
  • Apagar o diretório temporário na pasta home.

Com isto você conseguirá ver as duas ações principais que um MBean deve executar: monitorar seu sistema (quantos arquivos e diretórios?) e também gerenciar a plataforma (criando e removendo pastas).

Convenções

São pouquíssimas, e é bem fácil de seguir. Vamos a elas.

Interface
Todo MBean deve ter uma interface que siga o seguinte padrão: [Nome do Bean]MBean.

Nosso MBean se chamará Arquivos, sendo assim devemos escrever uma interface chamada ArquivosMBean, tal como no exemplo a seguir:

public interface ArquivosMBean {

    long getNumArquivos();

    long getNumDiretorios();

    void criarDiretorioTemporario();

    void removerDiretorioTemporario();

}

Notou como é apenas um padrão de nome? Outro ponto importante: para obter ou escrever dados no MBean, siga o padrão JavaBeans (é iniciante e não sabe o que são JavaBeans? Segue um link da Wikipedia. Essencialmente são os getters e setters que você escreve).

Se você tem operações, estas podem usar nomes como os que coloquei: criarDiretorioTemporario, ou qualquer outro padrão que deseje expor.

Implementação

O importante a saber é: o JMX exporá apenas o que estiver na interface, jamais a implementação será exposta. Bom, por falar em implementação, vamos ver a nossa? Segue o código fonte:

public class Arquivos implements ArquivosMBean {

@Override
public void criarDiretorioTemporario() {
  new File(System.getProperty("user.home") + "/temporario").mkdirs();
}

public void removerDiretorioTemporario() {
   // só pra fins didáticos, vai dar erro
   // se o diretório estiver cheio!
  new File(System.getProperty("user.home") + "/temporario").delete();
}

@Override
public long getNumArquivos() {
  File diretorio = new File(System.getProperty("user.home"));
  return Arrays.stream(diretorio.listFiles()).filter(t -> t.isFile()).count();
}

@Override
public long getNumDiretorios() {
  File diretorio = new File(System.getProperty("user.home"));
  return Arrays.stream(diretorio.listFiles()).filter(t -> t.isDirectory()).count();
  }
}

Novamente uma convenção. O nome da sua classe deve ser o nome da interface SEM o sufixo MBean. Vamos a alguns exemplos pra treinar?

Nome da interface Nome da implementação
ArquivosMBean Arquivos
RedeMBean Rede
TransacoesMBean Transacoes

Segundo andar: Agents

No primeiro andar conhecemos o coração do JMX que são os MBeans: agora vamos entender como estes são carregados e executados. Esta é a função do MBean Server.

MBean Server

Se você já usou o Spring, pense no MBean Server como se fosse um container responsável por ter registrados todos os MBeans do seu projeto. Inicialmente seu uso é para disponibilização local dos mesmos. E seu uso é bastante simples, apesar de requerer alguns detalhes que vamos expor aqui.

Vamos começar registrando o MBean que implementamos no primeiro andar. Observe o código a seguir:

import javax.management.*;
import java.lang.management.ManagementFactory;

try {
  
  MBeanServer server = ManagementFactory.getPlatformMBeanServer();
  ObjectName name = new ObjectName("br.com.itexto:type=basic,name=arquivos");
  server.registerMBean(new Arquivos(), name);
} catch (MalformedObjectNameException | 
         InstanceAlreadyExistsException |
         MBeanRegistrationException | 
         NotCompliantMBeanException e) {
   // fins didáticos, faça melhor que isto aqui embaixo
   e.printStackTrace();
}

O primeiro passo é a obtenção do MBeanServer, o que é feito chamando o método getPlatformMBeanServer() da classe ManagementFactory. Obtido o servidor, o próximo passo consiste em darmos um nome ao nosso MBean, muita atenção ao objeto do tipo ObjectName (javadoc pra maiores detalhes).

Todo MBean deve ter um nome único no MBeanServer, e isto é feito através da classe ObjectName. Há um padrão que deve ser seguido:

[domínio]:[propriedades no formato chave/valor separadas por vírgula]

Domínio é bom que seja um nome único para que possamos achar o seu MBean pelas ferramentas administrativas. No nosso caso, usamos "br.com.itexto". Já quanto às propriedades, estas informam o grupo em que incluiremos o MBean (basic) e o nome como é apresentado (Arquivos). Executando o jconsole novamente para monitorar a aplicação, vamos ver como nosso MBean aparece?

Finalmente, temos o registro da instância no MBean Server no trecho a seguir:

MBeanServer server = ManagementFactory.getPlatformMBeanServer();
ObjectName name = new ObjectName("br.com.itexto:type=basic,name=arquivos");
server.registerMBean(new Arquivos(), name);

Com a aplicação em execução, é possível vermos nossos pontos de monitoramento mudando em tempo real, tal como mostro a vocês no vídeo a seguir, em que executamos algumas operações e, como resultado, vemos as mudanças nos pontos monitorados. Veja o vídeo:

Você pode ver claramente o monitoramento em tempo real: clicando duas vezes sobre os valores monitorados (número de arquivos e diretórios), é possível ver que são renderizados gráficos que apontam os valores medidos de segundo a segundo.

Logo na sequência, é possível ver também como nós executamos operações. Estas aparecem como botões na interface do jconsole. Basta clicar ali para termos nossas ações de gerenciamento executadas. Imagine quão útil é isto em um ambiente remoto? Bem lindo, né?

Dica: leia este texto bem antigo sobre o jconsole: é uma ferramenta bem simples, mas MUITO útil.

Terceiro andar: Gerenciamento Remoto

No terceiro andar temos toda a infraestrutura necessária para que possamos acessar o JMX remotamente. É um andar muito importante para que você possa, inclusive, evitar sérios problemas de segurança. Apenas imagine que você implemento uma ação chamada "zerar banco de dados vital", você não quer isto exposto na rede, quer? :)

Este é um texto introdutório à tecnologia e, devo confessar, questões relativas ao gerenciamento remoto não são minha especialidade, sendo assim tratarei aqui apenas das informações essenciais que você deve ter para que possa se aprofundar adiante no assunto.

Expondo seu JMX ao mundo

Primeiro vamos tornar público o JMX: por padrão este só é acessível localmente. Vamos começar pela forma menos segura possível.

Da forma mais simples - sem autenticação ou SSL

Para expor o JMX de sua aplicação da forma mais simples possível é necessário passar as seguintes propriedades de sistema à JVM monitorada, que serão comentados abaixo:

# Indica que o JMX será acessível remotamente
-Dcom.sun.management.jmxremote
# Qual a porta do JMX estará disponível
-Dcom.sun.management.jmxremote.port=9010
# Indica que o JMX não requer autenticação para ser acessado
-Dcom.sun.management.jmxremote.authenticate=false
# Indica que o JMX não está acessível somente localmente
-Dcom.sun.management.jmxremote.local.only=false
# Indica que não estamos usando SSL para estabelecer a comunicação
-Dcom.sun.management.jmxremote.ssl=false
# Fundamental: o IP da máquina em que o JMX está disponível
-Djava.rmi.server.hostname=192.168.0.30

Nesta configuração algumas notas são importantes: primeiro é tornar claro que temos aqui a forma menos segura possível de configurar o acesso ao JMX, pois não usamos nem SSL nem credenciais de acesso.

O segundo ponto diz respeito ao hostname do RMI. Muitas vezes o localhost encontra-se configurado como loopback no servidor, sendo assim é obrigatório que você torne claro qual o IP da sua máquina para que possa ser acessada por outras.

Voltando ao cliente, a conexão com este é muito fácil. Levando em consideração que temos aqui uma aplicação no ip 192.168.0.30 na porta 9010, usando o jconsole, basta preencher tal como no print abaixo:

Bem simples: com a conexão aceita o jconsole perguntará se você quer fazer uma conexão não segura, diga que sim, tal como no print a seguir:

Definindo parâmetros de autenticação - ainda inseguro

Uma versão um pouco mais segura de acesso é a autenticação baseada em arquivos do tipo plain text. É um pouco mais difícil de ser configurado, requer inclusive alterar arquivos da sua JVM, porém é viável para fins de desenvolvimento.

Talvez agora você entenda por que este recurso não é tão popular: configurar a segurança não é trivial. O primeiro passo consiste em, no diretório da sua JVM, localizar a pasta conf/management.

No interior deste diretório você encontrará um arquivo chamado jmxremote.password.template, copie-o no mesmo diretório com o nome jmxremote.password. Este arquivo é que contém as senhas em texto simples ou cifrado (neste post falaremos apenas do texto simples) para os dois perfis padrão definidos para o acesso ao JMX:

  • monitorRole - que te permite apenas monitorar o processo, isto é, apenas acompanhar os indicadores (somente leitura).
  • controlRole - que te permite acesso total, ou seja, além de monitorar você também poderá gerenciar via JMX (leitura e escrita).

Estes são também os usernames padrão que você usará para se autenticar. Bom: criando a cópia do seu arquivo jmxremote.password, é importante também que haja permissão de leitura apenas para o usuário que iniciou o processo java (chmod 400 jmxremote.password - portanto).

Modifiquei o meu arquivo para que sua última linha ficasse tal como a apresentada a seguir:

controlRole kicolobo

O nome do usuário é controlRole, a senha, kicolobo. (dica: no diretório conf/management, confira o arquivo jmxremote.access, há informações interessantes ali).

Pronto, agora é só conectar no servidor tal como no exemplo a seguir:

Como não estamos usando SSL, aquela mesma mensagem avisando que você está trafegando por um canal de comunicação inseguro aparecerá. Diga para continuar e, voilá, você está dentro do console JMX.

Mais detalhes sobre este tipo de configuração você encontra neste link.

Ah, quer mudar a localização do arquivo ou mesmo seu nome? Use a propriedade de sistema com.sun.management.jmxremote.password.file apontando para o arquivo a ser carregado!

Conectando de forma segura ao JMX

Há ainda mais duas opções de se estabelecer autenticação com o servidor JMX. A primeira delas é via LDAP (ou Active Directory). Mais detalhes sobre como implementar esta configuração você encontra neste link.

E, claro, para aprender a configurar seu certificado SSL e aplicá-lo ao JMX, siga as instruções deste link.

Clientes JMX que valem à pena mencionar

Neste post até agora mencionei apenas o jconsole como cliente por ser a opção mais popular: praticamente toda distribuição do JDK vêm com ele, mas há outras opções que também costumam acompanhar a JVM:

  • JVisualVM - que antes fazia parte do próprio JDK, mas agora é um projeto separado. Tem ferramentas de visualização muito interessantes, inclusive um profilador que é extremamente útil quando bem usado para detectar gargalos.
  • Mission Control - (comando jmc nas JVMs da Oracle) - costumava vir junto com o JDK, agora não mais, é uma ferramenta excelente que tem como diferencial um recurso chamado "flight recorder", que essencialmente grava toda a telemetria da sua aplicação para que você possa depois analisar com calma aonde encontram-se os gargalos.

Concluindo

Existem diversas ferramentas de monitoramento de ambiente no mercado, mas para quem desenvolve em Java talvez o JMX seja a melhor das opções por nos permitir acompanhar em tempo real o que ocorre de fato no interior da JVM. Há diversas ferramentas que se limitam a consumo de CPU, memória, sistemas de arquivos, porém o JMX nos dá este "raio x" da aplicação em execução que é único.

Seu ponto fraco na minha opinião é a questão da segurança que não é tão simples de ser configurada. Talvez por isto não seja tão popular, porém soluções como o Actuator do Spring podem ser encaixadas aqui (inclusive tirando proveito do JMX) para resolver este gap.

Um detalhe importante do JMX é que este é profundamente integrado à JVM e a diversos containers de aplicação e bibliotecas. Sendo assim você literalmente pode ver TUDO o que ocorre no seu ambiente de produção:

  • Como anda seu pool de conexões.
  • Quais as conexões ativas.
  • Quantas threads e classes estão carregadas ao longo do tempo.
  • Usuários autenticados.
  • Enfim... se não tudo, quase tudo.

Há mais pontos que poderia colocar neste post mas que ficarão para o futuro:

  • O mecanismo de eventos do JMX que permite projetar soluções de manutenção pró-ativas.
  • Os adaptadores de protocolos de comunicação do JMX, que nos permite fazer o RMI se comunicar com qualquer outro protocolo, inclusive gerando outros tipos de visualização, como HTML, por exemplo.
  • Um review dos outros clientes JMX disponíveis, ou mesmo mostrar como, a partir do seu código Java, você pode interagir com JMX remoto (isto é MUITO interessante).
  • Como integrar JMX com Spring (isto é post futuro GARANTIDO quando for falar de Actuator).

É um assunto bem amplo sobre uma ferramenta que tá em toda JVM e pouca gente fala a respeito. Não raro em consultorias inicio o jconsole pra identificar gargalos e os olhos ao meu redor brilham, espero que os seus tenham brilhado também neste post.

Agora que vimos um pouco sobre monitoramento é hora de vermos o Actuator. Bora lá!

Para aprender mais

Segue uma lista de links para você se aprofundar no assunto:

19