Il y a 4 ans -
Temps de lecture 10 minutes
Patterns de microservices avec Spring Cloud (2/2)
Le premier article de cette série a présenté la gestion de la configuration, avec Spring Config, et la découverte de services, avec Eureka. Dans ce second article, on aborde la passerelle de services avec Zuul et les notions de circuit breakers, de fallback processing et de bulkhead avec Hystrix.
Passerelle de services – Spring Cloud / Netflix Zuul
Introduction du problème
Dans une architecture distribuée comme celle des microservices, vous devrez vous assurer que des problématiques clés tels que la sécurité, la journalisation et le suivi des utilisateurs surviennent lors de plusieurs appels de service. Pour implémenter cette fonctionnalité, vous souhaiterez que ces attributs soient systématiquement appliqués à tous vos services sans que chaque équipe de développement ait besoin de créer ses propres solutions.
Une passerelle de services sert d’intermédiaire entre un service et le client qui l’appelle. Le client ne parle qu’à une seule URL gérée par la passerelle de services. La passerelle de services distingue le chemin provenant de l’appel du client et détermine le service que le client tente d’invoquer.
Diagramme du pattern
Sur le diagramme, vous pouvez voir comment la passerelle de services dirige l’utilisateur vers une instance du microservice cible.
Description du pattern
Dans la mesure où une passerelle de services se situe entre tous les appels du client aux services individuels, elle agit également comme un PEP (Policy Enforcement Point) central pour les appels de service. L’utilisation d’un PEP centralisé signifie que les problèmes de services transversaux peuvent être mis en œuvre à un seul endroit sans que les équipes de développement aient à mettre en oeuvre individuellement des solutions. Les exemples de problèmes transversaux pouvant être implémentés dans une passerelle de services incluent :
- Routage statique : une passerelle de services place tous les appels de service derrière une URL et une route d’API uniques. Cela simplifie le développement car les développeurs n’ont besoin de connaître qu’un seul endpoint final de service pour chacun de leurs services.
- Routage dynamique : une passerelle de services peut inspecter les appels entrants et, en fonction des données de la demande entrante, effectuer un routage intelligent en fonction de l’identité de l’appelant. Par exemple, les clients participant à un programme bêta peuvent avoir tous les appels d’un service acheminés vers un cluster de services spécifique qui exécute une version de code différente de celle utilisée par le reste des utilisateurs.
- Authentification et autorisation : étant donné que tous les appels de service passent par une passerelle de services, cette dernière est un endroit naturel pour vérifier si le client s’est authentifié et est autorisé à appeler le service.
- Collecte et journalisation des métriques : Une passerelle de services peut être utilisée pour collecter des métriques et enregistrer des informations lorsqu’un appel de service passe par la passerelle de services. Vous pouvez également utiliser la passerelle de services pour vous assurer que les informations essentielles sont en place dans la demande de l’utilisateur afin de garantir une journalisation uniforme. Cela ne veut pas dire que vous ne devriez pas continuer à collecter des métriques au sein de vos services individuels, mais uniquement qu’une passerelle de services vous permet de centraliser la collecte de vos métriques de base, telles que le nombre de fois où le service est appelé et le temps de réponse du service.
Configuration de Zuul
[xml]<!– Les dépendances de Zuul –>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zuul</artifactId>
</dependency>[/xml]
[xml]server:
port: 5555
spring:
application:
name: zuul-gateway
eureka:
instance:
preferIpAddress: true
client:
registerWithEureka: true
fetchRegistry: true
serviceUrl:
defaultZone: http://localhost:8761/eureka/[/xml]
[java]@SpringBootApplication
@EnableZuulProxy
public class ZuulApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulApplication.class, args);
}
}[/java]
Nous allons utiliser Postman pour appeler l’endpoint /message du service Message-service (dont le port est 8080) via Zuul (dont le port est 5555).
En cas d’appel à http://localhost:5555/message-service/message, Zuul récupère son adresse IP auprès d’Eureka, redirige l’appel à Message-service (http://localhost:8080/message) et récupère le message ‘Hello from dev properties’ que vous pouvez voir sur la capture d’écran.
Le ‘message-service’ dans l’url est l’ID du service, l’ID se configure dans application.yml sous le nom spring.application.name.
Le ‘/message’ correspond au l’endpoint de Message-service.
Par conséquent, http://localhost:5555/message-service/message est équivalent à http://localhost:8080/message (8080 est le port de Message-service et 5555 est le port de Zuul).
Résumé
-
- Spring Cloud facilite la création d’une passerelle de services.
- La passerelle de services Zuul s’intègre au serveur Eureka de Netflix et peut automatiquement mapper les services enregistrés auprès d’Eureka sur une route Zuul.
- Zuul peut préfixer tous les itinéraires gérés, afin que vous puissiez facilement préfixer vos itinéraires avec quelque chose comme /api.
- En utilisant le serveur Spring Cloud Config, vous pouvez recharger dynamiquement les mappings de route sans avoir à redémarrer le serveur Zuul.
Circuit breaker, Fallback, Bulkhead – Spring Cloud / Netflix Hystrix
Introduction du problème
Les patterns de résilience des clients sont axés sur la protection d’un client de ladite ressource (un autre appel de microservice ou une base de données) en cas de défaillance de la ressource distante. Le but de ces modèles est de permettre au client d’échouer rapidement (Fail-fast), de ne pas consommer de ressources, telles que les connexions de base de données et les pools de threads, et d’empêcher le problème du service distant de se propager «en amont» aux consommateurs du client.
Description des patterns
Circuit breaker
Lorsqu’un service à distance est appelé, le circuit breaker surveillera l’appel. Si les appels prennent trop de temps, le circuit breaker interviendra et met fin à l’appel. De plus, le circuit breaker surveillera tous les appels vers une ressource distante et si trop d’appels échouent, l’implémentation de la coupure de circuit apparaîtra, échouant rapidement et empêchant les futurs appels vers la ressource distante défaillante.
Fallback processing
Avec le fallback pattern, lorsqu’un appel de service distant échoue, plutôt que de générer une exception, le consommateur de service exécute un code alternatif et tente d’exécuter une action par un autre moyen. Cela implique généralement la recherche de données provenant d’une autre source de données ou la mise en file d’attente de la demande de l’utilisateur en vue d’un traitement ultérieur. L’appel de l’utilisateur ne fera pas l’objet d’une exception signalant un problème, mais il se peut qu’il soit averti que sa demande devra être satisfaite à une date ultérieure.
Bulkhead
En utilisant le bulkhead pattern, vous pouvez grouper les appels de ressources distantes dans leurs propres pools de threads et réduire le risque qu’un problème lié à une lenteur sur un appel de ressource distante entraîne la destruction de toute l’application. Les pools de threads agissent comme des cloisons pour votre service. Chaque ressource distante est séparée et assignée au pool de threads. Si un service répond lentement, le pool d’unités d’exécution pour ce type d’appel de service devient saturé et arrête le traitement des demandes. Les appels à d’autres services ne deviennent pas saturés car ils sont attribués à d’autres pools de threads.
Configuration de Hystrix
Hystrix n’est plus en développement actif et est actuellement en mode maintenance. Resilience4j constitue une alternative, mais Spring Cloud ne l’utilise pas encore. Voir ici pour les détails de la configuration.
[xml]<!– Les dépendances de Hystrix –>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>[/xml]
[java]@SpringBootApplication
@EnableCircuitBreaker
public class MessageServiceApplication {
public static void main(String[] args) {
SpringApplication.run(MessageServiceApplication.class, args);
}
}[/java]
[java]@RestController
public class MessageController {
@Value("${custom-message}")
private String message;
@GetMapping("/message")
@HystrixCommand
public String getMessage() {
return message;
}
}[/java]
L’annotation @HystrixCommand active le circuit breaker
Maintenant, avec l’annotation @HystrixCommand en place, le service interrompra un appel vers sa base de données si la requête prend trop de temps. Si les appels de base de données prennent plus de 1 000 millisecondes pour exécuter le wrapping de code Hystrix, votre appel de service lève une exception com.nextflix.hystrix.exception.HystrixRuntimeException que nous pouvons voir sur la capture d’écran.
Maintenant nous allons changer la configuration du MessageController pour configurer la Fallback processing et un pool de threads (Bulkhead).
[java]@RestController
public class MessageController {
@Value("${custom-message}")
private String message;
@GetMapping("/message")
@HystrixCommand(fallbackMethod = "buildFallbackMessage", threadPoolKey = "messageThreadPool")
public String getMessage() {
return message;
}
private String buildFallbackMessage() {
return "Hello, this is a fallback message";
}
}[/java]
L’annotation @HystrixCommand(fallbackMethod = « buildFallbackMessage ») va utiliser la méthode ‘buildFallbackMessage’ si la méthode getMessage prend trop de temps pour s’exécuter.
Sur la capture d’écran nous pouvons voir le message « Hello, this is a fallback message » qui est construit dans la méthode buildFallbackMessage.
Dans une application basée sur une architecture en microservices, il est souvent nécessaire d’appeler plusieurs microservices pour effectuer une tâche particulière. Sans utiliser de bulkhead pattern, le comportement par défaut de ces appels est que ceux-ci sont exécutés à l’aide des mêmes threads que ceux réservés pour la gestion des demandes pour l’ensemble du conteneur Java. En cas de volumes élevés, des problèmes de performances avec un service sur plusieurs peuvent avoir pour résultat que tous les threads du conteneur Java sont saturés et en attente de traitement, tandis que les nouvelles demandes de travail sont sauvegardées. Le conteneur Java finira par planter. Le bulkhead sépare les appels de ressources distantes dans leurs propres pools de threads, de sorte qu’un seul service défectueux puisse être contenu et ne provoque pas le blocage du conteneur.
Hystrix utilise un pool de threads pour déléguer toutes les demandes de services distants. Par défaut, toutes les commandes Hystrix partageront le même pool de threads pour traiter les demandes. Ce pool de threads comportera 10 threads pour traiter les appels de service distant. Voir ici pour plus de détails sur les propriétés de @HystrixCommand. Il est également possible de faire des appels à des services REST, à une base de données, etc.
Lorsque la cloison est activée, le pool de threads est utilisé, par exemple messageThreadPool comme nous pouvons voir sur la capture d’écran.
Résumé
- Lors de la conception d’applications hautement distribuées telles qu’une application basée sur une architecture en microservices, la résilience du client doit être prise en compte.
- Un seul service peu performant peut déclencher un effet en cascade d’épuisement des ressources car les threads du client appelant sont bloqués dans l’attente de la fin du service.
- Les principaux modèles de résilience des clients sont le circuit breaker, le fallback et le bulkhead.
- Le circuit breaker cherche à supprimer les appels système lents et dégradés afin que les appels échouent rapidement et évitent l’épuisement des ressources.
- Le fallback vous permet, en tant que développeur, de définir d’autres chemins de code en cas d’échec d’un appel de service distant.
- Le bulkhead sépare les appels de ressources distantes les uns des autres, isolant les appels d’un service distant dans leur propre pool de threads. Si un ensemble d’appels de service échoue, ses échecs ne doivent pas être autorisés à consommer toutes les ressources du conteneur d’applications.
- Spring Cloud et les bibliothèques Netflix Hystrix fournissent des implémentations pour les patterns circuit breaker, fallback et bulkhead.
Pour aller plus loin, le code source du projet utilisé comme exemple est disponible sur Github : https://github.com/slynko/springmicroservices/tree/zuul_hystrix
Commentaire