Introduzione ad Apache Curator

1. Introduzione

Apache Curator è un client Java per Apache Zookeeper, il popolare servizio di coordinamento per applicazioni distribuite.

In questo tutorial, introdurremo alcune delle funzionalità più rilevanti fornite dal curatore:

  • Gestione delle connessioni: gestione delle connessioni e criteri di ripetizione dei tentativi
  • Async: miglioramento del client esistente mediante l'aggiunta di funzionalità async e l'uso di lambda Java 8
  • Gestione della configurazione: avere una configurazione centralizzata per il sistema
  • Modelli fortemente tipizzati: lavorare con modelli tipizzati
  • Ricette: implementazione dell'elezione del leader, blocchi o contatori distribuiti

2. Prerequisiti

Per cominciare, si consiglia di dare una rapida occhiata ad Apache Zookeeper e alle sue caratteristiche.

Per questo tutorial, presumiamo che esista già un'istanza Zookeeper autonoma in esecuzione su 127.0.0.1:2181 ; ecco le istruzioni su come installarlo ed eseguirlo, se hai appena iniziato.

Per prima cosa, dobbiamo aggiungere la dipendenza curator-x-async al nostro pom.xml :

 org.apache.curator curator-x-async 4.0.1   org.apache.zookeeper zookeeper   

L'ultima versione di Apache Curator 4.XX ha una forte dipendenza con Zookeeper 3.5.X che è ancora in beta al momento.

Quindi, in questo articolo, utilizzeremo invece l'ultima versione stabile di Zookeeper 3.4.11.

Quindi dobbiamo escludere la dipendenza Zookeeper e aggiungere la dipendenza per la nostra versione Zookeeper al nostro pom.xml :

 org.apache.zookeeper zookeeper 3.4.11 

Per ulteriori informazioni sulla compatibilità, fare riferimento a questo collegamento.

3. Gestione della connessione

Il caso d'uso di base di Apache Curator è la connessione a un'istanza Apache Zookeeper in esecuzione .

Lo strumento fornisce una fabbrica per creare connessioni a Zookeeper utilizzando i criteri di ripetizione:

int sleepMsBetweenRetries = 100; int maxRetries = 3; RetryPolicy retryPolicy = new RetryNTimes( maxRetries, sleepMsBetweenRetries); CuratorFramework client = CuratorFrameworkFactory .newClient("127.0.0.1:2181", retryPolicy); client.start(); assertThat(client.checkExists().forPath("/")).isNotNull();

In questo rapido esempio, riproveremo 3 volte e aspetteremo 100 ms tra i tentativi in ​​caso di problemi di connettività.

Una volta connessi a Zookeeper utilizzando il client CuratorFramework , ora possiamo esplorare i percorsi, ottenere / impostare i dati ed essenzialmente interagire con il server.

4. Async

Il modulo Curator Async racchiude il client CuratorFramework di cui sopra per fornire funzionalità non bloccanti utilizzando l'API Java 8 di CompletionStage.

Vediamo come appare l'esempio precedente usando il wrapper Async:

int sleepMsBetweenRetries = 100; int maxRetries = 3; RetryPolicy retryPolicy = new RetryNTimes(maxRetries, sleepMsBetweenRetries); CuratorFramework client = CuratorFrameworkFactory .newClient("127.0.0.1:2181", retryPolicy); client.start(); AsyncCuratorFramework async = AsyncCuratorFramework.wrap(client); AtomicBoolean exists = new AtomicBoolean(false); async.checkExists() .forPath("/") .thenAcceptAsync(s -> exists.set(s != null)); await().until(() -> assertThat(exists.get()).isTrue());

Ora, l' operazione checkExists () funziona in modalità asincrona, non bloccando il thread principale. Possiamo anche concatenare le azioni una dopo l'altra utilizzando invece il metodo thenAcceptAsync () , che utilizza l'API CompletionStage.

5. Gestione della configurazione

In un ambiente distribuito, una delle sfide più comuni è gestire la configurazione condivisa tra molte applicazioni. Possiamo usare Zookeeper come archivio dati in cui conservare la nostra configurazione.

Vediamo un esempio utilizzando Apache Curator per ottenere e impostare i dati:

CuratorFramework client = newClient(); client.start(); AsyncCuratorFramework async = AsyncCuratorFramework.wrap(client); String key = getKey(); String expected = "my_value"; client.create().forPath(key); async.setData() .forPath(key, expected.getBytes()); AtomicBoolean isEquals = new AtomicBoolean(); async.getData() .forPath(key) .thenAccept(data -> isEquals.set(new String(data).equals(expected))); await().until(() -> assertThat(isEquals.get()).isTrue());

In questo esempio, creiamo il percorso del nodo, impostiamo i dati in Zookeeper, quindi lo recuperiamo verificando che il valore sia lo stesso. Il campo chiave potrebbe essere un percorso del nodo come / config / dev / my_key .

5.1. Osservatori

Un'altra caratteristica interessante di Zookeeper è la possibilità di controllare chiavi o nodi. Ci consente di ascoltare le modifiche alla configurazione e aggiornare le nostre applicazioni senza dover ridistribuire .

Vediamo come appare l'esempio sopra quando si usano gli osservatori:

CuratorFramework client = newClient() client.start(); AsyncCuratorFramework async = AsyncCuratorFramework.wrap(client); String key = getKey(); String expected = "my_value"; async.create().forPath(key); List changes = new ArrayList(); async.watched() .getData() .forPath(key) .event() .thenAccept(watchedEvent -> { try { changes.add(new String(client.getData() .forPath(watchedEvent.getPath()))); } catch (Exception e) { // fail ... }}); // Set data value for our key async.setData() .forPath(key, expected.getBytes()); await() .until(() -> assertThat(changes.size()).isEqualTo(1));

Configuriamo il watcher, impostiamo i dati e quindi confermiamo che l'evento osservato è stato attivato. Possiamo guardare un nodo o un insieme di nodi contemporaneamente.

6. Modelli fortemente tipizzati

Zookeeper lavora principalmente con array di byte, quindi abbiamo bisogno di serializzare e deserializzare i nostri dati. Questo ci consente una certa flessibilità per lavorare con qualsiasi istanza serializzabile, ma può essere difficile da mantenere.

Per aiutare qui, Curator aggiunge il concetto di modelli tipizzati che delega la serializzazione / deserializzazione e ci permette di lavorare direttamente con i nostri tipi . Vediamo come funziona.

Innanzitutto, abbiamo bisogno di un framework serializzatore. Il curatore consiglia di utilizzare l'implementazione Jackson, quindi aggiungiamo la dipendenza Jackson al nostro pom.xml :

 com.fasterxml.jackson.core jackson-databind 2.9.4 

Ora proviamo a mantenere la nostra classe personalizzata HostConfig :

public class HostConfig { private String hostname; private int port; // getters and setters }

Dobbiamo fornire la mappatura delle specifiche del modello dalla classe HostConfig a un percorso e utilizzare il wrapper del framework modellato fornito da Apache Curator:

ModelSpec mySpec = ModelSpec.builder( ZPath.parseWithIds("/config/dev"), JacksonModelSerializer.build(HostConfig.class)) .build(); CuratorFramework client = newClient(); client.start(); AsyncCuratorFramework async = AsyncCuratorFramework.wrap(client); ModeledFramework modeledClient = ModeledFramework.wrap(async, mySpec); modeledClient.set(new HostConfig("host-name", 8080)); modeledClient.read() .whenComplete((value, e) -> { if (e != null) { fail("Cannot read host config", e); } else { assertThat(value).isNotNull(); assertThat(value.getHostname()).isEqualTo("host-name"); assertThat(value.getPort()).isEqualTo(8080); } });

Il metodo whenComplete () durante la lettura del percorso / config / dev restituirà l' istanza HostConfig in Zookeeper.

7. Recipes

Zookeeper provides this guideline to implement high-level solutions or recipes such as leader election, distributed locks or shared counters.

Apache Curator provides an implementation for most of these recipes. To see the full list, visit the Curator Recipes documentation.

All of these recipes are available in a separate module:

 org.apache.curator curator-recipes 4.0.1 

Let's jump right in and start understanding these with some simple examples.

7.1. Leader Election

In a distributed environment, we may need one master or leader node to coordinate a complex job.

This is how the usage of the Leader Election recipe in Curator looks like:

CuratorFramework client = newClient(); client.start(); LeaderSelector leaderSelector = new LeaderSelector(client, "/mutex/select/leader/for/job/A", new LeaderSelectorListener() { @Override public void stateChanged( CuratorFramework client, ConnectionState newState) { } @Override public void takeLeadership( CuratorFramework client) throws Exception { } }); // join the members group leaderSelector.start(); // wait until the job A is done among all members leaderSelector.close();

When we start the leader selector, our node joins a members group within the path /mutex/select/leader/for/job/A. Once our node becomes the leader, the takeLeadership method will be invoked, and we as leaders can resume the job.

7.2. Shared Locks

The Shared Lock recipe is about having a fully distributed lock:

CuratorFramework client = newClient(); client.start(); InterProcessSemaphoreMutex sharedLock = new InterProcessSemaphoreMutex( client, "/mutex/process/A"); sharedLock.acquire(); // do process A sharedLock.release();

When we acquire the lock, Zookeeper ensures that there's no other application acquiring the same lock at the same time.

7.3. Counters

The Counters recipe coordinates a shared Integer among all the clients:

CuratorFramework client = newClient(); client.start(); SharedCount counter = new SharedCount(client, "/counters/A", 0); counter.start(); counter.setCount(counter.getCount() + 1); assertThat(counter.getCount()).isEqualTo(1);

In this example, Zookeeper stores the Integer value in the path /counters/A and initializes the value to 0 if the path has not been created yet.

8. Conclusion

In questo articolo, abbiamo visto come utilizzare Apache Curator per connettersi ad Apache Zookeeper e sfruttare le sue caratteristiche principali.

Abbiamo anche introdotto alcune delle principali ricette in Curator.

Come al solito, i sorgenti possono essere trovati su GitHub.