Java EE Clustering in Docker

Andrea Scanzani
Architetture Digitali
9 min readDec 1, 2020

In questo articolo scopriremo come fare per conteinerizzare una Java EE Web Application su Docker, creando un cluster tra le istanze della nostra applicazione che andremo a deployare su Docker, mantendo i meccanismi tipici della clusterizzazione quali HTTP Session Replication ed EJB Clustering tra i nodi applicativi.

Clustering, Scalability & HA (High Availability)

Un Cluster è una serie di macchine connesse tra loro che lavorano in parallelo; L’utilizzo di un sistema Cluster permette di eseguire operazioni complesse distribuendo il carico di elaborazione su tutti i nodi che compongono il cluster.

I benefici che comporta l’introduzione di un Cluster sono le seguenti:

  • Scalabilità
  • Altà Affidabilità

Scalabilità

È la proprietà di un sistema, o di un’applicazione, di gestire grandi quantità di lavoro con la capacità di aumentare o diminuire le risorse in funzione ad una maggiore domanda.

Esistono due tipi di scalabilità:

  • Scalabilità Orizzontale (Horizontal Scalability — Scale Out)

In questo processo, vengono aggiunte più istanze o nodi server al Cluster. Tutti i server che comopongono il Cluster lavorano in parallelo, con un bilanciamento del carico in modo che le richieste possano essere distribuite tra tutti i nodi. Il bilanciamo delle richieste viene fatto con strumenti di Load Balancing.

Horizontal Scalability — Scale Out
Horizontal Scalability — Scale Out
  • Scalabilità Verticale (Vertical Scalability — Scale Up)

Un sistema scala verticalmente quando vengono aggiunte risorse (es: RAM, processori, etc..) sui nodi già esistenti.

Vertical Scalability — Scale Up
Vertical Scalability — Scale Up

Alta Affidabilità

Il termine affidabilità descrive il modo in cui un sistema fornisce risorse in un determinato periodo di tempo. L’alta affidabilità garantisce un livello assoluto di continuità funzionale all’interno di una finestra temporale espressa come rapporto tra uptime e downtime. L’alta affidabilità è utile nei contesti di Applicazioni IT che sono soggette a SLA (Service Level Agreement), che potrebbero includere dei livelli di disponibilità per l’applicazione.

High Availability
High Availability

Come si vede dall’immagine, in questo scenario il Load Balancer implementa l’Alta Affidabilità andando a dirigere le richieste in ingresso verso i soli nodi attivi nel Cluster.

Gestione del Cluster

Nel mondo reale quasi tutte le applicazioni hanno uno Stato, ad esempio le Web Application devono gestire le HTTP Sessions come stato, le applicazioni Java EE dovranno gestire lo stato degli EJB Stateful o i messaggi sulle code JMS.

Per un corretto funzionamento è necessario replicare o distribuire gli Stati dell’applicazione all’interno del Cluster. Ci sono differenze sostanziali tra i due approcci:

  • Distribuito → Lo stato è suddiviso in set di dati e partizionato per i nodi del Cluster. Un esempio di uno stato Distribuito è il meccanismo di Sticky Session, che invia le richieste HTTP allo stesso nodo che ha creato la HTTP Session:
State — Distributed
State — Distributed

Ovviamente questo meccanismo non garantisce l’Alta Affidabilità, in quanto se il Nodo che ha la Sessione HTTP è down, l’utente di sessione avrà perso il suo stato e andrà incontro ad un errore.

  • Replicato → Tutti i nodi del Cluster hanno tutte le informazione dello Stato. Questo garantisce sempre l’Alta Affidabilità in quanto le informazioni di Stato (EJB, HTTP Session, JMS, …) sono replicate in maniera automatica su tutti i nodi.
State — Replicated
State — Replicated

E’ anche possibile salvare lo stato di un’applicazione in un Layer separato, come ad esempio strumenti di Cache Management distribuiti come Hazelcast o Redis.

State — Chache Layer
State — Chache Layer

In questo articolo andremo a creare una Web Application in Java EE, con l’utilizzo di:

  • JSF
  • CDI
  • EJB
  • JAX-RS

E andremo ad costruire un Cluster su Docker utilizzando Wildfly come Application Server, Traefik come Load Balancer; implementando l’Alta Affidabilità e capendo come Scalare l’applicazione gestendo lo Stato in maniera Replicata tra i vari nodi.

Applicazione Java EE

Il codice sorgente completo è disponibile al seguente git repos:

Come prima cosa iniziamo a creare un progetto Maven multi-modulo per generare un EAR che contenga un JAR per gli EJB e un WAR per la componente Web.

Il file pom.xml del progetto Maven che fa da root al multi-modulo è questo:

pom.xml

Nel pom.xml abbiamo dichiarto i nostri tre moduli (module-ear, module-ejb e module-web) e aggiunto la dipendenza di JavaEE (con scope=provided in quanto l’implementazione è fornita da Wildfly), e configurato il plugin maven-ear-plugin per la generazione del pacchetto EAR dell’applicazione.

Nel nostro modulo EJB, creiamo:

  • un EJB Stateless → per generare il valore UUID
  • un EJB Stateful → con il conteggio di quanti parametri sono stati inseriti in sessione
EJBs

Ora configuramo il modulo Web, iniziando dal file web.xml (all’interno del modulo Web):

web.xml

Nel file web.xml abbiamo inserito due servlet-mapping per abilitare JSF (javax.faces.webapp.FacesServlet) e JAX-RS (javax.ws.rs.core.Application), eindichiamo quale è il nostro welcome-file, ed aggiungiamo il tag:

<distributable />

questo tag serve per dire al Application Server di abilitare la replica delle sessione nella nostra Applicazione!

Passiamo al codice Java, scriviamo il nostro CDI Bean per la pagina JSF:

MyJSFBean.java

Il nostro bean è di tipo @SessionScoped così da mostrare come viene gestita la replica delle sessioni HTTP all’interno del Cluster; inoltre iniettiamo un EJB Stateful (sempre per mostrare la replica dello stato) e un EJB Stateless.

Ora scriviamo la nostra pagina JSF, che si aggancierà al Bean creato sopra:

index.xhtml

Nella pagina abbiamo inserito:

  1. Un paragrafo con le informazioni del Nodo sul quale siamo atterrati
  2. Un paragrafo con le informazione della Sessione (sessionId)
  3. Un paragrafo con i dati recuperati dai due EJB
  4. Un form per aggiungere parametri alla Sessione HTTP
  5. Una lista che mostra tutti i parametri presenti nella Sessione HTTP

Per concludere l’esercizio creiamo anche una REST API in GET che torna il valore UUID (Universal Unique Identifier) e il nodo di esecuzione:

RestApi.java

La nostra applicazione è finita, dalla root del progetto ed eseguendo il comando:

mvn clean package

Maven generare il file JavaEEClusterApp.ear

Deployandola su Wildfly standalone avremo i seguenti path:

Deploy su Docker

Nella root del nostro progetto creiamo il file Dockerfile in maniera da generare l’immagine Docker.

Dockerfile

All’interno del Dockerfile specifichiamo quale immagine del Application Server utilizzare (jboss/wildfly:20.0.0.Final) e copiamo il nostro pacchetto EAR all’interno della directory di deployments di Wildfly.

Successivamo creiamo il nostro Username per accedere alla console di amministrazione di Wildfly, questo grazie al comando:

RUN /opt/jboss/wildfly/bin/add-user.sh admin Password1 --silent

Ora possiamo accedere alla console con admin/Password1.

Come ultima cosa diamo indicazione di come startare Wildfly all’interno del Container:

ENTRYPOINT /opt/jboss/wildfly/bin/standalone.sh -b=0.0.0.0 -bmanagement=0.0.0.0 -Djboss.server.default.config=standalone-full-ha.xml -Djboss.node.name=$(hostname -i) -Djava.net.preferIPv4Stack=true -Djgroups.bind_addr=$(hostname -i) -Djboss.messaging.cluster.password=cluster_password1

i parametri che indichiamo sono i seguenti:

  • -b=0.0.0.0 → configurazione di default per bindare Wildfly con 0.0.0.0
  • -bmanagement=0.0.0.0 → come sopra, ma per la console di amministrazione
  • -Djboss.server.default.config=standalone-full-ha.xml → Indichiamo quale profilo Wildfly dovra usare. Full sta per il pieno caricamento delle componenti Java EE, mentre HA sta per High Availability.
  • -Djboss.node.name=$(hostname -i) → Settiamo il valore del nodo Wildfly con l’IP che genera Docker (all’interno del container)
  • -Djava.net.preferIPv4Stack=true → Per comunicare a Wildfly di utilizzare IPv4
  • -Djgroups.bind_addr=$(hostname -i) → Per bindare JGroups al IP che Docker assegna al container. Il sottosistema JGroups fornisce supporto di comunicazione di gruppo per i servizi HA sotto forma di canali JGroups. Questa configurazione serve anche per il Multicast di Wildfly.
  • -Djboss.messaging.cluster.password=cluster_password1 → Impostiamo la password del cluster HornetQ

La nostra applicazione ora è pronta per essere containerizzata:

mvn clean packagedocker build -t javaee-clustered-app .

Al termine del processo troveremo la nostra immagine “javaee-clustered-app” nel repository locale di Docker:

> docker image lsREPOSITORY                                TAG                 IMAGE ID            CREATED             SIZE
javaee-clustered-app latest e0f2ff1a9003 2 days ago 766MB

Ora che la nostra immagine è pronta e disponibile possiamo testarla con la creazione del container:

docker run -it -p 8080:8080 javaee-clustered-app

Dal browser potremo vedere la nostra applicazione:

Stoppiamo il container con CTRL+C, e procediamo a scrivere il file per creare lo Stack applicativo:

docker-compose.yaml

Il nostro stack si compone di due servizi:

  1. traefik → Applicazione che si occupa di fare da Load Balancer dell’applicazione. In ascolto su due porte: 80 per Load Balancing e 8080 per la console di amministrazione
  2. javaee_clusterd_app → è la nostra applicazione JavaEE. Aggiungiamo le labels per comunicare con traefik (come il PathPrefix, e l’indicazione della porta 8080 con il quale traefik dirottera il traffico http)

Procediamo con il deploy dello stack:

docker stack deploy --compose-file docker-compose.yaml javaee_stack

Creato il nostro stack, possiamo verificarne l’esito:

#Verifichiamo lo stack
> docker stack ls
#Verifichiamo la composizione del nostro stack
> docker stack ps javaee_stack
#Vediamo i servizi attivi
> docker service ls

Dovremo avere il risultato seguente:

docker stack
docker stack

Colleghiamoci alla Console di Traefik per verificare la configurazione del routing HTTP verso la nostra applicazione Java EE. Da browser andiamo su :

http://localhost:8080/dashboard

Sotto il tab HTTP Routers dovremmo trovare il nostro routing con il PathPrefix(‘/JavaEEClusterApp’) (quello specificato nel file docker-compose.yaml):

Traefik — HTTP Routes
Traefik — HTTP Routes

Sotto il tab HTTP Services troveremo il nostro backend composto dalla nostra applicazione Java EE:

Traefik — HTTP Services
Traefik — HTTP Services

Entrando nel dettaglio vedremo come il nostro Servizio è composto da un solo Server:

Traefik — HTTP Services — Detail
Traefik — HTTP Services — Detail

L’indirizzo che vediamo sotto Servers è l’IP interno di Docker, non visibile dall’esterno ma solo dalla network creata nello stack docker (di cui Traefik ne fa parte).

Possiamo testare la configurazione dal nostro browser:

http://localhost/JavaEEClusterApp/

Ora creiamo il nostro Cluster applicativo, aggiungendo altri 2 nodi a quello esistente.

Utiliziamo il seguente comando:

docker service scale javaee_stack_javaee_clustered_app=3

Da console vedremo come Docker crea altre due istanze/nodi del nostro Cluster, creando altri due container.

Eseguendo i comandi di verifica, vedremo come ora la nostra applicazione ha una replica di 3:

docker stack replicated
docker stack replicated

Come per magia avremo i due nuovi nodi anche sotto il bilanciatore di Traefik:

Traefik — HTTP Services — Detail Load Balancing
Traefik — HTTP Services — Detail Load Balancing

Testiamo il Cluster

Ora che abbiamo un Cluster composto da 3 nodi, pronto possiamo iniziare una verifica del meccanismo di replica degli stati della nostra applicazione Java EE.

Iniziamo verificando il corretto bilanciamento dei nostri 3 server tramite invocazioni alla nostra API Rest.

Utilizzando il tool Curl invochiamo 3 volte la nostra API:

curl -X GET http://localhost/JavaEEClusterApp/api/uuid

e vedremo che ogni volta l’attributo nodeName avrà un diverso valore, indicando come il carico vengo bilanciato su tutti i nodi!

Load Balancing — Rest
Load Balancing — Rest

Procediamo con il test della parte WEB, verificando i meccanismi di replica delle Sessioni HTTP e degli EJB Stateful.

Da browser andiamo sulla pagina:

http://localhost/JavaEEClusterApp/

Aggiungiamo degli attributi in sessione, e cliccando sul bottone “Refresh Page” è possibile vedere come cambiando il Nodo su cui atterriamo dal bilanciatore (campo Node Name) sia il campo Session ID che la lista degli attributi di Sessione sia sempre la stessa!

Notare anche come lo stato mantenuto dal EJB Stateful rimane invariato anche cambiando nodo d’esecuzione!

Vi riporto anche un video con la dimostrazione del risultato finale:

--

--

Andrea Scanzani
Architetture Digitali

IT Solution Architect and Project Leader (PMI-ACP®, PRINCE2®, TOGAF®, PSM®, ITIL®, IBM® ACE).