docker, technique

Microservices avec SpringCloud, Netflix et Docker

Avec autant de buzzwords dans un titre je sens les regards rigolards et les réflexions du genre «Les microservices, tout le monde va en revenir».

On verra. En attendant, je continue l’exploration de cette architecture et des moyens de la déployer.

Le code élaboré pour cet article est disponible sur github. Les différentes versions du projet sont accessibles via des branches distinctes, de V1 à V4.

Architecture microservices cible

Je ne reviendrai pas sur la description de cette architecture qui a été évoquée à maintes reprises (voir http://blog.xebia.fr/2015/03/02/microservices-les-concepts/).

Ce qui m’intéresse dans l’architecture des microservices, c’est la possibilité de scinder des problématiques distinctes en portions de code autonomes.

Prenons l’exemple d’une application de CRM. L’application principale gère des clients, des contrats mais également des fichiers, des appels téléphoniques, des mails, …

Application à base de microservices

Outre les domaines métiers distincts, les problématiques techniques sont très variées. Les appels téléphoniques sont par exemples gérés via un fournisseur de VoIp qui peut fournir des APIs (https://api.ovh.com/console/#/telephony). L’interface avec ce système externe n’a rien à faire dans l’application principale de CRM. Ses besoins sont de connaître les logs d’appels avec les clients. La délégation à un microservice permet de masquer complètement les choix techniques sous-jascents et le cas échéant de les modifier (passer de OVH à AirCall par exemple).

La gestion de fichiers, dans le cloud ou en local, la gestion des mails sont également des cibles parfaites de services indépendants.

Dans mon cas, il ne s’agit pas de découper les services pour faire de la répartition de charge. Mes services sont tous sur la même machine (solution la moins couteuse).

Les besoins sont:

  • la disponibilité globale de la solution,
  • la possibilité de faire évoluer des composants sans arrêt du service global,
  • une isolation de l’environnement des microservices (ceux-ci doivent pouvoir être totalement indépendant)

Nous allons voir comment les framework de netflix, spring et docker répondent à ces besoins.

Une première version avec RestTemplate

Il s’agit d’une version naïve de notre architecture. Une application appelle un service pour déléguer une partie de son traitement (accéder à la branche V1).

Les ports utilisés par le service et l’application sont définis dans les fichiers de configuration.

Depuis l’application l’appel au service ressemble à ceci :

@RestController
class MyServiceRestController {

  @RequestMapping("/")
  String home() {
    String who = "world";
    String content = "Calling greeter with :: " + who;
    RestTemplate restTemplate = new RestTemplate();
    ResponseEntity entity = restTemplate.getForEntity(
        "http://localhost:8081/greeter/greet/" + who,
        String.class);
    return content + "<br>" + entity.getBody();
  }
}

Même si la localisation du service est externalisée dans le fichier de configuration de l’application, celle-ci doit connaître le service.

En cas de non disponibilité du service, le système est inutilisable.

CircuitBreaker pattern avec Netflix Hystrix

Le pattern de développement CircuitBreaker permet d’éviter les erreurs en cascade lors d’appels entre composants.

Dans une architectures à base de microservices, un service non disponible ou en erreur rendrait le système entier non disponible.

L’intégration de Hystrix par spring permet via une simple annotation de remplacer l’appel en erreur par une méthode bouchon. Le système entier est dégradé mais toujours disponible (accéder à la branche V2).

@RestController
class MyServiceRestController {

  @Autowired GreeterClient greeterClient;

  @RequestMapping("/")  String home() {
    String who = "world";
    String content = "Calling greeter with :: " + who;
    return content + "<br>" + greeterClient.greet(who);
  }
}

@Component
class GreeterClient {

  @HystrixCommand(fallbackMethod = "defaultGreet")
  public String greet(String who) {
  return new RestTemplate()
     .getForEntity(
          "http://localhost:8081/greeter/greet/" + who,
           String.class).getBody();
  }

  public String defaultGreet(String who){
    return who;
  }

}

Cette version améliore le comportement général de l’application en autorisant une mise à jour d’un service sans que tout s’arrête.

Attention: Pour que cela fonctionne, il faut que l’appel à la méthode annotée avec @HystrixCommand se fasse depuis un autre composant. En effet, pour mettre en place le mécanisme, spring positionne un proxy devant la méthode. Si l’appel avait lieu du @RestController vers une de ses propres méthodes, le proxy serait contourné.

Simplification de l’appel Rest avec Netflix Feign

Le code peut être légèrement amélioré avec la mise en place de Feign. Cette librairie encapsule les appels HTTP et facilite la lecture en proposant l’utilisation d’interfaces et de classes métiers pour réaliser les appels REST (accéder à la branche V2.1).

@FeignClient(url = "http://localhost:8081")
interface GreeterClient {

  @RequestMapping(method = RequestMethod.GET,
                   value = "/greeter/greet/{who}")
  String greet(@PathVariable("who") String who);
}

Suppression de la configuration manuelle avec le registre de service Netflix Eureka

Il est temps de supprimer cette affreuse url nécessaire à l’appel du service.

C’est le composant Netflix Eureka qui va nous offrir les fonctionnalités nécessaires à cette découverte à la volée des services.

Sa mise en oeuvre est à nouveau facilitée par spring. Une classe minimaliste et un fichier de configuration suffisent pour disposer d’un serveur:

@EnableEurekaServer
@SpringBootApplication
  public class EurekaServer {
    public static void main(String[] args) {
      SpringApplication.run(EurekaServer.class, args);
    }
}
server:
  port: 8761

eureka:
  instance:
    hostname: localhost
  client:
    registerWithEureka: false
    fetchRegistry: false

C’est sur ce serveur que vont se déclarer nos services. Les clients y récupèreront les URLs pour leurs appels distants.

On complète à cet effet le fichier application.ml de notre service:

spring:
  application:
    name: my-service

eurekaServer: localhost

eureka:
  client:
   serviceUrl:
     defaultZone: http://${eurekaServer}:8761/eureka/

L’interface d’eureka affiche les services qui se sont enregistrés.

Eureka server

 

Dès lors, l’application cliente peut simplifier la référence vers le service:

@FeignClient("my-service")
interface GreeterClient {

  @RequestMapping(method = RequestMethod.GET,
                   value = "/greeter/greet/{who}")
    String greet(@PathVariable("who") String who);
  }

Dockerisation et changement des services à la volée

Dernière étape de notre projet test:

  • isoler les services dans des containers docker,
  • permettre le remplacement d’un service de manière transparente.

Comme tous nos projets sont en java et basés sur maven, nous utilisons notre image docker maven standard.

La dockerisation du serveur eureka est simple. Au lancement du container, nous exposons le port du serveur pour accéder à la page de statut.

docker build -t eureka-mvn .

docker rm -f eureka-compil
docker run \
  --name eureka-compil \
  -v /.m2:/root/.m2 \
  eureka-mvn install

docker rm -f eureka
docker run -d \
  --name eureka \
  --volumes-from eureka-compil \
  --workdir="/project/eureka" \
  -p 8761:8761 \
  eureka-mvn spring-boot:run

Les containers du service et de l’application sont ensuite créés avec un lien vers le serveur eureka.

docker build -t microservices-mvn .

docker rm -f my-service-compil
docker run \
  --name my-service-compil \
  -v /.m2:/root/.m2 \
  microservices-mvn install

docker run -d \
  --volumes-from my-service-compil \
  --link eureka:eureka \
  --workdir="/project/my-service" \
  microservices-mvn spring-boot:run

Le client eureka, utilisé pour l’enregistrement, est paramétré pour pointer sur le container eureka. On modifie également la stratégie d’enregistrement pour utiliser un enregistrement par adresse IP plutôt que par nom.

spring:
  application:
    name: my-service

server:
  port: 8081

eurekaServer: eureka

eureka:
  client:
    serviceUrl:
      defaultZone: http://${eurekaServer}:8761/eureka/
  instance:
    preferIpAddress: true

En effet, docker, au démarrage du daemon, va créer un sous-réseau pour l’ensemble de ces containers. Les containers se voient attribuer au démarrage des adresses IP propres qu’ils peuvent utiliser pour communiquer entre eux. Habituellement, on lie les containers entre eux au démarrage via l’option --link. Dans le cas de microservices, l’intérêt est de ne pas avoir de lien fort et de passer par un mécanisme de découverte pour communiquer avec les services désirés. Il est donc hors de question de lier les containers entre eux.

Avec ce paramétrage, chaque service, démarré dans un container avec sa propre adresse IP, s’enregistre au travers de cette adresse IP. Il est alors possible de faire une modification sur un service et de déployer la nouvelle version en parallèle de la précédente. Eureka affiche alors les 2 versions.

Service déployé par docker en 2 versions

Le mécanisme interne de communication fourni par netflix (load balancing côté client par ribbons) masque complètement la gestion des appels vers ce service.

On peut ainsi faire évoluer les services sans interruption de notre solution.

Conclusions

La mise en oeuvre d’une architecture microservice est, bien entendu, plus complexe que la gestion d’une base de code monolithique. L’écosystème de projets open-source proposé par netflix est cohérent et élégant. Les différents composants, même s’ils ont été conçus pour des architectures différentes des projets que nous gérons au jour le jour, sont utilisables dans des contextes autres que le cloud sur amazon.

Spring Cloud apporte une intégration intéressante des ces composants en limitant le code nécessaire à leur utilisation.

2 Comments

  1. Dans le cas d’une solution en Java les services OSGi simplifient l’usage par rapport à la solution proposée ici.
    Le runtime OSGi (Equinox, Karaf, Félix…) charge des modules JAR (appelés bundles) à la demande et gère les problématiques de dépendances de services, de remplacement de bundles.

    C’est la base de certains serveurs J2EE tels que Websphere ou JONas, et un complément d’autres comme JBoss ou Tomcat (Virgo).

    Sources :
    http://paulonjava.blogspot.fr/2014/04/micro-services-vs-osgi-services.html
    http://wiki.osgi.org/wiki/Declarative_Services
    http://karaf.apache.org

  2. Intéressant. Je ne connaissais pas cette approche.

    Dans mon cas, j’ai des services qui disposent de leur propre base. L’isolation via Docker me semble alors de mise.

Répondre à gaetan Annuler la réponse.

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

Vous pouvez utiliser ces balises et attributs HTML : <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>