jueves, 11 de noviembre de 2021

Hazelcast: Introducción

Hoy vamos a ver como implementar Hazelcast, una red de datos en memoria a.k.a IMDG de las más conocidas y utilizadas actualmente. Y, ¿Que es una IMDG? Según la definición de la propia Hazelcast, es un conjunto de nodos conectados en red que permiten la compartición de sus datos en memoria entre las aplicaciones que son parte de la red. A dichos nodos o sus propios clientes también se les conoce como instancias de Hazelcast. 

La elección de Hazelcast para su uso en un proyecto vino en mi caso debido a la necesidad de tener una única instancia de un objeto a compartir, no solo entre distintas aplicaciones y los servidores clusterizados. Pero compartiendo la instancia de hazelcast entre las distintas aplicaciones de un mismo servidor. Este será el esquema:

El ejemplo esta centrado únicamente en la característica de Hazelcast que nos permite compartir objetos, evitando la duplicidad y reduciendo sensiblemente los tiempos de ejecución. Y por tanto obviando otras importantes características como el procesamiento de largas cantidades de datos, que se escapan del objetivo de este post/ejemplo. 

Empecemos como siempre indicando la librería necesaria para el funcionamiento de nuestro ejemplo. En este caso con una única librería tendremos todo lo necesario: hazelcast-all. Y aquí viene uno de los puntos importantes del ejemplo. Si indicamos la librería como dependencia de tipo compile en cada uno de los proyectos. A la hora de crear la instancia de forma programática, como cada aplicación tendrá su propio classloader, se generarán tantas instancias de hazelcast en el servidor como aplicaciones haya. Por tanto la librería la debemos poner con ámbito provided e incluirla dentro del propio servidor. 

El siguiente paso será crear un ServletContextListener en nuestra aplicación que se lance cuando es arrancada y que nos permita obtener la instancia de hazelcast. Ya creamos un nodo del clúster de Hazelcast o nos conectemos como clientes a uno de los nodos existentes, obtendremos un objeto de tipo HazelcastInstance que nos permitirá manejar la información del mismo. 

@WebListener
public class HazelcastStartupListener implements ServletContextListener {
	@Override
	public void contextDestroyed(final ServletContextEvent sce) {
		Hazelcast.shutdownAll();
	}
	@Override
	public void contextInitialized(final ServletContextEvent sce) {
		String instance = System.getProperty("hazelcast.instance.name");
		HazelcastInstance hzi = Hazelcast.getHazelcastInstanceByName(instance);
		if (hzi == null) {
			hzi = Hazelcast.getOrCreateHazelcastInstance();
		}
		HazelcastHelper.getINSTANCE().setHz(hzi);
	}
}

La clase estática tiene varios métodos que te permiten crear u obtener la instancia de Hazelcast. Al crear la instancia de forma programática podemos indicar las diferentes opciones de configuración a través de la clase Config que podemos pasar al constructor. En el caso no lo hagamos se configurará con los valores por defecto o los indicados en el fichero hazelcast.xml que se encuentre en el classpath. 

<?xml version="1.0" encoding="UTF-8"?>
<!--suppress XmlDefaultAttributeValue -->
<hazelcast xmlns="http://www.hazelcast.com/schema/config"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://www.hazelcast.com/schema/config
           http://www.hazelcast.com/schema/config/hazelcast-config-4.2.xsd">
    <!--
    https://github.com/hazelcast/hazelcast/blob/v4.2.2/hazelcast/src/main/resources/hazelcast-full-example.xml
        Specifies the cluster name. It allows creating separate sub-clusters with different names.
        The name is also referenced in the WAN Replication configuration.
    -->
    <cluster-name>hz-cluster</cluster-name>
    <!--
        When Hazelcast instances are created, they are put in a global registry with their creation names.
        <instance-name> elements gives you the ability to get a specific Hazelcast instance from this registry
        by giving the instance's name.
    -->
    <instance-name>${hazelcast.instance.name}</instance-name>
    <network>
        <join>
            <auto-detection enabled="false"/>
            <multicast enabled="false"></multicast>
            <tcp-ip enabled="true">
                <member-list>
                    <member>tomcat-server1</member>
                    <member>tomcat-server2</member>
                </member-list>
            </tcp-ip>
        </join>
    </network>
</hazelcast>

Hazelcast permite la detección de los nodos de forma automática y a través de varios métodos. Pero la mejor forma es configurarlo a través de las direcciones IP de los nodos miembros o en su defecto, como es en este caso, con los nombres DNS. Es más, si configuramos la conexión de otra forma, una vez que se reconozcan se comunicarán a través de este método. 

Aparte también indicaremos el nombre de la instancia. El cual como vimos en el código Java anterior, será utilizado para obtener la instancia del nodo. En el ejemplo, utilizaremos una característica de los ficheros de configuración, y es la posibilidad de indicar determinados valores en función de propiedades del sistema. 

El despliegue de los servidores lo realizaremos a través de un docker compose donde podemos ver la configuración indicada anteriormente. 

version: '3.7'

services:
  tomcat-server1:
    image: tomcat:9.0
    container_name: tomcat-server1
    ports:
      - 8080:8080
    volumes:
         - ../../../target/RestEasyService.war:/usr/local/tomcat/webapps/RestEasyService.war
         - ../../../target/RestEasyService.war:/usr/local/tomcat/webapps/DuplicateService.war
         - ./lib/hazelcast-all-4.2.2.jar:/usr/local/tomcat/lib/hazelcast-all-4.2.2.jar
    environment:
      JAVA_OPTS: "-Dhazelcast.instance.name=hzInstance1"
    networks:
      sandbox-net:
        aliases:
          - tomcat-server1

  tomcat-server2:
    image: tomcat:9.0
    container_name: tomcat-server2
    ports:
      - 8082:8080
    volumes:
         - ../../../target/RestEasyService.war:/usr/local/tomcat/webapps/RestEasyService.war
         - ../../../target/RestEasyService.war:/usr/local/tomcat/webapps/DuplicateService.war
         - ./lib/hazelcast-all-4.2.2.jar:/usr/local/tomcat/lib/hazelcast-all-4.2.2.jar
    environment:
      JAVA_OPTS: "-Dhazelcast.instance.name=hzInstance2"
    networks:
      sandbox-net:
        aliases:
          - tomcat-server2

networks:
  sandbox-net:
    ipam:
      driver: default
      config:
        - subnet: "172.16.238.0/24"

La aplicación como vemos en el docker-compose la duplicaremos en el servidor y nos permitirá comprobar a través de una sencilla API el funcionamiento de hazelcast. Tendremos un recurso que almacenará el valor de una propiedad y otro recurso que nos permitirá obtener el valor de la propiedad. 

Por tanto al arrancar podremos ver como se crean las instancias:

Para almacenar los valores tendremos distintos tipos de objetos de tipo Collection como Set, Queue, List o utilidades que posibilitan la concurrencia como FencedLock, ISemaphore o AtomicReference. 

@Log4j2
@Path("/info")
public class InfoServiceImple {
	private String getMachine() {
		return HazelcastHelper.getINSTANCE().getHz().getCluster()
			.getLocalMember().getSocketAddress().getHostName();
	}
	@GET
	@Path("/{property}")
	@Produces({ MediaType.APPLICATION_JSON })
	public Info getProperty(@PathParam("property") final String property) {
		IMap<String, String> map = HazelcastHelper.getINSTANCE()
			.getHz().getMap("infoProperties");
		return new Info(getMachine(), property, map.get(property));
	}
	@POST
	@Path("/{property}/{value}")
	@Produces({ MediaType.APPLICATION_JSON })
	public Info setProperty(@PathParam("property") final String property, 
				@PathParam("value") final String value) {
		IMap<String, String> map = HazelcastHelper.getINSTANCE()
			.getHz().getMap("infoProperties");
		Info info = new Info(getMachine(), property, map.get(property));
		map.put(property, value);
		return info;
	}
}

Ahora podremos invocar cualquiera de las APIs para almacenar un valor y cualquiera de las otras para poder obtener el valor almacenado. 

#curl --request POST 'http://localhost:8082/RestEasyService/rest/info/property/value'
{"machine":"tomcat-server2","property":"property","value":null}
#curl --request GET 'http://localhost:8080/DuplicateService/rest/info/property'
{"machine":"tomcat-server1","property":"property","value":"value"}

Por último unos concejos:

  • El clúster debería esta formado por al menos tres nodos. 
  • En el caso de que utilicemos una de las utilidades de concurrencia debemos configurar y subsistema CP. 
  • Los nodos deberían arrancarse como contenedores Docker o ejecutando los binarios que proporcionan. Y en el código Java usar la API a través de un cliente. 
Como siempre, el código completo lo podeis ver aquí

No hay comentarios:

Publicar un comentario