In de IT krijgen we steeds vaker te maken met Big Data software oplossingen met alle uitdagingen die daarbij komen kijken. Zowel Oracle, IBM en VMware hebben producten die vaak kostbaar zijn en veelal niche markten targetten. Er zijn echter ook soortgelijke producten van andere partijen uit de open source wereld, waaronder Hazelcast.
Hazelcast is een in-memory data grid die developers ondersteunt om data gedistribueerd op te slaan. Het kan worden gebruikt als een clustering oplossing, een gedistribueerde cache en een NoSQL key-value store. In Nederland wordt Hazelcast door meerdere bedrijven gebruikt, waaronder bij het Havenbedrijf Rotterdam, Guerrilla Games (de makers van de game Killzone) en sinds kort ook bij de Nederlandse Spoorwegen. In dit artikel gaan wij onder meer in op hoe Hazelcast werkt, hoe eenvoudig dit te implementeren is in een bestaand systeem en bieden we een aantal opgedane best practices.
Nederlandse Spoorwegen
Binnen de NS wordt altijd eerst een plan gemaakt om alle treinen te kunnen laten rijden op de meest efficiënte manier die het best aansluit bij de reizigerswensen. Deze planning wordt opgeleverd aan de bijsturing. De bijsturing zorgt er vervolgens voor dat de geplande treinen ook bij onvoorziene omstandigheden kunnen blijven rijden door middel van bijsturing op het plan.
planning NS
Het doel van het project B@M is om één van de bestaande systemen te vervangen. Het systeem B@M gaat de materieel bijstuurders helpen om de bijsturing van het materieel uit te voeren. Het gewenste systeem is dermate groot en complex dat drie multidisciplinaire Scrumteams parallel werken aan de benodigde features welke naar verwachting tussen de 400.000 en 600.000 lines of code zal gaan beslaan.
Hazelcast en conflictsignalering
Bij een afwijking op het plan tijdens de uitvoering moet zo snel mogelijk beoordeeld worden of hierdoor problemen ontstaan. Als eerder bijgestuurd kan worden, hebben reizigers hier zo min mogelijk last van. Om dit te bevorderen detecteert het B@M systeem automatisch, in real-time, deze zogenaamde conflicten.
Er zijn veel scenario’s waardoor een conflict kan ontstaan. Dit kan bijvoorbeeld een boom zijn die op de rails is gevallen, een treinstel dat defect is of een machinist die onverwacht ziek is. Op ieder moment van de dag rijden honderden treinen tegelijk over het spoor die allemaal gemonitord moeten worden. Dit gebeurt door middel van GPS op treinstellen, sensoren in het spoor en handmatig ingevoerde meldingen (bijvoorbeeld: ‘kapotte ruitenwisser’). Wanneer iets op het spoort is gebeurd, dan zal dit worden doorgegeven aan B@M via berichten op een service bus. Momenteel wordt op piekmomenten gemiddeld één bericht per seconde richting B@M gestuurd en potentiële conflicten in het hele land moeten direct bepaald worden.
Dit betekent dat er realtime complexe berekeningen moeten worden gemaakt over grote hoeveelheden data. Tevens moet het systeem 24/7 beschikbaar zijn en mag de conflictsignalering module de rest van de applicatie niet vertragen of in de weg zitten. Dit alles maakt het bouwen van de conflictsignalering module een enorm grote uitdaging!
Berekeningen moeten snel kunnen worden gemaakt. De performance bij de reeds bestaande conflictsignaleringsmodule voor de planning waarbij een relationele database is gebruikt, is niet voldoende voor de bijsturing. Daarom is gezocht naar een manier om berekeningen over grote hoeveelheden data sneller te kunnen uitvoeren.
Hiervoor is gekozen voor een in-memory datastore. Na een proof of concept is besloten om Hazelcast in te zetten als dedicated in-memory datastore voor conflictsignalering.
Waarom dan Hazelcast?
Daar zijn meerdere redenen voor. Allereerst is het open source, waardoor het gemakkelijk kan worden uitgeprobeerd. Zowel het maken van een proof of concept als het draaien op productie kan zonder enige licentie te hebben aangeschaft. Ten tweede is Hazelcast zeer lightweight. Het bestaat uit één enkele JAR van nog geen 3 megabyte, welke los te downloaden is of met Maven in het project kan worden gebruikt. Verder is de leercurve om Hazelcast te gebruiken zeer laag. Als je bekend bent met Java dan is er, in tegenstelling tot vergelijkbare producten, nagenoeg geen leercurve. Dit is ook goed terug te zien in het onderstaande code voorbeeld waar Hazelcast wordt gebruikt in combinatie met de standaard Java Map.
HazelcastInstance hazelcastInstance = Hazelcast.newHazelcastInstance();
Map<Integer, Employee> map = hazelcastInstance.getMap("employees");
map.put(1, new Employee());
Listing 1
In Hazelcast wordt alle data opgeslagen in speciale collecties die Hazelcast aanbiedt. Deze collecties zijn Interfaces boven op de bestaande implementaties van de Set, List en Map uit de java.util package. Hazelcast zelf is vanwege deze overeenkomsten gemakkelijk op te pakken wanneer je bekend bent met Java. De makers van Hazelcast hebben goed nagedacht over de eenvoud van het gebruik, maar hoe kan Hazelcast geïntegreerd worden in een bestaand systeem, zonder daarvoor het hele systeem om te hoeven bouwen? Met deze vraag in het achterhoofd zijn de makers van Hazelcast aan de slag gegaan en dat is ook goed te zien in het eindresultaat.
Nadelen?
Na Hazelcast een tijd te hebben gebruikt, hebben we ontdekt dat ze op enkele gebieden nog niet volwassen zijn. Zo hebben wij een aantal bugs ontdekt met betrekking tot transactioneel werken, hoewel deze bugs na de melding hiervan snel werden opgelost.
How to Hazelcast?
Het activeren van Hazelcast in een applicatie is kinderlijk eenvoudig, om precies te zijn één regel Java code: “Hazelcast.newHazelcastInstance();”. Wanneer deze methode wordt uitgevoerd, zal vanzelf een cluster van één Hazelcast node worden gevormd. Tot zover klinkt het geweldig, maar er is nog geen sprake van een ‘echt’ cluster, aangezien er maar één Hazelcast node is opgestart op één computer.
Om Hazelcast geclusterd te laten draaien, kan simpelweg een nieuwe node worden gestart. Door middel van Multicasting kunnen de twee nodes elkaar vinden en een cluster vormen. Uiteraard is dit, zonder configuratie, enkel mogelijk wanneer de twee computers zijn aangesloten op hetzelfde lokale netwerk.
Hazelcast gebruikt standaard multicasting om andere nodes te vinden in een netwerk. Met Multicasting kunnen berichten naar meerdere ontvangers in een aangegeven groep worden verstuurd. In dit geval zijn alle Hazelcast nodes waarbij multicasting aanstaat standaard geabonneerd op dezelfde groep.
Vaak is het mogelijk om multicasting te gebruiken, maar is dit echter niet altijd gewenst. Er worden met multicasting namelijk continu pakketjes over de lijn gestuurd naar de andere nodes om de verbinding in stand te houden. De standaard instelling van Hazelcast kan hierdoor dus soms niet worden gebruikt. Hazelcast biedt echter wel de mogelijkheid om een TCP/IP cluster te configureren, dit kan worden gedaan in het hazelcast.xml bestand. Hierin kan multicasting expliciet worden uitgeschakeld, waarna TCP/IP expliciet moet worden ingeschakeld.
Na multicasting te hebben uitgeschakeld, kunnen twee nodes elkaar niet meer vinden door ‘pings’ over het netwerk te sturen. Hierdoor kan geen cluster meer ontstaan. Om dit alsnog te laten werken, moeten de IP-adressen van de nodes worden geconfigureerd. Hierbij hoeven niet alle IP-addressen bij alle nodes bekend te zijn. Door middel van daisy chaning kan Hazelcast ook een cluster vormen. Node 1 heeft een indirecte verbinding met Node 3, terwijl Node 1 enkel Node 2 in de configuratie heeft staan.
Daisy chaining
Bij het starten van de nodes zal Hazelcast ervoor zorgen dat de nodes aan elkaar worden gekoppeld. In de logging van Hazelcast is te zien dat een cluster is gevormd met deze drie nodes, wanneer alle drie nodes zijn gestart.
Members [3] {
Member [192.168.190.119]:5703
Member [192.168.190.119]:5704 this
Member [192.168.190.119]:5705
}
Listing 2
Robuustheid
Hazelcast distribueert maps naar meerdere JVM’s (cluster members). In maps staan lijsten van simpele Java Objecten, ook wel POJO’s (Plain Old Java Objects). Elke JVM bevat naast een portie van de data standaard ook een back-up van één andere cluster member.
Voorbeeld: Er zitten 4 nodes in een cluster. Elke node bevat +/- 25% van de data en bevat daarnaast ook nog een back-up van een van de andere nodes. Mocht een node onverhoopt ermee stoppen, dan heeft een andere node de data hiervan nog als back-up. Hazelcast zal er vervolgens voor zorgen dat de data weer wordt geherstructureerd over de nodes heen. Er blijven dus uiteindelijk drie nodes over die ieder +/- 33% van de data bezitten.
De hoeveelheid back-ups die van één clustermember wordt gemaakt, is configureerbaar. Een back-up kan op twee manieren ingesteld worden, namelijk Synchroon of Asynchroon. Wanneer een synchrone back-up wordt gemaakt, zal Hazelcast wachten op antwoord van de node waarop de back-up wordt geplaatst. Bij een asynchrone back-up wordt een zogenaamd ‘fire and forget’-bericht gestuurd wat minder betrouwbaar is, maar wel een performance voordeel biedt, omdat niet gecontroleerd hoeft te worden of het bericht daadwerkelijk is aangekomen.
Queries
Om de data uit de Maps te halen, kunnen de standaard Java Map methoden worden gebruikt. Hiermee kan je alleen niet specifiek filteren op de inhoud van objecten die zich in de map bevinden. Hiervoor heeft Hazelcast een oplossing. Het is mogelijk om de Maps te benaderen via een Query, net zoals bij een standaard database. Hiervoor hebben de makers van Hazelcast gekeken naar reeds bestaande manieren en de meest gebruikte overgenomen.
De Maps kunnen worden benaderd op twee verschillende manieren, enerzijds met een SQL Query en anderzijds door een look-a-like van de Java Persistence API, door middel van predicaten. Er wordt in onderstaande code gebruik gemaakt van het ‘Employee’ object, een Employee heeft een leeftijd (age) en een “active” status (in dienst/uit dienst). In onderstaande listing zijn codevoorbeelden te zien van verschillende query mogelijkheden.
HazelcastInstance hazelcastInstance = Hazelcast.newHazelcastInstance();
IMap<UUID, MaterieelEenheidDto> materieelEenhedenMap =
hazelcastInstance.getMap("materieelEenheden");
materieelEenhedenMap.addIndex("materieelsoort", true);
materieelEenhedenMap.addIndex("aantalbakken", true);
Collection<MaterieelEenheidDto> materieelEenhedenVanSoortICMMetMeerDan4BakkenSql =
materieelEenhedenMap.values(
new SqlPredicate(
"materieelsoort = 'ICM' AND aantalbakken > 4"));
EntryObject e = new PredicateBuilder().getEntryObject();
Collection<MaterieelEenheidDto> materieelEenhedenVanSoortICMMetMeerDan4Bakken =
materieelEenhedenMap.values(
e.get("materieelsoort").equal("ICM").and(
e.get("aantalbakken").greaterThan(4)));
Listing 3
Transacties
Hazelcast biedt verschillende mogelijkheden om transacties te ondersteunen. Zo kan je in Hazelcast een transactie starten die meedraait in de container transactie. Hiervoor moet de Hazelcast Resource adapter worden configureert op de applicatie container, zoals GlassFish, JBoss of WebLogic.
In de B@M applicatie van de NS is data-integriteit van groot belang. Om de data-integriteit van de applicatie te waarborgen, worden veel extra maatregelen genomen. Eén van die maatregelen is om ervoor te zorgen dat elke verandering die binnenkomt, waar de conflictsignalering op reageert, transactioneel verwerkt wordt. Denk bijvoorbeeld aan een binnenkomend bericht dat een treinstel defect is geraakt. Dit bericht wordt dan afgelezen van een topic en de veranderingen die daarbij horen, worden verwerkt in het Hazelcast datagrid. Wanneer dit verwerkt is, moet ook nog gekeken worden welke conflicten dit oplevert. Deze conflicten worden vervolgens gepubliceerd naar de cliëntapplicatie. Dit hele proces vindt plaats binnen één transactie en dit geeft aan hoe belangrijk data-integriteit op dit terrein is.
Snelheid optimalisaties
Alle data die in Hazelcast gezet wordt, moet kunnen worden geserialiseerd. Hiervoor moeten deze objecten minimaal de standaard java.io.Serializable interface implementeren. Hazelcast kan hiermee alle eigen gemaakte objecten omzetten naar een instantie van com.hazelcast.nio.serialization.Data. Data is een binaire representatie van jouw object geoptimaliseerd door Hazelcast. Hazelcast weet ook waar in dit object de waardes staan van de attributen en kan dus snel het geserialiseerde object doorzoeken.
Deze eigen manier van opslaan van Data door Hazelcast werkt ook handig in combinatie met de mogelijkheid om indexen te plaatsen op mappen. Wanneer op een Map een index wordt gezet, kan Hazelcast hier sneller op zoeken. Zonder index zal een zoekopdracht door het datagrid over elke member gaan die data heeft waarin gezocht moet worden. Met een Index weet Hazelcast welke member deze data bevat en dan kan hij zelfs nog per member alleen de relevante data doorzoeken.
Wanneer veel verschillende acties worden uitgevoerd op een specifiek gedeelte van het Datagrid en als dit ook nog eens lokaal (op dezelfde node) wordt gedaan, dan kan ook gebruik gemaakt worden van een ander ‘in-memory format’. Standaard worden de objecten in binair (geserialiseerd) formaat opgeslagen in Hazelcast, dit kan ook in object formaat. Dit zorgt ervoor dat hier geen overhead ontstaat door het serialiseren en deserialiseren.
MapStore
Hazelcast biedt een zogenaamde MapStore aan. Een MapStore kan worden gebruikt om data die in Maps wordt gezet ook nog op een andere plaats te verwerken. Hierbij worden gedacht aan een data-base om data persistent op te slaan, maar ook een simpel tekst bestand kan worden gebruikt voor bijvoorbeeld logging. De implementatie van de MapStore ligt geheel in eigen hand. Een MapStore kan worden aangemaakt door een interface te implementeren welke Hazelcast aanbiedt. Deze bevatten CRUD methodes welke zelf moeten worden geïmplementeerd om aan te sluiten op de database omgeving (in dit geval Oracle).
De MapStore draait mee in de Hazelcast TransactieContext. Pas wanneer TransactionContext.commitTransaction() wordt aangeroepen, zal de MapStore zijn ding gaan doen. Wanneer een Hazelcast rollback plaatsvindt, zal de MapStore niet worden aangeroepen. Wanneer tijdens het plaatsen van data in de database een exceptie ontstaat, zal ook de Hazelcast Map worden terug gedraaid.
Support
Door het open source aspect van Hazelcast is een community ontstaan waar vragen kunnen worden gesteld en antwoorden op korte termijn worden gegeven. Vaak geven medewerkers van Hazelcast zelf het antwoord, waaruit blijkt dat ze erg actief aanwezig zijn. Ook bij het oplossen van bugs zijn ze er zeer snel bij. De laatste bug die wij hadden ingeschoten was binnen 24 uur opgelost!
Hazelcast Enterprise
Hazelcast biedt ook een betaalde versie aan. Hierbij worden aantal handige extra’s geboden. Zo kan gebruik worden gemaakt van WAN replicatie, is een .NET en C++ client beschikbaar en wordt JAAS Security ondersteunt.
Tevens biedt Hazelcast Enterprise ondersteuning voor off-heap storage, genaamd Elastic Memory. Standaard slaat Hazelcast de data gedistribueerd op in de Java Heap. Op de Java Heap wordt door de JVM Garbage Collection uitgevoerd. Als de Heap groter wordt, dan heeft de Garbage Collector meer tijd nodig om zijn werk te verrichten. Dit kan leiden tot haperingen in het systeem en dus performance verlies. Elastic Memory zorgt ervoor dat de data off-heap (wel in-memory) wordt opgeslagen. Hierdoor wordt het haperen van het systeem door Garbage Collection voorkomen, omdat de Garbage Collector alleen binnen de JVM werkt.
Conclusie
Met één simpele JAR maak je een applicatie volledig gedistribueerd, zonder daarvoor veel te hoeven configureren. Het gebruik van Hazelcast is in tegenstelling tot alternatieven van bekende grote partijen veel eenvoudiger. De leercurve is zeer klein en vanwege het feit dat Hazelcast open source is, hoeven geen licentiekosten betaald te worden. Hazelcast is door het gebruik van standaard Java interfaces eenvoudig te integreren in een bestaand systeem.