Quantcast
Channel: Publicis Sapient Engineering – Engineering Done Right
Viewing all articles
Browse latest Browse all 1865

Utiliser Zookeeper avec Curator API pour du « Service Discovery »

$
0
0

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 !!


Viewing all articles
Browse latest Browse all 1865

Trending Articles