Hace poco vimos una Introducción a Terracotta, donde con un pequeño ejemplo repasamos las características principales de esta librería que permite crear un área de memoria compartida por distintas máquinas virtuales Java.
En este artículo veremos un ejemplo un poco más complejo, compartiendo un objeto Cola y varios productores y consumidores sobre la misma, programado como si la Cola fuera un objeto "local" accedido por muchos hilos... pero cada hilo se ejecutará en una máquina virtual distinta.
Para finalizar, realizaremos una configuración de Alta Disponibilidad con Terracotta, y ejecutaremos a los productores y consumidores contra el cluster. A no asustarse que no es dificil. ¡Empecemos!
El ejemplo de Productor-Consumidor
Vamos a realizar una aplicación muy simple que constará de una Cola, un Productor y un Consumidor. La Cola será una única instancia en donde los Productores agregarán elementos (objetos de tipo Invasor) y los Consumidores quitarán elementos para procesar.
Entonces, las clases que intervienen en este ejemplo son:
- Cola
- Productor
- Consumidor
- Invasor
- Main (clase que permite la ejecución por línea de comandos)
La clase Cola deberá tener la lógica necesaria para poder ser accedida por múltiples hilos de ejecución, por lo que sincronizará internamente a su estructura donde mantiene los datos (un ArrayList).
Estas clases las empaquetamos dentro de un jar terracota-demo.jar para facilitar su ejecución.
Veamos entonces las clases (también pueden Descargar un proyecto de ejemplo que incluye estas clases y archivos de configuración)
Cola.java
package com.dosideas.terracotta; import java.util.ArrayList; public class Cola { private ArrayList elementos = new ArrayList(); public Object obtener() { Object o = null; synchronized (elementos) { if (elementos.size() > 0) { o = elementos.get(0); elementos.remove(0); } } return o; } public void agregar(Object o) { synchronized (elementos) { elementos.add(o); } } public int size() { synchronized (elementos) { return elementos.size(); } } }
Productor.java
package com.dosideas.terracotta; public class Productor { private Cola elementos; public Productor(Cola elementos) { this.elementos = elementos; } public void producir(int cantidad) { System.out.println("Produciendo elementos..."); for (int i=0; i<cantidad; i++) { Invasor invasor = new Invasor(); invasor.setId("Id: " + i); invasor.setNombre("Nombre " + i); invasor.setCargo("Cargo " + i); elementos.agregar(invasor); System.out.printf("Producidos %s elementos\n", i); } } }
Consumidor.java
package com.dosideas.terracotta; public class Consumidor { private Cola elementos; public Consumidor(Cola elementos) { this.elementos = elementos; } public void consumir() { System.out.println("Consumiendo elementos..."); Invasor invasor = null; do { invasor = (Invasor) elementos.obtener(); if (invasor != null) { procesar(invasor); } else { System.out.println("Terminado"); } } while (invasor != null); } private void procesar(Invasor invasor) { System.out.printf("Procesando %s - quedan %s\n", invasor, elementos.size()); try { Thread.sleep(500); } catch (InterruptedException ex) { ex.printStackTrace(); } } }
Invasor.java
package com.dosideas.terracotta; public class Invasor { private String id; private String nombre; private String cargo; //getters y setters a continuacion ... }
Main.java
package com.dosideas.terracotta; public class Main { private static Cola elementos = new Cola(); public static void main(String[] args) { if (args.length != 1) { throw new IllegalArgumentException("Parametros incorrectos"); } if (args[0].equals("productor")) { Productor productor = new Productor(elementos); productor.producir(200); } else if (args[0].equals("consumidor")) { Consumidor consumidor = new Consumidor(elementos); consumidor.consumir(); } else { throw new IllegalArgumentException("Parametros incorrectos"); } } }
Configurando Terracotta
Como vemos, las clases son "comunes" y no tienen referencia a ningún elemento de Terracotta. De hecho, están programadas para poder ser ejecutadas en un entorno de multi-hilos, de manera tradicional.
Nos queda crear el archivo de configuración para Terracotta, llamado tc-config.xml, en donde:
- Indicaremos que el atributo "elementos" de la clase Main será compartido por Terracotta.
- Instrumentamos la clase Cola y la clase Invasor (porque está contenida dentro de Cola, un objeto compartido).
- Definimos los métodos que necesitarán bloqueos de concurrencia.
tc-config.xml
<?xml version="1.0" encoding="UTF-8"?> <tc:tc-config xmlns:tc="http://www.terracotta.org/config" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.terracotta.org/schema/terracotta-4.xsd"> <servers> <server host="localhost" name="sample"/> <update-check> <enabled>true</enabled> </update-check> </servers> <system> <configuration-model>development</configuration-model> </system> <application> <dso> <roots> <root> <field-name> com.dosideas.terracotta.Main.elementos </field-name> </root> </roots> <instrumented-classes> <include> <class-expression> com.dosideas.terracotta.Cola </class-expression> </include> <include> <class-expression> com.dosideas.terracotta.Invasor </class-expression> </include> </instrumented-classes> <locks> <autolock> <lock-level>write</lock-level> <method-expression> void com.dosideas.terracotta.Cola.agregar(..) </method-expression> </autolock> <autolock> <lock-level>write</lock-level> <method-expression> Object com.dosideas.terracotta.Cola.obtener() </method-expression> </autolock> </locks> </dso> </application> </tc:tc-config>
Ejecución
La ejecución del ejemplo consiste en:
- Levantar el servidor de Terracotta
- Ejecutar uno (o varios) productores
- Ejecutar uno (o varios) consumidores
Recordemos que el objeto Cola estará en un área de "memoria compartida", y cada máquina virtual usará la misma instancia del atributo "elementos" de la clase Main. Así, los productores y consumidores que ejecutemos usarán la misma instancia para compartir datos.
Ejecución del server de Terracotta
Levantamos el server de Terracotta con el comando:
TERRACOTTA_HOME/bin/start-tc-server.sh
Ejecución de un Productor
Nos ubicamos en el directorio con el jar terracotta-demo.jar y el archivo de configuración tc-config.xml y ejecutamos:
TERRACOTTA_HOME/bin/dso-java.sh -jar terracotta-demo.jar productor
El productor agregará 200 elementos a la instancia de Cola y finalizará. Podemos ejecutarlo varias veces para ir agregando más elementos. Veremos algo como lo siguiente:
Ejecución de los Consumidores
A continuación ejecutamos a nuestros consumidores:
TERRACOTTA_HOME/bin/ds-java.sh -jar terracotta-demo.jar consumidor
El consumidor irá quitando elementos de la Cola, hasta que la misma quede vacía. Al quitar un elemento el Consumidor lo "procesa" (simulando una demora). Podemos ir ejecutando muchos consumidores en distintas terminales de forma simulatánea: todos irán consumiendo elementos de la misma instancia del objeto Cola, y podremos ver por consola cómo va decreciendo el tamaño de la misma (o incrementándose, si también ejecutamos un Productor mientras se están consumiendo elementos).
En la imagen a continuación vemos a dos consumidores trabajando concurrentemente. Noten cómo se van alternando los ID entre ambos procesos, y cómo varía la cantidad de elementos restantes.
Alta Disponibilidad con Terracotta
Hasta aquí tenemos un Productor-Consumidor que usan un único servidor de Terracotta para compartir los datos. Pero, ¿qué pasa si necesitamos de alta disponibilidad?
Es posible crear un cluster de servidores Terracotta. Estos servidores funcionan en una configuración Activo-Pasivo. Es decir, de todos los nodos del cluster, sólo uno es activo y realiza procesamiento. El resto de los nodos están pasivos, recibiendo la información y el estado. Cuando el nodo activo falla (por ejemplo, porque se cae detiene el proceso manualmente), el cluster elige a un nuevo nodo activo. Este nodo toma la responsabilidad del nodo saliente, y la ejecución de los clientes sigue de manera transparente.
Es decir, los clientes (las instancias de la aplicación) siguen funcionando normalmente pese a la falla de un nodo del cluster.
Para crear un cluster de Terracotta es necesario dos configuraciones:
- del lado del servidor, para indicar qué nodos formarán el cluster. Archivo server-config.xml
- del lado del cliente, para indicar qué nodos existen en el cluster. Archivo tc-config.xml
El cliente se conectará a un nodo activo, y cuando falle saltará automáticamente la ejecución al próximo nodo activo que aparezca.
Para el siguiente ejemplo usaremos dos equipos físicos distintos, llamados "Dib" y "Gaz". Estos son los nombres de los equipos en la red (pueden usar también la dirección IP).
Configurando los servidores: server-config.xml
Crearemos el archivo server-config.xml que usaremos en cada uno de los servidores de Terracotta que usaremos. En este caso declaramos a los servidores "Dib" y "Gaz".
<?xml version="1.0" encoding="UTF-8" ?> <tc:tc-config xmlns:tc="http://www.terracotta.org/config"> <servers> <server host="Dib" name="Equipo-Dib"> <l2-group-port>9530</l2-group-port> <data>%(user.home)/terracotta/server-data</data> <logs>%(user.home)/terracotta/server-logs</logs> <statistics>%(user.home)/terracotta/server-statistics</statistics> </server> <server host="Gaz" name="Equipo-Gaz"> <l2-group-port>9530</l2-group-port> <data>%(user.home)/terracotta/server-data</data> <logs>%(user.home)/terracotta/server-logs</logs> <statistics>%(user.home)/terracotta/server-statistics</statistics> </server> <ha> <mode>networked-active-passive</mode> <networked-active-passive> <election-time>5</election-time> </networked-active-passive> </ha> </servers> <clients> <logs>%(user.home)/terracotta/client-logs</logs> <statistics>%(user.home)/terracotta/client-statistics</statistics> </clients> </tc:tc-config>
Configurando los clientes: tc-config.xml
Para los clientes será necesario editar el archivo tc-config.xml (el mismo usado anteriormente) para agregar más servidores a la lista:
<servers> <server host="Dib" name="Equipo-Dib"/> <server host="Gaz" name="Equipo-Gaz"/> <update-check> <enabled>true</enabled> </update-check> </servers>
Ejecución en cluster
Leventamos el cluster de Terracotta
La ejecución del cluster es realmente muy sencilla. En el equipo Dib ejecutamos:
TERRACOTTA_HOME/start-tc-server.sh -f server-config.xml -n Equipo-Dib
Dib se convierte así en el primer particpante el cluster, y en el nodo Activo del mismo.
En el equipo Gaz ejecutamos:
TERRACOTTA_HOME/start-tc-server.sh -f server-config.xml -n Equipo-Gaz
Gaz se une al cluster, y se convierte en un nodo Pasivo.
Ejecutamos a los clientes
La ejecución de los clientes (Productores y Consumidores) es exactamente la misma. Tanto el Productor como el Consumidor se conectarán al nodo activo para trabajar.
La magia ocurre cuando un nodo falla... así que, vamos a simular la caida de Dib. Mientras un Consumidor esté funcionando, vayan a la consola de Dib (el nodo activo) y paren el proceso (presionen CTRL + C, o cierren la ventana).
Verán que los Consumidores se detienen momentáneamente. El nodo pasivo (Gaz) tomará el control, informará que se convierte en el nodo Activo, y los clientes resumiran su actividad de manera totalmente transparente.
Los nodos pasivos tenían la misma información del objeto Cola, por lo que pueden asumir el control del cluster.