Implementación del algoritmo de consenso RAFT para almacenamiento distribuido de KV en Java

Hola de nuevo. Hace unos días, comenzó la capacitación en un nuevo grupo sobre el curso "Arquitecto de software" , y hoy nos gustaría compartir un artículo escrito por uno de los estudiantes del curso, Anton Pleshakov (jefe de desarrollo de Logística de programas y cofundador de Clusterra).




Actualmente, los sistemas de microservicios distribuidos se han convertido prácticamente en el estándar de la industria, y no solo en el mundo empresarial. Los beneficios del uso de sistemas distribuidos se han descrito y discutido más de una vez. Las ventajas de los microservicios han sido conocidas por todos desde hace mucho tiempo: tecnologías para la tarea, componibilidad, escalabilidad, escalamiento de desarrollo, reducción de TTM, etc. Es obvio que el desarrollo de aplicaciones distribuidas proporciona más opciones para una respuesta oportuna a las crecientes demandas comerciales y la digitalización de todo lo que hay a su alrededor.

También es importante tener en cuenta que en este momento un factor muy importante que influye en la elección de una estrategia de desarrollo a favor de los microservicios es la disponibilidad de todo tipo de soluciones de infraestructura preparadas que se encargan de la solución de problemas asociados con los costos adicionales de operar un sistema distribuido. Estamos hablando de sistemas de orquestación de contenedores, mash de servicios, medios de rastreo distribuido, monitoreo, registro, etc. Se puede afirmar con seguridad que la mayoría de los factores mencionados anteriormente como los inconvenientes del enfoque de microservicio en la actualidad no tienen tanta influencia como hace un par de años.

Basado en las realidades modernas, la mayoría de los desarrolladores buscan la primera oportunidad para cambiar de una estructura monolítica a una de microservicio. Uno de los primeros pasos que se pueden tomar sin recurrir a la refactorización total y la descomposición grave es lograr un sistema de escalabilidad horizontal. Es decir, convertir su aplicación monolítica en un clúster, posiblemente incluso formado por los mismos monolitos, pero que le permite variar dinámicamente su número.

Cuando se trata de lograr la escalabilidad horizontal, surge la cuestión de la sincronización de datos dentro de un clúster de manera muy rápida y muy aguda. Afortunadamente, todos los DBMS modernos admiten la replicación de datos entre nodos de una forma u otra. El desarrollador solo necesita seleccionar el DBMS para la tarea y decidir qué propiedades del sistema (según el teorema de CAP) necesita, CP o AP, y el problema se resuelve. En el caso de que se requiera CP y los requisitos de consistencia sean altos, uno de los métodos para resolver el problema de sincronización de datos es usar un clúster que admita el algoritmo de consenso RAFT.

Este algoritmo bastante nuevo (desarrollado en 2012) ofrece una alta garantía de consistencia y es muy popular. Decidí averiguar cómo funciona y escribí mi implementación de un repositorio de valores clave consistente en Java (Spring Boot).

¿Tiene sentido implementar algún algoritmo distribuido usted mismo? Está claro que puede tomar una implementación lista para usar de cualquier algoritmo distribuido, y con el mayor grado de probabilidad, esta implementación será mejor que una "bicicleta" casera. Por ejemplo, puede usar un DBMS que mantenga el nivel de consistencia requerido. O puede implementar Zookeeper . O puede encontrar un marco adecuado para su idioma. Para Java, existe Atomix , que resuelve perfectamente los problemas de sincronización de datos distribuidos.

Pero del otro lado. Si toma una solución llave en mano, el uso de una aplicación externa generalmente agrega un punto adicional de falla a su sistema. Y los marcos pueden ser redundantes o difíciles de operar y aprender, o pueden no existir en absoluto para su lenguaje de programación. Además, la implementación independiente del algoritmo de consenso es una tarea de ingeniería extremadamente interesante que amplía sus horizontes y brinda una comprensión de cómo resolver los problemas que surgen cuando los servicios interactúan en un clúster utilizando el método más óptimo.

Dado que la especificación del algoritmo contiene un conjunto de medidas para mantener la integridad de los datos, puede usar el conocimiento adquirido e incluso usar el algoritmo en su totalidad. Cualquier parte del algoritmo puede ser útil en la vida real. Supongamos que tiene un conjunto de trabajadores para analizar archivos en paralelo. Los trabajadores son equivalentes, pero desea designar a uno de los trabajadores como coordinador, y cuando el trabajador coordinador se caiga, asigne a cualquier otro trabajador libre como coordinador. La primera mitad del algoritmo RAFT, que describe cómo elegir un líder entre nodos equivalentes, lo ayudará con esto. O, por ejemplo, si solo tiene dos nodos en relación con maestro-esclavo, puede usar las reglas de replicación descritas en la especificación RAFT para organizar el intercambio de datos en su caso más simple.

El artículo es esencialmente una guía práctica sobre cómo implementar RAFT usted mismo. El algoritmo en sí y los aspectos teóricos de su trabajo no serán entendidos. Puede leer una breve descripción aquí en este excelente artículo o estudiar la especificación completa aquí . Allí puede encontrar una visualización muy clara del algoritmo.

Descripción general de la solución


La parte del código que está directamente relacionada con la implementación del algoritmo se analiza en el artículo. Al final del artículo hay un enlace al repositorio, donde puede ver el código completo.

La tarea fue la siguiente. Desarrolle un sistema distribuido que le permita almacenar datos en una base de datos de valores clave. Los datos de cada nodo deben ser consistentes, es decir, si los datos cayeron en la base de datos de un nodo y la mayoría de los nodos confirmaron que también recibieron estos datos, tarde o temprano estos datos estarán en la base de datos de cada nodo. Cuando una parte del clúster se desconecta y cuando se vuelve a conectar, los nodos que estaban fuera del clúster deben ponerse al día con el clúster principal y sincronizarse. Cada nodo proporciona una API REST para escribir y leer datos de la base de datos. El sistema consta de dos módulos para dos tipos de nodos: cliente y servidor. A continuación consideramos las características de la implementación del servidor en sí. El código del cliente está en el repositorio.

Un nodo de servidor puede operar en tres estados:

  • Seguidor (seguidor). Acepta solicitudes de lectura del cliente. Toma un latido del líder
  • Candidato (candidato). Acepta solicitudes de lectura del cliente. Envía solicitudes de voto a otros nodos
  • Líder Acepta solicitudes de lectura y escritura. Envía solicitudes de latidos a otros nodos. Envía datos de solicitudes de anexos a otros nodos.

El período de "liderazgo" de uno de los nodos se llama ronda (término). Un nuevo candidato abre una nueva ronda.

Almacenamiento de datos


Cada nodo proporciona acceso al repositorio del registro de operaciones, en el que las operaciones para cambiar datos se registran secuencialmente.

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 operación, además de los datos y el tipo (insertar, cambiar, eliminar), contiene el número de la ronda en la que se creó. Además, cada operación tiene un índice que aumenta secuencialmente. Es importante que todas las operaciones se inserten en los registros de seguidores en el mismo orden en que se insertan en el registro del líder.

Cada nodo tiene acceso a una base de datos en la que los datos se almacenan directamente.

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

En la implementación actual, las soluciones integradas en memoria se utilizan tanto para el registro como para la base de datos (Lista y Mapa competitivo ordinario). Si es necesario, simplemente puede implementar la interfaz adecuada para admitir otros tipos de almacenamiento.

La aplicación de operaciones desde el registro a la base de datos se lleva a cabo mediante una máquina de estado distribuida. Una máquina de estado es un mecanismo que es responsable de cambiar el estado de un clúster, restringiendo el uso de cambios incorrectos (operaciones fuera de servicio o un nodo desconectado que se considera un líder). Para que los cambios se consideren válidos y para que se apliquen a la base de datos, deben pasar una serie de comprobaciones y cumplir con ciertos criterios, que es precisamente lo que proporciona la máquina de estados.

Para un líder, una operación se aplica a la base de datos si la mayoría de los nodos han confirmado el hecho de que la operación también se replica en su registro. Para un seguidor, la operación se aplica a la base de datos si se recibe una señal del líder que ella ingresó a su base de datos.

Temporizadores


Cada nodo proporciona intercambio de datos con otros nodos.

Se admiten dos tipos de consultas:

  • votar cuando se realiza una ronda de votación
  • anexe, también conocido como latido del corazón (si no tiene datos), para replicar los datos de registro a los seguidores y evitar el inicio de una nueva ronda de votación.

El hecho del inicio de un evento está determinado por el temporizador. Se lanzan dos tipos de temporizadores en el nodo:

  • votar. Para comenzar una ronda de votación. Cada nodo tiene su propio intervalo, después del cual intentará iniciar una nueva votación. La cuenta regresiva comienza de nuevo cuando recibe un latido del líder.
  • latido del corazón. Para enviar una solicitud a los seguidores por parte del líder adjunto. Si el nodo no recibe un latido y el temporizador de votación ha expirado, se convierte en candidato e inicia elecciones, aumenta el número de la ronda de votación y envía solicitudes de votación a otros nodos. Si el nodo recoge la mayoría de los votos, se convierte en el líder y comienza a enviar latidos.

El estado actual del nodo.


Cada nodo almacena datos sobre el estado actual.

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

Un nodo líder también almacena metadatos para los nodos a los que replica datos.

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

El líder actualiza los metadatos del nodo cuando recibe respuestas de los seguidores. El líder los utiliza para determinar qué próxima operación de índice el seguidor está listo para aceptar y qué operaciones ya se han agregado al registro del seguidor.

Votación


La clase ElectionService es responsable de votar

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

Enviar una solicitud de votación


Si el nodo es un seguidor y no recibe un latido durante el período establecido para la espera, entonces aumenta su ronda actual, se declara candidato y comienza a enviar solicitudes de voto a otros nodos. Si logra reunir un quórum y la mayoría de los nodos emiten su voto, se convertirá en el nuevo líder. En términos RAFT, el quórum es más de la mitad de todos los nodos (51%).

Analicemos el método de processElectionclase ElectionServiceImpl, que es llamado por el temporizador de votación cuando expira la votación y envía a los nodos una solicitud de votación .

//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. Establecer el estado de "Candidato". Eleve el número redondo y vote por nosotros mismos.
  2. , ( ). - , , heartbeat .
  3. - , . , , -.
  4. , . , heartbeat .
  5. ¡El nodo votó por nosotros! Aumentamos el número de nodos que emiten votos por nosotros y corregimos que este nodo votó por nosotros.
  6. Votados no por nosotros, también creemos.
  7. Si se reúne el quórum y el nodo gana la elección, establecemos el estado de "Líder". De lo contrario, nos convertimos en seguidores y esperamos.

También debe tenerse en cuenta que cuando un nodo se convierte en un líder, el siguiente índice se establece para cada nodo en la lista de nodos almacenados en el líder, que es igual al último índice en el registro del líder más 1. A partir de este índice, el líder intentará actualizar los registros del seguidor. De hecho, este índice almacenado por el líder puede no corresponder con el índice real del registro del seguidor y el valor real se obtendrá solo al intercambiar datos con el seguidor y se ajustará. Pero se necesita algún punto de partida .

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

       );
   }

Procesamiento de solicitud de votación


Al votar, cada nodo recibe una solicitud del siguiente formulario del candidato :

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

Ahora veamos el procedimiento de voteclase ElectionServiceImpl, procesa la solicitud de voto del candidato y devuelve una decisión con respecto a su candidatura para el 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);
   }

Al recibir una solicitud de un candidato, el nodo realiza dos verificaciones: verifica la ronda del candidato y la longitud de su registro. Si la ronda del candidato es más alta y su registro es más largo o igual, entonces el nodo le da un voto al candidato.

  1. Si la ronda actual del nudo es mayor que la ronda del candidato, nos negamos, porque esta es una solicitud de algún nudo rezagado, que, aparentemente, estuvo fuera del grupo durante algún tiempo y comenzó el proceso de elección porque no vio al líder titular.
  2. , , , , , , ; . — .
  3. ,
  4. . , , , , .
  5. Con un resultado positivo, arreglamos el hecho de que el nodo participó en las elecciones y votó por el candidato.
  6. Enviar el resultado al candidato

Seguramente, las condiciones podrían escribirse algo más cortas y más elegantes, pero dejé una opción más "ingenua" para no confundirme y no confundir a nadie.

Replicación


El líder del temporizador envía seguidores de latidos a todos los nodos para restablecer sus temporizadores de votación. Como el líder almacena en sus índices de metadatos las últimas operaciones de todos los seguidores, puede evaluar si es necesario enviar la operación a los nodos. Si el registro de operaciones del líder se vuelve más largo que el registro de cualquier seguidor, entonces él, junto con los latidos del corazón, le envía secuencialmente las operaciones que faltan. Llamarlo anexar solicitud. Si la mayoría de los nodos confirman la recepción de nuevas operaciones, el líder aplica estas operaciones a su base de datos y aumenta el índice de la última operación aplicada. Este índice también se envía a los seguidores junto con una solicitud de latido. Y si el índice líder es más alto que el índice seguidor, entonces el seguidor también aplica operaciones a su base de datos para igualar los índices.

Este tipo de solicitud de anexos que el líder envía al 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; //
}

Hay implementaciones en las que las operaciones se transfieren en lotes de varios por solicitud. En la implementación actual, solo se puede transmitir una operación por

solicitud. La clase responde al envío y procesamiento de la solicitud heartbeat-append:

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 una solicitud de cambio de datos


Considere un fragmento de un método desendAppendForOnePeer clase ReplicationServiceImpl

. El método es responsable de generar una solicitud al seguidor y enviarla .

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. Metadatos del seguidor
  2. , . ( ), , , , . , , , ,
  3. , , ; , , ,

A continuación, considere el método de appendRequestclase ReplicationServiceImpl, que es responsable de enviar la solicitud de anexo y procesar el resultado a todos los 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 la solicitud hasta que recibamos una respuesta de todos los seguidores de que la replicación fue exitosa. Dado que se envía una operación por solicitud, puede llevar varias iteraciones sincronizar los registros de seguidores
  2. Enviar solicitudes a todos los seguidores y obtener una lista con respuestas
  3. Consideramos respuestas solo de seguidores disponibles
  4. Si resulta que la ronda de uno de los seguidores es más que la ronda del líder, detenemos todo y nos convertimos en seguidores.
  5. Si el seguidor respondió que todo fue exitoso, actualizamos los metadatos del seguidor: guardamos el último índice del registro del seguidor y el índice de la próxima operación esperada por el seguidor.
  6. , , , , . , , . , . , .
  7. , . .


Ahora veamos cómo procesa exactamente el seguidor la solicitud de adición del líder.
Método de appendclaseReplicationServiceImpl

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. Si la ronda del líder es menor que la ronda del seguidor, entonces enviamos al líder una ronda y una señal de que su solicitud ha sido rechazada. Tan pronto como el líder reciba una ronda más grande que la suya en respuesta, se convertirá en un seguidor.
  2. Si la ronda del líder es más que la ronda del seguidor, establezca esta ronda para el seguidor.
  3. Dado que la solicitud fue recibida del líder, independientemente de si hay datos allí o no, restablecemos el temporizador de votación y, si no éramos seguidores, nos convertimos en él.
  4. , , , , , , . , ,
  5. , . . , , - , , , , . , .
  6. , . ,
  7. ,
  8. , , , .


Solo queda descubrir cómo el líder aplica las operaciones desde el registro a la base de datos. En el proceso de enviar operaciones a los seguidores y procesar las respuestas de ellos, el líder actualiza los metadatos de los nodos. Tan pronto como el número de nodos cuyo índice de la última operación en el registro es mayor que el índice de la última operación aplicada a la base de datos por el líder se vuelve igual al quórum, podemos afirmar que la mayoría de los nodos recibieron la operación y podemos aplicarlo a la base de datos del líder. En otras palabras, si un líder envió una operación a sus seguidores y la mayoría de ellos lo insertó en su registro y respondió al líder, entonces podemos aplicar esta operación a la base de datos del líder y aumentar el índice de la última operación aplicada. Este índice con la próxima solicitud append-heartbeat volará al seguidor y aplicará la operación con el mismo índice desde su registro a su base de datos.

Analicemos el método de la tryToCommitclase.ReplicationServiceImpl

  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. Obtenemos el siguiente índice de la operación aplicada a la base de datos.
  2. Contamos cuántos seguidores tienen una operación con dicho índice en sus registros, y no olvidemos agregar un líder
  3. Si el número de seguidores es quórum y la operación con dicho índice está en el registro del líder, y la ronda de esta operación es equivalente a la actual, entonces el líder aplica la operación a la base de datos y aumenta el índice de la última operación aplicada. Las operaciones de la ronda anterior no pueden aplicarse, porque otro líder fue responsable de ellas y podría surgir un conflicto. Cada líder aplica operaciones solo de su ronda actual.

Conclusión


Cualquier algoritmo distribuido, el representante de la familia de los cuales es RAFT, es una poderosa solución integrada que garantiza el logro del resultado, sujeto a todas las reglas descritas en la especificación.

Hay muchos algoritmos distribuidos y son diferentes. Existe ZAB, que se implementa en Zookeeper y se utiliza, por ejemplo, para sincronizar datos en Kafka. Existen algoritmos con requisitos de consistencia menos estrictos, por ejemplo, la gran cantidad de implementaciones del protocolo Gossip que se utilizan en los sistemas AP. Existen algoritmos que siguen los principios de RAFT y, al mismo tiempo, utilizan el protocolo de chismes para intercambiar registros como MOKKA, que también utiliza cifrado.

Creo que tratar de descubrir cualquiera de estos algoritmos es extremadamente útil para cualquier desarrollador, y como mencioné anteriormente, las soluciones pueden ser interesantes tanto de manera integral como en partes separadas. Y, obviamente, definitivamente debe buscar en esta dirección a aquellos cuyas actividades están relacionadas con el desarrollo de sistemas distribuidos y con respecto a la sincronización de datos, incluso si usan soluciones industriales estándar.

Referencias



Esperamos que el material te haya sido útil. Y si quieres tomar un curso , puedes hacerlo ahora mismo.

All Articles