Architecture Microservices avec Spring Cloud

Dans ce billet, j’aimerais vous présenter les différentes briques techniques permettant de mettre en œuvre une architecture microservices reposant sur Spring Boot, Spring Cloud, Netflix OSS et Docker. Pour m’y aider, je m’appuierai sur l’application démo Spring Petclinic Microservices que je vous avais déjà brièvement présenté en 2016 et que j’ai récemment migrée vers Spring Cloud Finchley et Spring Boot 2.

Ce fork a été construit à partir de l’application monolithique spring-petclinic-angularjs. Cette dernière a été découpée en plusieurs services, chacun responsable d’un domaine métier de la clinique vétérinaire : les animaux et leurs propriétaires, leurs visites à la clinique et les vétérinaires.

Au final, Spring Petclinic Microservices est construit autour de petits services indépendants (quelques centaines de ligne de code), s’exécutant dans leur propre JVM et communiquant sur HTTP via une API REST.
Ces microservices sont tous écrits en Java. Mais on aurait pu utiliser Kotlin pour développer certains d’entre eux. Le front est quant à lui codé en JavaScript.

Architecture technique

Pour fonctionner, les différents microservices composant l’application Petclinic reposent sur différentes briques techniques matérialisées sur le diagramme d’architecture ci-dessous :

Remarque : pour simplifier le diagramme, les flèches partant de customers-service et visits-service peuvent être transposées aux « xxx-services » homologues.

Vue d’ensemble des briques techniques :

  • En bleu, les 3 microservices back exposent le fonctionnel de l’application au travers d’une API REST. Pour assurer la scalabilité horizontale et la tolérance aux pannes, plusieurs instances d’un même microservice peuvent être démarrées simultanément. Afin d’être autonome, chaque microservice dispose de sa propre base de donnée. Cependant, toutes les instances d’un même microservice partagent la même base de données (qui pourrait être clusterisé). Le front-end JavaScript n’attaque pas directement ces 3 microservices : il passe par le microservice front API Gateway qui s’occupe également de desservir les ressources statiques (code JavaScript, pages HTML et CSS).
  • En vert, on retrouve les microservices d’infrastructures :
    • L’annuaire de service Eureka va permettre aux microservices de s’enregistrer puis de communiquer sans connaître par avance leurs adresses IP.
    • Les microservices (ainsi que les autres serveurs) vont charger leur configuration applicative depuis le Serveur de Config. Les fichiers de configuration sont versionnés sur le dépôt de code GitHub spring-petclinic-microservices-config.
    • Optionnel, les traces des différents appels peuvent être remontées dans le serveur de logs distribués Zipkin.

Enfin, pour superviser et administrer les différents microservices, les Ops peuvent compter sur le serveur Spring Boot Admin.

Spring Boot taillé pour les Microservices

Bien que ne faisant pas partie des microframeworks Java, Spring Boot se prête parfaitement au développement de Microservices par ses nombreux atouts :

  1. Génération d’un microservice packagé sous forme d’un JAR. Ce JAR inclue le serveur web (conteneur de Servlets ou Netty). Le microservice ne nécessite qu’une simple JVM pour s’exécuter.
  2. Diminution de la base de code par un allègement important de la configuration des différents frameworks (dans Petclinic : Spring MVC, Spring Data JPA, EhCache). Spring Boot détecte les frameworks présents dans le classpath et les configure automatiquement. Des starters additionnels viennent étendre cette fonctionnalité.
  3. Monitoring et gestion des microservices en Production au travers d’endpoint REST exposés à l’aide des Actuator.

Chacun des 4 microservices métiers customers, vets, visits et api-gateway est une application au sens Spring Boot. Chacun dispose de son propre module Maven contenant quelques classes Java et fichiers de configuration.

Par exemple, le module spring-petclinic-visits-service comporte 4 classes :

  1. VisitsServiceApplication: la classe main du microservice annotée avec l’annotation @SpringBootApplication ainsi que l’annotation Spring Cloud @EnableDiscoveryClient dont nous verrons l’intérêt par la suite.
  2. Visit : entité JPA représentant une visite et référençant l’animal par son ID (et non son type Java) dans un soucis de découplage des microservices.
  3. VisitRepository: interface Spring Data JPA implémentant le pattern Repository et permettant d’accéder aux visites stockées dans une base relationnelle (HSQLDB ou MySQL).
  4. VisitResource : contrôleur REST exposant une API pour créer une visite et lister les visites d’un animal. L’usage d’annotations Lombok permet d’alléger le code, mais n’a rien d’obligatoire.

Comme vous le constatez, mise à part l’annotation Spring Cloud @EnableDiscoveryClient, le code Java de ce microservice ressemble à une application REST Spring Boot des plus classique.
Une différence significative se trouve au niveau de leur configuration Maven (pom.xml) et de leur configuration applicative (.yml).

Intégration de Spring Cloud

Pour fonctionner de concert dans un environnement distribué, ces microservices vont s’appuyer sur un ensemble d’outils proposés par Spring Cloud : une gestion centralisée da la configuration, la découverte automatisée des autres microservices, la répartition de charge et le routage d’API.

Intégrer ces différentes fonctionnalités Spring Cloud dans une application Spring Boot commence par la déclaration de starters Spring Cloud au niveau du pom.xml. Pour utiliser ces starters sans se soucier de leur version, un prérequis consiste à importer le BOM spring-cloud-dependencies (ex : le pom parent de Spring Petclinic Microservices) :

Dans tous les différents microservices de Petclinic, on a commencé par ajouter le starter spring-cloud-starter-config qui permet d’aller récupérer la configuration applicative auprès du serveur de configuration :

Configuration Spring Cloud

Par convention, la configuration d’une application Spring Boot est centralisée dans le fichier de configuration application.properties (ou application.yml). Via le mécanisme de hiérarchie de contextes, une application Spring Cloud initie un contexte de bootstrap qui charge sa configuration depuis le fichier bootstrap.yml.

Le fichier bootstrap.yml est minimaliste. On y retrouve le nom du microservice et l’URL du serveur de configuration. Exemple issu de vets-service :

Pendant les développements, l’exécution de vets-service (et des autres microservices) demande à ce qu’un serveur de configuration soit démarré en local sur le port 8888. Au démarrage, le microservice ira récupérer le reste de sa configuration (complétant application.properties/yml et bootstrap.yml) auprès du le serveur de configuration.
Veuillez noter ici que l’URL du serveur de configuration doit être connue et ne peut pas être découverte au runtime.

Serveur de configuration

Le serveur de configuration utilisée par Petclinic est un serveur développé par les ingénieurs de Pivotal. Il fait partie intégrante de l’offre Spring Cloud. Ce serveur peut être mutualisé pour l’ensemble des microservices d’un SI.
Toute la configuration applicative est versionnée dans un dépôt Git. Changer la configuration ne nécessite plus de rebuilder les applications ou de les redéployer. Un simple redémarrage est suffisant. Au travers de l’annotation @RefreshScope ou de l’événement EnvironmentChangeEvent, lorsque l’application a été designée pour, il est même possible de changer à chaud la configuration des beans Spring.

Le serveur de config est packagé sous forme d’un JAR Spring Cloud. Pour créer le module spring-petclinic-config-server, un peu de dév a été nécessaire :

  1. Générer une application minimaliste Spring Boot (par exemple via https://start.spring.io)
  2. Inclure une dépendance vers l’artefact spring-cloud-config-server :

  1. Ajouter l’annotation @EnableConfigServer sur la classe main.

Dans son fichier de configuration bootstrap.yml, on retrouve le port 8888 utilisé précédemment, mais surtout l’URL du repo Git hébergeant les fichiers de configuration :

Pendant la phase de développement, pour tester ses changements de configuration, il n’est pas nécessaire de les pousser sur le dépôt Git distant. Le profile Spring « local » permet d’aller chercher les fichiers dans un dépôt Git local au poste de dév en passant ces 2 paramètres à la JVM :

-Dspring.profiles.active=local -DGIT_REPO=/projects/spring-petclinic-microservices-config

Par simplicité, ni l’accès au dépôt, ni l’accès au serveur de configuration n’ont été sécurisés. C’est bien entendu nécessaire en entreprise. Le contenu des fichiers de configuration (comme les mots de passe) peut également être chiffré. Je vous renvoie à la documentation pour consulter toutes les options possibles.

Une fois démarré, le serveur de configuration met à disposition la configuration au travers d’une API REST exposant plusieurs endpoints :

Pour en revenir avec notre exemple, lors de son démarrage depuis un poste de dév (avec le profile « default » de Spring activé), l’application vets-service récupère sa configuration via un GET sur l’URL http://localhost:8888/vets-service-default.yml
Un navigateur, une commande curl ou Postman renverrait la réponse suivante :

Lors du traitement de la requête, le serveur de configuration fusionne le contenu de 2 fichiers du dépôt Git :

  1. application.yml: la configuration transverse à tous les microservices
  2. vets-service.yml: la configuration spécifique à l’application vets-service

Le serveur prend également en considération le ou les profiles Spring actifs côté appelant (mais pas celui déclaré dans le application.properties).

Les logs de démarrage de vets-service confirment ce comportement :

Le démarrage de vets-service échoue quelques millisecondes plus tard. Sa configuration est bien chargée, mais il n’arrive pas à s’enregistrer auprès de l’annuaire de service.

Annuaire de service Eureka

Pour communiquer, les microservices doivent savoir se co-localiser. Dans une architecture microservices hébergée dans le Cloud, nous pouvons difficilement anticiper le nombre d’instances d’un même microservice (dépendant de la charge) ni même où elles seront déployées (et donc sur quelle IP et quel port elles seront accessibles). C’est là où le serveur Eureka rentre en jeu : il va mettre en relation les microservices. Chaque microservice va :

  1. S’enregistrer au démarrage puis donner périodiquement signe de vie (heartbeat toutes les 30 secondes)
  2. Récupérer l’adresse de leurs adhérences à partir d’un identifiant, en l’occurrence le nom de l’application déclaré via la propriété application.name (ex : vets-service) du boostrap.yml (chargé avant le application.properties)

Eureka fait partie des projets OSS de Netflix supportés par Spring Cloud.
A l’instar de ce qui a été fait pour le serveur de configuration, il est nécessaire de mettre en œuvre un serveur Eureka (module spring-petclinic-discovery-server). Cela se fait très simplement :

  1. Partir d’une application vierge Spring Boot
  2. Déclarer l’artefact spring-cloud-starter-netflix-eureka-server
  3. Ajouter l’annotation @EnableEurekaServer sur la classe main
  4. Déclarer l’artefact spring-cloud-starter-config et configurer l’adresse du serveur de configuration (idem pour tous les microservices)

Configurer le serveur Eureka par l’intermédiaire du fichier discovery-server.xml (le nom de fichier correspond au nom de l’application spring.application.name) :

Nous indiquons au serveur d’opérer dans la zone géographique par défaut et de ne pas s’enregistrer auprès d’autres instances d’Eureka. En production, redonder les instances d’Eureka renforcerait la tolérance aux pannes et lui éviterait d’être un Single Point of Failure (SPOF).
A ce stade, le serveur Eureka peut être démarré.

Chaque microservice doit ensuite intégrer un client Eureka chargé de dialoguer avec le serveur Eureka :

  1. Commencer par déclarer le starter spring-cloud-starter-netflix-eureka-client
  2. Sur la classe main du microservice, ajouter l’annotation @EnableDiscoveryClient entraperçu sur la classe VisitsServiceApplication.

L’annotation @EnableDiscoveryClient active l’implémentation Eureka de l’interface Spring Cloud DiscoveryClient chargée d’enregistrer le microservice et de localiser ses pairs. A noter que Spring Cloud supporte d’autres annuaires de service : Consul de Hashicorp et Apache Zookeeper.

Dans les logs de démarrage du microservice vets-service, la phase d’enregistrement Eureka intervient en dernier :

Le serveur Eureka vient avec une petite interface de supervision accessible en local à l’adresse http://localhost:8761/. Le statut des différents microservices et le nombre d’instances y sont visibles :

A ce stade, nous avons vu comment faire pour enregistrer un microservice auprès de l’annuaire Eureka, mais pas encore comment fait appel à un microservice depuis un autre microservice.

Appeler un microservice

Dans Petclinic, le microservice front API Gateway centralise les appels aux API REST des 3 microservices back. On peut l’assimiler à un Backend for Frontend. Il permet de gérer les problématiques de CORS tout en assurant l’équilibrage de charge.

Par exemple, lorsque l’utilisateur souhaite consulter l’écran de consultation d’un propriétaire, le code JavaScript du navigateur fait appel à l’URL : http://localhost:8080/api/gateway/owners/1

Le contrôleur REST Spring MVC ApiGatewayController a la responsabilité de traiter cette requête HTTP. Il délègue son traitement au service CustomersServiceClient qui fait à son tour un appel REST au microservice customers-service :

Le host de l’URL a une particularité : ce n’est ni un nom de domaine, ni un nom de serveur, ni même une adresse IP. Ici, on utilise l’ID du microservice, celui utilisé pour s’enregistrer auprès du serveur Eureka.
L’autre particularité concerne le nom donné à l’instance du bean implémentant l’interface Spring MVC RestTemplate : loadBalancedRestTemplate.

Dans la configuration Spring du microservice ApiGatewayApplication, le bean RestTemplate est annoté avec l’annotation Spring Cloud @LoadBalanced :

De manière transparente pour le développeur, l’annotation @LoadBalanced configure le RestTemplate pour utiliser un répartiteur de charge (load-balancer) côté client. L’implémentation par défaut du LoadBalancerClient est Netflix Ribbon. La classe de configuration LoadBalancerAutoConfiguration se charge de positionner l’intercepteur LoadBalancerInterceptor sur le RestTemplate.

Cet intercepteur va faire appel au service Eureka pour localiser les différentes instances de customers-service disponibles. Il va ensuite appliquer l’algorithme round-robin pour appeler successivement chaque instance et ainsi répartir la charge. D’autres algorithmes sont bien entendu disponible dans Ribbon (ils implémentent tous l’interface IRule).

Remarque : par programmation, grâce à l’annotation @EnableDiscoveryClient, il est possible d’interroger le service Eureka pour récupérer manuellement la liste des instances disponibles et d’exploiter le tuple host/port de SeviceInstance :

Router les appels

Le microservice front API Gateway centralise les appels du navigateur. Bien qu’il puisse jouer un rôle d’agrégateur, la plupart des appels sont directement destinés aux microservices back : on a du 1 pour 1. Développer des contrôleurs REST chargés d’aiguiller la requête aux back n’a que peu d’intérêt.

Pour éviter tout boilerplate code, Spring Cloud Netflix propose d’utiliser le proxy Zuul. Activable via l’annotation @EnableZuulProxy, Zull va permettre de forwarder les requêtes reçues par l’API Gateway vers les microservices back. Il fait office de reverse proxy (comme le ferait un Apache ou un Nginx).

Pour bénéficier de Zuul, il est nécessaire d’ajouter au module spring-petclinic-api-gateway le starter spring-cloud-starter-netflix-zuul :

La configuration des routes est fait dans le fichier api-gateway.yml dont voici un extrait :

La requête http://localhost:8080/api/vet/vets est automatiquement routée par Zuul vers http://vets-service/vets. En interne, le proxy utilise Eureka pour localiser les instances de vets-setvice.

Console d’administration

Le serveur Eureka propose une interface permettant de consulter la liste des microservices enregistrés et disponibles. C’est bien, mais insuffisant pour administrer toutes les applications d’un SI.
Développé par codecentric AG, Spring Boot Admin est un projet communautaire permettant de monitorer et d’administrer des applications Spring Boot déployées en production. Développée en Vue.js, l’IHM de Spring Boot Admin fait appel aux Actuators de Spring Boot pour connaître l’état des applications. Supportant Spring Cloud, il s’interface directement à Eureka pour récupérer la liste des différentes applications Spring Boot.

Parmi les fonctionnalités proposées par Spring Boot Admin, on peut lister :

  • La consultation du statut de chaque application
  • La récupération de différentes métriques : JVM, mémoire, Micrometer.io, pool de connections, cache …
  • La consultation des informations sur le build : date de création, sha1 du commit Git, GAV Maven …
  • La gestion des logs : téléchargement des fichiers de logs, modification à chaud du niveau de logs
  • La gestion des heapdump : consultation et téléchargement
  • En cas de changement d’état d’une application, des notifications par mail, Slack, Hipchat, PageDuty …

Petclinic intègre Spring Boot Admin dans le module spring-petclinic-admin-server.

Parti d’une application Spring Cloud rudimentaire, 2 dépendances Maven ont été ajoutées :

L’annotation @EnableAdminServer a été ajoutée sur la classe main :

Côté client, une dépendance vers Jolokia a été ajoutée dans les pom.xml. Jolokia permet d’exposer sur HTTP les beans JMX.

Spring Boot Admin s’appuie sur les différents Actuators proposés par Spring Boot : heapdump, threadump, loggers, scheduledtasks … Depuis Spring Boot 2, pour des raisons de sécurité, seuls les Actuators health et info sont exposés par défaut. Il est nécessaire d’activer explicitement les autres actuators. Dans le fichier de configuration application.yml, a été ajoutée la ligne suivante :  management.endpoints.web.exposure.include: "*"

Une fois démarré, Spring Boot Admin est accessible sur l’URL http://localhost:9090/ :

En sélectionnant une des 2 instances de customers-service, on accède aux différents outils d’administration, dont par exemple ici le suivi de la consommation de ressources (mémoire, thread, CPU) :

Spring Boot Admin n’est pas limité à l’affichage d’informations dans de joli graphes. Un administrateur peut aller changer le niveau de log d’un logger Logback. Le changement de niveau est immédiat. Aucun redémarrage n’est nécessaire.

Traces distribuées

Afin de pouvoir tracer et debugger les appels HTTP entre nos microservices, un mécanisme de traces distribuées a été mis en œuvre à l’aide du client Spring Cloud Sleuth et du serveur Zipkin. L’interface graphique du serveur Zipkin permet de les consulter les piles d’appel et les adhérences entre microservices.

En pratique, le serveur Zipkin se déploie dans une image Docker. Sa personnalisation n’est plus supportée par l’équipe de Dév. Dans Petclinic, par simplicité, son intégration a été réalisée dans le module spring-petclinic-tracing-server sous forme d’une application Spring Boot configurée avec l’annotation dépréciée @EnableZipkinServer.

L’interface est disponible sur l’URL : http://localhost:9411/zipkin/

Sur une période de temps, Zipkin sait générer un diagramme de dépendances entre microservices :

Containerisation

L’architecture de Petclinic repose sur un ensemble de 8 microservices, tous basés sur Spring Boot. Livrables sous forme d’un simple JAR, leur déploiement ne nécessite qu’un simple JRE Java 8.
Pour déployer Petclinic chez un fournisseur Cloud proposant une offre de type Container as a Service (CaaS), les microservices doivent être packagés sous forme d’images Docker. Petclinic vient avec un exemple de packaging Docker pour un déploiement local (sur le poste de dév) à l’aide de Docker Compose.

Dans le POM parent, le profile Maven buildDocker permet de construire les images Docker à l’aide du plugin Maven de Spotify :  ./mvnw clean install -PbuildDocker

Une fois les images construites, on peut toutes les démarrer en une seule commande :  docker-compose up

Les images Docker reposent toutes sur le même Dockerfile (à noter que l’ENTRYPOINT est redéfini dans le docker-compose.yml):

Introduits depuis Java 8 update 131, les flags UnlockExperimentalVMOptions et UseCGroupMemoryLimitForHeap ordonnent à la JVM d’utiliser ¼ de la mémoire allouée à l’OS (si Xmx non spécifié). Ils fonctionnent de pair avec le paramètre mem_limit spécifié dans le docker-compose.yml pour chaque image Docker.

Une autre spécificité du Dockerfile concerne l’utilisation du script wait-for-it.sh. En effet, l’ordre de démarrage des microservices est important : le serveur de Configuration doit être démarré en premier, suivi de l’annuaire de Services et du serveur Zipkin. Les autres microservices peuvent ensuite être démarrés simultanément. Le script wait-for-it.sh permet de se mettre en attente de la disponibilité d’une application web. Dans le docker-compose.yml, l’entrypoint du container discovery-server attend que le config-server soit démarré avant de démarrer sa JVM :
entrypoint: ["./wait-for-it.sh","discovery-server:8761","--timeout=60","--","java", ]

Les applications Spring Boot démarrent avec le profile Spring docker. Dans le fichier de configuration Spring Cloud de chaque microservice, ce profile écrase des valeurs par défaut utilisées pour un déploiement hors container.
Si l’on prend comme exemple un extrait du fichier de configuration customers-service.yml :

On remarque que :

  • Le port HTTP est hard-codé et fixé à 8081. En effet, le Docker Compose ne démarre qu’une seule instance de customers-service. Son numéro de port n’a pas besoin d’être alloué dynamiquement par Spring Boot.
  • L’URL du serveur Eureka et du serveur Zipkin référencent les paramètres container_name et ports du docker-compose.yml.

Conclusion

Ce long billet nous aura permis de voir comment mettre en place une architecture microservices à l’aide de Spring Boot, Spring Cloud et Netflix OSS. Comme support, nous nous serons appuyés sur l’application démo « Spring Petclinic Microservices » créée par Maciej Szarliński. De nombreuses améliorations sont d’ores et déjà prévues :

  1. Récupération des métriques à l’aide de Micrometer et Prometheus
  2. Support des JDK 10 et 11
  3. Utilisation du client Feign à la place de RestTemplate

Ce projet est communautaire : vos contributions sont les bienvenues.
Enfin, pour celles et ceux que cela intéresse, sachez que d’autres fork de Spring Petclinic existent. Je vous renvoie vers cet ancien billet pour une présentation générale.

Ressources :

Laisser un commentaire

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

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.