(Italiano) 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. è 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 . Vedremo come configurare JMX in un'applicazione Java basata su e come connettersi a un server JMX utilizzando 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 e come ambiente di deploy e runtime la .
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 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 , 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 , 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:
- abilitare l'estensione OpenShift per Quarkus (tramite il comando
quarkus add-extension io.quarkus:quarkus-openshift
); - configurare la variabile d'ambiente
JAVA_OPTS
per il runtime di OpenShift per abilitare JMX; - 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 e (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 . Per connettersi a un server JMX, è possibile utilizzare JDK Mission Control e specificare l'indirizzo IP e la porta del server JMX.
- Avviare JDK Mission Control utilizzando il comando
jmc
. - Dal menù
File
selezionare la voceConnect
. - Selezionare la voce
Create a new connection
per aggiungere una nuova connessione JMX. - Specificare l'indirizzo IP e la porta del server JMX (es.
localhost:9091
). - Specificare un nome per la connessione (es.
MyApp JMX Connection
). - 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. - 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.
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.
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.
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 .
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).
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.
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).
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 per ulteriori dettagli.