Introdução ao Apache Curator

Introdução ao Apache Curator

1. Introdução

Apache Curator é um cliente Java paraApache Zookeeper, o serviço de coordenação popular para aplicativos distribuídos.

Neste tutorial, apresentaremos alguns dos recursos mais relevantes fornecidos pelo Curator:

  • Gerenciamento de conexões - gerenciando conexões e novas políticas

  • Async - aprimorando o cliente existente adicionando recursos assíncronos e o uso de Java 8 lambdas

  • Gerenciamento de configuração - com uma configuração centralizada para o sistema

  • Modelos fortemente tipados - trabalhando com modelos tipificados

  • Receitas - implementação da eleição do líder, bloqueios ou contadores distribuídos

2. Pré-requisitos

Para começar, é recomendável dar uma olhada rápida emApache Zookeepere seus recursos.

Para este tutorial, presumimos que já haja uma instância independente do Zookeeper em execução em127.0.0.1:2181; Instruçõeshere are sobre como instalar e executá-lo, se você estiver apenas começando.

Primeiro, precisaremos adicionar a dependênciacurator-x-async ao nossopom.xml:


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

The latest version of Apache Curator 4.X.X has a hard dependency with Zookeeper 3.5.X que ainda está em beta agora.

E então, neste artigo, vamos usar o atualZookeeper 3.4.11 estável mais recente.

Portanto, precisamos excluir a dependência do Zookeeper e adicionarthe dependency for our Zookeeper version ao nossopom.xml:


    org.apache.zookeeper
    zookeeper
    3.4.11

Para obter mais informações sobre compatibilidade, consultethis link.

3. Gerenciamento de Conexão

The basic use case of Apache Curator is connecting to a running Apache Zookeeper instance.

A ferramenta fornece uma fábrica para criar conexões com o Zookeeper usando políticas de repetição:

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();

Neste exemplo rápido, tentaremos novamente 3 vezes e esperaremos 100 ms entre as tentativas em caso de problemas de conectividade.

Uma vez conectado ao Zookeeper usando o clienteCuratorFramework, podemos agora navegar pelos caminhos, obter / definir dados e essencialmente interagir com o servidor.

4. Assíncrono

The Curator Async module wraps the above CuratorFramework client to provide non-blocking capabilities usandothe CompletionStage Java 8 API.

Vamos ver como fica o exemplo anterior usando o 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());

Agora, a operaçãocheckExists() funciona em modo assíncrono, não bloqueando a thread principal. Também podemos encadear ações uma após a outra usando o métodothenAcceptAsync(), que usaCompletionStage API.

5. Gerenciamento de configurações

Em um ambiente distribuído, um dos desafios mais comuns é gerenciar a configuração compartilhada entre muitos aplicativos. We can use Zookeeper as a data store where to keep our configuration.

Vamos ver um exemplo usando o Curador Apache para obter e definir dados:

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());

Neste exemplo, criamos o caminho do nó, configuramos os dados no Zookeeper e depois recuperamos, verificando se o valor é o mesmo. O campokey pode ser um caminho de nó como/config/dev/my_key.

5.1. Observadores

Outro recurso interessante no Zookeeper é a capacidade de observar teclas ou nós. It allows us to listen to changes in the configuration and update our applications without needing to redeploy.

Vamos ver como fica o exemplo acima ao usar observadores:

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));

Configuramos o inspetor, configuramos os dados e, em seguida, confirmamos que o evento assistido foi acionado. Podemos assistir um nó ou um conjunto de nós ao mesmo tempo.

6. Modelos fortemente tipados

O Zookeeper trabalha principalmente com matrizes de bytes, portanto, precisamos serializar e desserializar nossos dados. Isso nos permite flexibilidade para trabalhar com qualquer instância serializável, mas pode ser difícil de manter.

Para ajudar aqui, o Curator adiciona o conceito detyped models quedelegates the serialization/deserialization and allows us to work with our types directly. Vamos ver como isso funciona.

Primeiro, precisamos de uma estrutura de serializador. O curador recomenda usar a implementação Jackson, então vamos adicionarthe Jackson dependency ao nossopom.xml:


    com.fasterxml.jackson.core
    jackson-databind
    2.9.4

Agora, vamos tentar persistir nossa classe personalizadaHostConfig:

public class HostConfig {
    private String hostname;
    private int port;

    // getters and setters
}

Precisamos fornecer o mapeamento de especificação do modelo da classeHostConfig para um caminho e usar o wrapper de estrutura modelado fornecido pelo Curador Apache:

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);
     }
   });

O métodowhenComplete() ao ler o caminho/config/dev retornará a instânciaHostConfig no Zookeeper.

7. Receitas

Zookeeper fornecethis guideline para implementarhigh-level solutions or recipes such as leader election, distributed locks or shared counters.

O Apache Curator fornece uma implementação para a maioria dessas receitas. Para ver a lista completa, visitethe Curator Recipes documentation.

Todas essas receitas estão disponíveis em um módulo separado:


    org.apache.curator
    curator-recipes
    4.0.1

Vamos começar a entendê-los com alguns exemplos simples.

7.1. Eleição de Líder

Em um ambiente distribuído, podemos precisar de um nó mestre ou líder para coordenar um trabalho complexo.

É assim que o uso dethe Leader Election recipe no Curator se parece com:

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();

Quando iniciamos o seletor líder, nosso nó se junta a um grupo de membros dentro do caminho/mutex/select/leader/for/job/A. Assim que nosso nó se tornar o líder, o métodotakeLeadership será invocado e nós, como líderes, podemos retomar o trabalho.

7.2. Bloqueios compartilhados

The Shared Lock recipe é sobre ter um bloqueio totalmente distribuído:

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

sharedLock.acquire();

// do process A

sharedLock.release();

Quando adquirimos o bloqueio, o Zookeeper garante que nenhum outro aplicativo adquira o mesmo bloqueio ao mesmo tempo.

7.3. Contadores

The Counters recipe coordena umInteger compartilhado entre todos os clientes:

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);

Neste exemplo, Zookeeper armazena o valor deInteger no caminho/counters/Ae inicializa o valor para0 se o caminho ainda não foi criado.

8. Conclusão

Neste artigo, vimos como usar o Apache Curator para conectar-se ao Apache Zookeeper e tirar proveito de seus principais recursos.

Também introduzimos algumas das principais receitas no Curator.

Como de costume, as fontes podem ser encontradasover on GitHub.