19 février 2020, par Mody Sow

BeopenIT

 

Aujourd’hui, Istio a fait un grand bond en avant sur la mise en place et le déploiement des architectures de microservices. Apportant ainsi pas mal de solutions sur tout ce dont on a besoin pour exposer et faire communiquer des microservices de la manière la plus efficace et sécurisée possible et de plus avec une parfaite compatibilité avec les outils cloud native comme kubernetes. Mais toujours est-il,  qu’il existait d’autres solutions avant Istio à l’image de Spring Cloud & Netflix 0SS qui permettent de faire presque la même chose que Istio mais différemment. 

Dans cet article, il s’agira de voir comment est ce qu’on peut déployer une architecture de microservices dans un écosystème Spring Cloud & Netflix 0SS afin de mieux appréhender dans la suite la différence qui réside entre cette approche et celle de Istio.

Pour ce faire, nous allons prendre l’exemple de l’application Bookinfo et vous montrer étape par étape, comment nous l’avons implémentée avec Spring Cloud.

       1. Présentation de l’application Bookinfo

L’application Bookinfo affiche des informations sur un livre, semblable à une librairie en ligne. La page affiche une description du livre, les détails du livre (ISBN, nombre de pages, etc.) et quelques critiques de livres.

Elle est composée de quatre microservices distincts:

  • Productpage – ce microservice appelle les microservices Details et Reviews pour remplir la vue UI.
  • Détails – c’est le microservice qui contient des informations sur les livres.
  • Reviews – ce microservice contient des critiques de livres. Il existe 3 versions de ce microservice:
    • La version v1 qui n’appelle pas le microservice ratings.
    • La version v2 qui appelle le microservice Ratings et affiche chaque note de 1 à 5 étoiles de couleurs noires.
    • La version v3 appelle le microservice Ratings et affiche chaque note sous la forme de 1 à 5 étoiles de couleurs rouges.
  • Ratings – c’est le microservice qui contient les notes qui accompagnent les reviews.

       2. Découverte de services & Équilibrage de charge

Pour qu’une architecture de microservices puisse respecter les critères d’élasticité et de couplage lâche, et assurer une communication inter-microservices fluide, il faut disposer d’un bon système de découverte de services et d’équilibrage de charge. De sorte que les microservices puissent à tout moment connaître la liste de tous leurs homologues en cours d’exécution et les informations permettant de les contacter, à savoir : nom, adresse IP et numéro de port.   

                 2.1.  Découverte de services & équilibrage de charge dans Spring Cloud

L’équilibrage de charge et la découverte de service dans l’écosystème Spring Cloud se fait avec les deux composants suivants.

                        2.1.1.  Serveur de discovery

Le serveur de discovery est un service où tous les microservices de l’écosystème s’enregistrent en renseignant leurs informations d’identification et de routage: nom, adresse IP et numéro de port. En plus d’un endpoint et optionnellement quelques métriques pour le health check et le load balancing. Dans l’écosystème spring cloud, les deux services de discovery les plus populaires sont Eureka et Consul.

                       2.1.3.  Client discovery

Le client discovery est une dépendance à importer et à configurer au niveau de chaque microservice. Ce composant se chargera à notre place de communiquer avec le serveur de discovery. Assurant ainsi en background la découverte de service et le load Balancing à chaque fois que l’on veut contacter un microservice avec RestTemplate ou un client Feign.

                 2.2.  Application à l’application Bookinfo

Voici l’architecture que nous obtenons à la fin de cette partie.

                       2.2.1.  Le serveur de discovery

Pour déployer un serveur de discovery, nous avons dû créer une application Spring Boot au sein de laquelle, nous avons embarqué un serveur eureka.

Pour cela, il nous a suffit d’ajouter la dépendance suivante au niveau du fichier pom.xml.

<dependency>
     <groupId>org.springframework.cloud</groupId>
     <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>

Et d’annoter la classe principale avec @EnableEurekaServer.

                       2.2.2  Les clients discovery

Puisqu’il doit y avoir un client discovery au niveau de chaque microservice afin que celui-ci puisse communiquer avec le serveur de discovery et faire le load balancing, la façon d’intégrer un client discovery à un microservice dépend du langage et framework utilisés. Et puisque là nous avons une application polyglotte écrite avec deux technologies différentes: Spring boot et Nodejs, nous allons vous montrer étape par étape, pour chaque technologie, comment l’intégration des clients discovery a été effectuée. 

                                 2.2.2.1  Configuration d’un client discovery avec Spring Boot

Voici les différentes étapes que nous avons suivies afin d’intégrer les clients discovery au niveau de chacun des microservices Productpage, Détails et Reviews qui sont tous des projet java Spring à la différence du microservice Ratings qui est en Nodejs.

i) Nous avons d’abord ajouté comme suit la dépendance eureka client au niveau des fichiers pom.xml.

<dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

ii) Puis annoté les classes principales avec @EnableDiscoveryClient

iii) Ensuite créé un Bean RestTemplate avec l’annotation @LoadBallancer. Pour cela il nous a suffit d’ajouter le code suivant au niveau des classes principales:

@LoadBalanced

@Bean

RestTemplate restTemplate() {

      return new RestTemplate();

}

iv) Et enfin configuré comme suit nos fichiers application.yml

server:

  port: 8081    #default port where the service will be started

spring:

  application:

    name: <microserviceName>   #current service name to be used by the eureka server

eureka:         #tells about the Eureka server details and its refresh time

  instance:

    leaseRenewalIntervalInSeconds: 1

    leaseExpirationDurationInSeconds: 2

  client:

    serviceUrl:

      defaultZone: http://localhost:8761/eureka/

    healthcheck:

      enabled: true

    lease:

      duration: 5

 

                                 2.2.2.2  Configuration d’un client discovery avec Nodejs

De la même façon qu’il existe un client discovery pour spring, il existe un client eureka pour Nodejs. Voici comment nous l’avons configuré au niveau du microservice Ratings afin que celle-ci puisse intégrer notre système de découverte de service.

i)  Nous avons ajouté la dépendance avec
npm install eureka-js-client –save

ii)  Puis ajouter le contenu suivant au niveau de notre fichier app.js afin de configurer et démarrer le client eureka.

const Eureka = require('eureka-js-client').Eureka;

const client = new Eureka({

   // application instance information

   instance: {

       app: 'ratings',

       hostName: 'localhost',

       ipAddr: '0.0.0.0',

       port: {

           '$': port,

           '@enabled': 'true',

       },

       vipAddress: 'ratings',

       statusPageUrl: 'http://localhost:' + port + '/actuator/info',

       dataCenterInfo: {

           '@class': 'com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo',

           name: 'MyOwn',

       },

   },

   // eureka serveur information

   eureka: {

       host: 'localhost',

       port: 9090,

       servicePath: '/eureka/apps/',

   },

});

client.start();

iii)  Enfin ajouter le endpoint pour le health check

 app.get('/actuator/info', function (req, res) {
     res.json({});
});

       3. Passerelle API

                   3.1 Principe

Dans une  architecture de microservice bien conçue, un client ne doit pas directement interagir avec les microservices. Toutes les requêtes émanant des clients doivent forcément passer par un intermédiaire que l’on appelle proxy qui se chargera de les router vers des instances de microservices appropriées après consultation du serveur de discovery et load balancing. Dans l’écosystème Spring Cloud, il y’a le proxy Zuul qui couvre l’ensemble de ces fonctionnalités.

                   3.2 Application à l’application Bookinfo

Appliqué à l’application bookinfo, voici ce que nous obtenons.

Voici comment nous avons procédé afin d’intégrer Zuul dans notre application Bookinfo.

Nous avons:

i)  Généré un projet Spring,

ii)  Ajouté les dépendances suivantes au niveau du pom.xml,

<dependency>
       <groupId>org.springframework.cloud</groupId>
       <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
       <groupId>org.springframework.cloud</groupId>
       <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>

iii)  Annoté comme suit la classe principale avec @EnableZuulProxy et @EnableDiscoveryClient pour activer Zuul et le client discovery.

package com.bookinfo;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.SpringBootApplication;

import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

import org.springframework.cloud.netflix.zuul.EnableZuulProxy;


@EnableZuulProxy

@EnableDiscoveryClient

@SpringBootApplication

public class ZuulApplication {

   public static void main(String[] args) {

       SpringApplication.run(ZuulApplication.class, args);

   }

}

iv)  Et enfin, ajouté le contenu suivant au niveau du fichier application.yml

 server:

   port: 8080    #default port where the service will be started

 spring:

   application:

     name: zuul  #current service name to be used by the eureka server

 eureka:        #tells about the Eureka server details and its refresh time

   instance:

     leaseRenewalIntervalInSeconds: 1

     leaseExpirationDurationInSeconds: 2

   client:

     serviceUrl:

       defaultZone: http://localhost:8761/eureka/

     healthcheck:

       enabled: true

     lease:

       duration: 5

 zuul:

   routes:

     productpage:

       path: /productpage/**

       serviceId: productpage

       stripPrefix: false

     details:

       path: /details/**

       serviceId: details

       stripPrefix: false

     reviews:

       path: /reviews/**

       serviceId: reviews

       stripPrefix: false

     ratings:

       path: /ratings/**

       serviceId: ratings

       stripPrefix: false

       4. Résilience des services et tolérance aux pannes

                   4.1 Principe

Dans un système distribué, a fortiori dans une architecture de microservices, il est fréquent de se retrouver avec des requêtes qui impliquent plusieurs microservices qui s’appellent en cascade. Par exemple dans l’appli bookinfo, le microservice ProductPage, appelle le microservice Reviews, qui à son tour appelle le microservice Détails. Dans pareille circonstance, si les différents composants communiquent de façon synchrone comme lorsqu’on utilise le protocole http et qu’on ne définit pas une bonne stratégie de gestion des pannes, l’indisponibilité d’un des microservices de la pile d’appel peut entraîner des dommages sur l’ergonomie, la disponibilité et les performances du système. Par exemple on peut se retrouver avec des délais d’attente très longs consommant ainsi trop de ressources, des erreurs 500 qui sont retournées aux utilisateurs, etc.

Dans ces genres de situations, la technique que l’on utilise souvent pour résoudre le problème est la méthode de disjonction, plus connue sous le nom de circuit breaker. Cette technique consiste à suspendre l’appel d’une méthode malsaine et de la remplacer avec une autre pendant une période de temps. Ainsi lorsque dans une méthode, on fait appel à un autre composant indisponible, au lieu que la méthode tombe avec une erreur 500 suite à une longue période d’attente de réponse sans succès, elle sera gracieusement interrompue en appelant une méthode de rappel à la place après un délai bien défini. Et lorsque la méthode échoue plusieurs fois jusqu’à atteindre un seuil, on ouvre le circuit et ainsi tous les futurs appels seront directement redirigés vers la méthode de rappel. Et derrière, en background on effectue des vérifications de santé de la méthode et une fois que la méthode redevient saine, on referme le circuit.

                   4.2 Résilience des services et tolérance aux pannes dans Spring Cloud

Dans l’écosystème Spring Cloud, cette technique peut être facilement implémenté avec Netflix Hystrix. Et de plus, on peut combiner Hystrix et Turbine pour exposer des métriques et monitorer notre système. Néanmoins il existe des alternatives de Netflix Hystrix à l’image de Resilience4J et de Spring Retry.

                   4.3 Implémentation dans l’application Bookinfo

Tel que mentionné précédemment, nous avons besoin de surveiller la pile d’appel qui existe entre les microservices Productpage, Reviews et Ratings. Pour cela, nous avons procédé comme suit.

i)  Nous avons ajouté la dépendance de Hystrix au niveau des fichiers pom.xml des microservices Productpage et Reviews.

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>

ii)  Puis ajouté l’annotation @EnableCircuitBreaker au niveau des classes principales des deux microservices.

iii)  Au niveau du microservice Productpage, on annote comme suit la méthode de l’endpoint /productpage qui est chargé de contacter les microservices Reviews et de retourner la vue UI.

@HystrixCommand(fallbackMethod = "fallback_front", commandProperties = {
    @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1000")
})
public String fallback_front(HttpServletRequest request, Model model){
    ... 
}

iv)  Puis on donne l’implémentation de la méthode de rappelle avec les mêmes paramètres et le même type de retour que la méthode principal.

public String fallback_front(HttpServletRequest request, Model model) {
      log.debug("fallback_frond called.");
      ...
}

v)  De même au niveau du microservice Reviews, On annote comme suit la méthode de l’endpoint /reviews/{idProduct} chargé de contacter le microservice Ratings et de retourner les reviews d’un produit.

 @HystrixCommand(fallbackMethod = "fallback_reviews", commandProperties = {
    @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1000")

 })
public List<Reviews> getReviews(@PathVariable int idProduct) {
      ...
}

vi)  Et on donne l’implémentation de la méthode de rappelle.

 public List<Reviews> fallback_reviews(@PathVariable int idProduct) {
      log.debug("fallback_reviews called.");
      ...
 }

Conclusion

Comme nous l’avons vu au niveau de cet article Spring Cloud & Netflix OSS offrent une gamme d’outils couvrant en grande partie la complexité qui réside sur la mise en place des applications basées sur des architectures de microservices. Cependant les solutions qu’offrent Spring Cloud & Netflix OSS  impliquent de la configuration à faire et des dépendances à importer au niveau des microservices. Ce qui empiète un peu sur le principe de la responsabilité unique des microservices et de la séparation des rôles. Normalement avec ces deux principes phares qui régissent le style architectural des microservices, la gestion des spécifications techniques de l’architecture comme la logique de gestion de la découverte des services ou du système de gestion des pannes, doit être externe aux microservices. Les développeurs doivent pouvoir développer leurs microservices sans se soucier de tout ce qui est découverte de service, load balancing, gestion des pannes, sécurité etc. Nous verrons avec l’article qui va suivre comment cela est rendu possible avec Istio.

Pour accéder au code source de l’application bookinfo, vous pouvez suivre ce lien: https://github.com/dakario/spring-cloud-bookinfo/