Kategorie
java

Hazelcast, czyli In-Memory Data Grid

Jeśli słysząc nazwę Hazelcast myślisz o cache, to nie do końca jest to pełny obraz. Tak, rzeczywiście Hazelcast swoją historię rozpoczął od bycia rozproszonym cache’em. Jednak stwierdzenie, że to tylko i wyłącznie cache to mijanie się z prawdą o lata świetlne 😊 Obecnie jest to bardzo rozbudowanym projekt, który zapewnia wiele ciekawych mechanizmów.

Junior Java Developer Handbook

Hazelcast IMDG

Żeby mieć lepszy kontekst na temat Hazelcast IMDG, warto zastanowić się nad tym, czym jest właśnie to całe In-Memory Data Grid. Rozbijając poszczególne elementy nazwy na składowe:

  • In-Memory, jak sama nazwa wskazuje, oznacza, że nasze dane nie są zapisywane w pamięci. Są ulotne, a co za tym idzie kształt naszego rozwiązania powinien brać to pod uwagę.
  • Data dotyczy oczywiście danych. Chodzi o przechowywanie w pamięci, a również o ich możliwe przetwarzanie. Bo tak, Hazelcast ma również mechanizmy umożliwiające przetwarzanie danych.
  • Grid w tym kontekście oznacza rozproszenie węzłów.

Jak zainstalować?

Hazelcast dostarcza nam możliwość uruchomienia swoich instancji w dwóch trybach:

  • embedded, czyli możemy dodać sobie jar Hazelcasta do naszego projektu i osadzić go w kodzie. To bardzo fajne rozwiązanie, które pozwala na bardzo szybki start pracy.
  • standalone oznaczający uruchomienie instancji z boku naszej aplikacji. Nasza aplikacja będzie po prostu klientem, a instancja Hazelcasta serwerem. Gotową paczkę w takim przypadku można pobrać z tego linku.

Aby skorzystać z wersji embedded Hazelcasta dodajemy w projekcie zależność:

<dependencies>
    <dependency>
        <groupId>com.hazelcast</groupId>
        <artifactId>hazelcast</artifactId>
        <version>4.0.2</version>
    </dependency>
</dependencies>

Tak samo wyglądać będą zależności, jeśli tylko będziemy chcieli skorzystać z klienta Hazelcasta. Być może jest to trochę zaskakujące, że klient i serwer mieszkają w tym samym pliku jar. Jednak od wersji 4, czyli wydanej w lutym tego roku (2020), zespół Hazelcast przyjął taką strategię dystrybucyjną. Wcześniej, gdy korzystałem z linii 3.x, było tutaj rozbicie na oddzielne jar’y.

Efektem tego jest to, że od razu wciągamy dosyć dużo MB do naszej aplikacji. Aktualnie jar z Hazelcastem to niecałe 11 MB (całkiem sporo, gdy patrzymy na niego, jak na bibliotekę). Wcześniej klient zajmował niecały 1 MB.

Pomimo tego, że sam Hazelcast jest złożony i korzysta z różnych innych bibliotek, to jest on również bardzo dobrze enkapsulowany. Zależności nie wypływają z niego na zewnątrz, dzięki czemu nasze drzewo zależności wygląda bardzo czysto:

To bardzo dobrze, bo dzięki temu mamy możliwość unikania potencjalnych konfliktów między wersjami bibliotek. Pewnie sam wiesz, że to zwykle jest bardzo denerwujące 😊

Jak uruchomić?

W każdym razie możemy teraz uruchomić sobie instancję w kodzie poprzez:

HazelcastInstance hazelcastInstance = 
     Hazelcast.newHazelcastInstance();

Możemy również skorzystać z metody newHazelcastInstance() z dodatkową konfiguracją, gdzie mamy możliwość ustawienia wielu parametrów klastra. Jednak na tę chwilę nie będziemy wnikać w szczegóły konfiguracji.

Generalnie jeśli chodzi o Hazelcast, to przykłady warto pokazywać na większej liczbie node’ów. Utworzymy sobie prostą mapę klientów. Nasz scenariusz będzie zakładał, że utworzenie wpisu w tej mapie będzie następowało z jednej instancji, natomiast odczyt będzie realizowany przez instancję drugą.

Zobaczmy, jak to działa.

  @Test
  public void hazelcastMap() {
    HazelcastInstance instance_1 = 
        Hazelcast.newHazelcastInstance();
    HazelcastInstance instance_2 = 
        Hazelcast.newHazelcastInstance();

    IMap<String, String> customers_1 = 
        instance_1.getMap("customers");
    customers_1.put("1", "Jan Kowalski");

    IMap<String, String> customers_2 = 
        instance_2.getMap("customers");
    String resolvedValue = customers_2.get("1");

    assertEquals("Jan Kowalski", resolvedValue);

    instance_1.shutdown();
    instance_2.shutdown();
  }

Jeśli popatrzymy na logi podczas tego testu, to zauważymy, że nasz klaster składa się z dwóch węzłów:

INFO: [192.168.1.9]:5702 [dev] [4.0.2] 

Members {size:2, ver:2} [
	Member [192.168.1.9]:5701 - 503d1e4f-797f-4f54-83f5-3d8941a845c2
	Member [192.168.1.9]:5702 - a4c1bf53-5af7-4cfd-a143-f154b1839a73 this
]

Omówny sam test. W liniach 3 – 6 tworzymy sobie dwa węzły Hazelcasta. Następnie w liniach 8 – 10 wstawiamy do pierwszej z nich klucz 1 z wartością Jan Kowalski. Linie 12 – 14 to pobranie tej wartości z drugiej instancji. A linia 16 to sprawdzenie, czy wszystko się zgadza.

Prawda, że proste? No jasne. Hazelcast ujął mnie kiedyś właśnie swoim API. Prosto wyglądające, jednak kryjące pod spodem mnóstwo ciekawych możliwości.

Osobiście miałem okazję korzystać z Hazelcast kilkukrotnie. Głównie z linii 3.x.

W jednym przypadku pomagał on w przechowywaniu obiektów, które pobierane były z zewnętrznego źródła danych o niskiej przepustowości. Wymóg wysokiej dostępności tych danych w wieloinstancyjnym stateless’owym systemie skierował nas właśnie w stronę Hazelcast. Jak się później okazało, był to strzał w dziesiątkę.

Innym przypadkiem zastosowania było skorzystanie z powiadomień w ramach Hazelcast. Gdy pewne dane znikały z cache’a wówczas system korzystał z tego faktu i mógł wykonać synchronizację z zewnętrznym źródłem danych. Aktualnie nieco podobne zachowanie można uzyskać już w ramach samego Hazelcasta (strategia write-ahead dla cache).

Inne przykłady

Pamiętasz multimapy z artykułu o Guavie? To tym razem możemy je sobie rozproszyć. Hazelcast ma taką możliwość.

Zobacz:

  private String LOGIN_1 = "user-1";
  private String LOGIN_2 = "user-2";

  @Test
  public void hazelcastMultimap() {
    HazelcastInstance instance_1 = Hazelcast.newHazelcastInstance();
    HazelcastInstance instance_2 = Hazelcast.newHazelcastInstance();

    MultiMap<String, String> multimap_1 = instance_1.getMultiMap("multimap");

    multimap_1.put(LOGIN_1, "ROLE_1");
    multimap_1.put(LOGIN_1, "ROLE_2");
    multimap_1.put(LOGIN_1, "ROLE_3");

    multimap_1.put(LOGIN_2, "ROLE_8");
    multimap_1.put(LOGIN_2, "ROLE_9");

    MultiMap<String, String> multimap_2 = instance_2.getMultiMap("multimap");

    assertEquals(2, multimap_2.keySet().size());
    assertEquals(3, multimap_2.get(LOGIN_1).size());
    assertEquals(2, multimap_2.get(LOGIN_2).size());

    instance_1.shutdown();
    instance_2.shutdown();
  }

Możemy też rozproszyć listy i zbiory:

  @Test
  public void hazelcastList() {
    HazelcastInstance instance_1 = Hazelcast.newHazelcastInstance();
    HazelcastInstance instance_2 = Hazelcast.newHazelcastInstance();

    IList<String> list_1 = instance_1.getList("myList");

    list_1.add(LOGIN_1);
    list_1.add(LOGIN_2);

    IList<String> list_2 = instance_2.getList("myList");

    assertEquals(2, list_2.size());
    assertTrue(list_2.contains(LOGIN_1));
    assertTrue(list_2.contains(LOGIN_2));

    instance_1.shutdown();
    instance_2.shutdown();
  }
  @Test
  public void hazelcastSet() {
    HazelcastInstance instance_1 = Hazelcast.newHazelcastInstance();
    HazelcastInstance instance_2 = Hazelcast.newHazelcastInstance();

    ISet<String> set_1 = instance_1.getSet("myset");

    set_1.add(LOGIN_1);
    set_1.add(LOGIN_2);

    ISet<String> set_2 = instance_2.getSet("myset");

    assertEquals(2, set_2.size());
    assertTrue(set_2.contains(LOGIN_1));
    assertTrue(set_2.contains(LOGIN_2));

    instance_1.shutdown();
    instance_2.shutdown();
  }

Na co jeszcze zwrócić uwagę?

W przypadku Hazelcast możemy zwrócić uwagę jeszcze na mechanizmy messagingu, czyli:

  • IQueue za pomocą hazelcastInstance.getQueue(…), czyli kolejki wiadomości.
  • ITopic, który możemy uzyskać poprzez hazelcastInstance.getTopic(…), czyli wariant mechanizmu Pub-Sub.

Warto wiedzieć, że te mechanizmy są dostępne w Hazelcast. I że można z nich w prosty sposób skorzystać między wieloma węzłami. Należy przy tym pamiętać, że Hazelcast nie konkuruje z pełnymi rozwiązaniami do messagingu, np. RabbitMQ, czy ActiveMQ. Nie to jest jego rolą, ale taki mechanizm też udostępnia.

Gdy zachodzi pokusa policzenia czegoś na danych w Hazelcast lub też zrobienia zapytania, które pobierze nam tylko określony zbiór rekordów z grida, to też możemy to zrobić. Hazelcast udostępnia do tego celu mechanizm do agregacji oraz wykonywania zapytań query.

Warto wspomnieć również o tym, że nie musimy zawsze tworzyć instancji HazelcastInstance poprzez Hazelcast.newHazelcastInstance(). Możemy też połączyć się z instancjami wyłącznie jako klient. Kod jest bardzo podobny i … też zwracana jest instancja interfejsu HazelcastInstance.

HazelcastInstance client = 
    HazelcastClient.newHazelcastClient();

A może standalone?

Jeśli zdecydujemy się na ściągnięcie wersji w postaci gotowej paczki, wówczas w katalogu bin dystrybucji wystarczy wywołać polecenie start.bat lub start.sh w zależności od naszego systemu operacyjnego. I dostaniemy działający węzeł Hazelcast.

W ramach ciekawostki. Zarówno w przypadku użycia Hazelcast jako biblioteki jar, jak i w wersji standalone możemy np. umożliwi wysyłanie zapytań REST po dane w naszym miniklastrze. Jest to szczególnie pomocne podczas testów. W tym celu w naszej instalacji przechodzimy do katalogu bin.

i uzupełniamy plik hazelcast.xml o potrzebną konfigurację:

    ...
    </executor-service>
        <rest-api enabled="false">
            <endpoint-group name="CLUSTER_READ" enabled="false"/>
            <endpoint-group name="CLUSTER_WRITE" enabled="false"/>
            <endpoint-group name="HEALTH_CHECK" enabled="false"/>
            <endpoint-group name="HOT_RESTART" enabled="false"/>
            <endpoint-group name="WAN" enabled="false"/>
            <endpoint-group name="DATA" enabled="true"/>
        </rest-api>
    <security>
     ...

Teraz, dzięki prostemu wysyłaniu żądań HTTP możemy wykonywać operacje z poziomu klienta REST. Przykład z Visual Studio Code:

POST http://hazelcast-cluster:5701/hazelcast/rest/maps/customers/11
Content-Type: text/plain

Jan Kowalski

###

GET http://hazelcast-cluster:5701/hazelcast/rest/maps/customers/11

Pomocne. Zwłaszcza, jak chcemy szybko dostać się do danych bez pisania kodu.

Co jeszcze dostarcza Hazelcast?

Mamy możliwość skorzystania z konsoli administracyjnej, która nazwana została Hazelcast Managment Center. Jest to aplikacja web’owa, w które możemy śledzić nasz klaster, jego wydajność i podstawowe elementy konfiguracyjne.

Hazelcast Management Center Dashboard
Hazelcast Managment Center Dashboard

Warto też wspomnieć, że Hazelcast udostępnia również drugi produkt o nazwie Hazelcast JET. Jest to silnik do przetwarzania strumieniowego. Może on pracować z Hazelcastem IMDG, ale również z innymi platformami dostarczającymi dane.

Jeśli zapoznałeś się wcześniej z moim cyklem artykułów o Quarkus, to pewnie zaciekawi Cię też fakt, że jest dostępny również klient Hazelcasta dla tego szkieletu – link.

Hazelcast podsumowanie

Mam nadzieję, że udało mi się zaciekawić Cię Hazelcastem. To potężne narzędzie, które w pewnej grupie zastosowań jest wręcz niesamowite.

Daj znać w komentarzach, czy korzystałeś już z Hazelcasta i jakie są Twoje wrażenia

5 2 votes
Article Rating
Subscribe
Powiadom o
guest
2 komentarzy
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Rafał
Rafał
1 miesiąc temu

Fajne by było porównanie z Redisem.

Bartłomiej Chmielewski
Bartłomiej Chmielewski
1 miesiąc temu
Reply to  Rafał

Coś w rodzaju: skoro masz Hazelcast’a, to jaki driver mógłby sprawić, żeby użyć Redisa? 🙂