Introdução ao Atomix
*1. Visão geral *
A maioria dos aplicativos distribuídos exige que algum componente com estado seja consistente e tolerante a falhas. Atomix é uma biblioteca incorporável que ajuda a obter tolerância a falhas e consistência para recursos distribuídos.
Ele fornece um rico conjunto de APIs para gerenciar seus recursos, como coleções, grupos e ferramentas para simultaneidade.
Para começar, precisamos adicionar a seguinte dependência do Maven em nosso pom:
<dependency>
<groupId>io.atomix</groupId>
<artifactId>atomix-all</artifactId>
<version>1.0.8</version>
</dependency>
Essa dependência fornece um transporte baseado em Netty necessário para que seus nós se comuniquem.
===* 2. Inicializando um cluster *
Para iniciar o Atomix, precisamos inicializar um cluster primeiro.
O Atomix consiste em um conjunto de réplicas usadas para criar recursos distribuídos com estado. Cada réplica mantém uma cópia do estado de cada recurso existente no cluster.
As réplicas são dois tipos em um cluster: ativo e passivo.
Alterações de estado de recursos distribuídos são propagadas por meio de réplicas ativas, enquanto réplicas passivas são mantidas em sincronia para manter a tolerância a falhas.
====* 2.1 Inicializando um cluster incorporado *
Para inicializar um cluster de nó único, precisamos criar uma instância de AtomixReplica primeiro:
AtomixReplica replica = AtomixReplica.builder(
new Address("localhost", 8700))
.withStorage(storage)
.withTransport(new NettyTransport())
.build();
Aqui a réplica é configurada com Storage e Transport. Snippet de código para declarar armazenamento:
Storage storage = Storage.builder()
.withDirectory(new File("logs"))
.withStorageLevel(StorageLevel.DISK)
.build();
Depois que a réplica é declarada e configurada com armazenamento e transporte, podemos inicializá-la simplesmente chamando bootstrap () _ - que retorna um _CompletableFuture que pode ser usado para bloquear até que o servidor seja inicializado chamando o método associado _join () _ de bloqueio:
CompletableFuture<AtomixReplica> future = replica.bootstrap();
future.join();
Até agora, construímos um cluster de nó único. Agora podemos adicionar mais nós a ele.*
Para fazer isso, precisamos criar outras réplicas e juntá-las ao cluster existente; precisamos gerar um novo thread para chamar o método _join (Address) _:
AtomixReplica replica2 = AtomixReplica.builder(
new Address("localhost", 8701))
.withStorage(storage)
.withTransport(new NettyTransport())
.build();
replica2
.join(new Address("localhost", 8700))
.join();
AtomixReplica replica3 = AtomixReplica.builder(
new Address("localhost", 8702))
.withStorage(storage)
.withTransport(new NettyTransport())
.build();
replica3.join(
new Address("localhost", 8700),
new Address("localhost", 8701))
.join();
Agora, temos um cluster de três nós inicializado. Como alternativa, podemos inicializar um cluster passando uma List de endereços no método _bootstrap (List <Address>) _:
List<Address> cluster = Arrays.asList(
new Address("localhost", 8700),
new Address("localhost", 8701),
new Address("localhsot", 8702));
AtomixReplica replica1 = AtomixReplica
.builder(cluster.get(0))
.build();
replica1.bootstrap(cluster).join();
AtomixReplica replica2 = AtomixReplica
.builder(cluster.get(1))
.build();
replica2.bootstrap(cluster).join();
AtomixReplica replica3 = AtomixReplica
.builder(cluster.get(2))
.build();
replica3.bootstrap(cluster).join();
Precisamos gerar um novo thread para cada réplica.
2.2 Inicializando um cluster autônomo
O servidor Atomix pode ser executado como um servidor autônomo, que pode ser baixado do Maven Central. Simplificando - é um arquivo Java que pode ser executado através do terminal, fornecendo
Simplificando - é um arquivo Java que pode ser executado através do terminal, fornecendo o parâmetro host: port no sinalizador de endereço e usando o sinalizador -bootstrap.
Aqui está o comando para inicializar um cluster:
java -jar atomix-standalone-server.jar
-address 127.0.0.1:8700 -bootstrap -config atomix.properties
Aqui atomix.properties é o arquivo de configuração para configurar o armazenamento e o transporte. Para criar um cluster multinode, podemos adicionar nós ao cluster existente usando o sinalizador -join.
O formato para isso é:
java -jar atomix-standalone-server.jar
-address 127.0.0.1:8701 -join 127.0.0.1:8700
*3. Trabalhando com um cliente *
O Atomix suporta a criação de um cliente para ter acesso remoto ao cluster, por meio da API AtomixClient.
Como os clientes não precisam ter estado, o AtomixClient não possui armazenamento. Simplesmente precisamos configurar o transporte enquanto criamos o cliente, pois o transporte será usado para se comunicar com um cluster.
Vamos criar um cliente com um transporte:
AtomixClient client = AtomixClient.builder()
.withTransport(new NettyTransport())
.build();
Agora precisamos conectar o cliente ao cluster.
Podemos declarar uma List de Address e passar a List como argumento para o método _connect () _ do cliente:
client.connect(cluster)
.thenRun(() -> {
System.out.println("Client is connected to the cluster!");
});
===* 4. Recursos de Manuseio *
O verdadeiro poder do Atomix reside em seu forte conjunto de APIs para criar e gerenciar recursos distribuídos.* Os recursos são replicados e mantidos em um cluster e são reforçados por uma máquina de estado replicada *- gerenciada por sua implementação subjacente do Raft Consensus Protocol.
Os recursos distribuídos podem ser criados e gerenciados por um de seus métodos get () _. Podemos criar uma instância de recurso distribuído a partir de _AtomixReplica.
Considerando que replica é uma instância de AtomixReplica, o trecho de código para criar um recurso de mapa distribuído e definir um valor para ele:
replica.getMap("map")
.thenCompose(m -> m.put("bar", "Hello world!"))
.thenRun(() -> System.out.println("Value is set in Distributed Map"))
.join();
Aqui o método join () _ bloqueará o programa até que o recurso seja criado e o valor definido para ele. Podemos obter o mesmo objeto usando _AtomixClient e recuperar o valor com o método _get (“bar”) _.
Podemos usar o método _get () _ no final para aguardar o resultado:
String value = client.getMap("map"))
.thenCompose(m -> m.get("bar"))
.thenApply(a -> (String) a)
.get();
===* 5. Consistência e tolerância a falhas *
O Atomix é utilizado para conjuntos de dados de pequena escala de missão crítica, cuja consistência é uma preocupação muito maior que a disponibilidade.
*Fornece forte consistência configurável através da linearizabilidade para leituras e gravações* . Na linearização, uma vez que uma gravação é confirmada, todos os clientes têm certeza de estar cientes do estado resultante.
A consistência no cluster do Atomix é garantida pelo algoritmo de consenso Raft subjacente, em que um líder eleito terá todas as gravações que tiveram sucesso anteriormente.
Todas as novas gravações passam pelo líder do cluster e são replicadas de forma síncrona para a maioria do servidor antes da conclusão.
Para manter a tolerância a falhas, o servidor majoritário do cluster precisa estar ativo . Se o número minoritário de nós falhar, os nós serão marcados como inativos e serão substituídos por nós passivos ou de espera.
Em caso de falha do líder, os servidores restantes no cluster iniciarão uma nova eleição de líder. Enquanto isso, o cluster estará indisponível.
No caso de partição, se um líder estiver no lado não quorum da partição, ele renuncia e um novo líder é eleito no lado com um quorum.
E, se o líder estiver do lado majoritário, continuará sem alterações. Quando a partição é resolvida, os nós do lado não quorum ingressam no quorum e atualizam seu log de acordo.
6. Conclusão
Como o ZooKeeper, o Atomix fornece um conjunto robusto de bibliotecas para lidar com problemas de computação distribuída.
E, como sempre, o código fonte completo para esta tarefa está disponível over no GitHub.