Il y a 2 années · 8 minutes · Back

Utiliser Zookeeper avec Curator API pour du « Service Discovery »

Suite à la présentation de Zookeeper dans un article précédent, la mise en oeuvre d’un cas pratique s’imposait ! Je vous propose donc d’utiliser Zookeeper pour mettre en oeuvre un « Service Discovery ». Nous utiliserons le framework Curator qui offre une API haut niveau pour manipuler Zookeeper.

Le framework Curator a été initié par les développeurs de Netflix afin de rendre plus digeste l’API original de Zookeeper. Et bien leur en a pris car grâce à cette API, toutes les solutions qu’apporte Zookeeper ne sont plus que quelques lignes de code à ajouter à votre projet. Sachez que l’API Curator vous permet de mettre en oeuvre d’autres “recettes” telle que Leader Election, Locks, Barriers, Cache, … bref je vous invite à aller jeter un coup d’oeil à la page des recettes du projet Curator.

Pour réaliser notre tutorial nous allons utiliser spring-boot. Ne vous inquiétez pas si vous ne connaissez pas ou peu spring-boot son utilisation sera réduite au stricte minimum. Vous pouvez trouver le code source de cet article sur mon github.

Nous utiliserons une instance standalone de Zookeeper. Je vous invite à utiliser le tuto de l’article précédent pour installer et configurer votre instance (ne vous inquiétez pas cela ne vous prendra pas plus de 10 minutes).

L’API Curator pour le « Service Discovery »

Pour répondre au problème de « Service Discovery », il faut d’abord que notre service s’enregistre auprès de Zookeeper pour annoncer « Hey oh ! Je m’appelle service-truc. Je suis en version x.y et si on veut m’appeler je suis disponible à l’adresse http://service-truc« .

Ensuite une application cliente va vouloir consommer le service-truc. Elle va donc aller voir Zookeeper et lui demander « Alors voilà. J’ai besoin d’appeler le service-truc vous en l’auriez en stock ? » (oui l’application client est polie elle vouvoie Zookeeper).

Pour voir ce que cela donne en terme de code, nous allons mettre en oeuvre un exemple tout simple avec un service nommé « simple-tax-api » qui permet de calculer un prix TTC à partir d’un prix HT fournit en entrée. Nous allons enregistrer le service auprès de Zookeeper puis nous développerons une application cliente de « simple-tax-api » et qui, pour l’utiliser, demandera à Zookeeper de lui fournir toutes les informations nécessaires pour l’appeler.

Dépendances Maven

Ajouter les dépendances suivantes pour utiliser l’API Curator :

<dependency>
  <groupId>org.apache.curator</groupId>
  <artifactId>curator-framework</artifactId>
  <version>2.7.1</version>
</dependency>
<dependency>
  <groupId>org.apache.curator</groupId>
  <artifactId>curator-x-discovery</artifactId>
  <version>2.7.1</version>
</dependency> 

Développer le service de calcul du prix TTC

Cette partie a pour objectif d’être anecdotique, voici à quoi ressemble cet incroyable service REST :

@RestController
@RequestMapping(value = "/taxapi")
public class TaxRest {
 
   private static final double TVA = 0.2;
   
   @RequestMapping(value = "/ttc", method = RequestMethod.GET)
   public double add(@RequestParam("ht") double ht) {
      return ht * (1 + TVA);
   }

} 

Enregistrer le service dans Zookeeper

Bien entendu la première chose à faire est de se connecter à Zookeeper. Pour cela nous utilisons CuratorFrameworkFactory. Dans l’exemple de code ci-dessous, nous demandons à nous connecter à un Zookeeper déployé en local avec 3 tentatives maximum. Les tentatives de connexion commencent lorsque l’on appel start() sur notre instance de CuratorFramework:

CuratorFramework curator = CuratorFrameworkFactory.newClient("localhost", new ExponentialBackoffRetry(1000, 3));

curator.start();

Une fois connecté à Zookeeper, nous utilisons ServiceDiscoveryBuilder pour préciser dans quel rangement le service veut s’enregistrer. Dans notre cas, nous n’allons pas être très imaginatif, nous allons nous enregistrer dans le rangement “services”. A noter que JsonInstanceSerializer nous permet d’annoncer que nous allons stocker des informations supplémentaires (nommé payload par Curator framework) qui peuvent être assez complètes et faciles à écrire grâce à l’utilisation de JSON. Dans notre cas ce payload sera une simple String qui contiendra la version du service qui viendra s’enregistrer auprès du naming service.

JsonInstanceSerializer<String> serializer = new JsonInstanceSerializer<String>(String.class);

ServiceDiscovery<String> discovery = ServiceDiscoveryBuilder.builder(String.class)
      .client(curator())
      .basePath("services")
      .serializer(serializer)
      .build();

Avec cela, nous avons tous les éléments nécessaires pour enregistrer concrètement notre service auprès du « Naming Service ». Dans ce morceau de code, nous déclarons concrètement que notre service nommé « simple-tax-api » en version 1.0 est disponible sur « http://localhost:{serverPort}/taxapi »

ServiceInstance<String> instance =
      ServiceInstance.<String>builder()
              .name("simple-tax-api")
              .payload("1.0")
              .address("localhost")
              .port(serverPort)
              .uriSpec(new UriSpec("{scheme}://{address}:{port}/taxapi"))
              .build();
discovery.registerService(instance);

Le code suivant récapitule ce que nous venons de voir cette fois sous la forme d’une configuration Java Spring

@Configuration
public class ServiceDiscoveryConfiguration implements CommandLineRunner{
  @Autowired
  ServiceDiscovery<String> discovery;
  /**
   * serverPort est fourni au démarrage en ligne de commande
   */
  @Value("${server.port}")
  private int serverPort;
  public void run(String... args) throws Exception {
      ServiceInstance<String> instance =
              ServiceInstance.<String>builder()
                      .name("simple-tax-api")
                      .payload("1.0")
                      .address("localhost")
                      .port(serverPort)
                      .uriSpec(new UriSpec("{scheme}://{address}:{port}/taxapi"))
                      .build();
      discovery.registerService(instance);
  }
  @Bean(initMethod = "start", destroyMethod = "close")
  public CuratorFramework curator() {
      return CuratorFrameworkFactory.newClient("localhost", new ExponentialBackoffRetry(1000, 3));
  }
  @Bean(initMethod = "start", destroyMethod = "close")
  public ServiceDiscovery<String> discovery() {
      JsonInstanceSerializer<String> serializer =
              new JsonInstanceSerializer<String>(String.class);
      return ServiceDiscoveryBuilder.builder(String.class)
              .client(curator())
              .basePath("services")
              .serializer(serializer)
              .build();
  }
}

Développons l’application utilisant « simple-tax-api »

Le départ du code est identique à l’enregistrement du service dans Zookeeper puisqu’il s’agit de s’y connecter puis d’obtenir une instance de ServiceDiscovery. Cette partie du code étant strictement identique, je ne reviens pas dessus.

Pour récupérer des informations sur le « service-tax-api », nous utilisons l’instance de ServiceDiscovery en lui demandant de nous retourner la liste des instances actuellement disponibles pour nous fournir le service :

Collection<ServiceInstance<String>> services = discovery.queryForInstances("simple-tax-api");

Ensuite dans notre exemple, nous nous limitons à prendre le premier élément disponible et appelons le service dessus:

if (services.iterator().hasNext()) {
  // Nous utilisons par défaut le premier dans la liste
  ServiceInstance<String> serviceInstance = services.iterator().next();
  logger.debug("version du service: {}", serviceInstance.getPayload());
  String serviceUrl = serviceInstance.buildUriSpec() + "/ttc?ht={ht}";
  Map<String,Double> params = new HashMap<String, Double>();
  params.put("ht", totalHT);
  totalTTC = restTemplate.getForObject(serviceUrl, Double.class, params);
}

et voilà !

Pour référence rapide, voici ci-dessous le code des deux principales classes composant notre application client du service « simple-tax-api ».

Pour récupérer le code complet vous pouvez le trouvez sur mon github.

@Configuration
public class ServiceDiscoveryClientConfiguration {
  @Bean(initMethod = "start", destroyMethod = "close")
  public ServiceDiscovery<String> discovery() {
      JsonInstanceSerializer<String> serializer =
              new JsonInstanceSerializer<String>(String.class);
      return ServiceDiscoveryBuilder.builder(String.class)
              .client(curator())
              .basePath("services")
              .serializer(serializer)
              .build();
  }
  @Bean(initMethod = "start", destroyMethod = "close")
  public CuratorFramework curator() {
      return CuratorFrameworkFactory.newClient("localhost", new ExponentialBackoffRetry(1000, 3));
  }
}

@RestController
@RequestMapping(value = "/ecommerce/api")
public class PriceCalculatorRest {
  Logger logger = LoggerFactory.getLogger(getClass());
  @Autowired
  ServiceDiscovery<String> discovery;
  RestTemplate restTemplate = new RestTemplate();
  @RequestMapping(value = "/total", method = RequestMethod.POST,
          consumes = MediaType.APPLICATION_JSON_VALUE)
  public double totalPanier(@RequestBody List<Article> articles) throws Exception {
      double totalHT = 0;
      double totalTTC = 0;
      for (Article article : articles) {
          totalHT += article.getPriceHT();
      }
      Collection<ServiceInstance<String>> services = discovery.queryForInstances("simple-tax-api");
      logger.debug("Le service 'simple-tax-api' est fourni par {} instance(s)", services.size());
      if (services.iterator().hasNext()) {
          // Nous utilisons par défaut le premier dans la liste
          ServiceInstance<String> serviceInstance = services.iterator().next();
          logger.debug("version du service: {}", serviceInstance.getPayload());
          String serviceUrl = serviceInstance.buildUriSpec() + "/ttc?ht={ht}";
          Map<String,Double> params = new HashMap<String, Double>();
          params.put("ht", totalHT);
          totalTTC = restTemplate.getForObject(serviceUrl, Double.class, params);
      }
      return totalTTC;
  }
}

Assemblons le tout

Lancement du service « simple-tax-api »

Tout d’abord nous nous assurons que Zookeeper en standalone est bien démarré.

./zkServer.sh start

Comme nous avons développé l’application avec spring-boot, nous pouvons produire facilement un jar avec un tomcat embarqué avec la commande maven suivante:

mvn clean package spring-boot:repackage

puis nous lançons deux instances de notre service

java -Dserver.port=8000 -jar simple-tax-api-1.0-SNAPSHOT.jar
java -Dserver.port=9000 -jar simple-tax-api-1.0-SNAPSHOT.jar

Lancement de l’application cliente

De manière identique nous produisons un jar avec un tomcat embarqué avec la commande maven:

mvn clean package spring-boot:repackage

puis pour exécuter le jar

java -jar simple-client-1.0-SNAPSHOT.jar

Pour déclencher l’appel à service-tax-api, nous appelons le service REST de notre application simple-client:

curl -X POST -H "Content-Type: application/json" -H "Cache-Control: no-cache" -d '[
{
    "name":"toto",
    "priceHT": 45.0
},
{
    "name":"titi",
    "priceHT": 55.0
}
]
' http://localhost:8080/ecommerce/api/total

Nous avons alors les logs suivantes qui apparaissent:

Le service 'simple-tax-api' est fourni par 2 instance(s)
version du service: 1.0
appel de l'url: http://localhost:8000/taxapi/ttc?ht={ht}

Conclusion

Grâce à l’API Curator manipuler Zookeeper est accessible à tous, alors … à vous de jouer !!

Une réflexion au sujet de « Utiliser Zookeeper avec Curator API pour du « Service Discovery » »

  1. Publié par Anas, Il y a 2 années

    Mise à part l’apport technologique (… ZooKeeper c’est super), je ne vois pas d’apport fonctionnel par rapport à un registre UDDI ou à un simple registre d’URLs fourni par un serveur HTTP pour les service REST ou consoeurs. La solution technique est astucieuses mais elle reste iso-fonctionnelle du point de vue métier avec les précédentes.

    Comment on garanti que deux services redondants mais nommés différement ne sont pas déployé sur le registre ?

    Quand c’est une poignés de service la cohérence peut se faire par une « gouvernance manuelle » (design authority ou commité d’architecture avec les acteur métiers et techniques clefs), mais quand le nombre de service devient imporant, la gouvernance manuelle ne fait pas l’affaire, il faut de l’outillage

    C’est la question qui n’a pas été résolue par les solutions précédentes et qui aboutira au même résultat si elle n’est pas résolue par la solution proposées.

    A suivre :-)

  2. Publié par Benjamin, Il y a 2 années

    Très bonne question Anas.
    Dommage qu’il n’y ait pas eu de réponses et d’échange sur ce sujet qui me semble important…

  3. Publié par Nicolas R., Il y a 3 mois

    2 services redondants nommés différemment ? Bizarre mais je pense que pour s’affranchir de cela, ces services redondants peuvent être exposés au travers d’une VIP …

    De plus il me semble que l’auteur a présenté une des fonctionnalités de zookeeper et que cette boîte à outils n’a pas pour vocation de s’affranchir des problématiques d’urbanisme inhérentes à chaque système d’information.

    L’analogie avec un registre UDDI ou un simple serveur HTTP/REST est un peu réductrice : il faut avouer que les mécanismes proposés par zookeeper sont bien supérieurs (watchers etc …?).

Laisser un commentaire

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