HazelCast: In-Memory Data Grid Prêt-à-porter
HazelCast è un data grid opensource, che permette di memorizzare dati ed effettuare operazioni direttamente tramite la memoria volatile (RAM). Alcune delle principali feature di questo progetto sono le seguenti
- Distributed java.util.{Queue, Set, List, Map}
- Distributed java.util.concurrency.locks.Lock
- Distributed java.util.concurrent.ExecutorService
- Distributed Topic for publish/subscribe messaging
- Write-Through and Write-Behind persistence for maps
- Java Client for accessing the cluster remotely
- Dynamic HTTP session clustering
- Support for cluster info and membership events
- Dynamic discovery
- Dynamic scaling
- Dynamic partitioning with backups
- Dynamic fail-over
Il fatto di poter gestire oggetti distribuiti, unito alla semplicità di poter creare dei cluster, fanno di questo prodotto un progetto molto interessante e soprattutto versatile per diverse situazioni. In questi anni sono stati creati molti progetti simili a HazelCast come Oracle Coherence, MongoDB, TerraCotta e Cassandra. Da uno studio indipendente che è stato fatto ultimamente HazelCast sembrerebbe avere delle performance davvero interessanti. Tralasciando questi test, HazelCast è comunque un progetto interessante che per alcune sue peculiarità può tornare molto utile nei nostri progetti.
Hello World
Vediamo quindi come poter creare un Map condiviso da un server, agganciare un client e leggere questo Map. Possiamo creare un semplice progetto Maven includendo le seguenti dipendenze
<dependency> <groupId>com.hazelcast</groupId> <artifactId>hazelcast-client</artifactId> <version>3.1.3</version> </dependency> <dependency> <groupId>com.hazelcast</groupId> <artifactId>hazelcast</artifactId> <version>3.1.3</version> </dependency>
La classe che crea il Map e fa partire un’istanza di HazelCast è HelloServer, che richiamando il metodo statico newHazelcastInstance di com.hazelcast.core.Hazelcast crea un nuovo server e memorizza al suo interno un Map.
package com.javastaff.hazelcast.tutorial.hello; import com.hazelcast.config.Config; import com.hazelcast.config.NetworkConfig; import com.hazelcast.core.Hazelcast; import com.hazelcast.core.HazelcastInstance; import java.util.Map; public class HelloServer { public static void main( String[] args ) { Config cfg = new Config(); NetworkConfig network = cfg.getNetworkConfig(); network.setPort(5701); HazelcastInstance instance = Hazelcast.newHazelcastInstance(cfg); Map<Integer, String> map = instance.getMap("helloworld"); map.put(1, "Hello"); map.put(2, "World"); } }
Se eseguite questa classe vedrete prima di tutto questa informazione nel log
Members [1] { Member [127.0.0.1]:5701 this }
che praticamente vi dice che attualmente il cluster HazelCast è composto da un solo nodo, in ascolto sulla porta 5701. Poi come avrete notato avviando HelloServer, la sua esecuzione non termina, visto che se non viene fatto lo shutdown del server questo rimane in ascolto. Passiamo ora al client che si connette e legge la Map che abbiamo condiviso.
package com.javastaff.hazelcast.tutorial.hello; import com.hazelcast.client.HazelcastClient; import com.hazelcast.client.config.ClientConfig; import com.hazelcast.core.HazelcastInstance; import com.hazelcast.core.IMap; public class HelloClient { public static void main(String[] args) { ClientConfig clientConfig = new ClientConfig(); clientConfig.addAddress("127.0.0.1:5701"); HazelcastInstance client = HazelcastClient.newHazelcastClient(clientConfig); IMap map = client.getMap("helloworld"); System.out.println("Map Size:" + map.size()); System.out.println(map.get(1)+" "+map.get(2)); client.shutdown(); } }
In questo caso viene condiviso un Map, ma HazelCast ci permette di rendere distribuiti anche altre tipologie di oggetti. Di seguito viene riportata una lista degli oggetti che è possibile distribuire con il link alla relativa documentazione ufficiale:
Publish & Subscribe
Come avete visto dalla precedente lista di oggetti, è possibile anche distribuire un Topic, che è qualcosa di simile al Topic presente nella specifica JMS. Infatti HazelCast, pur non implementando la specifica JMS, può essere utilizzato anche per lo scambio di messaggi, facendo riferimento al modello publish/subscribe (pub/sub). Vediamo quindi l’esempio di una classe che pubblica ogni 5 secondi diverse stringhe
package com.javastaff.hazelcast.tutorial.messaging; import com.hazelcast.core.Hazelcast; import com.hazelcast.core.HazelcastInstance; import com.hazelcast.core.ITopic; import java.util.Random; import java.util.concurrent.TimeUnit; public class TopicPublisher implements Runnable { private volatile boolean running; private HazelcastInstance hzInstance; private String[] cityList = {"Roma", "Milano", "Torino" , "Napoli", "Palermo"}; public TopicPublisher() { this.running = true; this.hzInstance = Hazelcast.newHazelcastInstance(); } public static void main(String[] args) { TopicPublisher tp = new TopicPublisher(); Thread thread = new Thread(tp); thread.start(); } public void run() { do { publish(); sleep(); } while (running); } private void publish() { java.util.Random r=new Random(); String posto=cityList[r.nextInt(cityList.length)]; ITopic topic = hzInstance.getTopic("default"); topic.publish("Allerta meteo per "+posto); } private void sleep() { try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } } }
Utilizzando l’oggetto HazelcastInstance prendiamo il riferimento al topic su cui pubblicare la notizia per l’allerta meteo (scusate ma non trovavo un esempio significativo 😛 ) e successivamente richiamiamo il metodo publish di ITopic. In questo caso viene inserita nel topic una semplice stringa ma sarebbe stato possibile anche distribuire un qualsiasi oggetto definito da noi. Passiamo ora al codice della classe che legge dal topic
package com.javastaff.hazelcast.tutorial.messaging; import com.hazelcast.core.Hazelcast; import com.hazelcast.core.HazelcastInstance; import com.hazelcast.core.ITopic; import com.hazelcast.core.Message; import com.hazelcast.core.MessageListener; public class TopicSubscriber implements MessageListener<String>{ public TopicSubscriber() { HazelcastInstance hzInstance = Hazelcast.newHazelcastInstance(); ITopic<String> topic = hzInstance.getTopic("default"); topic.addMessageListener(this); } public void onMessage(Message<String> msg) { System.out.println(msg.getPublishTime()+ " - "+msg.getMessageObject()); } public static void main(String a[]) { new TopicSubscriber(); } }
In questo caso dobbiamo implementare l’interfaccia com.hazelcast.core.MessageListener che prevede la definizione del metodo onMessage (molto simile alla controparte JMS). Nel costruttore di TopicSubscriber ci agganciamo ad HazelCast, recuperiamo il topic d’interesse e aggiungiamo la stessa classe come listener.
Configurazione
La configurazione dei nodi e delle informazioni distribuite può essere fatta in due diverse modalità: file di configurazione o in maniera programmatica. Il file di configurazione XML viene prima ricercato nel file specificato dalla proprietà di sistema hazelcast.config. Se non è presente questa proprietà allora viene ricercato nel classpath il file hazelcast.xml e se nemmeno questo è presente viene caricato il file di default hazelcast-default.xml presente nel file hazelcast.jar. Per configurarlo in maniera programmatica invece possiamo agire direttamente sull’oggetto Config come riportato nel seguente esempio
Config cfg = new Config(); cfg.setPort(5900); cfg.setPortAutoIncrement(false); NetworkConfig network = cfg.getNetworkConfig(); JoinConfig join = network.getJoin(); join.getMulticastConfig().setEnabled(false); join.getTcpIpConfig().addMember("10.45.67.32").addMember("10.45.67.100") .setRequiredMember("192.168.10.100").setEnabled(true); network.getInterfaces().setEnabled(true).addInterface("10.45.67.*");
Per quanto riguarda la scalabilità di HazelCast c’è da evidenziare prima di tutto le seguenti feature comuni a tutti i tipi di dati distribuiti su questo data grid:
- I dati presenti sono partizionati su tutti i nodi del cluster.
- Se un membro del cluster va giù, il nodo designato come sua replica di backup inizia a ridistribuire i suoi dati sugli altri nodi.
- Quando un nuovo nodo si aggiunge al cluster, gli vengono assegnati una frazione dei dati presenti nel cluster dei quali è responsabile (alleggerendo il carico degli altri nodi) ed eventualmente ne aggiunge altri al cluster.
- Non esiste un singolo nodo/cluster master che può causare quello che viene chiamato un single point of failure. Ogni nodo ha eguali responsabilità.
Oltre a queste caratteristiche in HazelCast possiamo effettuare una configurazione davvero dettagliata a livello di nodi del cluster, partizionamento della rete, listener di migrazione fra diversi nodi etc. etc. Per un analisi dettagliata di queste feature vi rimando alla documentazione ufficiale su questo argomento
MapLoader
HazelCast prevede la possibilità di caricare i map distribuiti da un’altra fonte dati. Uno scenario tipico potrebbe essere quello in cui vogliamo rendere disponibili in sola lettura delle informazioni che vengono richieste frequentemente, senza dover accedere direttamente al database. In questo caso potremmo popolare HazelCast con i valori prendendoli dal database quando vengono richiesti. Proprio per questo esiste l’interfaccia MapLoader che prevede i seguenti metodi
- load : Ritorna il valore di una specifica chiave
- loadAll: Ritorna un map contenente le coppie chiave-valore per certe chiavi richieste
- loadAllKeys: Ritorna tutte le chiavi presenti
Ipotizziamo quindi di voler trasferire su un map distribuito tutte le coppie chiave-valore presenti in un nostro file di properties. Ecco come dovrebbe essere implementata la classe che effettua questa gestione
package com.javastaff.hazelcast.tutorial.store; import com.hazelcast.core.MapLoader; import java.io.IOException; import java.io.InputStream; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; public class MyStore implements MapLoader<String,String>{ private static Properties properties; private static final String FILE_PROPERTIES = "data.properties"; public MyStore() { properties = new Properties(); InputStream inputStream = getClass().getClassLoader() .getResourceAsStream(FILE_PROPERTIES); try { properties.load(inputStream); } catch (IOException ex) { Logger.getLogger(MyStore.class.getName()) .log(Level.SEVERE, null, ex); } } public String load(String k) { return properties.getProperty(k); } public Set<String> loadAllKeys() { System.out.println("MyStore.loadAllKeys "); Set keys = properties.keySet(); return keys; } public Map<String,String> loadAll(Collection keys) { System.out.println("MyStore.loadAll keys " + keys); Map<String,String> map=new HashMap<String, String>(); Iterator iterator=keys.iterator(); String tempKey=null; while(iterator.hasNext()) { tempKey=(String)iterator.next(); map.put(tempKey, properties.getProperty(tempKey)); } return map; } }
Praticamente la classe si carica in memoria il properties e poi in base al metodo che viene richiamato ci permette di leggere i valori. Quando HazelCast vede che viene richiesto un valore non presente per un determinato map, se è stato configurato un MapLoader cerca di caricarlo utilizzando questa classe. Ovviamente una volta lette le coppie chiave-valore, queste saranno memorizzati nel map distribuito di HazelCast, quindi non verrà più richiamato nessun metodo load (a meno che non venga settato a null questo valore). Per il client che effettua la chiamata questa operazione di caricamento è trasparente, nel nostro esempio la classe potrebbe essere la seguente
package com.javastaff.hazelcast.tutorial.store; import com.hazelcast.client.HazelcastClient; import com.hazelcast.client.config.ClientConfig; import com.hazelcast.core.HazelcastInstance; import com.hazelcast.core.IMap; public class StoreClient { public static void main(String[] args) { ClientConfig clientConfig = new ClientConfig(); clientConfig.addAddress("127.0.0.1:5701"); HazelcastInstance client = HazelcastClient.newHazelcastClient(clientConfig); IMap map = client.getMap("dati"); System.out.println("Map Size:" + map.size()); System.out.println(map.get("key1")+" "+map.get("key2")); client.shutdown(); } }
Per configurare il MapLoader possiamo procedere, come abbiamo visto in precedenza, attraverso il file di configurazione o in maniera programmatica. Per farlo attraverso lo start di nodo possiamo utilizzare la seguente classe
package com.javastaff.hazelcast.tutorial.store; import com.hazelcast.config.Config; import com.hazelcast.config.MapConfig; import com.hazelcast.config.MapStoreConfig; import com.hazelcast.core.Hazelcast; import java.util.Map; public class HazelCastServer { public static void main(String a[]) { Config myConfig = new Config(); Map<String, MapConfig> myHazelcastMapConfigs = myConfig.getMapConfigs(); MapConfig myMapConfig = new MapConfig(); myMapConfig.setName("dati"); myMapConfig.setTimeToLiveSeconds(1000); MyStore myStore=new MyStore(); MapStoreConfig mapStoreConfig = new MapStoreConfig(); mapStoreConfig.setEnabled(true); mapStoreConfig.setWriteDelaySeconds(0); mapStoreConfig.setImplementation(myStore); myMapConfig.setMapStoreConfig(mapStoreConfig); myHazelcastMapConfigs.put("dati", myMapConfig); Hazelcast.newHazelcastInstance(myConfig); } }
Praticamente nell’oggetto di tipo Config che utilizziamo per avviare il nodo andiamo a specificare che ci sarà un map di nome “dati” e che avrà associato un MapStore di tipo MyStore. Volendo utilizzare la configurazione xml ecco cosa dobbiamo riportare
<map name="dati"> <map-store enabled="true"> <class-name> com.javastaff.hazelcast.tutorial.store.MyStore </class-name> <write-delay-seconds>0</write-delay-seconds> </map-store> </map>
MapStore
Abbiamo visto come poter popolare un map con dei valori presi da un’altra fonte dati, ora vediamo come riversare quello che c’è nel map altrove. In maniera similare a MapLoader, è stata definita l’interfaccia MapStore che prevede i metodi per salvare e rimuovere le chiavi da un’altra eventuale fonte dati. Per utilizzare questa feature dobbiamo utilizzare la stessa configurazione usata per MapLoader e quindi possiamo far implementare l’interfaccia MapStore alla nostra classe MyStore. Provando ad inserire dei metodi che effettuano un semplice log vi renderete conto che, quando un client richiede l’aggiornamento di un valore del map o la cancellazione, verranno richiamati questi metodi.
Alternativa a Memcached
Memcached è un famoso sistema di cache distribuita ed esistono molte librerie e client che permettono di configurare il nostro applicativo per il suo utilizzo. HazelCast può essere utilizzato in alternativa a Memcached senza nessun cambiamento in quanto fornisce la stessa interfaccia per la connessione. Di seguito un esempio riportato dalla documentazione ufficiale dove c’è un classico esempio di connessione da parte di un applicativo PHP, che può essere utilizzato anche nel caso in cui lo facciamo connettere al nostro server HazelCast
$memcache->connect(’10.20.17.1′, 5701) or die (“Could not connect”); $memcache->set(‘key1′,’value1′,0,3600); $get_result = $memcache->get(‘key1′); //retrieve your data var_dump($get_result); //show it
Web Clustering
Nelle applicazioni web dove sono presenti diversi server in cluster c’è spesso l’esigenza di avere la sessione replicata all’interno del cluster. In questo modo se l’utente con una sessione attiva sta dialogando con un server che cade, abbiamo la possibilità di continuare a lavorare su un altro server del cluster senza nessun problema. HazelCast può essere utilizzato anche in questo modo, effettuando una semplice configurazione sul web.xml della nostra applicazione. Dopo aver aggiunto le seguenti librerie di HazelCast al nostro progetto
<dependency> <groupId>com.hazelcast</groupId> <artifactId>hazelcast-wm</artifactId> <version>3.1.3</version> </dependency> <dependency> <groupId>com.hazelcast</groupId> <artifactId>hazelcast</artifactId> <version>3.1.3</version> </dependency>
possiamo aggiungere la seguente configurazione al web.xml
<filter> <filter-name>hazelcast-filter</filter-name> <filter-class>com.hazelcast.web.WebFilter</filter-class> <init-param> <param-name>map-name</param-name> <param-value>my-sessions</param-value> </init-param> <init-param> <param-name>sticky-session</param-name> <param-value>true</param-value> </init-param> <init-param> <param-name>debug</param-name> <param-value>true</param-value> </init-param> </filter> <filter-mapping> <filter-name>hazelcast-filter</filter-name> <url-pattern>/*</url-pattern> <dispatcher>FORWARD</dispatcher> <dispatcher>INCLUDE</dispatcher> <dispatcher>REQUEST</dispatcher> </filter-mapping> <listener> <listener-class> com.hazelcast.web.SessionListener </listener-class> </listener>
Abbiamo definito un nuovo filtro nella nostra applicazione, istanza della classe com.hazelcast.web.WebFilter. Questo filtro è stato inizializzato con i seguenti parametri:
- map-name: il nome del map distribuito che conterrà le nostre sessioni http
- sticky-session: specifica se la vostra applicazione ha attiva la configurazione sticky-session sul load balancer
- debug: abilita le informazioni di debug
Oltre a questo è stato anche definito un listener di tipo com.hazelcast.web.SessionListener. Ed ecco le sessioni clusterizzate senza troppa fatica.
Conclusioni
In questo articolo abbiamo visto una serie di feature di HazelCast, che sicuramente ci fanno capire le potenzialità di questo prodotto opensource. Senza entrare nella discussione riguardante le performance di questo prodotto, sicuramente possiamo notare quanto sia estremamente flessibile e versatile per diverse situazioni. Qui di seguito trovate il link al progetto GitHub contenente i sorgenti dell’articolo.

Looking for a right “about me”…
Commenti recenti