Implementando o algoritmo de consenso RAFT para armazenamento KV distribuído em Java

Olá de novo. Há alguns dias, o treinamento começou em um novo grupo no curso "Arquiteto de Software" e hoje gostaríamos de compartilhar um artigo escrito por um dos alunos do curso, Anton Pleshakov (chefe de desenvolvimento da Program Logistics e cofundador da Clusterra).




Atualmente, os sistemas distribuídos de microsserviços tornaram-se praticamente o padrão do setor, e não apenas no mundo corporativo. Os benefícios do uso de sistemas distribuídos foram descritos e discutidos mais de uma vez. As vantagens dos microsserviços são conhecidas por todos: tecnologias para a tarefa, composição, escalabilidade, escala de desenvolvimento, redução de TTM e assim por diante. É óbvio que o desenvolvimento de aplicativos distribuídos oferece mais opções para resposta oportuna às crescentes demandas de negócios e digitalização de tudo o que está ao redor.

Também é importante observar que, no momento, um fator muito importante que afeta a escolha de uma estratégia de desenvolvimento em favor dos microsserviços é a disponibilidade de todos os tipos de soluções de infraestrutura prontas que resolvem os problemas associados aos custos adicionais da operação de um sistema distribuído. Estamos falando de sistemas de orquestração de contêiner, mash de serviço, meios de rastreamento distribuído, monitoramento, registro e assim por diante. Pode-se afirmar com segurança que a maioria dos fatores mencionados anteriormente como desvantagens da abordagem de microsserviço hoje não tem tanta influência quanto alguns anos atrás.

Com base nas realidades modernas, a maioria dos desenvolvedores procura, na primeira oportunidade, mudar de uma estrutura monolítica para uma de microsserviço. Um dos primeiros passos que podem ser tomados sem recorrer à refatoração total e à decomposição séria é conseguir um sistema de escalabilidade horizontal. Ou seja, para transformar seu aplicativo monolítico em um cluster, possivelmente até constituído pelos mesmos monólitos, mas permitindo variar dinamicamente o número deles.

Ao tentar alcançar a escalabilidade horizontal, surge a questão da sincronização de dados dentro de um cluster, com muita rapidez e agilidade. Felizmente, todos os DBMSs modernos oferecem suporte à replicação de dados entre nós de uma maneira ou de outra. O desenvolvedor só precisa selecionar o DBMS para a tarefa e decidir quais propriedades do sistema (de acordo com o teorema da CAP) ele precisa, CP ou AP, e o problema foi resolvido. No caso em que o CP é necessário e os requisitos de consistência são altos, um dos métodos para resolver o problema de sincronização de dados é usar um cluster que suporte o algoritmo de consenso RAFT.

Esse algoritmo bastante novo (desenvolvido em 2012) oferece uma alta garantia de consistência e é muito popular. Decidi descobrir como ele funciona e escrevi minha implementação de um repositório consistente de valores-chave em Java (Spring Boot).

Faz sentido implementar algum algoritmo distribuído você mesmo? É claro que você pode ter uma implementação pronta de um algoritmo distribuído e, com o maior grau de probabilidade, essa implementação será melhor do que uma "bicicleta" caseira. Por exemplo, você pode usar um DBMS que mantenha o nível de consistência necessário. Ou você pode implantar o Zookeeper . Ou você pode encontrar uma estrutura adequada para o seu idioma. Para java, existe o Atomix , que resolve perfeitamente os problemas de sincronização de dados distribuídos.

Mas do outro lado. Se você optar por uma solução pronta para usar, o uso de um aplicativo externo geralmente adiciona um ponto adicional de falha ao seu sistema. E as estruturas podem ser redundantes ou difíceis de operar e aprender, ou podem não existir para a sua linguagem de programação. Além disso, a implementação independente do algoritmo de consenso é uma tarefa de engenharia extremamente interessante que amplia seus horizontes e fornece uma compreensão de como resolver os problemas que surgem quando os serviços interagem em um cluster usando o método mais ideal.

Como a especificação do algoritmo contém um conjunto de medidas para manter a integridade dos dados, você pode usar o conhecimento adquirido e até usar o algoritmo na sua totalidade. Qualquer parte do algoritmo pode ser útil na vida real. Suponha que você tenha um conjunto de trabalhadores para analisar arquivos em paralelo. Os trabalhadores são equivalentes, mas você deseja designar um dos trabalhadores como coordenador e, quando o trabalhador de coordenação cair, atribua qualquer outro trabalhador livre como coordenador. A primeira metade do algoritmo RAFT, que descreve como escolher um líder entre nós equivalentes, o ajudará com isso. Ou, por exemplo, se você tiver apenas dois nós em relação ao mestre-escravo, poderá usar muito bem as regras de replicação descritas na especificação RAFT para organizar a troca de dados no seu caso mais simples.

O artigo é essencialmente um guia prático sobre como implementar o RAFT. O próprio algoritmo e os aspectos teóricos de seu trabalho não serão compreendidos. Você pode ler uma breve descrição aqui neste excelente artigo ou estudar a especificação completa aqui . Lá você pode encontrar uma visualização muito clara do algoritmo.

Descrição geral da solução


A parte do código diretamente relacionada à implementação do algoritmo é analisada no artigo. No final do artigo, há um link para o repositório, onde você pode ver o código inteiro.

A tarefa foi a seguinte. Desenvolva um sistema distribuído que permita armazenar dados em um banco de dados de valores-chave. Os dados de cada nó devem ser consistentes, a saber, se os dados entraram no banco de dados de um nó e a maioria dos nós confirmou que eles também receberam esses dados, mais cedo ou mais tarde esses dados estarão no banco de dados de cada nó. Quando uma parte do cluster é desconectada e quando é conectada novamente, os nós que estavam fora do cluster devem alcançar o cluster principal e sincronizar. Cada nó fornece uma API REST para gravar e ler dados do banco de dados. O sistema consiste em dois módulos para dois tipos de nós: cliente e servidor. Abaixo, consideramos os recursos da implementação do próprio servidor. O código do cliente está no repositório.

Um nó do servidor pode operar em três estados:

  • Seguidor (seguidor). Aceita solicitações de leitura do cliente. Leva um batimento cardíaco do líder
  • Candidato (candidato). Aceita solicitações de leitura do cliente. Envia solicitações de voto para outros nós
  • Líder Aceita pedidos de leitura e gravação. Envia solicitações de pulsação para outros nós. Envia anexar dados de solicitações para outros nós.

O período de "liderança" de um dos nós é chamado de rodada (termo). Um novo candidato abre uma nova rodada.

Armazenamento de dados


Cada nó fornece acesso ao repositório do log de operações, no qual as operações para alterar dados são gravadas seqüencialmente.

https://github.com/pleshakoff/raft/blob/master/server/src/main/java/com/raft/server/operations/OperationsLog.java


public interface OperationsLog {
   void append(Operation operation);
   Operation get(Integer index);
   List<Operation> all();

   Long getTerm(Integer index);
   Integer getLastIndex();
   Long getLastTerm();

   void removeAllFromIndex(int newOperationIndex);
}

Cada operação, além de dados e tipo (inserir, alterar, excluir), contém o número da rodada em que foi criada. Além disso, cada operação possui um índice que aumenta sequencialmente. É importante que todas as operações sejam inseridas nos logs de seguidores na mesma ordem em que são inseridas no log do líder.

Cada nó tem acesso a um banco de dados no qual os dados são armazenados diretamente.

https://github.com/pleshakoff/raft/blob/master/server/src/main/java/com/raft/server/storage/Storage.java

public interface Storage {
   List<Entry> all();
   String get(Long key);
   void insert(Long key, String val);
   void update(Long key, String val);
   void delete(Long key);
}

Na implementação atual, soluções incorporadas na memória são usadas tanto para o log quanto para o banco de dados (Lista e Mapa competitivos comuns). Se necessário, você pode simplesmente implementar a interface apropriada para suportar outros tipos de armazenamento.

A aplicação das operações do log ao banco de dados é realizada por uma máquina de estado distribuída. Uma máquina de estado é um mecanismo responsável por alterar o estado de um cluster, restringindo o uso de alterações incorretas (operações fora de ordem ou um nó desconectado que se considera um líder). Para que as alterações sejam consideradas válidas e para serem aplicadas ao banco de dados, elas devem passar por uma série de verificações e atender a certos critérios, que é precisamente o que a máquina de estado fornece.

Para um líder, uma operação é aplicada ao banco de dados se a maioria dos nós confirmou o fato de que a operação também é replicada em seu log. Para um seguidor, a operação é aplicada ao banco de dados se um sinal for recebido do líder que ela entrou no banco de dados.

Temporizadores


Cada nó fornece troca de dados com outros nós.

Dois tipos de consultas são suportados:

  • votar ao conduzir uma rodada de votação
  • anexar, também conhecido como pulsação (se não houver dados), para replicar dados de log para seguidores e impedir o início de uma nova rodada de votação.

O fato do início de um evento é determinado pelo cronômetro. Dois tipos de cronômetros são ativados no nó:

  • voto. Para iniciar uma rodada de votação. Cada nó tem seu próprio intervalo, após o qual tentará iniciar uma nova votação. A contagem regressiva começa novamente ao receber um batimento cardíaco do líder.
  • batimento cardiaco. Para enviar uma solicitação aos seguidores pelo líder do anexo. Se o nó não recebe uma pulsação e o cronômetro de votação expirou, ele se torna um candidato e inicia as eleições, aumenta o número da rodada de votação e envia solicitações de votação para outros nós. Se o nó coletar a maioria dos votos, ele se tornará o líder e começará a enviar batimentos cardíacos.

O estado atual do nó


Cada nó armazena dados sobre o estado atual.

https://github.com/pleshakoff/raft/blob/master/server/src/main/java/com/raft/server/context/Context.java

public interface Context {
   Integer getId(); //    
   State getState();//: , ,  
   Integer getVotedFor(); 
               //          
   Long getCurrentTerm(); //  
   Integer getCommitIndex(); //    
   List<Peer> getPeers(); //      
}

Um nó líder também armazena metadados para os nós nos quais ele replica dados.

https://github.com/pleshakoff/raft/blob/master/server/src/main/java/com/raft/server/node/peers/Peer.java

public interface Peer {
   Integer getId(); //  
   Integer getNextIndex(); //  ,    
   Integer getMatchIndex();//   
   Boolean getVoteGranted(); //     
}

Os metadados do nó são atualizados pelo líder ao receber respostas de seguidores. Eles são usados ​​para determinar pelo líder qual operação do índice seguinte o seguidor está pronto para aceitar e quais operações já foram adicionadas ao log do seguidor.

Votação


A classe ElectionService é responsável pela votação

public interface ElectionService {
   void processElection();
   AnswerVoteDTO vote(RequestVoteDTO requestVoteDTO);
} 

Enviando um pedido de votação


Se o nó for um seguidor e não receber uma pulsação durante o período definido para a espera, ele aumentará sua rodada atual, se declarará candidato e começará a enviar solicitações de voto para outros nós. Se ele conseguir reunir um quorum e a maioria dos nós votar, ele se tornará o novo líder. Em termos de RAFT, o quorum é mais da metade de todos os nós (51%).

Vamos analisar o método de processElectionclasse ElectionServiceImpl, que é chamado pelo cronômetro de votação quando a votação expira e envia aos nós uma solicitação de votação .

//1
context.setState(CANDIDATE); 
Long term = context.incCurrentTerm(); 
context.setVotedFor(context.getId()); 

List<Integer> peersIds = context.getPeers().stream().map(Peer::getId).collect(Collectors.toList());
long voteGrantedCount = 1L;
long voteRevokedCount = 0L;

//2
while (checkCurrentElectionStatus(term)) {
   List<AnswerVoteDTO> answers = getVoteFromAllPeers(term, peersIds);
   peersIds = new ArrayList<>();
   for (AnswerVoteDTO answer : answers) {
       //3
       if (answer.getStatusCode().equals(OK)) {
           //4
           if (answer.getTerm()>context.getCurrentTerm()) {
               context.setTermGreaterThenCurrent(answer.getTerm());
               return;
           }
           if (answer.isVoteGranted()) {
               //5 
               context.getPeer(answer.getId()).setVoteGranted(true);
               voteGrantedCount++;
           } else
               //6 
               voteRevokedCount++;
       } else {
          peersIds.add(answer.getId());
       }
   }
  //7
  if (voteGrantedCount >= context.getQuorum()) {
       winElection(term);
       return;
   } else if (voteRevokedCount >= context.getQuorum()) {
       loseElection(term);
       return;
   } 

  1. Defina o status de "Candidato". Aumente o número da rodada e vote em nós mesmos.
  2. , ( ). - , , heartbeat .
  3. - , . , , -.
  4. , . , heartbeat .
  5. O nó votou em nós! Aumentamos o número de nós que votam em nós e corrigimos que esse nó votou em nós.
  6. Votou não para nós, também acreditamos.
  7. Se o quorum for coletado e o nó vencer a eleição, estabelecemos o status de "Líder". Caso contrário, nos tornamos seguidores e esperamos.

Também deve ser observado que, quando um nó se torna um líder, o Próximo Índice é definido para cada nó na lista de nós armazenados pelo líder, que é igual ao último índice no log do líder mais 1. A partir desse índice, o líder tentará atualizar os logs do seguidor. De fato, esse índice armazenado pelo líder pode não corresponder ao índice real do registro do seguidor e o valor real será obtido apenas ao trocar dados com o seguidor e será ajustado. Mas é necessário algum ponto de partida .

  private void winElection(Long term) {
       context.setState(LEADER);
       context.getPeers().forEach(peer ->
               peer.setNextIndex(operationsLog.getLastIndex()+1)

       );
   }

Processamento de solicitação de votação


Ao votar, cada nó recebe uma solicitação do seguinte formulário do candidato :

class RequestVoteDTO {
   private final Long term; //     
   private final Integer candidateId; //  
   private final Integer lastLogIndex; //     
   private final Long lastLogTerm; //       
}

Agora, vejamos o procedimento de voteclasse ElectionServiceImpl, ele processa a solicitação de voto do candidato e retorna uma decisão sobre sua candidatura ao papel de líder.

https://github.com/pleshakoff/raft/blob/eba5ea1984e2623702f4c299cf1b0af7a6ba0d14/server/src/main/java/com/raft/server/election/ElectionServiceImpl.java#L178


public AnswerVoteDTO vote(RequestVoteDTO dto) {
   
       boolean termCheck;
       //1
       if (dto.getTerm() < context.getCurrentTerm())
           return new AnswerVoteDTO(context.getId(),context.getCurrentTerm(),false);
       else //2
       if (dto.getTerm().equals(context.getCurrentTerm())) {
           termCheck = (context.getVotedFor() == null||
                          context.getVotedFor().equals(dto.getCandidateId()));
       }
       else
       {   //3
           termCheck = true;
             context.setTermGreaterThenCurrent(dto.getTerm());
       }

       //4  
       boolean logCheck = !((operationsLog.getLastTerm() > dto.getLastLogTerm()) ||
               ((operationsLog.getLastTerm().equals(dto.getLastLogTerm())) &&
                       (operationsLog.getLastIndex() > dto.getLastLogIndex())));


       boolean voteGranted = termCheck&&logCheck;

       //5
       if (voteGranted) {
           context.setVotedFor(dto.getCandidateId());
       }
       //6   
       return new AnswerVoteDTO(context.getId(),context.getCurrentTerm(),voteGranted);
   }

Ao receber uma solicitação de um candidato, o nó faz duas verificações: verifica a ronda do candidato e o comprimento do seu log. Se a rodada do candidato for maior e seu log for maior ou igual, então o nó votará no candidato.

  1. Se a atual rodada do nó for maior que a do candidato, recusamos, porque este é um pedido de um nó atrasado, que, aparentemente, ficou fora do cluster por algum tempo e iniciou o processo de eleição porque não viu o líder atual.
  2. , , , , , , ; . — .
  3. ,
  4. . , , , , .
  5. Com um resultado positivo, corrigimos o fato de que o nó participou das eleições e votamos no candidato.
  6. Envie o resultado de volta ao candidato

Certamente, as condições poderiam ter sido escritas um pouco mais curtas e mais elegantes, mas deixei uma opção tão "ingênua" para não me confundir e não confundir ninguém.

Replicação


O líder do cronômetro envia seguidores de pulsação para todos os nós para redefinir seus cronômetros de votação. Como o líder armazena em seus índices de metadados as últimas operações de todos os seguidores, ele pode avaliar se é necessário enviar a operação para os nós. Se o registro de operações do líder se tornar maior que o registro de qualquer seguidor, ele, juntamente com os batimentos cardíacos, enviará sequencialmente as operações ausentes. Chame-o de anexar solicitação. Se a maioria dos nós confirmar o recebimento de novas operações, o líder aplicará essas operações ao banco de dados e aumentará o índice da última operação aplicada. Esse índice também é enviado aos seguidores junto com uma solicitação de pulsação. E se o índice líder for maior que o índice seguidor, o seguidor também aplicará operações ao seu banco de dados para equalizar os índices.

Esse tipo de solicitação anexa que o líder envia ao seguidor

class RequestAppendDTO {
   private final Long term; //   
   private final Integer leaderId; //   

   private final Integer prevLogIndex;//   
   private final Long prevLogTerm;//   
   private final Integer leaderCommit;//      
   private final Operation operation; //
}

Existem implementações nas quais as operações são transferidas em lotes de várias por solicitação. Na implementação atual, apenas uma operação pode ser transmitida por

solicitação.A classe responde ao envio e processamento da solicitação de anexação de pulsação:

https://github.com/pleshakoff/raft/blob/eba5ea1984e2623702f4c299cf1b0af7a6ba0d14/server/src/main/java/com/raft/server/replication/ReplicationService.java

public interface ReplicationService {
   void appendRequest();
   AnswerAppendDTO append(RequestAppendDTO requestAppendDTO);
}

Enviar uma solicitação de alteração de dados


Considere um fragmento de um método desendAppendForOnePeer classe ReplicationServiceImpl

, o qual é responsável por gerar uma solicitação ao seguidor e enviá-la .

private CompletableFuture<AnswerAppendDTO> sendAppendForOnePeer(Integer id) {
   return CompletableFuture.supplyAsync(() -> {
       try {
           //1
           Peer peer = context.getPeer(id);

           Operation operation;
           Integer prevIndex;
           //2    
           if (peer.getNextIndex() <= operationsLog.getLastIndex()) {
               operation = operationsLog.get(peer.getNextIndex());
               prevIndex = peer.getNextIndex() - 1;
           } else 
           //3  
           {
               operation = null;
               prevIndex = operationsLog.getLastIndex();
           }


           RequestAppendDTO requestAppendDTO = new RequestAppendDTO(
                   context.getCurrentTerm(), //   
                   context.getId(), //  
                   prevIndex,//      
                   operationsLog.getTerm(prevIndex),//  
                   context.getCommitIndex(),
                               //      
                   Operation //
           );

...
/*   http     */
}

  1. Metadados do seguidor
  2. , . ( ), , , , . , , , ,
  3. , , ; , , ,

Em seguida, considere o método de appendRequestclasse ReplicationServiceImpl, responsável por enviar a solicitação de acréscimo e processar o resultado para todos os seguidores.

https://github.com/pleshakoff/raft/blob/eba5ea1984e2623702f4c299cf1b0af7a6ba0d14/server/src/main/java/com/raft/server/replication/ReplicationServiceImpl.java#L109

public void appendRequest() {
       List<Integer> peersIds = context.getPeers().stream().map(Peer::getId).collect(Collectors.toList());

       //1 
       while (peersIds.size() > 0) {
           //2 
           List<AnswerAppendDTO> answers = sendAppendToAllPeers(peersIds);
           peersIds = new ArrayList<>();
           for (AnswerAppendDTO answer : answers) {
               //3
               if (answer.getStatusCode().equals(OK)) {
                   //4
                   if (answer.getTerm() > context.getCurrentTerm()) {
                        context.setTermGreaterThenCurrent(answer.getTerm());
                       return;
                   }
                   Peer peer = context.getPeer(answer.getId());
                   //5     
                   if (answer.getSuccess()) {                      
                       peer.setNextIndex(answer.getMatchIndex() + 1);
                       peer.setMatchIndex(answer.getMatchIndex());
                       if (peer.getNextIndex() <= operationsLog.getLastIndex())
                           peersIds.add(answer.getId());
                   //6      
                   } else {
                       peer.decNextIndex();
                       peersIds.add(answer.getId());
                   }
               }
           }
           //7
           tryToCommit();
       }
}

  1. Repetimos a solicitação até recebermos uma resposta de todos os seguidores de que a replicação foi bem-sucedida. Como uma operação é enviada por solicitação, pode levar várias iterações para sincronizar os logs dos seguidores
  2. Envie solicitações para todos os seguidores e obtenha uma lista com respostas
  3. Consideramos respostas apenas de seguidores disponíveis
  4. Se a rodada de um dos seguidores for maior que a do líder, paramos tudo e nos tornamos seguidores
  5. Se o seguidor responder que tudo foi bem-sucedido, atualizamos os metadados do seguidor: salvamos o último índice do log do seguidor e o índice da próxima operação esperada pelo seguidor.
  6. , , , , . , , . , . , .
  7. , . .


Agora vamos ver como exatamente o seguidor processa a solicitação de acréscimo do líder.
Método de appendclasseReplicationServiceImpl

public AnswerAppendDTO append(RequestAppendDTO dto) {
     
       //1     
       if (dto.getTerm() < context.getCurrentTerm()) {
           return new AnswerAppendDTO(context.getId(),context.getCurrentTerm(),false, null);
       } else if (dto.getTerm() > context.getCurrentTerm()) {
           //2 
           context.setCurrentTerm(dto.getTerm());
           context.setVotedFor(null);
       }
       //3  
       applicationEventPublisher.publishEvent(new ResetElectionTimerEvent(this));

       if (!context.getState().equals(FOLLOWER)) {
           context.setState(FOLLOWER);
       }
        
       //4  
       if ((dto.getPrevLogIndex() > operationsLog.getLastIndex()) ||                                                                                        !dto.getPrevLogTerm().equals(operationsLog.getTerm(dto.getPrevLogIndex()))) {
                      return new AnswerAppendDTO(context.getId(), context.getCurrentTerm(), false, null);
       }


       Operation newOperation = dto.getOperation();
       if (newOperation != null) {
           int newOperationIndex = dto.getPrevLogIndex() + 1;
           
         synchronized (this) {
               //5
               if ((newOperationIndex <= operationsLog.getLastIndex()) &&
                      (!newOperation.getTerm().equals(operationsLog.getTerm(newOperationIndex)))){
                   operationsLog.removeAllFromIndex(newOperationIndex);
               }
               //6
               if (newOperationIndex <= operationsLog.getLastIndex())
               {
                 return new AnswerAppendDTO(context.getId(), context.getCurrentTerm(), true,      operationsLog.getLastIndex());
               }
               //7
               operationsLog.append(newOperation);
           }
        }
        //8 
        if (dto.getLeaderCommit() > context.getCommitIndex()) {
           context.setCommitIndex(Math.min(dto.getLeaderCommit(), operationsLog.getLastIndex()));
       }

                 
       return new AnswerAppendDTO(context.getId(), context.getCurrentTerm(), true, operationsLog.getLastIndex());
   }

  1. Se a rodada do líder for menor que a do seguidor, enviaremos uma rodada ao líder e um sinal de que sua solicitação foi rejeitada. Assim que o líder receber uma rodada maior que a dele em resposta, ele se tornará um seguidor
  2. Se a rodada do líder for maior que a do seguidor, defina-a para o seguidor.
  3. Como a solicitação foi recebida do líder, independentemente de haver ou não dados, redefinimos o cronômetro de votação e, se não fossemos um seguidor, o tornamos
  4. , , , , , , . , ,
  5. , . . , , - , , , , . , .
  6. , . ,
  7. ,
  8. , , , .


Resta apenas descobrir como o líder aplica operações do log ao banco de dados. No processo de enviar operações aos seguidores e processar respostas deles, o líder atualiza os metadados dos nós. Assim que o número de nós cujo índice da última operação no log for maior que o índice da última operação aplicada ao banco de dados pelo líder se tornar igual ao quorum, podemos afirmar que a maioria dos nós recebeu a operação e podemos aplicá-lo ao banco de dados do líder. Em outras palavras, se um líder enviou uma operação aos seguidores e a maioria a inseriu em seu log e respondeu ao líder, podemos aplicar essa operação ao banco de dados do líder e aumentar o índice da última operação aplicada. Esse índice com a próxima solicitação de acréscimo de pulsação voará para o seguidor e aplicará a operação com o mesmo índice do log para o banco de dados.

Vamos analisar o método de tryToCommitclasseReplicationServiceImpl

  private void tryToCommit() {
       while (true) {
           //1
           int N = context.getCommitIndex() + 1;
           //2
           Supplier<Long> count = () ->
               context.getPeers().stream().map(Peer::getMatchIndex).
                       filter(matchIndex -> matchIndex >= N).count() + 1;

           //3 
           if (operationsLog.getLastIndex() >= N &&
                   operationsLog.getTerm(N).equals(context.getCurrentTerm())&&
                      count.get()>=context.getQuorum()
           )
           {
               context.setCommitIndex(N);
           } else
               return;
       }
   }

  1. Obtemos o seguinte índice da operação aplicada ao banco de dados
  2. Contamos quantos seguidores têm uma operação com esse índice em seus logs e não esquecemos de adicionar um líder
  3. Se o número de seguidores é quorum e a operação com esse índice está no log do líder, e a rodada dessa operação é equivalente à atual, o líder aplica a operação ao banco de dados e aumenta o índice da última operação aplicada. As operações da rodada anterior não podem ser aplicadas, porque outro líder foi responsável por elas e um conflito pode surgir. Cada líder aplica operações apenas de sua rodada atual.

Conclusão


Qualquer algoritmo distribuído, cujo representante da família é RAFT, é uma poderosa solução integrada que garante a obtenção do resultado, sujeita a todas as regras descritas na especificação.

Existem muitos algoritmos distribuídos e eles são diferentes. Há o ZAB, que é implementado no Zookeeper e é usado, por exemplo, para sincronizar dados no Kafka. Existem algoritmos com requisitos menos rigorosos de consistência, por exemplo, a massa de implementações do protocolo Gossip que são usadas em sistemas AP. Existem algoritmos que seguem os princípios do RAFT e, ao mesmo tempo, usam o protocolo de fofocas para trocar logs como o MOKKA, que também usa criptografia.

Acredito que tentar descobrir qualquer um desses algoritmos é extremamente útil para qualquer desenvolvedor e, como mencionei acima, as soluções podem ser interessantes de maneira abrangente e em partes separadas. E, obviamente, você definitivamente precisa olhar nessa direção para aquelas cujas atividades estão relacionadas ao desenvolvimento de sistemas distribuídos e a problemas de sincronização de dados, mesmo que eles usem soluções industriais padrão.

Referências



Esperamos que o material tenha sido útil para você. E se você quiser fazer um curso , pode fazê-lo agora.

All Articles