loader-logo

Gérer les écritures de base de données dans la plateforme de cloud native en utilisant le « leader election pattern »

Bonjour à tous.

Dans cet article, nous allons explorer quelques-unes des “communes” préoccupations lorsque vous traitez avec un système « cloud native » distribué ; Lorsque de nombreuses instances distribuées du même service doivent effectuer des mises à jour de la même base de données partagée en exécutant une tâche planifiée.

Imaginez que nous avons 3 instances de service fonctionnant sur une plate-forme native de cloud distribué, qui exécute une tâche planifiée qui obtient des données de l’externe (ou interne) ressources, effectue certains calculs, puis écrit à une base de données partagée en même temps dans la journée. Comment pouvons-nous composer avec les opérations d’écriture simultanées et quels sont les mécanismes/modèles possibles que nous pouvons utiliser pour gérer cette situation ?

Solution optimiste/pessimiste basée sur le verrouillage

L’une des solutions possibles pour les mises à jour simultanées des bases de données est d’utiliser une stratégie de verrouillage optimiste/pessimiste au niveau de la base de données, nous n’allons pas explorer ces sujets, mais si vous voulez avoir plus d’informations à ce sujet, vous pouvez vérifier le verrouillage pessimiste et optimiste (site Web jboss).

Beaucoup de fournisseurs de base de données utilisent une mise en œuvre de l’une de ces stratégies; Couchbase utilise CAS (Comparer et échanger) qui est une sorte de verrouillage optimiste, le CAS est une valeur représentant l’état actuel d’un élément. Chaque fois que l’article est modifié, son SAE change et il est retourné dans les métadonnées d’un document chaque fois qu’on y accède.

 

Les inconvénients de ce genre de solution sont qu’il est mis en œuvre au niveau de la base de données et étendu au niveau de l’application. Le jour où vous changerez le type de base de données, vous serez obligé d’adapter votre application comme vous le ferez. En outre, dans le cas de tâches programmées, toutes les instances s’exécutent en même temps en gaspillant des ressources d’E/S et de bande passante.

Solution fondée sur l’élection du leader

Le but de ce modèle est de choisir une instance de service pour coordonner les tâches entre autres. À un certain moment, le leader peut “posséder” l’exécution d’une fonction opérationnelle ou l’accès à une ressource. Il est couramment utilisé pour assurer la cohérence des données. De plus si le nœud accueillant le leader meurt un autre prendra le leadership et ainsi de suite…

Assez de définitions passons au code :

Dans notre exemple, nous utiliserons Zookeeper qui fournit la mise en œuvre du mécanisme d’élection des leaders, Docker et intégration de Spring donc assurez-vous que vous avez Java et Docker installé dans votre machine et commençons.

Qu’allons-nous faire ?

  1. Nous exécuterons 3 nœuds de Zookeeper en utilisant des images de docker (Le cluster Zookeeper est appelé ensemble, 3 serveurs Zookeeper est la taille minimale recommandée pour un ensemble).
  2. Nous utiliserons spring-integration-zookeeper pour connecter notre application Spring boot au Zookeeper.
  3. Nous définirons une tâche planifiée à exécuter à l’aide du planificateur de Spring.
  4. Nous allons exécuter 3 instances de notre application sur 3 ports différents et puis regarder les logs.

Étape 1 : Exécuter l’ensemble Zookeeper :

Aller dans dockerhub et rechercher Zookeeper, copier/passer l’exemple de contenu pour docker-compose dans le fichier stack.yml :

version: '3.1'

services:
  zoo1:
    image: zookeeper
    restart: always
    hostname: zoo1
    ports:
      - 2181:2181
    environment:
      ZOO_MY_ID: 1
      ZOO_SERVERS: server.1=0.0.0.0:2888:3888;2181 server.2=zoo2:2888:3888;2181 server.3=zoo3:2888:3888;2181

  zoo2:
    image: zookeeper
    restart: always
    hostname: zoo2
    ports:
      - 2182:2181
    environment:
      ZOO_MY_ID: 2
      ZOO_SERVERS: server.1=zoo1:2888:3888;2181 server.2=0.0.0.0:2888:3888;2181 server.3=zoo3:2888:3888;2181

  zoo3:
    image: zookeeper
    restart: always
    hostname: zoo3
    ports:
      - 2183:2181
    environment:
      ZOO_MY_ID: 3
      ZOO_SERVERS: server.1=zoo1:2888:3888;2181 server.2=zoo2:2888:3888;2181 server.3=0.0.0.0:2888:3888;2181

Et exécutez cette commande sur votre terminal :

docker-compose -f stack.yml up -d

Ceci démarrera l’ensemble ZooKeeper.

Étape 2 : Se connecter à l’ensemble Zookeeper et créer un service programmé

Dans votre application de démarrage Spring, ajoutez la dépendance spring-integration-zookeeper :

<dependency>
    <groupId>org.springframework.integration</groupId>
    <artifactId>spring-integration-zookeeper</artifactId>
    <version>5.1.7.RELEASE</version>
</dependency>

 

Créer une classe de configuration :

@Configuration
public class ZookeeperConfig {


    public static final String ZOOKEEPER_CLIENT_OP= "localhost:2181";
    public static final String ZOOKEEPER_LEADER_STORAGE= "/my-app/leader";
    public static final String ZOOKEEPER_LEADER_ROLE= "my-app-leadership";


    @Value("${project.basedirectory}")
    private String baseDir;


    /**
     * Config Curator that handles the complexity of managing
     * connections to the ZooKeeper cluster and retrying operations
     * @return
     */
    @Bean
    public CuratorFrameworkFactoryBean curatorFramework(){
        return new CuratorFrameworkFactoryBean(ZOOKEEPER_CLIENT_OP);
    }


    @Bean
    public LeaderInitiatorFactoryBean leaderInitiator(CuratorFramework client){
        String zookeeperLeaderDataLocation = String.format("%s%s", baseDir, ZOOKEEPER_LEADER_STORAGE);
        System.out.println(zookeeperLeaderDataLocation);
        return new LeaderInitiatorFactoryBean().setClient(client)
                .setPath(zookeeperLeaderDataLocation)
                .setRole(ZOOKEEPER_LEADER_ROLE);
    }
}

Pour rendre la chose plus intéressante, nous allons créer un Aspect autour de l’exécution d’une méthode annotée avec une annotation personnalisée (que nous allons créer), de sorte que nous puissions facilement annoter les méthodes qui pourraient s’exécuter simultanément et dans l’aspect nous allons vérifier si l’instance de service en cours fonctionne pour le leader ou non :

/**
 * Annotate method that will be executed in the same time by many instances
 * Only the leader will execute method annotated with {@link Leader}
 *
 * @author Abdelghani ROUSSI
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Leader {
}

Et puis notre aspect

/**
 * Aspect that watch for @Leader annotated method
 *
 * @author Abdelghani ROUSSI
 */
@Slf4j
@Aspect
@Component
public class LeaderElectionAspect {


    private final LeaderInitiator leaderInitiator;


    public LeaderElectionAspect(LeaderInitiator leaderInitiator) {
        this.leaderInitiator = leaderInitiator;
    }


    /**
     * Execute method annotated with {@link Leader} only if the current node is a leader
     * @param joinPoint
     * @throws Throwable
     */
    @Around(value = "@annotation(com.example.leaderelection.aspect.Leader)")
    public void aroundLeaderAnnotation(ProceedingJoinPoint joinPoint) throws Throwable {
        if(this.leaderInitiator.getContext().isLeader()){
            log.info("=====  I'm the leader I'll execute the Scheduled tasks =====");
            joinPoint.proceed();
        }
    }
}

Enfin, nous allons créer la méthode programmée et l’annoter avec @Leader

@Leader
	@Scheduled(cron = "0/30 * * * * *")
	public void runBatch(){
		log.info("===== START BATCH ====== {}");
	}

N’oubliez pas d’ajouter @EnableScheduling pour permettre l’ordonnancement et @EnableAspectJAutoProxy pour activer l’aspect.

Etape 3 : Exécuter 3 instances de notre service dans différents ports.

Enfin, exécutez 3 instances de votre application sur différents ports :

./mvnw spring-boot:run -Dserver.port=8001
./mvnw spring-boot:run -Dserver.port=8002
./mvnw spring-boot:run -Dserver.port=8003

 

Vous verrez ensuite dans l’un de vos terminaux :

[eaderSelector-0] o.s.integration.leader.DefaultCandidate  : DefaultCandidate{role=my-app-leadership, id=bd74b45e-3011-4416-bba0-ec5c88432e64} has been granted leadership; context: CuratorContext{role=my-app-leadership, id=bd74b45e-3011-4416-bba0-ec5c88432e64, isLeader=true}
[   scheduling-1] c.e.l.aspect.LeaderElectionAspect        : =====  I'm the leader I'll execute the Scheduled tasks =====
[   scheduling-1] c.e.l.LeaderElectionApplication          : ===== START BATCH ====== {}

Cela signifie que le nœud spécifique a été élu comme leader, et il sera propriétaire de l’exécution de la tâche programmée, pour d’autres ils seront en mode veille (Juste observer le leader, s’il meurt l’un d’eux sera élu comme chef et exécutera le service).

Enfin, comme nous l’avons vu, l’élection des chefs semble être une façon simple et efficace de gérer l’exécution simultanée des tâches pour l’application Cloud Native distribuée, qui nous donnera :

  • Cohérence des données.
  • Fournisseur de stockage des données agnosique car il est fait sur la couche de service.
  • Conserve les ressources en réduisant les chances que le travail soit effectué par plusieurs instances de service.
  • Simple, efficace et peut être personnalisé pour ajouter des règles d’affaires avant et après le déclenchement.
  • Peut être introduit comme configuration automatique dans n’importe quel micro-service, en ajoutant simplement la dépendance et annoter les méthodes souhaitées avec @Leader.

C’est tout ! J’espère que vous avez apprécié cet article.

Abdelghani

 

 

Partager sur facebook
Partager sur twitter
Partager sur linkedin
Partager sur pinterest
Partager sur whatsapp

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Sélectionnez votre pays et votre langue​

Maroc

Tunis

Allemagne

Switzerland

France

Global