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.