Come usare Java JMX in ambienti container

Come usare Java JMX in ambienti container

Con l’adozione crescente delle architetture a microservizi e dei container per la distribuzione delle applicazioni, il monitoraggio delle prestazioni e la gestione dei servizi diventano aspetti critici. Java Management Extensions (JMX) è uno strumento potente che consente agli sviluppatori di monitorare e gestire le applicazioni Java. In ambienti containerizzati, come quelli orchestrati da Kubernetes, Docker, Podman o altro, l’utilizzo di JMX presenta alcune sfide e richiede configurazioni specifiche.

Questo articolo fornirà una guida dettagliata su come usare Java JMX in ambienti container e in particolare su Red Hat OpenShift. Vedremo come configurare JMX in un’applicazione Java basata su Quarkus e come connettersi a un server JMX utilizzando JDK Mission Control per monitorare le prestazioni, gestire le risorse, e cambiare il livello di log a runtime.

Per l’esempio pratico faremo riferimento all’applicazione di esempio Event Bus Logging Filter JAX-RS e come ambiente di deploy e runtime la Dev Sandbox di Red Hat OpenShift.

Qualcuno potrebbe domandarsi: JMX è ancora attuale?

Sì, Java Management Extensions (JMX) è ancora attuale e rilevante nel contesto dello sviluppo e della gestione delle applicazioni Java. Anche se alcune tecnologie evolvono e nuovi strumenti emergono, JMX continua a essere una componente fondamentale per diverse ragioni.

Standardizzazione e Compatibilità

JMX è una specifica standardizzata e parte integrante della piattaforma Java SE (Standard Edition). Questo garantisce la compatibilità con tutte le implementazioni di Java, rendendolo un’opzione affidabile per il monitoraggio e la gestione delle applicazioni.

Integrazione con Strumenti Moderni

Molti strumenti di monitoraggio e gestione delle applicazioni Java, come JConsole, VisualVM, e strumenti di terze parti come Prometheus e Grafana, offrono integrazioni native o plugin per lavorare con JMX. Questo permette di utilizzare JMX come base per la raccolta di metriche e la gestione delle risorse, integrandolo con soluzioni più moderne per la visualizzazione e l’analisi dei dati.

Utilizzo in Architetture Distribuite

Con l’aumento dell’adozione delle architetture a microservizi e dei container, JMX rimane utile per il monitoraggio delle applicazioni Java distribuite. Ad esempio, molti container orchestrator come Kubernetes possono essere configurati per raccogliere metriche esposte tramite JMX (Quarkus e Spring Boot lo consentono), facilitando il monitoraggio centralizzato delle applicazioni distribuite.

Flessibilità ed Estensibilità

JMX è altamente flessibile ed estendibile. Gli sviluppatori possono definire nuovi MBean per esporre metriche specifiche o per aggiungere funzionalità di gestione personalizzate. Questa estensibilità lo rende adattabile a molteplici scenari, sia per applicazioni legacy che moderne.

Anche se JMX rimane rilevante, è importante considerare che nuovi paradigmi di monitoraggio, come l’osservabilità completa (tracing, logging, e metriche combinate) con strumenti come OpenTelemetry, stanno guadagnando popolarità. Tuttavia, questi strumenti spesso possono essere configurati per raccogliere dati da sorgenti JMX, mostrando come JMX continui a integrarsi con le tecnologie emergenti.

Perché JMX nei Container?

L’utilizzo di JMX nei container permette di:

  • monitorare le prestazioni e lo stato delle applicazioni Java;
  • ottenere visibilità in tempo reale sulle risorse e sui thread dell’applicazione;
  • gestire dinamicamente le configurazioni senza necessità di riavviare l’applicazione.

Inoltre, JMX nei container può essere integrato con strumenti di monitoraggio e gestione delle applicazioni, come Prometheus, Grafana, JConsole o strumenti di APM (Application Performance Management), per raccogliere e visualizzare metriche, analizzare i dati, e rispondere in modo pro attivo ai problemi di prestazioni.

Configurazione di JMX in un’applicazione Java

Per abilitare JMX in un’applicazione Java, è necessario configurare il server JMX per esporre le metriche e MBean. Questo può essere fatto tramite le opzioni di avvio della JVM, specificando le proprietà di sistema per abilitare il server JMX e definire la porta di ascolto. Ad esempio, è possibile utilizzare le seguenti opzioni di avvio.

-Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.port=9091
-Dcom.sun.management.jmxremote.rmi.port=9091
-Djava.rmi.server.hostname=127.0.0.1
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.ssl=false
-Dcom.sun.management.jmxremote.local.only=true

Configurazione 1 – Opzioni di avvio per abilitare JMX

Queste opzioni abilitano il server JMX sulla porta 9091, consentono la connessione da host remoti specificando l’indirizzo IP del server, disabilitano l’autenticazione e l’uso di SSL per semplificare la configurazione. È importante notare che queste opzioni sono solo un esempio e possono essere personalizzate in base alle esigenze specifiche dell’applicazione.

Se volessi abilitare JMX su di una applicazione Java basata su Quarkus e installata su Red Hat OpenShift, come dovrei fare?

Utilizzando gli strumenti messi a disposizione da Quarkus, tutto è molto semplice. Gli step sono i seguenti:

  1. abilitare l’estensione OpenShift per Quarkus (tramite il comando quarkus add-extension io.quarkus:quarkus-openshift);
  2. configurare la variabile d’ambiente JAVA_OPTS per il runtime di OpenShift per abilitare JMX;
  3. configurare i port mapping per esporre la porta JMX e consentire la connessione da host remoti.

Per aggiungere la variabile d’ambiente JAVA_OPTS per abilitare JMX in Quarkus su OpenShift, è possibile utilizzare la seguente configurazione sul file application.properties. Sono le stesse opzioni di avvio che abbiamo visto in precedenza ma configurate come variabile d’ambiente.

quarkus.openshift.env.vars.java-opts=-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=9091 -Dcom.sun.management.jmxremote.rmi.port=9091 -Djava.rmi.server.hostname=127.0.0.1 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.local.only=true

Configurazione 2 – Variabile d’ambiente JAVA_OPTS per abilitare JMX in Quarkus su OpenShift

Per soddisfare il terzo punto della precedente lista, occorre configurare il Deployment e Service (oggetti K8s) per esporre la porta JMX e consentire quindi la connessione da host remoti. Questo può essere fatto utilizzando le seguenti configurazioni (sempre sul file di configurazione application.properties).

# Define the ports that should be exposed by the container
# for the JMX monitoring
quarkus.openshift.ports."jmx".protocol=TCP
quarkus.openshift.ports."jmx".container-port=9091

Configurazione 3 – Configurazione dei port mapping per JMX in Quarkus su OpenShift

Le configurazioni mostrate in precedenza, faranno in modo che, in fase di build e deploy su OpenShift, siano creati i corretti oggetti K8s per esporre la porta JMX e consentire la connessione da host remoti; non ci dobbiamo preoccupare di creare manualmente i file di configurazione YAML per il Deployment e Service, di cui a seguire sono riportati gli estratti con le informazioni necessarie.

apiVersion: apps/v1
kind: Deployment
metadata:
spec:
  replicas: 2
  selector:
    matchLabels:
  template:
    spec:
      containers:
        - env:
            - name: JAVA_OPTS
              value: -Dcom.sun.management.jmxremote
                -Dcom.sun.management.jmxremote.port=9091 -Dcom.sun.management.jmxremote.rmi.port=9091
                -Djava.rmi.server.hostname=127.0.0.1 -Dcom.sun.management.jmxremote.authenticate=false
                -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.local.only=true
            - name: JAVA_APP_JAR
              value: /deployments/quarkus-run.jar
          image: eventbus-logging-filter-jaxrs:1.2.7-SNAPSHOT
          imagePullPolicy: Always
          name: eventbus-logging-filter-jaxrs
          ports:
            - containerPort: 9091
              name: jmx
              protocol: TCP

Configurazione 4 – Configurazione del Deployment per abilitare JMX in Quarkus su OpenShift

Nota: in realtà, la configurazione del Service per esporre la porta JMX non è strettamente necessaria, in quanto il Service viene creato automaticamente dal plugin OpenShift di Quarkus. Come vedremo in seguito, la connessione sarà instaurata direttamente con il pod che esegue l’applicazione Java.

apiVersion: v1
kind: Service
metadata:
  name: eventbus-logging-filter-jaxrs
spec:
  ports:
    - name: jmx
      port: 9091
      protocol: TCP
      targetPort: 9091
  selector:
    app.kubernetes.io/name: eventbus-logging-filter-jaxrs
    app.kubernetes.io/version: 1.2.7-SNAPSHOT
  type: ClusterIP

Configurazione 5 – Configurazione del Service per abilitare JMX in Quarkus su OpenShift

Nota: le configurazioni mostrate in precedenza sono generate automaticamente dal plugin OpenShift di Quarkus e disponibili all’interno del folder target/kubernetes.

A questo punto è possibile procedere con build e il deploy dell’applicazione su OpenShift. Una volta completato il deploy, sarà possibile connettersi al server JMX esposto dall’applicazione Java e monitorare le prestazioni, le risorse e i MBean.

Verifica del deploy e preparazione connessione a JMX

Per verificare che l’applicazione Java sia stata correttamente deployata su OpenShift e che il server JMX sia attivo, è possibile utilizzare il comando oc per ottenere informazioni sul pod.

# Get information about the pod
oc get pod

# Output
NAME READY STATUS RESTARTS AGE
activemq-artemis-675cf6d9-wpbrj 1/1 Running 0 3h26m
eventbus-logging-filter-jaxrs-66586f8dc4-5k7t4 1/1 Running 0 4m25s
eventbus-logging-filter-jaxrs-66586f8dc4-vx8sh 1/1 Running 0 4m36s
mongo-7d488b9474-vg8wq 1/1 Running 0 3h25m
postgresql-7fbdfbcd64-7b76h 1/1 Running 0 3h26m

Console 1 – Output del comando oc get pod

I pod (in questo caso due) dell’applicazione Java eventbus-logging-filter-jaxrs sono stati deployati correttamente e sono in stato Running. Adesso verifichiamo che la variabile di ambiente JAVA_OPTS sia stata correttamente configurata per abilitare JMX. Possiamo usare sempre il comando oc (in diverse forme) per ottenere informazioni sulle variabili d’ambiente del pod. A seguire sono mostrati due possibili modi.

# Get environment variables for the pod
# 1. Using the get pod command with the -o json flag to get the JSON output
# To execute this command, you need to have the jq utility installed
oc get pod eventbus-logging-filter-jaxrs-66586f8dc4-5k7t4 -o json | jq '.spec.containers[].env'

# 2. Using the exc command to get the environment variables
oc exec eventbus-logging-filter-jaxrs-66586f8dc4-5k7t4 -- env | grep JAVA_OPTS

Console 2 – Output dei comandi oc per ottenere le variabili d’ambiente del pod

A seguire l’output dei comandi oc per ottenere le variabili d’ambiente del pod e in particolare quella di nostro interesse JAVA_OPTS . Dall’output ottenuto, la variabile d’ambiente JAVA_OPTS è stata correttamente configurata e impostata per abilitare JMX.

# Output of the oc get pod eventbus-logging-filter-jaxrs-66586f8dc4-5k7t4 -o json | jq '.spec.containers[].env'
[
{
"name": "JAVA_OPTS",
"value": "-Xms100M -Xmx500M -XX:+UseG1GC -XX:+UnlockExperimentalVMOptions -XX:MaxGCPauseMillis=200 -XX:InitiatingHeapOccupancyPercent=45 -XX:G1ReservePercent=10 -XX:ConcGCThreads=4 -XX:G1NewSizePercent=5 -XX:G1MaxNewSizePercent=60 -XX:ParallelGCThreads=4 -XX:+ExitOnOutOfMemoryError -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=9091 -Dcom.sun.management.jmxremote.rmi.port=9091 -Djava.rmi.server.hostname=127.0.0.1 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.local.only=true"
},
{
"name": "JAVA_APP_JAR",
"value": "/deployments/quarkus-run.jar"
}
]

# Output of the oc exec eventbus-logging-filter-jaxrs-66586f8dc4-5k7t4 -- env | grep JAVA_OPTS
JAVA_OPTS=-Xms100M -Xmx500M -XX:+UseG1GC -XX:+UnlockExperimentalVMOptions -XX:MaxGCPauseMillis=200 -XX:InitiatingHeapOccupancyPercent=45 -XX:G1ReservePercent=10 -XX:ConcGCThreads=4 -XX:G1NewSizePercent=5 -XX:G1MaxNewSizePercent=60 -XX:ParallelGCThreads=4 -XX:+ExitOnOutOfMemoryError -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=9091 -Dcom.sun.management.jmxremote.rmi.port=9091 -Djava.rmi.server.hostname=127.0.0.1 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.local.only=true

Console 3 – Output dei comandi oc per ottenere le variabili d’ambiente del pod (dettaglio)

Affinchè dalla nostra macchina sia possibile eseguire la connessione JMX, è necessario creare un tunnel che reindirizza una porta del computer locale a una porta di un pod specifico; la porta di nostro interesse è la 9091. Questo tipo di operazione può essere fatta utilizzando il comando oc port-forward così come mostrato di seguito.

# Enable port forwarding for the JMX port
oc port-forward pod/eventbus-logging-filter-jaxrs-66586f8dc4-5k7t4 9091:9091

# If the port forwarding is successful, you should see the following output
Forwarding from 127.0.0.1:9091 -> 9091
Forwarding from [::1]:9091 -> 9091

Console 4 – Comando oc port-forward per abilitare il forwarding della porta JMX

A questo punto, l’applicazione Java è pronta per essere monitorata tramite JMX. Per connettersi al server JMX, è possibile utilizzare strumenti come JConsole, VisualVM, o qualsiasi altro client JMX compatibile. Nel nostro caso utilizzeremo JDK Mission Control.

Connessione a JMX tramite JDK Mission Control

JDK Mission Control è uno strumento avanzato di monitoraggio e gestione delle applicazioni Java, incluso nella suite di strumenti Java Flight Recorder (JFR). Per connettersi a un server JMX, è possibile utilizzare JDK Mission Control e specificare l’indirizzo IP e la porta del server JMX.

  1. Avviare JDK Mission Control utilizzando il comando jmc.
  2. Dal menù File selezionare la voce Connect.
  3. Selezionare la voce Create a new connection per aggiungere una nuova connessione JMX.
  4. Specificare l’indirizzo IP e la porta del server JMX (es. localhost:9091).
  5. Specificare un nome per la connessione (es. MyApp JMX Connection).
  6. Fare clic su Test Connection per verificare che la connessione vada a buon fine con i parametri specificati. Se la connessione avverrà con successo, sarà visualizzato un messaggio di conferma.
  7. Fare clic su Finish per terminare la configurazione della connessione.

La figura a seguire mostra la creazione di una nuova connessione JMX in JDK Mission Control e l’esito positivo del test di connessione.

Figura 1 - Creazione di una nuova connessione JMX in JDK Mission Control
Figura 1 – Creazione di una nuova connessione JMX in JDK Mission Control

Una volta definita la connessione JMX, è possibile visualizzare quali MBean sono esposti dall’applicazione Java, monitorare le prestazioni e le risorse, e gestire dinamicamente l’applicazione avviando la console JMX così come indicato dalla figura seguente.

Figura 2 - Avvio della console JMX in JDK Mission Control
Figura 2 – Avvio della console JMX in JDK Mission Control

Non appena connessi, dovreste vedere la dashboard di Mission Control con le metriche e i MBean esposti dall’applicazione Java. Da qui è possibile monitorare le prestazioni, analizzare i dati e gestire l’applicazione in tempo reale.

Figura 3 - Dashboard di JDK Mission Control con le metriche JMX
Figura 3 – Dashboard di JDK Mission Control con le metriche JMX

Accedendo alla sezione MBean Browser di JDK Mission Control, è possibile esplorare quali MBean sono esposti dall’applicazione Java e visualizzare le metriche e le operazioni disponibili per ciascun MBean. La figura seguente mostra per esempio le metriche che riguardano il connection pool e in particolare di Agroal.

Figura 4 - Browser MBean di JDK Mission Control
Figura 4 – Browser MBean di JDK Mission Control

Come cambiare il livello di log a runtime

Quante volte vi è capitato di dover cambiare il livello di log di un’applicazione Java a runtime, senza dover riavviare l’applicazione stessa? Con JMX è possibile farlo in modo semplice e veloce.

Una delle caratteristiche più potenti di JMX è la possibilità di gestire dinamicamente le risorse e le configurazioni dell’applicazione Java senza necessità di riavviare l’applicazione.

Per farlo utilizzeremo JDK Mission Control e il MBean java.util.logging:type=Logging per cambiare il livello di log a runtime. Questo MBean consente di modificare il livello di log per i logger specifici dell’applicazione Java, consentendo di aumentare o diminuire il livello di dettaglio dei log senza dover riavviare l’applicazione.

Come primo step dobbiamo identificare il logger per cui desideriamo cambiare il livello di log. Questo può essere fatto esplorando i MBean esposti dall’applicazione Java e cercando il MBean java.util.logging:type=Logging (vedi figura a seguire).

Figura 5 - MBean `java.util.logging:type=Logging` in JDK Mission Control
Figura 5 – MBean `java.util.logging:type=Logging` in JDK Mission Control

Supponiamo di aver identificato il logger it.dontesta.eventbus.consumers.events.handlers.Dispatcher per cui vogliamo cambiare il livello di log a DEBUG. Prima di procedere con la modifica, verifichiamo il livello di log corrente (che dovrebbe essere INFO) per il logger utilizzando il metodo getLoggerLevel del MBean java.util.logging:type=Logging (vedi figura a seguire) e successivamente cambieremo il livello di log utilizzando il metodo setLoggerLevel.

Per accedere alle operazioni dello specifico MBean, fare clic sul nome del MBean e selezionare la voce Operations per visualizzare i metodi disponibili.

Figura 6 - Verifica del livello di log attuale per il logger it.dontesta.eventbus.consumers.events.handlers.Dispatcher
Figura 6 – Verifica del livello di log attuale per il logger it.dontesta.eventbus.consumers.events.handlers.Dispatcher

Dal risultato visibile dalla precedente figura, il livello di log attuale per il logger it.dontesta.eventbus.consumers.events.handlers.Dispatcher è INFO. Per cambiarlo a DEBUG, dobbiamo utilizzare il metodo setLoggerLevel del MBean java.util.logging:type=Logging specificando il nome del logger e il nuovo livello di log (vedi figura a seguire).

Figura 7 - Cambio del livello di log a `DEBUG` per il logger it.dontesta.eventbus.consumers.events.handlers.Dispatcher
Figura 7 – Cambio del livello di log a `DEBUG` per il logger it.dontesta.eventbus.consumers.events.handlers.Dispatcher

Una volta cambiato il livello di log, l’applicazione Java inizierà a registrare i log con il nuovo livello di dettaglio senza necessità di riavviare l’applicazione. Questo permette di gestire dinamicamente i log dell’applicazione Java in tempo reale, adattandoli alle esigenze di monitoraggio e debug. A seguire un esempio di log registrato dall’applicazione Java con il nuovo livello di log DEBUG.

# Log output with DEBUG level
oc logs -f eventbus-logging-filter-jaxrs-66586f8dc4-5k7t4

# Output
2024-06-09 14:23:38,806 DEBUG [it.don.eve.con.eve.han.Dispatcher] (vert.x-eventloop-thread-0) Sending event message to target virtual address: nosql-trace
2024-06-09 14:23:38,807 DEBUG [it.don.eve.con.eve.han.Dispatcher] (vert.x-eventloop-thread-0) Sending event message to target virtual address: queue-trace
2024-06-09 14:23:38,807 DEBUG [it.don.eve.con.eve.han.Dispatcher] (vert.x-eventloop-thread-0) Received response from target virtual address: nosql-trace with result: Documents inserted successfully with Id BsonObjectId{value=6665baea279ab051ec5bce3d}
2024-06-09 14:23:38,808 DEBUG [it.don.eve.con.eve.han.Dispatcher] (vert.x-eventloop-thread-0) Received response from target virtual address: queue-trace with result: Message sent to AMQP queue successfully!
2024-06-09 14:23:38,808 DEBUG [it.don.eve.con.eve.han.Dispatcher] (vert.x-eventloop-thread-0) Received response from target virtual address: nosql-trace with result: Documents inserted successfully with Id BsonObjectId{value=6665baea279ab051ec5bce3e}
2024-06-09 14:23:38,808 DEBUG [it.don.eve.con.eve.han.Dispatcher] (vert.x-eventloop-thread-0) Received response from target virtual address: queue-trace with result: Message sent to AMQP queue successfully!

Console 5 – Output dei log con livello DEBUG

Conclusioni

In questo articolo abbiamo esplorato come utilizzare Java JMX in ambienti container, in particolare su Red Hat OpenShift. Abbiamo visto come configurare JMX in un’applicazione Java basata su Quarkus e come connettersi a un server JMX utilizzando JDK Mission Control per monitorare le prestazioni, gestire le risorse, e cambiare il livello di log a runtime.

Vorrei sottolineare il fatto che JMX può essere configurato in modo indipendente dal framework o dalla piattaforma utilizzata, e può essere utilizzato per monitorare e gestire qualsiasi applicazione Java.

La possibilità di utilizzare JMX risulta particolarmente utile quando per esempio siamo ancora in fase di sviluppo o in fase di test, (non abbiamo un strumento di APM completo) e dobbiamo monitorare le prestazioni e le risorse dell’applicazione Java, o quando dobbiamo cambiare il livello di log a runtime per debuggare un problema specifico.

Ho volutamente scelto di utilizzare Java Mission Control perché questo apre la strada all’uso di Java Flight Recorder (JFR) che è uno strumento molto potente per il monitoraggio delle prestazioni e l’analisi dei dati. Inoltre, Mission Control è uno strumento gratuito e open source, disponibile per tutti gli sviluppatori Java.

In tema di Java Flight Recorder, Red Hat sponsorizza il progetto Cryostat, che è un’alternativa open source a Mission Control, che consente di utilizzare Java Flight Recorder in ambienti containerizzati. Invito a leggere l’articolo Monitoring Quarkus JVM Mode With Cryostat per ulteriori dettagli.

0 Condivisioni

Antonio Musarra

I began my journey into the world of computing from an Olivetti M24 PC (http://it.wikipedia.org/wiki/Olivetti_M24) bought by my father for his work. Day after day, quickly taking control until … Now doing business consulting for projects in the enterprise application development using web-oriented technologies such as J2EE, Web Services, ESB, TIBCO, PHP.

Potrebbero interessarti anche...