Architecture de Spring microservice
Eureka
Eureka is a service registry that allows microservices to register themselves so they can be discovered by others.
- Service Registration and Discovery
- Instance management in dynamic cloud environments
- Supports high availability through failover mechanisms
- Rest-based client-server interactions
Exemple
Étapes pour configurer un projet Spring Cloud
Voici un guide étape par étape pour configurer une architecture de microservices avec Spring Cloud.
Étape 1: Créer un projet Spring Boot
- Accédez à https://start.spring.io/
- Sélectionnez les dépendances Spring Web et Spring Cloud Dependencies
- Générez le projet et importez-le dans votre IDE préféré (IntelliJ, Eclipse, etc.)
Voir Example 2
Eureka Server, OpenFeign & Spring API Gateway
Composants Clés de Notre Architecture
Netflix Eureka
Service Discovery - Permet aux services de se trouver automatiquement dans l'architecture. Il joue le rôle de registre central où chaque service s'enregistre et peut découvrir les autres services.
Fonctionnement : Lorsqu'un service démarre, il s'enregistre auprès d'Eureka avec ses coordonnées (IP, port). Les autres services peuvent alors interroger Eureka pour trouver les services dont ils ont besoin.
OpenFeign
Client HTTP Déclaratif - Simplifie la communication entre microservices en permettant d'écrire des clients REST avec des annotations simples.
Avantage : OpenFeign génère automatiquement les implémentations des interfaces clientes.
Spring API Gateway
Point d'Entrée Unique - Agit comme un reverse proxy intelligent qui route les requêtes vers les bons services, applique des filtres, et offre des fonctionnalités de sécurité et de monitoring.
Rôle : Centralise la gestion des requêtes clientes, offrant une couche d'abstraction entre les clients et les microservices internes.
Phase 1 : Création du Serveur Eureka
Étape 1 : Initialisation du Projet Spring Boot
Créez un nouveau projet Spring Boot via Spring Initializr avec les dépendances suivantes :
Cette dépendance est nécessaire pour créer une application web capable d'exposer des endpoints REST. Elle inclut Tomcat comme serveur embarqué et Spring MVC pour le développement web.
Pourquoi cette dépendance ? Le serveur Eureka doit exposer des endpoints HTTP pour que les autres services puissent s'enregistrer et récupérer les informations de découverte. Spring Boot Web fournit l'infrastructure nécessaire pour cela.
Cette dépendance contient l'implémentation complète du serveur Eureka, y compris tous les contrôleurs REST, les services de découverte, et l'interface web d'administration.
Contenu de la dépendance : Elle inclut le moteur de découverte Eureka, l'interface web de monitoring, les mécanismes de heartbeat, et la logique de gestion du registre de services.
Architecture du Serveur Eureka
Le serveur Eureka agit comme un registre centralisé qui maintient une liste de tous les services disponibles dans le système. Il utilise un mécanisme de heartbeat pour surveiller la disponibilité des services.
Étape 2 : Configuration de la Classe Principale
Ouvrez la classe principale générée et ajoutez l'annotation @EnableEurekaServer :
package com.example.eurekaserver;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
details
- @SpringBootApplication : Cette annotation est une combinaison de trois annotations :
@Configuration: Marque la classe comme source de définitions de beans@EnableAutoConfiguration: Active la configuration automatique de Spring Boot@ComponentScan: Active le scanning automatique des composants Spring
- @EnableEurekaServer : Cette annotation active toutes les fonctionnalités du serveur Eureka :
- Configure automatiquement les contrôleurs REST pour l'API Eureka
- Initialise le service de découverte et de registre
- Active l'interface web d'administration
- Configure les mécanismes de heartbeat et de nettoyage
Ce que fait @EnableEurekaServer en détail
Lorsque vous ajoutez cette annotation, Spring Boot configure automatiquement :
- Un serveur HTTP pour recevoir les requêtes des clients Eureka
- Un registre en mémoire pour stocker les informations des services
- Des endpoints REST pour l'enregistrement, la découverte et le monitoring
- Un système de heartbeat pour surveiller la disponibilité des services
- Un mécanisme de nettoyage pour supprimer les services déconnectés
- L'interface web d'administration accessible via le navigateur
Étape 3 : Configuration du Fichier application.properties
Modifiez le fichier src/main/resources/application.properties :
# Port du serveur Eureka
server.port=8761
# Ne pas s'enregistrer auprès d'un autre serveur Eureka
eureka.client.register-with-eureka=false
# Ne pas récupérer le registre d'autres serveurs
eureka.client.fetch-registry=false
# URL par défaut pour le service Eureka
eureka.client.service-url.defaultZone=http://localhost:8761/eureka/
# Durées de rafraîchissement (en millisecondes)
eureka.server.eviction-interval-timer-in-ms=30000
eureka.instance.lease-renewal-interval-in-seconds=30
eureka.instance.lease-expiration-duration-in-seconds=90
Explication Détaillée des Propriétés
- server.port=8761 :
Port standard pour le serveur Eureka. Ce port est devenu une convention dans l'écosystème Spring Cloud. Tous les services clients Eureka chercheront par défaut ce port.
- eureka.client.register-with-eureka=false :
Le serveur Eureka ne doit pas s'enregistrer lui-même auprès d'un autre serveur Eureka. C'est le registre principal, pas un client.
- eureka.client.fetch-registry=false :
Le serveur Eureka ne doit pas récupérer le registre d'autres serveurs. Il maintient son propre registre.
- eureka.client.service-url.defaultZone=http://localhost:8761/eureka/ :
URL du serveur Eureka pour l'enregistrement. Même si le serveur ne s'enregistre pas, cette URL est utilisée comme référence par les clients.
- eureka.server.eviction-interval-timer-in-ms=30000 :
Intervalle de nettoyage des services déconnectés. Toutes les 30 secondes, Eureka vérifie quels services n'ont pas envoyé de heartbeat récemment et les supprime du registre.
- eureka.instance.lease-renewal-interval-in-seconds=30 :
Intervalle d'envoi du heartbeat par les instances clientes. Chaque service enregistré envoie un signal de vie toutes les 30 secondes pour indiquer qu'il est toujours actif.
- eureka.instance.lease-expiration-duration-in-seconds=90 :
Durée avant de considérer une instance comme déconnectée. Si Eureka ne reçoit pas de heartbeat pendant 90 secondes, il considère que le service est indisponible.
Mécanisme de Heartbeat
Le mécanisme de heartbeat est crucial pour la résilience de l'architecture :
- Enregistrement initial : Lorsqu'un service démarre, il s'enregistre auprès d'Eureka
- Heartbeat périodique : Le service envoie un signal de vie toutes les 30 secondes
- Surveillance : Eureka surveille les heartbeats reçus
- Détection de panne : Si aucun heartbeat pendant 90 secondes, le service est marqué comme DOWN
- Nettoyage : Toutes les 30 secondes, les services DOWN sont supprimés du registre
Étape 4 : Construction et Lancement du Serveur
Lancez le serveur Eureka :
Vérifiez que le serveur fonctionne en accédant à :
http://localhost:8761
Interface Web d'Eureka
L'interface web d'Eureka affiche plusieurs informations cruciales :
- System Status : État général du serveur (UP/DOWN)
- Instances currently registered with Eureka : Liste des services enregistrés
- General Info : Informations techniques sur le serveur
- Instance Info : Détails sur l'instance du serveur Eureka lui-même
Validation
Vous devriez voir l'interface web d'Eureka avec "Instances currently registered with Eureka" vide (aucun service encore enregistré). Le statut du système devrait être "UP".
Phase 2 : Création des Microservices
Étape 1 : Création du Service Utilisateur
Créez un nouveau projet Spring Boot avec les dépendances suivantes :
Pour créer des endpoints REST et exposer les fonctionnalités du service utilisateur via HTTP.
Fonctionnalité : Cette dépendance permet de créer des contrôleurs REST, de gérer les requêtes HTTP, et de sérialiser/désérialiser les objets JSON automatiquement.
Pour permettre au service de s'enregistrer automatiquement auprès du serveur Eureka et de découvrir d'autres services.
Mécanisme : Lorsque le service démarre, il contacte automatiquement Eureka (via l'URL configurée) pour s'enregistrer et recevoir les informations de découverte.
Pour le monitoring et la santé du service, fournissant des endpoints de gestion et de surveillance.
Endpoints utiles : /actuator/health, /actuator/info, /actuator/metrics pour surveiller l'état du service.
Configurez la classe principale :
package com.example.userservice;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
@SpringBootApplication
@EnableEurekaClient
public class UserServiceApplication {
public static void main(String[] args) {
SpringApplication.run(UserServiceApplication.class, args);
}
}
Différence entre @EnableEurekaClient et @EnableDiscoveryClient
Bien que ces deux annotations fonctionnent de manière similaire :
- @EnableEurekaClient : Spécifique à Netflix Eureka, optimisé pour cette implémentation
- @EnableDiscoveryClient : Générique, fonctionne avec n'importe quel service discovery (Eureka, Consul, Zookeeper)
- Dans notre cas : Les deux fonctionneraient, mais @EnableEurekaClient est plus explicite
Étape 2 : Configuration du Service Utilisateur
Configurez le fichier src/main/resources/application.properties :
# Port du service utilisateur
server.port=8080
# Nom du service (doit être unique dans Eureka)
spring.application.name=user-service
# URL du serveur Eureka
eureka.client.service-url.defaultZone=http://localhost:8761/eureka/
# Configuration de l'instance
eureka.instance.prefer-ip-address=true
eureka.instance.instance-id=${spring.application.name}:${server.port}
# Actuator endpoints
management.endpoints.web.exposure.include=health,info
Explication Détaillée des Propriétés
- server.port=8080 :
Port d'écoute du service utilisateur. Chaque service doit avoir un port unique pour éviter les conflits.
- spring.application.name=user-service :
Nom unique du service dans Eureka. Ce nom est utilisé par les autres services pour le découvrir et communiquer avec lui.
- eureka.client.service-url.defaultZone=http://localhost:8761/eureka/ :
URL du serveur Eureka où ce service s'enregistrera. Doit correspondre à l'URL configurée dans le serveur Eureka.
- eureka.instance.prefer-ip-address=true :
Préfère utiliser l'adresse IP plutôt que le nom d'hôte pour l'enregistrement. Utile dans les environnements cloud où les noms d'hôte peuvent varier.
- eureka.instance.instance-id=${spring.application.name}:${server.port} :
Identifiant unique de cette instance du service. Permet à Eureka de distinguer plusieurs instances du même service.
- management.endpoints.web.exposure.include=health,info :
Expose les endpoints Actuator de santé et d'information. Ces endpoints sont utilisés pour le monitoring.
Processus d'Enregistrement dans Eureka
Lorsque le service utilisateur démarre :
- Il lit sa configuration (nom, port, URL Eureka)
- Il contacte le serveur Eureka à l'URL spécifiée
- Il s'enregistre en fournissant ses coordonnées (IP, port, nom)
- Il commence à envoyer des heartbeats toutes les 30 secondes
- Il peut maintenant être découvert par d'autres services
Étape 3 : Création d'un Contrôleur Simple
Créez une classe contrôleur de base :
package com.example.userservice.controller;
import org.springframework.web.bind.annotation.*;
import java.util.Arrays;
import java.util.List;
@RestController
@RequestMapping("/users")
public class UserController {
@GetMapping
public List<String> getAllUsers() {
return Arrays.asList("Jean Dupont", "Marie Martin", "Pierre Durand");
}
@GetMapping("/{id}")
public String getUserById(@PathVariable Long id) {
return "Utilisateur avec ID: " + id;
}
@PostMapping
public String createUser(@RequestBody String user) {
return "Utilisateur créé: " + user;
}
}
details du Contrôleur
- @RestController :
Combinaison de @Controller et @ResponseBody. Indique que cette classe est un contrôleur REST dont les méthodes retournent directement des données (pas de vues).
- @RequestMapping("/users") :
Définit le chemin de base pour tous les endpoints de ce contrôleur. Tous les mappings seront préfixés par "/users".
- @GetMapping :
Raccourci pour @RequestMapping(method = RequestMethod.GET). Mappe les requêtes HTTP GET vers cette méthode.
- @PathVariable :
Extrait une variable de l'URL. Par exemple, dans "/users/123", @PathVariable("id") récupère "123".
- @PostMapping :
Raccourci pour @RequestMapping(method = RequestMethod.POST). Mappe les requêtes HTTP POST.
- @RequestBody :
Indique que le paramètre doit être extrait du corps de la requête HTTP et désérialisé automatiquement.
Cycle de Traitement d'une Requête
- Requête HTTP arrive sur le port 8080
- Spring Boot route la requête vers le bon contrôleur
- Le contrôleur traite la requête et retourne une réponse
- Spring Boot sérialise automatiquement la réponse en JSON
- La réponse est envoyée au client
Étape 4 : Création du Service de Commandes
Créez un deuxième service avec les mêmes dépendances de base :
Classe principale :
package com.example.orderservice;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
@SpringBootApplication
@EnableEurekaClient
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
}
Configuration src/main/resources/application.properties :
# Port du service commande
server.port=8081
# Nom du service
spring.application.name=order-service
# URL du serveur Eureka
eureka.client.service-url.defaultZone=http://localhost:8761/eureka/
# Configuration de l'instance
eureka.instance.prefer-ip-address=true
eureka.instance.instance-id=${spring.application.name}:${server.port}
# Actuator endpoints
management.endpoints.web.exposure.include=health,info
Différences Clés avec le Service Utilisateur
- Port différent : 8081 au lieu de 8080 pour éviter les conflits
- Nom de service différent : "order-service" au lieu de "user-service"
- Même URL Eureka : Les deux services s'enregistrent auprès du même serveur
Contrôleur de commandes :
package com.example.orderservice.controller;
import org.springframework.web.bind.annotation.*;
import java.util.Arrays;
import java.util.List;
@RestController
@RequestMapping("/orders")
public class OrderController {
@GetMapping
public List<String> getAllOrders() {
return Arrays.asList("Commande #123", "Commande #456", "Commande #789");
}
@GetMapping("/user/{userId}")
public List<String> getOrdersByUser(@PathVariable Long userId) {
return Arrays.asList("Commande #123 pour utilisateur " + userId);
}
@PostMapping
public String createOrder(@RequestBody String order) {
return "Commande créée: " + order;
}
}
Architecture des Services
À ce stade, vous avez une architecture de base :
- Eureka Server (8761) : Registre central de services
- User Service (8080) : Gère les utilisateurs, enregistré dans Eureka
- Order Service (8081) : Gère les commandes, enregistré dans Eureka
Étape 5 : Lancement des Services
Lancez le service utilisateur :
Lancez le service commande :
Séquence de Démarrage
- Eureka Server démarre en premier (port 8761)
- User Service démarre et s'enregistre auprès d'Eureka
- Order Service démarre et s'enregistre auprès d'Eureka
- Eureka met à jour son registre avec les deux nouveaux services
- Les services commencent à envoyer des heartbeats
Vérifiez l'enregistrement dans Eureka :
http://localhost:8761
Ce que vous devriez voir dans Eureka
Dans la section "Instances currently registered with Eureka" :
- USER-SERVICE : 1 instance UP sur localhost:8080
- ORDER-SERVICE : 1 instance UP sur localhost:8081
Chaque service affiche ses coordonnées et son statut de santé.
Validation
Vous devriez maintenant voir "user-service" et "order-service" dans la section "Instances currently registered with Eureka", tous deux avec le statut "UP".
Phase 3 : Configuration d'OpenFeign
Étape 1 : Ajout de la Dépendance OpenFeign
Ajoutez la dépendance OpenFeign à votre service utilisateur (pom.xml) :
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
Ce que contient spring-cloud-starter-openfeign
Cette dépendance inclut :
- OpenFeign Core : Le framework de client HTTP déclaratif
- Spring Integration : L'intégration avec Spring Boot et Spring Cloud
- Hystrix : Pour le circuit breaker (résilience)
- Ribbon : Pour le load balancing client-side
- Jackson : Pour la sérialisation/désérialisation JSON
Activez OpenFeign dans la classe principale :
package com.example.userservice;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
public class UserServiceApplication {
public static void main(String[] args) {
SpringApplication.run(UserServiceApplication.class, args);
}
}
Fonction de @EnableFeignClients
Cette annotation active le scanning automatique des interfaces Feign :
- Scanning de classes : Recherche les interfaces annotées avec @FeignClient
- Génération de proxies : Crée automatiquement les implémentations des clients
- Intégration Eureka : Utilise Eureka pour la découverte des services
- Configuration par défaut : Applique les configurations globales de Feign
Étape 2 : Création du Client Feign
Créez une interface Feign pour communiquer avec le service de commandes :
package com.example.userservice.client;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import java.util.List;
@FeignClient(name = "order-service")
public interface OrderServiceClient {
@GetMapping("/orders/user/{userId}")
List<String> getOrdersByUserId(@PathVariable("userId") Long userId);
@GetMapping("/orders")
List<String> getAllOrders();
}
Comment OpenFeign fonctionne
Lorsque Spring Boot démarre :
- @EnableFeignClients scanne les interfaces @FeignClient
- Proxy generation : Crée une implémentation dynamique de l'interface
- Service discovery : Utilise Eureka pour trouver l'URL de "order-service"
- HTTP client : Génère des requêtes HTTP basées sur les annotations
- Serialization : Convertit automatiquement les objets en JSON et vice-versa
Explication des Annotations Feign
- @FeignClient(name = "order-service") :
Indique que ce client communique avec le service nommé "order-service" dans Eureka. OpenFeign utilisera Eureka pour trouver l'URL réelle.
- @GetMapping("/orders/user/{userId}") :
Définit une requête HTTP GET vers le chemin spécifié. La variable {userId} sera remplacée par la valeur du paramètre.
- @PathVariable("userId") :
Mappe le paramètre de méthode à la variable dans l'URL. Le nom doit correspondre à la variable dans le chemin.
Étape 3 : Utilisation du Client Feign
Injectez et utilisez le client Feign dans votre contrôleur :
package com.example.userservice.controller;
import com.example.userservice.client.OrderServiceClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.Arrays;
import java.util.List;
@RestController
@RequestMapping("/users")
public class UserController {
@Autowired
private OrderServiceClient orderServiceClient;
@GetMapping
public List<String> getAllUsers() {
return Arrays.asList("Jean Dupont", "Marie Martin", "Pierre Durand");
}
@GetMapping("/{id}")
public String getUserById(@PathVariable Long id) {
return "Utilisateur avec ID: " + id;
}
@GetMapping("/{userId}/orders")
public List<String> getUserOrders(@PathVariable Long userId) {
// Appel au service de commandes via Feign
return orderServiceClient.getOrdersByUserId(userId);
}
@GetMapping("/all-orders")
public List<String> getAllOrders() {
// Appel au service de commandes pour toutes les commandes
return orderServiceClient.getAllOrders();
}
@PostMapping
public String createUser(@RequestBody String user) {
return "Utilisateur créé: " + user;
}
}
Mécanisme d'Injection et d'Appel
- @Autowired : Spring injecte automatiquement l'implémentation générée du client Feign
- Appel de méthode : orderServiceClient.getOrdersByUserId(userId) est appelé
- Génération de requête : OpenFeign génère une requête GET vers /orders/user/{userId}
- Résolution d'URL : Eureka fournit l'URL réelle de order-service
- Exécution HTTP : La requête est envoyée via le client HTTP interne
- Désérialisation : La réponse JSON est convertie en List<String>
- Retour : Le résultat est retourné au contrôleur
Avantages de l'Approche Feign
- Code propre : Pas de boilerplate pour les appels HTTP
- Type-safe : Compilation vérifie les signatures de méthodes
- Intégré : Fonctionne nativement avec Eureka et Spring
- Extensible : Supporte les filtres, intercepteurs, et configurations
Étape 4 : Configuration Avancée d'OpenFeign
Ajoutez des configurations dans src/main/resources/application.properties :
# Configuration des timeouts pour OpenFeign
feign.client.config.default.connect-timeout=5000
feign.client.config.default.read-timeout=10000
# Activation de la compression
feign.compression.request.enabled=true
feign.compression.response.enabled=true
# Configuration du retry
ribbon.MaxAutoRetries=1
ribbon.MaxAutoRetriesNextServer=1
ribbon.ConnectTimeout=3000
ribbon.ReadTimeout=10000
Explication Détaillée des Propriétés
- feign.client.config.default.connect-timeout=5000 :
Timeout de 5 secondes pour établir la connexion HTTP. Si le serveur ne répond pas dans ce délai, l'appel échoue.
- feign.client.config.default.read-timeout=10000 :
Timeout de 10 secondes pour lire la réponse. Si le serveur met plus de temps à répondre, l'appel échoue.
- feign.compression.request.enabled=true :
Active la compression gzip des requêtes sortantes pour réduire la taille des données envoyées.
- feign.compression.response.enabled=true :
Active la décompression gzip des réponses entrantes pour réduire la taille des données reçues.
- ribbon.MaxAutoRetries=1 :
Nombre de tentatives sur la même instance de serveur en cas d'échec (1 tentative supplémentaire).
- ribbon.MaxAutoRetriesNextServer=1 :
Nombre de tentatives sur d'autres instances du même service (1 tentative sur un autre serveur).
- ribbon.ConnectTimeout=3000 :
Timeout de connexion spécifique à Ribbon (load balancer client).
- ribbon.ReadTimeout=10000 :
Timeout de lecture spécifique à Ribbon.
Stratégie de Résilience
La configuration ci-dessus implémente une stratégie de résilience :
- Timeouts raisonnables : Empêchent les appels bloquants indéfiniment
- Compression : Réduit l'utilisation de bande passante
- Retry mécanisme : Tente automatiquement de récupérer des échecs temporaires
- Load balancing : Distribue les requêtes entre plusieurs instances
Étape 5 : Test de la Communication Inter-Services
Testez l'appel inter-services :
GET http://localhost:8080/users/1/orders
Cheminement de la Requête
- Requête client : GET http://localhost:8080/users/1/orders
- Routage Spring : UserController.getUserOrders(1) est appelé
- Appel Feign : orderServiceClient.getOrdersByUserId(1)
- Résolution Eureka : Trouve l'URL de order-service (localhost:8081)
- Requête HTTP : GET http://localhost:8081/orders/user/1
- Traitement OrderService : OrderController.getOrdersByUser(1)
- Réponse : ["Commande #123 pour utilisateur 1"]
- Retour client : La réponse est renvoyée au client initial
Vous devriez recevoir la réponse du service de commandes :
["Commande #123 pour utilisateur 1"]
Validation
La communication entre microservices fonctionne ! Le service utilisateur appelle le service de commandes via OpenFeign et Eureka. Cela démontre :
- La découverte de services via Eureka
- L'appel HTTP déclaratif avec OpenFeign
- La sérialisation/désérialisation automatique
- La résilience avec les timeouts et retries
Phase 4 : Spring API Gateway - Configuration de Base
Étape 1 : Création du Projet API Gateway
Créez un nouveau projet Spring Boot avec les dépendances suivantes :
Framework web réactif basé sur Project Reactor. Nécessaire pour Spring Cloud Gateway qui est construit sur WebFlux.
Pourquoi WebFlux et pas Web MVC ? Spring Cloud Gateway utilise un modèle non-bloquant pour gérer un grand nombre de connexions concurrentes efficacement. WebFlux permet cela grâce à la programmation réactive.
Fournit toutes les fonctionnalités de l'API Gateway : routing, filtrage, load balancing, sécurité, etc.
Fonctionnalités incluses : Routeur intelligent, filtres pré/post-processing, intégration Eureka, rate limiting, circuit breaker, etc.
Pour que l'API Gateway puisse s'enregistrer dans Eureka et découvrir les services backend.
Rôle dans l'API Gateway : Permet à l'API Gateway de découvrir dynamiquement les services backend et de les router correctement.
Pour le monitoring et la santé de l'API Gateway, avec des endpoints spécifiques à Gateway.
Endpoints Gateway : /actuator/gateway/routes pour lister les routes, /actuator/gateway/refresh pour recharger la configuration.
Rôle de l'API Gateway
L'API Gateway agit comme un point d'entrée unique pour tous les clients :
- Routing : Dirige les requêtes vers les bons services backend
- Filtrage : Applique des transformations aux requêtes/réponses
- Sécurité : Centralise l'authentification et l'autorisation
- Monitoring : Fournit des métriques et logs centralisés
- Résilience : Gère les timeouts, retries, et circuit breaking
Étape 2 : Configuration de la Classe Principale
Configurez la classe principale de l'API Gateway :
package com.example.apigateway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
@SpringBootApplication
@EnableEurekaClient
public class ApiGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(ApiGatewayApplication.class, args);
}
}
Configuration Minimale Requise
Pour une API Gateway basique :
- @SpringBootApplication : Configuration Spring Boot standard
- @EnableEurekaClient : Permet à l'API Gateway de s'enregistrer dans Eureka et de découvrir les services
- Pas besoin de @EnableFeignClients : L'API Gateway utilise son propre mécanisme de routage
Étape 3 : Configuration de Base de l'API Gateway
Configurez le fichier src/main/resources/application.properties :
# Port de l'API Gateway
server.port=8082
# Nom de l'application
spring.application.name=api-gateway
# URL du serveur Eureka
eureka.client.service-url.defaultZone=http://localhost:8761/eureka/
# Configuration de l'instance
eureka.instance.prefer-ip-address=true
eureka.instance.instance-id=${spring.application.name}:${server.port}
# Configuration des routes
spring.cloud.gateway.routes[0].id=user-service
spring.cloud.gateway.routes[0].uri=lb://user-service
spring.cloud.gateway.routes[0].predicates[0]=Path=/api/users/**
spring.cloud.gateway.routes[0].filters[0]=StripPrefix=2
spring.cloud.gateway.routes[1].id=order-service
spring.cloud.gateway.routes[1].uri=lb://order-service
spring.cloud.gateway.routes[1].predicates[0]=Path=/api/orders/**
spring.cloud.gateway.routes[1].filters[0]=StripPrefix=2
# Activer la découverte des routes via Eureka
spring.cloud.gateway.discovery.locator.enabled=true
spring.cloud.gateway.discovery.locator.lower-case-service-id=true
# Actuator endpoints
management.endpoints.web.exposure.include=gateway,health,info
# Logging
logging.level.org.springframework.cloud.gateway=DEBUG
Explication Détaillée des Propriétés Clés
- server.port=8082 :
Port d'écoute de l'API Gateway. Les clients accéderont aux services via ce port unique.
- spring.application.name=api-gateway :
Nom de l'application dans Eureka. Permet aux autres services de découvrir l'API Gateway si nécessaire.
- spring.cloud.gateway.routes[0].id=user-service :
Identifiant unique de la route. Utilisé pour le logging et le monitoring.
- spring.cloud.gateway.routes[0].uri=lb://user-service :
URI de destination avec load balancing. "lb://" indique d'utiliser le load balancer pour trouver user-service via Eureka.
- spring.cloud.gateway.routes[0].predicates[0]=Path=/api/users/** :
Prédicat qui détermine quelles requêtes correspondent à cette route. Toutes les requêtes commençant par /api/users/ seront routées vers user-service.
- spring.cloud.gateway.routes[0].filters[0]=StripPrefix=2 :
Filtre qui retire les 2 premiers segments du chemin. /api/users/123 devient /users/123 pour le service backend.
- spring.cloud.gateway.discovery.locator.enabled=true :
Active la découverte automatique des routes via Eureka. Crée automatiquement des routes pour tous les services enregistrés.
- spring.cloud.gateway.discovery.locator.lower-case-service-id=true :
Utilise des IDs de service en minuscules dans les routes automatiques.
Fonctionnement du Routage
Exemple concret avec la route user-service :
- Requête entrante : GET http://localhost:8082/api/users/123
- Matching prédicat : Path=/api/users/** correspond
- Application filtre : StripPrefix=2 transforme /api/users/123 en /users/123
- Résolution service : lb://user-service utilise Eureka pour trouver l'URL réelle
- Requête sortante : GET http://localhost:8080/users/123 (vers user-service)
- Réponse : La réponse est renvoyée au client via l'API Gateway
Étape 4 : Lancement de l'API Gateway
Processus de Démarrage de l'API Gateway
- Chargement configuration : Lecture des routes depuis application.properties
- Connexion Eureka : Enregistrement auprès du serveur Eureka
- Initialisation routes : Création des routeurs et filtres
- Démarrage serveur : Lancement du serveur WebFlux sur le port 8082
- Prêt à router : L'API Gateway peut maintenant traiter les requêtes
Vérifiez l'enregistrement dans Eureka :
http://localhost:8761
État Final de l'Architecture
Dans Eureka, vous devriez maintenant voir :
- EUREKA-SERVER : UP sur port 8761
- USER-SERVICE : UP sur port 8080
- ORDER-SERVICE : UP sur port 8081
- API-GATEWAY : UP sur port 8082
Validation
Vous devriez maintenant voir "api-gateway" dans la liste des services enregistrés avec le statut "UP".
Phase 5 : Fonctionnalités Avancées de l'API Gateway
Fonctionnalité 1 : Filtrage des Requêtes
Filtres Prédéfinis
Les filtres permettent de modifier les requêtes et réponses qui passent par l'API Gateway. Ils existent en deux types :
- Pre-filters : Exécutés avant de router la requête vers le service backend
- Post-filters : Exécutés après avoir reçu la réponse du service backend
Configuration de filtres dans src/main/resources/application.properties :
# Configuration des routes avec filtres avancés
spring.cloud.gateway.routes[0].id=user-service
spring.cloud.gateway.routes[0].uri=lb://user-service
spring.cloud.gateway.routes[0].predicates[0]=Path=/api/users/**
spring.cloud.gateway.routes[0].filters[0]=StripPrefix=2
spring.cloud.gateway.routes[0].filters[1]=AddRequestHeader=X-Request-Source, api-gateway
spring.cloud.gateway.routes[0].filters[2]=AddResponseHeader=X-Response-Source, api-gateway
spring.cloud.gateway.routes[1].id=order-service
spring.cloud.gateway.routes[1].uri=lb://order-service
spring.cloud.gateway.routes[1].predicates[0]=Path=/api/orders/**
spring.cloud.gateway.routes[1].filters[0]=StripPrefix=2
spring.cloud.gateway.routes[1].filters[1]=name=Retry
spring.cloud.gateway.routes[1].filters[1].args.retries=3
spring.cloud.gateway.routes[1].filters[1].args.statuses=BAD_GATEWAY
Explication des Filtres
- AddRequestHeader=X-Request-Source, api-gateway :
Ajoute un header HTTP à toutes les requêtes sortantes vers le service user-service. Utile pour le tracing et l'identification de la source.
- AddResponseHeader=X-Response-Source, api-gateway :
Ajoute un header HTTP à toutes les réponses entrantes du service user-service. Permet d'identifier que la réponse est passée par l'API Gateway.
- name=Retry :
Filtre de retry qui tente automatiquement de renvoyer la requête en cas d'erreurs spécifiques.
- args.retries=3 :
Nombre maximum de tentatives de retry (3 tentatives supplémentaires).
- args.statuses=BAD_GATEWAY :
Codes HTTP qui déclenchent le retry. Ici, seulement 502 BAD_GATEWAY.
Cycle de Vie d'une Requête avec Filtres
- Requête entrante : GET /api/users/123
- Pre-filters :
- StripPrefix=2 : Transforme /api/users/123 en /users/123
- AddRequestHeader : Ajoute X-Request-Source: api-gateway
- Routage : Envoi vers user-service sur http://localhost:8080/users/123
- Post-filters :
- AddResponseHeader : Ajoute X-Response-Source: api-gateway à la réponse
- Réponse : Retour au client avec headers ajoutés
Fonctionnalité 2 : Rate Limiting
Limitation du Trafic
Le rate limiting protège vos services backend contre les surcharges en limitant le nombre de requêtes qu'un client peut faire dans une période donnée. C'est essentiel pour la protection DDoS et l'équité des ressources.
Configuration du rate limiting dans src/main/resources/application.properties :
# Configuration du rate limiting
spring.cloud.gateway.routes[2].id=user-service-rate-limited
spring.cloud.gateway.routes[2].uri=lb://user-service
spring.cloud.gateway.routes[2].predicates[0]=Path=/api/users/rate-limited/**
spring.cloud.gateway.routes[2].filters[0]=StripPrefix=2
spring.cloud.gateway.routes[2].filters[1]=name=RequestRateLimiter
spring.cloud.gateway.routes[2].filters[1].args.redis-rate-limiter.replenishRate=10
spring.cloud.gateway.routes[2].filters[1].args.redis-rate-limiter.burstCapacity=20
spring.cloud.gateway.routes[2].filters[1].args.key-resolver=#{@userKeyResolver}
# Configuration Redis (nécessaire pour le rate limiting)
spring.redis.host=localhost
spring.redis.port=6379
Explication du Rate Limiting
- redis-rate-limiter.replenishRate=10 :
Nombre de requêtes autorisées par seconde (10 requêtes/seconde).
- redis-rate-limiter.burstCapacity=20 :
Capacité maximale de requêtes en rafale (20 requêtes en même temps).
- key-resolver=#{@userKeyResolver} :
Référence à un bean Spring qui détermine comment identifier les clients (par IP, token, etc.).
Création du KeyResolver (classe Java) :
package com.example.apigateway.config;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Configuration
public class RateLimiterConfig {
@Bean
KeyResolver userKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress());
}
}
Algorithme du Rate Limiter
Spring Cloud Gateway utilise l'algorithme "token bucket" :
- Bucket : Chaque client a un bucket de tokens
- Replenish : 10 tokens sont ajoutés par seconde
- Max capacity : Maximum 20 tokens dans le bucket
- Consumption : Chaque requête consomme 1 token
- Rejet : Si pas de token disponible, requête rejetée (429 Too Many Requests)
Fonctionnalité 3 : Sécurité avec JWT
Authentification et Autorisation
La validation JWT centralisée dans l'API Gateway permet de sécuriser tous les services backend sans avoir à implémenter la sécurité dans chaque service individuellement.
Création d'un filtre personnalisé :
package com.example.apigateway.filter;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component
public class JwtAuthenticationFilter extends AbstractGatewayFilterFactory<JwtAuthenticationFilter.Config> {
public JwtAuthenticationFilter() {
super(Config.class);
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
// Vérifier la présence du header Authorization
if (!exchange.getRequest().getHeaders().containsKey("Authorization")) {
return onError(exchange, "Authorization header is missing", HttpStatus.UNAUTHORIZED);
}
// Extraire le token
String authHeader = exchange.getRequest().getHeaders().getFirst(" headset
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
return onError(exchange, "Invalid authorization header", HttpStatus.UNAUTHORIZED);
}
// Valider le token JWT (implémentation simplifiée)
String token = authHeader.substring(7);
if (!validateToken(token)) {
return onError(exchange, "Invalid token", HttpStatus.UNAUTHORIZED);
}
return chain.filter(exchange);
};
}
private Mono<Void> onError(ServerWebExchange exchange, String err, HttpStatus httpStatus) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(httpStatus);
return response.setComplete();
}
private boolean validateToken(String token) {
// Implémentation de validation JWT (simplifiée)
// Dans un vrai projet, utilisez une bibliothèque comme jjwt
return token != null && token.length() > 10;
}
public static class Config {
// Configuration du filtre
}
}
Fonctionnement du Filtre JWT
- Interception : Le filtre intercepte chaque requête qui correspond à sa route
- Vérification header : Vérifie la présence du header Authorization
- Extraction token : Récupère le token JWT du header Bearer
- Validation : Valide la signature, l'expiration, et les claims du token
- Autorisation : Si valide, laisse passer la requête; sinon, renvoie 401
Utilisation du filtre dans la configuration src/main/resources/application.properties :
# Route sécurisée avec JWT
spring.cloud.gateway.routes[3].id=secured-user-service
spring.cloud.gateway.routes[3].uri=lb://user-service
spring.cloud.gateway.routes[3].predicates[0]=Path=/api/secure/users/**
spring.cloud.gateway.routes[3].filters[0]=StripPrefix=3
spring.cloud.gateway.routes[3].filters[1]=JwtAuthenticationFilter
Avantages de la Sécurité Centralisée
- Consistance : Même politique de sécurité pour tous les services
- Simplicité : Les services backend ne gèrent pas l'authentification
- Performance : Validation une seule fois au niveau de la gateway
- Maintenance : Changements de sécurité centralisés
Fonctionnalité 4 : Load Balancing et Failover
Répartition de Charge
Le load balancing distribue le trafic entre plusieurs instances d'un même service, améliorant la performance et la disponibilité. Le failover gère automatiquement les pannes d'instances.
Configuration avancée du load balancing dans src/main/resources/application.properties :
# Configuration du retry et load balancing
spring.cloud.gateway.routes[4].id=user-service-load-balanced
spring.cloud.gateway.routes[4].uri=lb://user-service
spring.cloud.gateway.routes[4].predicates[0]=Path=/api/users/load-balanced/**
spring.cloud.gateway.routes[4].filters[0]=StripPrefix=3
spring.cloud.gateway.routes[4].filters[1]=name=Retry
spring.cloud.gateway.routes[4].filters[1].args.retries=3
spring.cloud.gateway.routes[4].filters[1].args.statuses=SERVICE_UNAVAILABLE,GATEWAY_TIMEOUT
spring.cloud.gateway.routes[4].filters[1].args.methods=GET,POST
spring.cloud.gateway.routes[4].filters[1].args.series=SERVER_ERROR
# Configuration globale du load balancer
spring.cloud.loadbalancer.retry.enabled=true
# Configuration Ribbon (si utilisé)
ribbon.ConnectTimeout=3000
ribbon.ReadTimeout=10000
ribbon.MaxAutoRetries=1
ribbon.MaxAutoRetriesNextServer=2
Explication des Paramètres de Load Balancing
- uri=lb://user-service :
Indique d'utiliser le load balancer pour trouver une instance de user-service. "lb://" est le préfixe pour load balancing.
- retries=3 :
Nombre de tentatives de retry en cas d'échec (3 tentatives supplémentaires).
- statuses=SERVICE_UNAVAILABLE,GATEWAY_TIMEOUT :
Codes HTTP qui déclenchent le retry. Ici, 503 et 504.
- methods=GET,POST :
Méthodes HTTP concernées par le retry (seulement GET et POST).
- series=SERVER_ERROR :
Séries de codes HTTP qui déclenchent le retry (5xx).
- ribbon.ConnectTimeout=3000 :
Timeout de connexion de 3 secondes pour chaque tentative.
- ribbon.ReadTimeout=10000 :
Timeout de lecture de 10 secondes pour chaque tentative.
- ribbon.MaxAutoRetries=1 :
1 tentative supplémentaire sur la même instance.
- ribbon.MaxAutoRetriesNextServer=2 :
2 tentatives sur d'autres instances du même service.
Stratégie de Retry et Load Balancing
Exemple de scénario avec 1 retry et 2 retries next server :
- Tentative 1 : Appel à instance 1 de user-service
- Si succès → réponse au client
- Si échec (503) → passe à tentative 2
- Tentative 2 (MaxAutoRetries=1) : Nouvel appel à instance 1
- Si succès → réponse au client
- Si échec → passe à tentative 3
- Tentative 3 (MaxAutoRetriesNextServer=2) : Appel à instance 2
- Si succès → réponse au client
- Si échec → passe à tentative 4
- Tentative 4 : Appel à instance 3
- Si succès → réponse au client
- Si échec → erreur 503 au client
Phase 6 : Tests et Validation
Étape 1 : Test des Routes de Base
Testez les routes via l'API Gateway :
GET http://localhost:8082/api/users
Ce qui se passe :
- L'API Gateway reçoit la requête sur /api/users
- Le prédicat Path=/api/users/** correspond
- Le filtre StripPrefix=2 transforme en /users
- Le load balancer trouve user-service sur localhost:8080
- La requête est envoyée à http://localhost:8080/users
- La réponse est renvoyée via l'API Gateway
GET http://localhost:8082/api/users/1
GET http://localhost:8082/api/orders
GET http://localhost:8082/api/orders/user/1
Architecture de Routage
Chaque requête suit ce chemin :
Client → API Gateway (8082) → Service Backend (8080/8081) → API Gateway → Client
Étape 2 : Test des Fonctionnalités Avancées
Testez le rate limiting :
GET http://localhost:8082/api/users/rate-limited/test
Comportement attendu :
- Les 10 premières requêtes par seconde réussissent
- Les requêtes supplémentaires reçoivent un 429 Too Many Requests
- Après 1 seconde, le quota est réinitialisé
Testez l'authentification JWT :
GET http://localhost:8082/api/secure/users
Headers: Authorization: Bearer votre-token-jwt
Résultats possibles :
- Avec token valide : 200 OK + réponse du service
- Sans header Authorization : 401 Unauthorized
- Avec token invalide : 401 Unauthorized
Testez le retry et load balancing :
GET http://localhost:8082/api/users/load-balanced/test
Scénario de test :
- Arrêtez une instance de user-service
- Faites des requêtes : le retry doit fonctionner
- L'API Gateway doit router vers l'instance disponible
Étape 3 : Surveillance de l'API Gateway
Vérifiez la santé de l'API Gateway :
GET http://localhost:8082/actuator/health
Réponse attendue :
{
"status": "UP",
"components": {
"diskSpace": {"status": "UP"},
"ping": {"status": "UP"},
"redis": {"status": "UP"}
}
}
Listez les routes configurées :
GET http://localhost:8082/actuator/gateway/routes
Information fournie :
- Toutes les routes configurées
- Prédicats et filtres appliqués
- URI de destination
- Statistiques d'utilisation
Vérifiez les métriques :
GET http://localhost:8082/actuator/metrics
Métriques importantes :
- gateway.requests : Nombre total de requêtes
- http.server.requests : Temps de réponse
- jvm.memory.used : Utilisation mémoire
Étape 4 : Architecture Finale
Vue d'Ensemble de l'Architecture
Votre architecture microservices complète comprend maintenant :
- Eureka Server (8761) :
Registre de services central qui maintient la liste de tous les services disponibles et surveille leur santé via heartbeat.
- API Gateway (8082) :
Point d'entrée unique qui route les requêtes vers les bons services, applique des filtres, gère la sécurité, et fournit du monitoring.
- User Service (8080) :
Service de gestion des utilisateurs, enregistré dans Eureka, accessible via l'API Gateway.
- Order Service (8081) :
Service de gestion des commandes, enregistré dans Eureka, accessible via l'API Gateway.
Flux de Communication Complet
- Démarrage : Tous les services s'enregistrent dans Eureka
- Requête client : Client envoie requête à l'API Gateway
- Routage : API Gateway utilise Eureka pour trouver le service cible
- Filtrage : API Gateway applique les filtres configurés
- Forward : Requête envoyée au service backend
- Processing : Service backend traite la requête
- Response : Réponse renvoyée via l'API Gateway au client
Sécurité de l'Architecture
- Communication interne : Services communiquent via réseau interne
- Exposition publique : Seule l'API Gateway est exposée publiquement
- Authentification centralisée : Gérée au niveau de la gateway
- Autorisation distribuée : Services backend vérifient les permissions
Félicitations !
Vous avez maintenant une architecture microservices complète avec Eureka, OpenFeign, et Spring API Gateway. Cette base solide vous permet de construire des applications évolutives et résilientes avec :
- Découverte automatique des services via Eureka
- Communication déclarative via OpenFeign
- Point d'entrée unifié via API Gateway
- Sécurité centralisée avec JWT
- Résilience avec retry et load balancing
- Monitoring avec Actuator et logs
Hystrix et Load Balancing
Introduction à Hystrix et Load Balancing
Qu'est-ce que Hystrix ?
Hystrix est une bibliothèque de tolérance aux pannes conçue pour contrôler l'interaction entre les services distribués en ajoutant des couches de latence et de tolérance aux pannes. Il permet aux systèmes de continuer à fonctionner même lorsqu'un ou plusieurs services rencontrent des problèmes.
Problèmes que Hystrix Résout
- Défaillances en cascade : Une panne dans un service ne doit pas faire tomber tout le système
- Latence élevée : Les requêtes lentes ne doivent pas bloquer tout le système
- Ressources épuisées : Les threads bloqués ne doivent pas consommer toutes les ressources
- Manque de visibilité : Difficile de comprendre l'état du système distribué
Architecture avec Hystrix
Architecture typique dans un système microservices avec Hystrix :
Client → Service A → Hystrix Command → Service B
↑
Circuit Breaker
Si Service B est lent ou indisponible :
Client → Service A → Hystrix Command → (Fallback) → Réponse Alternative
Qu'est-ce que le Load Balancing ?
Le Load Balancing (équilibrage de charge) est une technique utilisée pour distribuer le trafic réseau entre plusieurs serveurs ou instances de services. Dans une architecture microservices, il joue un rôle crucial pour optimiser l'utilisation des ressources et maximiser la disponibilité.
Importance dans les Microservices
Dans une architecture microservices, chaque service peut avoir plusieurs instances (scaling horizontal). Le load balancing permet de :
- Distribuer les requêtes entre toutes les instances disponibles
- Gérer automatiquement les pannes d'instances
- Adapter dynamiquement à la charge variable
- Améliorer la performance globale du système
Architecture avec Load Balancing
Architecture typique avec load balancing :
Client → Load Balancer → Service Instance 1
→ Service Instance 2
→ Service Instance 3
Intégration Hystrix et Load Balancing
Hystrix et Load Balancing travaillent ensemble pour créer un système résilient et performant :
Synergie des Deux Technologies
- Load Balancing : Distribue les requêtes entre les instances saines
- Hystrix : Protège contre les pannes et latences de chaque instance
- Ensemble : Créent un système hautement disponible et performant
Architecture Complète
Client → API Gateway → Hystrix Command → Load Balancer → Service Instance 1
→ Service Instance 2
→ Service Instance 3
Avantages de cette architecture :
- Résilience : Protection contre les pannes individuelles
- Performance : Distribution optimale de la charge
- Visibilité : Monitoring des performances et pannes
- Scalabilité : Facile d'ajouter de nouvelles instances
Concepts Fondamentaux de Hystrix
Command Pattern de Hystrix
HystrixCommand vs HystrixObservableCommand
Hystrix fournit deux types de commandes principales :
HystrixCommand
- Mode bloquant : Retourne une seule valeur (ou lève une exception)
- Méthode principale : run() - exécutée dans un thread séparé
- Méthode de fallback : getFallback() - appelée en cas de défaillance
- Usage : Pour les appels réseau simples, bases de données
HystrixObservableCommand
- Mode non-bloquant : Retourne un Observable (flux de valeurs)
- Méthode principale : construct() - retourne un Observable
- Méthode de fallback : resumeWithFallback() - retourne un Observable de fallback
- Usage : Pour les flux de données, WebSockets, streaming
Exemple de HystrixCommand basique :
import com.netflix.hystrix.HystrixCommand;
import com.netflix.hystrix.HystrixCommandGroupKey;
public class UserCommand extends HystrixCommand<User> {
private final Long userId;
private final UserServiceClient userServiceClient;
public UserCommand(Long userId, UserServiceClient userServiceClient) {
super(HystrixCommandGroupKey.Factory.asKey("UserGroup"));
this.userId = userId;
this.userServiceClient = userServiceClient;
}
@Override
protected User run() throws Exception {
// Logique principale - exécutée dans un thread séparé
return userServiceClient.getUserById(userId);
}
@Override
protected User getFallback() {
// Logique de fallback - appelée en cas de défaillance
return new User(userId, "Fallback User", "fallback@example.com");
}
}
Comment HystrixCommand Fonctionne
- Construction : Création de la commande avec une clé de groupe
- Exécution : Appel de execute() ou queue()
- Thread Pool : Exécution dans un pool de threads isolé
- Monitoring : Suivi des succès/échecs/latences
- Circuit Breaker : Vérification de l'état du circuit
- Résultat : Retour de la valeur ou fallback
Circuit Breaker Pattern
États du Circuit Breaker
Le Circuit Breaker a trois états principaux :
État CLOSED
- Normal : Les requêtes passent normalement
- Monitoring : Suivi des erreurs et latences
- Transition : Passe à OPEN si seuil d'erreurs atteint
État OPEN
- Bloqué : Toutes les requêtes sont immédiatement rejetées
- Timeout : Attend un certain temps avant de passer à HALF_OPEN
- Protection : Protège le service défaillant de plus de requêtes
État HALF_OPEN
- Test : Permet un nombre limité de requêtes de test
- Validation : Vérifie si le service est rétabli
- Transition : Retour à CLOSED si succès, reste OPEN si échec
Configuration du Circuit Breaker :
import com.netflix.hystrix.HystrixCommand;
import com.netflix.hystrix.HystrixCommandGroupKey;
import com.netflix.hystrix.HystrixCommandProperties;
public class CircuitBreakerUserCommand extends HystrixCommand<User> {
public CircuitBreakerUserCommand() {
super(Setter
.withGroupKey(HystrixCommandGroupKey.Factory.asKey("UserGroup"))
.andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
.withCircuitBreakerEnabled(true) // Activer le circuit breaker
.withCircuitBreakerRequestVolumeThreshold(20) // Seuil de requêtes
.withCircuitBreakerErrorThresholdPercentage(50) // Pourcentage d'erreurs
.withCircuitBreakerSleepWindowInMilliseconds(5000) // Temps d'attente en OPEN
.withExecutionTimeoutInMilliseconds(3000) // Timeout d'exécution
.withExecutionIsolationStrategy(
HystrixCommandProperties.ExecutionIsolationStrategy.THREAD) // Isolation par thread
)
);
}
@Override
protected User run() throws Exception {
// Logique d'appel au service
return userServiceClient.getUserById(userId);
}
@Override
protected User getFallback() {
// Fallback en cas de défaillance ou circuit breaker ouvert
return new User(userId, "Fallback User", "fallback@example.com");
}
}
Explication des Propriétés
- withCircuitBreakerEnabled(true) :
Active le circuit breaker (true par défaut).
- withCircuitBreakerRequestVolumeThreshold(20) :
Nombre minimum de requêtes avant d'évaluer le circuit breaker (20 par défaut).
- withCircuitBreakerErrorThresholdPercentage(50) :
Pourcentage d'erreurs qui déclenche l'ouverture du circuit (50% par défaut).
- withCircuitBreakerSleepWindowInMilliseconds(5000) :
Temps d'attente en millisecondes avant de passer à HALF_OPEN (5000ms par défaut).
- withExecutionTimeoutInMilliseconds(3000) :
Timeout d'exécution en millisecondes (1000ms par défaut).
Isolation des Ressources
Stratégies d'Isolation
Hystrix fournit deux stratégies d'isolation pour protéger les ressources :
Isolation par Thread (THREAD)
- Séparation complète : Chaque groupe de commandes a son propre pool de threads
- Protection totale : Une commande lente n'affecte pas les autres
- Surcharge : Création de threads supplémentaires
- Usage recommandé : Pour les appels réseau, bases de données
Isolation par Sémaphore (SEMAPHORE)
- Partage de threads : Utilise le thread appelant
- Moins de surcharge : Pas de création de threads
- Protection limitée : Les appels bloquants affectent le thread appelant
- Usage recommandé : Pour les opérations en mémoire, cache
Configuration de l'isolation par Thread :
public class ThreadIsolationCommand extends HystrixCommand<String> {
public ThreadIsolationCommand() {
super(Setter
.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ThreadGroup"))
.andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
.withExecutionIsolationStrategy(
HystrixCommandProperties.ExecutionIsolationStrategy.THREAD)
.withExecutionTimeoutInMilliseconds(5000)
)
.andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter()
.withCoreSize(10) // Taille du pool de threads
.withMaximumSize(20) // Taille maximale
.withMaxQueueSize(100) // Taille maximale de la file d'attente
.withQueueSizeRejectionThreshold(80) // Seuil de rejet
)
);
}
@Override
protected String run() throws Exception {
// Exécuté dans un thread séparé
return "Résultat de l'opération";
}
}
Configuration de l'isolation par Sémaphore :
public class SemaphoreIsolationCommand extends HystrixCommand<String> {
public SemaphoreIsolationCommand() {
super(Setter
.withGroupKey(HystrixCommandGroupKey.Factory.asKey("SemaphoreGroup"))
.andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
.withExecutionIsolationStrategy(
HystrixCommandProperties.ExecutionIsolationStrategy.SEMAPHORE)
.withExecutionTimeoutInMilliseconds(2000)
.withExecutionIsolationSemaphoreMaxConcurrentRequests(10) // Max concurrents
)
);
}
@Override
protected String run() throws Exception {
// Exécuté dans le thread appelant
return "Résultat de l'opération rapide";
}
}
Propriétés des Pools de Threads
- coreSize(10) :
Nombre de threads dans le pool (10 par défaut).
- maximumSize(20) :
Nombre maximum de threads (si allowMaximumSizeToDivergeFromCoreSize=true).
- maxQueueSize(100) :
Taille maximale de la file d'attente (-1 pour SynchronousQueue).
- queueSizeRejectionThreshold(80) :
Seuil dynamique de rejet de requêtes (80 par défaut).
Configuration de Base de Hystrix
Installation et Dépendances
Ajoutez la dépendance Hystrix dans votre pom.xml :
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<!-- Pour le dashboard Hystrix -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>
<!-- Pour Turbine (agrégation de métriques) -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-turbine</artifactId>
</dependency>
Ce que contient chaque dépendance :
- spring-cloud-starter-netflix-hystrix :
Core Hystrix, annotations, circuit breaker, thread pools
- spring-cloud-starter-netflix-hystrix-dashboard :
Interface web pour visualiser les métriques Hystrix
- spring-cloud-starter-netflix-turbine :
Agrégation des métriques de plusieurs services
Activez Hystrix dans votre classe principale :
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.netflix.hystrix.dashboard.EnableHystrixDashboard;
@SpringBootApplication
@EnableCircuitBreaker // Active Hystrix
@EnableHystrixDashboard // Active le dashboard (optionnel)
public class UserServiceApplication {
public static void main(String[] args) {
SpringApplication.run(UserServiceApplication.class, args);
}
}
Explication des Annotations
- @EnableCircuitBreaker :
Active l'auto-configuration de Hystrix dans l'application Spring Boot.
- @EnableHystrixDashboard :
Active le dashboard Hystrix pour la visualisation des métriques.
Configuration Globale
Configuration dans application.properties :
# Configuration globale de Hystrix
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=3000
hystrix.command.default.circuitBreaker.requestVolumeThreshold=20
hystrix.command.default.circuitBreaker.errorThresholdPercentage=50
hystrix.command.default.circuitBreaker.sleepWindowInMilliseconds=5000
# Configuration des thread pools
hystrix.threadpool.default.coreSize=10
hystrix.threadpool.default.maximumSize=20
hystrix.threadpool.default.maxQueueSize=100
hystrix.threadpool.default.queueSizeRejectionThreshold=80
# Configuration du métriques
hystrix.metrics.rollingStats.timeInMilliseconds=10000
hystrix.metrics.rollingStats.numBuckets=10
hystrix.metrics.healthSnapshot.intervalInMilliseconds=500
# Activation du stream Hystrix pour le dashboard
management.endpoints.web.exposure.include=hystrix.stream,health,info
Explication Détaillée des Propriétés
- hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=3000 :
Timeout d'exécution par défaut de 3 secondes pour toutes les commandes.
- hystrix.command.default.circuitBreaker.requestVolumeThreshold=20 :
20 requêtes minimum avant d'évaluer l'état du circuit breaker.
- hystrix.command.default.circuitBreaker.errorThresholdPercentage=50 :
50% d'erreurs déclenche l'ouverture du circuit.
- hystrix.threadpool.default.coreSize=10 :
10 threads dans le pool par défaut.
- hystrix.metrics.rollingStats.timeInMilliseconds=10000 :
Fenêtre de temps de 10 secondes pour les statistiques roulantes.
Configuration par service :
# Configuration spécifique pour le service utilisateur
hystrix.command.UserServiceCommand.execution.isolation.thread.timeoutInMilliseconds=5000
hystrix.command.UserServiceCommand.circuitBreaker.requestVolumeThreshold=10
hystrix.command.UserServiceCommand.circuitBreaker.errorThresholdPercentage=30
# Configuration spécifique du thread pool pour utilisateur
hystrix.threadpool.UserServicePool.coreSize=15
hystrix.threadpool.UserServicePool.maximumSize=30
Premier Exemple Simple
Créez une commande Hystrix simple :
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
@Service
public class UserService {
private final RestTemplate restTemplate;
public UserService(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
@HystrixCommand(fallbackMethod = "getUserFallback")
public User getUser(Long userId) {
// Appel HTTP qui peut échouer
return restTemplate.getForObject(
"http://user-service/users/" + userId,
User.class);
}
public User getUserFallback(Long userId) {
// Méthode de fallback appelée en cas de défaillance
return new User(userId, "Utilisateur par défaut", "default@example.com");
}
}
Utilisez le service dans un contrôleur :
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
// La méthode getUser utilisera Hystrix automatiquement
return userService.getUser(id);
}
}
Comment @HystrixCommand Fonctionne
- Interception : Spring AOP intercepte l'appel à la méthode annotée
- Wrapping : Crée automatiquement une HystrixCommand autour de la méthode
- Exécution : Exécute la méthode dans un contexte Hystrix
- Monitoring : Suit les succès, échecs, et latences
- Fallback : Appelle la méthode de fallback si nécessaire
Utilisation des Annotations Hystrix
@HystrixCommand - Base
Annotation de base avec fallback :
@Service
public class OrderService {
@HystrixCommand(fallbackMethod = "getDefaultOrders")
public List<Order> getOrdersByUser(Long userId) {
// Logique principale qui peut échouer
return orderRepository.findByUserId(userId);
}
public List<Order> getDefaultOrders(Long userId) {
// Fallback - données par défaut
return Arrays.asList(new Order(0L, "Commande par défaut", userId));
}
}
Configuration avancée avec commandProperties :
@HystrixCommand(
fallbackMethod = "getOrdersFallback",
commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "3000"),
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"),
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "50"),
@HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "10000")
}
)
public List<Order> getOrdersWithCustomConfig(Long userId) {
return orderServiceClient.getOrders(userId);
}
public List<Order> getOrdersFallback(Long userId) {
// Fallback avec logique personnalisée
logger.warn("Fallback activé pour les commandes de l'utilisateur: " + userId);
return Collections.emptyList();
}
Propriétés de Commande Importantes
- execution.isolation.thread.timeoutInMilliseconds :
Timeout d'exécution en millisecondes.
- circuitBreaker.requestVolumeThreshold :
Nombre minimum de requêtes avant évaluation du circuit breaker.
- circuitBreaker.errorThresholdPercentage :
Pourcentage d'erreurs qui ouvre le circuit.
- circuitBreaker.sleepWindowInMilliseconds :
Temps d'attente en OPEN avant passage à HALF_OPEN.
@HystrixCommand - Thread Pool
Configuration de thread pool personnalisé :
@HystrixCommand(
fallbackMethod = "getUserFallback",
commandProperties = {
@HystrixProperty(name = "execution.isolation.strategy", value = "THREAD"),
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "5000")
},
threadPoolProperties = {
@HystrixProperty(name = "coreSize", value = "20"),
@HystrixProperty(name = "maximumSize", value = "50"),
@HystrixProperty(name = "maxQueueSize", value = "100"),
@HystrixProperty(name = "queueSizeRejectionThreshold", value = "80")
},
threadPoolKey = "UserThreadPool"
)
public User getUserWithCustomThreadPool(Long userId) {
return userServiceClient.getUser(userId);
}
Partage de thread pool entre commandes :
// Première commande utilisant le même thread pool
@HystrixCommand(
fallbackMethod = "getUserFallback",
threadPoolKey = "SharedThreadPool"
)
public User getUser(Long userId) {
return userServiceClient.getUser(userId);
}
// Deuxième commande utilisant le même thread pool
@HystrixCommand(
fallbackMethod = "getProfileFallback",
threadPoolKey = "SharedThreadPool",
threadPoolProperties = {
@HystrixProperty(name = "coreSize", value = "15"),
@HystrixProperty(name = "maximumSize", value = "30")
}
)
public UserProfile getUserProfile(Long userId) {
return profileServiceClient.getProfile(userId);
}
Avantages du Partage de Thread Pool
- Ressources optimisées : Moins de threads créés
- Contrôle centralisé : Configuration commune pour plusieurs commandes
- Monitoring simplifié : Statistiques agrégées
- Protection cohérente : Même niveau de protection pour les services liés
@HystrixCommand - Gestion d'Exceptions
Ignorer certaines exceptions :
@HystrixCommand(
fallbackMethod = "getProductFallback",
ignoreExceptions = {ProductNotFoundException.class}
)
public Product getProduct(Long productId) {
Product product = productServiceClient.getProduct(productId);
if (product == null) {
throw new ProductNotFoundException("Produit non trouvé: " + productId);
}
return product;
}
// Cette méthode de fallback ne sera pas appelée pour ProductNotFoundException
public Product getProductFallback(Long productId) {
logger.info("Fallback pour le produit: " + productId);
return new Product(productId, "Produit par défaut", 0.0);
}
Fallbacks spécifiques par type d'exception :
@HystrixCommand(
fallbackMethod = "getDefaultResult",
commandProperties = {
@HystrixProperty(name = "fallback.isolation.semaphore.maxConcurrentRequests", value = "100")
}
)
public ServiceResult performOperation(OperationRequest request) {
return externalServiceClient.processRequest(request);
}
// Fallback général
public ServiceResult getDefaultResult(OperationRequest request) {
return new ServiceResult("DEFAULT", "Service indisponible");
}
// Fallback pour des exceptions spécifiques (nécessite une logique personnalisée)
public ServiceResult handleTimeout(OperationRequest request, Throwable throwable) {
if (throwable instanceof HystrixTimeoutException) {
return new ServiceResult("TIMEOUT", "Service trop lent");
}
return getDefaultResult(request);
}
Stratégies de Gestion d'Exceptions
- ignoreExceptions : Certaines exceptions ne déclenchent pas le fallback
- Fallbacks conditionnels : Logique différente selon le type d'erreur
- Logging : Enregistrement des erreurs pour le debugging
- Propagation : Certaines erreurs doivent être propagées au client
@HystrixCollapser - Agrégation de Requêtes
Pattern de Collapsing
HystrixCollapser permet d'agréger plusieurs requêtes individuelles en une seule requête batch pour améliorer l'efficacité.
Création d'un collapser :
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCollapser;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.command.AsyncResult;
import java.util.concurrent.Future;
@Service
public class UserService {
@HystrixCollapser(
batchMethod = "getUsersBatch",
collapserProperties = {
@HystrixProperty(name = "timerDelayInMilliseconds", value = "20"),
@HystrixProperty(name = "maxRequestsInBatch", value = "10")
}
)
public Future<User> getUser(Long userId) {
// Cette méthode est automatiquement transformée en batch
return null; // Retour ignoré - géré par Hystrix
}
@HystrixCommand
public List<User> getUsersBatch(List<Long> userIds) {
// Méthode batch - appelée avec plusieurs IDs
return userServiceClient.getUsers(userIds);
}
}
Utilisation du collapser :
@RestController
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) throws Exception {
// Les appels multiples dans un court laps de temps seront agrégés
Future<User> userFuture = userService.getUser(id);
return userFuture.get(); // Attend le résultat
}
@GetMapping("/users/batch")
public List<User> getUsers(@RequestParam List<Long> ids) {
// Appel batch direct
return userService.getUsersBatch(ids);
}
}
Propriétés du Collapser
- timerDelayInMilliseconds=20 :
Temps d'attente pour agrégation des requêtes (20ms par défaut).
- maxRequestsInBatch=10 :
Nombre maximum de requêtes dans un batch (10 par défaut).
- requestCache.enabled=true :
Active le cache de requêtes (true par défaut).
Avantages du Request Collapsing
- Réduction du trafic réseau : Une requête au lieu de plusieurs
- Meilleure utilisation des ressources : Moins de threads et connexions
- Performance améliorée : Temps de réponse réduit pour les clients
- Scalabilité : Supporte plus de requêtes concurrentes
Dashboard Hystrix
Configuration du Dashboard
Ajout de la dépendance :
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>
Activation dans la classe principale :
@SpringBootApplication
@EnableCircuitBreaker
@EnableHystrixDashboard
public class MonitoringApplication {
public static void main(String[] args) {
SpringApplication.run(MonitoringApplication.class, args);
}
}
Configuration des endpoints :
# Activer le stream Hystrix
management.endpoints.web.exposure.include=hystrix.stream,health,info
# Configuration du stream
management.endpoint.hystrix.stream.enabled=true
# Pour Turbine (agrégation multiple)
turbine.app-config=user-service,order-service,payment-service
turbine.cluster-name-expression=new String("default")
turbine.combine-host-port=true
Utilisation du Dashboard
Accès au dashboard :
http://localhost:8080/hystrix
Configuration du stream à monitorer :
http://localhost:8080/actuator/hystrix.stream
Éléments du Dashboard
- Circuit Name : Nom de la commande Hystrix
- Request Volume : Nombre de requêtes par seconde
- Error % : Pourcentage d'erreurs
- Mean Time : Temps moyen de réponse
- Median Time : Temps médian de réponse
- Circuit Status : État du circuit breaker (Closed/Open)
Couleurs du Dashboard
- Vert : Circuit fermé, service fonctionnel
- Rouge : Circuit ouvert, fallback actif
- Orange : Circuit en test (half-open)
- Bleu : Short-circuit (requêtes rejetées)
Turbine pour Agrégation
Configuration de Turbine :
@SpringBootApplication
@EnableTurbine
public class TurbineApplication {
public static void main(String[] args) {
SpringApplication.run(TurbineApplication.class, args);
}
}
Configuration dans application.properties :
# Services à agréger
turbine.app-config=user-service,order-service,payment-service
turbine.cluster-name-expression=new String("default")
turbine.combine-host-port=true
# URLs des streams Hystrix
turbine.instanceUrlSuffix.default=hystrix.stream
# Configuration réseau
server.port=8989
Stream agrégé :
http://localhost:8989/turbine.stream
Architecture avec Turbine
Service A → hystrix.stream (port 8080)
Service B → hystrix.stream (port 8081)
Service C → hystrix.stream (port 8082)
↑
Turbine (port 8989)
↑
Hystrix Dashboard
Configuration Avancée de Hystrix
Configuration Fine des Circuits
Configuration par commande :
# Configuration spécifique pour une commande
hystrix.command.GetUserCommand.execution.isolation.thread.timeoutInMilliseconds=2000
hystrix.command.GetUserCommand.circuitBreaker.requestVolumeThreshold=5
hystrix.command.GetUserCommand.circuitBreaker.errorThresholdPercentage=40
hystrix.command.GetUserCommand.circuitBreaker.sleepWindowInMilliseconds=15000
hystrix.command.GetUserCommand.circuitBreaker.forceOpen=false
hystrix.command.GetUserCommand.circuitBreaker.forceClosed=false
hystrix.command.GetUserCommand.execution.timeout.enabled=true
# Configuration du fallback
hystrix.command.GetUserCommand.fallback.isolation.semaphore.maxConcurrentRequests=20
hystrix.command.GetUserCommand.fallback.enabled=true
Propriétés Avancées
- circuitBreaker.forceOpen=true :
Force le circuit à être toujours ouvert (utile pour maintenance).
- circuitBreaker.forceClosed=true :
Force le circuit à être toujours fermé (utile pour tests).
- execution.timeout.enabled=false :
Désactive le timeout (déconseillé en production).
- fallback.enabled=false :
Désactive le fallback (erreurs propagées).
Configuration des métriques :
# Configuration des statistiques roulantes
hystrix.command.default.metrics.rollingStats.timeInMilliseconds=10000
hystrix.command.default.metrics.rollingStats.numBuckets=10
hystrix.command.default.metrics.rollingPercentile.enabled=true
hystrix.command.default.metrics.rollingPercentile.timeInMilliseconds=60000
hystrix.command.default.metrics.rollingPercentile.numBuckets=6
hystrix.command.default.metrics.rollingPercentile.bucketSize=100
# Configuration du snapshot de santé
hystrix.command.default.metrics.healthSnapshot.intervalInMilliseconds=500
# Configuration du cache de requêtes
hystrix.command.default.requestCache.enabled=true
hystrix.command.default.requestLog.enabled=true
Gestion des Thread Pools
Configuration avancée des pools :
# Configuration du pool par défaut
hystrix.threadpool.default.coreSize=10
hystrix.threadpool.default.maximumSize=20
hystrix.threadpool.default.maxQueueSize=100
hystrix.threadpool.default.queueSizeRejectionThreshold=80
hystrix.threadpool.default.keepAliveTimeMinutes=1
hystrix.threadpool.default.allowMaximumSizeToDivergeFromCoreSize=true
# Configuration d'un pool spécifique
hystrix.threadpool.UserServicePool.coreSize=15
hystrix.threadpool.UserServicePool.maximumSize=30
hystrix.threadpool.UserServicePool.maxQueueSize=200
hystrix.threadpool.UserServicePool.queueSizeRejectionThreshold=150
Propriétés des Thread Pools
- maximumSize=20 :
Taille maximale du pool (nécessite allowMaximumSizeToDivergeFromCoreSize=true).
- keepAliveTimeMinutes=1 :
Temps de vie des threads supplémentaires (en minutes).
- allowMaximumSizeToDivergeFromCoreSize=true :
Permet maximumSize > coreSize.
- maxQueueSize=100 :
Taille maximale de la file d'attente (-1 pour SynchronousQueue).
Monitoring des pools :
# Activer les métriques détaillées
management.endpoints.web.exposure.include=metrics,health,info,hystrix.stream
management.endpoint.metrics.enabled=true
# Métriques spécifiques Hystrix
management.metrics.enable.hystrix=true
management.metrics.distribution.percentiles-histogram.hystrix=true
Configuration Programmative
Configuration via Hystrix ConfigurationManager :
import com.netflix.config.ConfigurationManager;
import com.netflix.hystrix.HystrixCommandKey;
import com.netflix.hystrix.HystrixCommandProperties;
@Component
public class HystrixConfigurator {
@PostConstruct
public void configureHystrix() {
// Configuration programmatique
ConfigurationManager.getConfigInstance().setProperty(
"hystrix.command.GetUserCommand.execution.isolation.thread.timeoutInMilliseconds",
"3000"
);
ConfigurationManager.getConfigInstance().setProperty(
"hystrix.threadpool.UserServicePool.coreSize",
"20"
);
// Configuration dynamique à l'exécution
updateTimeoutDynamically("GetUserCommand", 5000);
}
public void updateTimeoutDynamically(String commandKey, int timeoutMs) {
ConfigurationManager.getConfigInstance().setProperty(
"hystrix.command." + commandKey + ".execution.isolation.thread.timeoutInMilliseconds",
String.valueOf(timeoutMs)
);
}
}
Configuration conditionnelle :
@Component
public class ConditionalHystrixConfig {
@Value("${environment:prod}")
private String environment;
@PostConstruct
public void setupEnvironmentSpecificConfig() {
if ("dev".equals(environment)) {
// Configuration pour développement
ConfigurationManager.getConfigInstance().setProperty(
"hystrix.command.default.circuitBreaker.enabled", "false"
);
} else if ("prod".equals(environment)) {
// Configuration pour production
ConfigurationManager.getConfigInstance().setProperty(
"hystrix.command.default.circuitBreaker.enabled", "true"
);
}
}
}
Patterns de Résilience avec Hystrix
Pattern du Fallback
Stratégies de Fallback
Le fallback est la réponse alternative fournie lorsque le service principal échoue.
Fallback statique :
@HystrixCommand(fallbackMethod = "getDefaultUser")
public User getUser(Long userId) {
return userServiceClient.getUser(userId);
}
public User getDefaultUser(Long userId) {
// Données statiques de fallback
return new User(userId, "Utilisateur temporairement indisponible", "unknown@example.com");
}
Fallback dynamique :
@HystrixCommand(fallbackMethod = "getCachedUser")
public User getUser(Long userId) {
return userServiceClient.getUser(userId);
}
public User getCachedUser(Long userId) {
// Fallback avec données du cache
User cachedUser = cacheService.getUser(userId);
if (cachedUser != null) {
return cachedUser;
}
// Fallback de fallback
return new User(userId, "Utilisateur par défaut", "default@example.com");
}
Fallback avec logique complexe :
@HystrixCommand(fallbackMethod = "getDegradedUser")
public User getUserWithProfile(Long userId) {
User user = userServiceClient.getUser(userId);
UserProfile profile = profileServiceClient.getProfile(userId);
user.setProfile(profile);
return user;
}
public User getDegradedUser(Long userId) {
// Service dégradé - données partielles
logger.warn("Service dégradé pour l'utilisateur: " + userId);
User user = new User(userId, "Utilisateur (service dégradé)", "degraded@example.com");
user.setProfile(new UserProfile("Données de profil non disponibles"));
// Notification de l'erreur
alertService.sendAlert("Fallback activé pour utilisateur: " + userId);
return user;
}
Pattern du Cache
Request Cache
Hystrix fournit un cache de requêtes pour éviter les appels redondants dans le même contexte.
Activation du cache :
@HystrixCommand(
fallbackMethod = "getCachedUser",
commandProperties = {
@HystrixProperty(name = "requestCache.enabled", value = "true")
}
)
public User getUser(Long userId) {
return userServiceClient.getUser(userId);
}
// Méthode de fallback utilisant le cache
public User getCachedUser(Long userId) {
// Vérifier d'abord le cache Hystrix
String cacheKey = "user-" + userId;
User cachedUser = (User) HystrixRequestCache.getInstance(
HystrixCommandKey.Factory.asKey("GetUserCommand"),
HystrixConcurrencyStrategyDefault.getInstance()
).get(cacheKey);
if (cachedUser != null) {
return cachedUser;
}
// Fallback par défaut
return new User(userId, "Utilisateur par défaut", "default@example.com");
}
Gestion manuelle du cache :
@Service
public class CachedUserService {
private final HystrixRequestContext context;
@PostConstruct
public void initializeContext() {
// Initialiser le contexte Hystrix pour le cache
this.context = HystrixRequestContext.initializeContext();
}
@HystrixCommand(commandKey = "GetUserWithCache")
public User getUser(Long userId) {
return userServiceClient.getUser(userId);
}
public void clearCache() {
// Nettoyer le cache à la fin de la requête
if (context != null) {
context.shutdown();
}
}
}
Pattern de Bulkhead
Isolation des Services
Le pattern Bulkhead isole les services pour éviter que la défaillance d'un service n'affecte les autres.
Isolation par groupes :
// Commandes pour le service utilisateur
@HystrixCommand(
groupKey = "UserServiceGroup",
threadPoolKey = "UserServicePool"
)
public User getUser(Long userId) {
return userServiceClient.getUser(userId);
}
// Commandes pour le service commande
@HystrixCommand(
groupKey = "OrderServiceGroup",
threadPoolKey = "OrderServicePool"
)
public List<Order> getOrders(Long userId) {
return orderServiceClient.getOrders(userId);
}
// Configuration des pools séparés
hystrix.threadpool.UserServicePool.coreSize=10
hystrix.threadpool.OrderServicePool.coreSize=15
Limitation des ressources :
@HystrixCommand(
groupKey = "CriticalServiceGroup",
threadPoolKey = "CriticalServicePool",
threadPoolProperties = {
@HystrixProperty(name = "coreSize", value = "50"),
@HystrixProperty(name = "maximumSize", value = "100"),
@HystrixProperty(name = "maxQueueSize", value = "50")
}
)
public CriticalData getCriticalData(String id) {
return criticalServiceClient.getData(id);
}
@HystrixCommand(
groupKey = "NonCriticalServiceGroup",
threadPoolKey = "NonCriticalServicePool",
threadPoolProperties = {
@HystrixProperty(name = "coreSize", value = "5"),
@HystrixProperty(name = "maximumSize", value = "10"),
@HystrixProperty(name = "maxQueueSize", value = "10")
}
)
public NonCriticalData getNonCriticalData(String id) {
return nonCriticalServiceClient.getData(id);
}
Pattern du Timeout
Gestion des Timeouts
Les timeouts protègent contre les services lents qui pourraient bloquer l'ensemble du système.
Configuration des timeouts :
@HystrixCommand(
fallbackMethod = "getTimeoutFallback",
commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "3000"),
@HystrixProperty(name = "execution.timeout.enabled", value = "true")
}
)
public ExpensiveOperation performExpensiveOperation(Data data) {
return expensiveServiceClient.process(data);
}
public ExpensiveOperation getTimeoutFallback(Data data) {
logger.warn("Timeout sur l'opération coûteuse");
return new ExpensiveOperation("Opération annulée - timeout");
}
Timeouts adaptatifs :
@HystrixCommand(
fallbackMethod = "getAdaptiveFallback",
commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "5000")
}
)
public ServiceResponse callService(ServiceRequest request) {
// Timeout basé sur la complexité de la requête
int timeout = calculateTimeout(request);
return serviceClient.call(request, timeout);
}
private int calculateTimeout(ServiceRequest request) {
// Logique adaptative de calcul du timeout
if (request.isComplex()) {
return 10000; // 10 secondes pour les requêtes complexes
}
return 3000; // 3 secondes pour les requêtes simples
}
public ServiceResponse getAdaptiveFallback(ServiceRequest request) {
return new ServiceResponse("Service temporairement indisponible");
}
Concepts Fondamentaux du Load Balancing
Load Balancing Côté Client vs Serveur
Load Balancing Côté Client
Le client (service appelant) est responsable de choisir l'instance à laquelle envoyer la requête.
Avantages :
- Latence réduite : Pas de saut supplémentaire vers un load balancer externe
- Flexibilité : Chaque client peut avoir sa propre stratégie de load balancing
- Résilience : Meilleure gestion des pannes d'instances individuelles
- Performance : Décisions de routage basées sur l'état local
Inconvénients :
- Complexité : Chaque client doit implémenter la logique de load balancing
- Consommation mémoire : Chaque client maintient la liste des instances
- Synchronisation : Nécessite une mise à jour régulière de la liste des instances
// Exemple de load balancing côté client avec Spring Cloud LoadBalancer
@Service
public class UserServiceClient {
@Autowired
private LoadBalancerClient loadBalancer;
public String callUserService() {
// Choix intelligent de l'instance
ServiceInstance instance = loadBalancer.choose("user-service");
String url = "http://" + instance.getHost() + ":" + instance.getPort() + "/users";
// Appel HTTP vers l'instance choisie
return restTemplate.getForObject(url, String.class);
}
}
Load Balancing Côté Serveur
Un load balancer dédié (comme NGINX, HAProxy) reçoit toutes les requêtes et les distribue aux instances backend.
Avantages :
- Simplicité : Les clients n'ont pas besoin de logique de load balancing
- Centralisation : Configuration et monitoring unifiés
- Sécurité : Le load balancer peut agir comme pare-feu
- Scalabilité : Peut gérer un très grand nombre de clients
Inconvénients :
- Point de défaillance unique : Si le load balancer tombe, tout le système est affecté
- Latence supplémentaire : Un saut réseau supplémentaire
- Moins de flexibilité : Difficile d'adapter la stratégie par client
# Exemple de configuration NGINX comme load balancer
upstream user-service {
server localhost:8080;
server localhost:8081;
server localhost:8082;
}
server {
listen 80;
location /users {
proxy_pass http://user-service;
}
}
Approche Hybride dans Spring Cloud
Spring Cloud combine les deux approches :
- Eureka agit comme registre de services (similaire à un load balancer serveur)
- Spring Cloud LoadBalancer fournit le load balancing côté client
- API Gateway combine les deux pour le routage externe
Terminologie Clé
Termes Importants
- Service Instance :
Une instance spécifique d'un service microservice, identifiée par son host et port.
- Service Registry :
Registre qui maintient la liste des instances de services disponibles (Eureka, Consul).
- Load Balancer Client :
Composant qui choisit l'instance de service à laquelle envoyer une requête.
- Load Balancer Rule :
Stratégie utilisée pour choisir l'instance (Round Robin, Random, Weighted).
- Health Check :
Mécanisme pour vérifier si une instance de service est disponible et répondante.
- Failover :
Processus de basculement vers une autre instance en cas de panne.
- Circuit Breaker :
Mécanisme pour éviter d'envoyer des requêtes à un service défaillant.
Cycle de Vie d'une Requête Load Balanced
- Découverte : Le client demande la liste des instances disponibles à Eureka
- Filtrage : Les instances non saines sont éliminées
- Sélection : L'algorithme de load balancing choisit une instance
- Appel : La requête est envoyée à l'instance sélectionnée
- Monitoring : Le résultat est surveillé pour les statistiques
- Mise à jour : L'état de l'instance est mis à jour (succès/échec)
Algorithmes de Load Balancing
Algorithmes Classiques
Round Robin
Distribue les requêtes de manière séquentielle entre les instances disponibles.
Fonctionnement :
- Maintient un pointeur vers la prochaine instance à utiliser
- Pour chaque requête, sélectionne l'instance suivante dans la liste
- Revient au début de la liste lorsque la fin est atteinte
- Ignore les instances non saines
Avantages :
- Équitable : Distribution égale entre toutes les instances
- Simple : Facile à comprendre et implémenter
- Déterministe : Comportement prévisible
Inconvénients :
- Ne tient pas compte de la charge : Instance chargée reçoit quand même des requêtes
- Ne considère pas les performances : Toutes les instances sont traitées équitablement
// Configuration Round Robin dans Spring Cloud LoadBalancer
@Configuration
public class LoadBalancerConfig {
@Bean
public ReactorLoadBalancer<ServiceInstance> reactorServiceInstanceLoadBalancer(
Environment environment,
LoadBalancerClientFactory loadBalancerClientFactory) {
String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
return new RoundRobinLoadBalancer(
loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class),
name);
}
}
Random
Sélectionne une instance aléatoire parmi les instances disponibles.
Fonctionnement :
- Obtient la liste des instances saines
- Génère un nombre aléatoire entre 0 et (nombre d'instances - 1)
- Sélectionne l'instance correspondante
- Répète pour chaque requête
Avantages :
- Simple : Implémentation très simple
- Distribution naturelle : Tendance vers une distribution égale à long terme
- Imprévisible : Difficile à manipuler intentionnellement
Inconvénients :
- Non déterministe : Difficile à tester et déboguer
- Pas de garantie d'équité : Peut y avoir des déséquilibres à court terme
- Ne tient pas compte de la charge : Comme Round Robin
// Configuration Random Load Balancer
@Configuration
public class RandomLoadBalancerConfig {
@Bean
public ReactorLoadBalancer<ServiceInstance> randomLoadBalancer(
Environment environment,
LoadBalancerClientFactory loadBalancerClientFactory) {
String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
return new RandomLoadBalancer(
loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class),
name);
}
}
Weighted Response Time
Attribue des poids aux instances basés sur leur temps de réponse historique.
Fonctionnement :
- Mesure le temps de réponse de chaque instance
- Calcule un poids inversement proportionnel au temps de réponse
- Instances plus rapides reçoivent des poids plus élevés
- Sélectionne aléatoirement en fonction des poids
Avantages :
- Adaptatif : S'adapte automatiquement aux performances
- Optimise le temps de réponse : Favorise les instances rapides
- Équilibré : Maintient une distribution raisonnable
Inconvénients :
- Complexe : Nécessite le suivi des statistiques
- Lag : Réaction avec délai aux changements de performance
- Surcharge : Coût supplémentaire pour le suivi des métriques
// Implémentation Weighted Response Time (conceptuelle)
public class WeightedResponseTimeLoadBalancer
implements ReactorLoadBalancer<ServiceInstance> {
private final Map<ServiceInstance, Long> responseTimes = new ConcurrentHashMap<>();
private final Map<ServiceInstance, Integer> weights = new ConcurrentHashMap<>();
@Override
public Mono<Response<ServiceInstance>> choose(Request request) {
return serviceInstanceListSupplier.get().next()
.map(serviceInstances -> {
// Calcul des poids basés sur les temps de réponse
calculateWeights(serviceInstances);
// Sélection pondérée
ServiceInstance selected = selectWeightedInstance(serviceInstances);
return new DefaultResponse(selected);
});
}
private void calculateWeights(List<ServiceInstance> instances) {
// Logique de calcul des poids
for (ServiceInstance instance : instances) {
Long avgResponseTime = responseTimes.getOrDefault(instance, 100L);
int weight = (int) (1000.0 / avgResponseTime); // Poids inverse
weights.put(instance, Math.max(weight, 1));
}
}
}
Algorithmes Avancés
Least Connections
Dirige les requêtes vers l'instance ayant le moins de connexions actives.
Principe :
Suppose que l'instance avec le moins de connexions est la moins chargée et peut traiter la requête plus rapidement.
Implémentation :
- Nécessite le suivi des connexions actives par instance
- Peut être complexe dans des environnements distribués
- Très efficace pour des charges variables
IP Hash
Utilise le hash de l'IP du client pour toujours diriger vers la même instance.
Avantages :
- Affinité de session : Même client toujours sur même instance
- Cache efficace : Les données en cache sont mieux utilisées
Inconvénients :
- Déséquilibre possible : Répartition inégale si clients inégalement répartis
- Problèmes de scaling : Ajout/suppression d'instances change tout le hashage
Adaptive Load Balancing
Combine plusieurs métriques pour prendre des décisions intelligentes.
Facteurs pris en compte :
- Temps de réponse historique
- Charge CPU/Mémoire de l'instance
- Nombre de connexions actives
- Taux d'erreurs
- Capacité de l'instance
Avantages :
- Optimisation globale : Prend en compte de multiples facteurs
- Adaptatif : S'ajuste automatiquement aux conditions
- Prédictif : Peut anticiper les problèmes de performance
Spring Cloud LoadBalancer
Configuration et Utilisation
Ajoutez la dépendance Spring Cloud LoadBalancer :
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
Ce que contient la dépendance :
- ReactorLoadBalancer : Interface principale pour le load balancing réactif
- RoundRobinLoadBalancer : Implémentation Round Robin
- RandomLoadBalancer : Implémentation Random
- ServiceInstanceListSupplier : Fournit la liste des instances
- LoadBalancerClientFactory : Fabrique pour créer des load balancers
- HealthCheckServiceInstanceListSupplier : Vérification de santé
Configuration de base dans application.properties :
# Activer Spring Cloud LoadBalancer (désactive Ribbon)
spring.cloud.loadbalancer.retry.enabled=true
spring.cloud.loadbalancer.ribbon.enabled=false
# Configuration des timeouts
spring.cloud.loadbalancer.retry.max-attempts=3
spring.cloud.loadbalancer.retry.backoff.enabled=true
spring.cloud.loadbalancer.retry.backoff.first-backoff=100ms
spring.cloud.loadbalancer.retry.backoff.max-backoff=1000ms
# Configuration du cache
spring.cloud.loadbalancer.cache.enabled=true
spring.cloud.loadbalancer.cache.ttl=30s
spring.cloud.loadbalancer.cache.capacity=1000
Explication des Propriétés
- spring.cloud.loadbalancer.retry.enabled=true :
Active le mécanisme de retry automatique en cas d'échec.
- spring.cloud.loadbalancer.ribbon.enabled=false :
Désactive Ribbon pour utiliser Spring Cloud LoadBalancer à la place.
- spring.cloud.loadbalancer.retry.max-attempts=3 :
Maximum 3 tentatives (1 tentative initiale + 2 retries).
- spring.cloud.loadbalancer.cache.enabled=true :
Active le cache pour améliorer les performances.
Utilisation avec WebClient
Configuration de WebClient avec load balancing :
@Configuration
public class WebClientConfig {
@Bean
@LoadBalanced
public WebClient.Builder loadBalancedWebClientBuilder() {
return WebClient.builder();
}
@Bean
public WebClient webClient(WebClient.Builder builder) {
return builder.build();
}
}
Comment @LoadBalanced fonctionne :
- Interception : Les URLs commençant par "lb://" sont interceptées
- Résolution : Le nom du service est résolu via Eureka
- Sélection : Spring Cloud LoadBalancer choisit une instance
- Transformation : "lb://user-service" devient "http://host:port"
- Exécution : La requête est envoyée à l'instance sélectionnée
Utilisation dans un service :
@Service
public class UserServiceClient {
private final WebClient webClient;
public UserServiceClient(@LoadBalanced WebClient.Builder webClientBuilder) {
this.webClient = webClientBuilder.build();
}
public Mono<List<User>> getAllUsers() {
return webClient
.get()
.uri("lb://user-service/users") // lb:// pour load balancing
.retrieve()
.bodyToFlux(User.class)
.collectList();
}
public Mono<User> getUserById(Long id) {
return webClient
.get()
.uri("lb://user-service/users/{id}", id)
.retrieve()
.bodyToMono(User.class);
}
}
Cycle de Vie d'une Requête WebClient Load Balanced
1. webClient.get().uri("lb://user-service/users")
2. Interceptor détecte "lb://"
3. ServiceInstanceListSupplier.getInstances("user-service")
4. LoadBalancer.choose(instances)
5. Transformation URI: lb://user-service → http://192.168.1.10:8080
6. Envoi requête HTTP
7. Réception réponse
8. Retour résultat
Configuration Avancée du LoadBalancer
Personnalisation de l'algorithme de load balancing :
@Configuration
public class CustomLoadBalancerConfiguration {
@Bean
ReactorLoadBalancer<ServiceInstance> randomLoadBalancer(
Environment environment,
LoadBalancerClientFactory loadBalancerClientFactory) {
String name = environment.getProperty(
LoadBalancerClientFactory.PROPERTY_NAME);
return new RandomLoadBalancer(
loadBalancerClientFactory
.getLazyProvider(name, ServiceInstanceListSupplier.class),
name);
}
}
Configuration par service :
@Configuration
@LoadBalancerClient(name = "user-service",
configuration = CustomLoadBalancerConfiguration.class)
public class UserServiceLoadBalancerConfig {
// Configuration spécifique à user-service
}
// Ou via properties
spring.cloud.loadbalancer.configurations[user-service]=com.example.config.CustomLoadBalancerConfiguration
Options de Configuration Avancées
- Stratégies de retry : Configurer quand et comment retenter les requêtes
- Filtres d'instances : Exclure certaines instances selon des critères
- Health checking : Personnaliser la vérification de santé
- Cache : Optimiser la mise en cache des listes d'instances
- Métriques : Suivre les performances du load balancing
Configuration Avancée du Load Balancing
Gestion de la Cache des Instances
Configuration du cache :
# Configuration du cache des instances
spring.cloud.loadbalancer.cache.enabled=true
spring.cloud.loadbalancer.cache.ttl=30s
spring.cloud.loadbalancer.cache.capacity=1000
spring.cloud.loadbalancer.cache.refresh-instances=true
# Cache spécifique par service
spring.cloud.loadbalancer.cache.configurations[user-service].ttl=60s
spring.cloud.loadbalancer.cache.configurations[user-service].capacity=500
Importance du Cache
Le cache améliore les performances en évitant des appels fréquents à Eureka :
- Réduction de latence : Pas de requêtes réseau pour chaque choix d'instance
- Moins de charge sur Eureka : Réduction du trafic vers le service registry
- Meilleure résistance aux pannes : Fonctionne même si Eureka est temporairement indisponible
- Performance : Sélections d'instances plus rapides
Configuration avancée du refresh :
# Configuration du refresh du cache
spring.cloud.loadbalancer.cache.refresh.enabled=true
spring.cloud.loadbalancer.cache.refresh.initial-delay=0s
spring.cloud.loadbalancer.cache.refresh.fixed-delay=30s
spring.cloud.loadbalancer.cache.refresh.use-refresh-timer=true
# Gestion des événements
spring.cloud.loadbalancer.cache.refresh.on-instance-change=true
spring.cloud.loadbalancer.cache.refresh.on-health-change=true
Filtrage des Instances
Configuration des filtres :
# Filtres par défaut
spring.cloud.loadbalancer.configurations.default.filters=healthy,zone
# Filtres personnalisés
spring.cloud.loadbalancer.configurations[user-service].filters=healthy,zone,metadata
spring.cloud.loadbalancer.configurations[user-service].filter.metadata.key=version
spring.cloud.loadbalancer.configurations[user-service].filter.metadata.value=v2
# Configuration des zones
spring.cloud.loadbalancer.configurations[user-service].filter.zone.prefer-same-zone=true
spring.cloud.loadbalancer.configurations[user-service].filter.zone.zone=us-east-1
Types de Filtres
- Healthy Filter : Exclut les instances non saines
- Zone Filter : Préfère les instances dans la même zone
- Metadata Filter : Filtre basé sur les métadonnées des instances
- Weight Filter : Filtre basé sur les poids des instances
- Custom Filter : Filtres personnalisés selon les besoins
Implémentation de filtres personnalisés :
@Component
public class VersionFilter implements Supplier<ServiceInstanceListSupplier> {
private final ServiceInstanceListSupplier delegate;
public VersionFilter(ServiceInstanceListSupplier delegate) {
this.delegate = delegate;
}
@Override
public Flux<List<ServiceInstance>> get() {
return delegate.get().map(this::filterByVersion);
}
private List<ServiceInstance> filterByVersion(List<ServiceInstance> instances) {
return instances.stream()
.filter(instance -> {
Map<String, String> metadata = instance.getMetadata();
return "v2".equals(metadata.get("version"));
})
.collect(Collectors.toList());
}
}
Configuration des Timeouts et Retry
Configuration des timeouts :
# Timeouts globaux
spring.cloud.loadbalancer.retry.max-attempts=3
spring.cloud.loadbalancer.retry.backoff.enabled=true
spring.cloud.loadbalancer.retry.backoff.first-backoff=100ms
spring.cloud.loadbalancer.retry.backoff.max-backoff=1000ms
spring.cloud.loadbalancer.retry.backoff.multiplier=2.0
# Timeouts par service
spring.cloud.loadbalancer.configurations[user-service].timeout.connect=5000ms
spring.cloud.loadbalancer.configurations[user-service].timeout.read=15000ms
spring.cloud.loadbalancer.configurations[user-service].timeout.write=10000ms
# Configuration des retry par service
spring.cloud.loadbalancer.configurations[user-service].retry.max-attempts=5
spring.cloud.loadbalancer.configurations[user-service].retry.retryable-status-codes=503,504,408
Explication des Timeouts
- Connect Timeout : Temps maximum pour établir la connexion
- Read Timeout : Temps maximum pour lire la réponse
- Write Timeout : Temps maximum pour envoyer la requête
- Backoff : Délai croissant entre les tentatives
Configuration avancée du retry :
# Retry avancé
spring.cloud.loadbalancer.retry.retry-on-connection-failure=true
spring.cloud.loadbalancer.retry.retry-on-all-operations=false
spring.cloud.loadbalancer.retry.retryable-exceptions=java.net.ConnectException,java.net.SocketTimeoutException
spring.cloud.loadbalancer.retry.not-retryable-exceptions=org.springframework.web.client.HttpClientErrorException
# Configuration des politiques de retry
spring.cloud.loadbalancer.retry.policy=exponential
spring.cloud.loadbalancer.retry.jitter.enabled=true
spring.cloud.loadbalancer.retry.jitter.range=50ms
Intégration Hystrix et Load Balancing
Configuration de Base
Ajout des dépendances :
<!-- Hystrix -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<!-- Load Balancer -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!-- WebClient -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
Configuration de l'application :
# Activer Hystrix
feign.hystrix.enabled=true
# Configuration LoadBalancer
spring.cloud.loadbalancer.retry.enabled=true
spring.cloud.loadbalancer.ribbon.enabled=false
# Configuration Hystrix
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=5000
hystrix.command.default.circuitBreaker.requestVolumeThreshold=10
hystrix.command.default.circuitBreaker.errorThresholdPercentage=50
# Actuator endpoints
management.endpoints.web.exposure.include=hystrix.stream,health,info,metrics
Service Client avec Hystrix et LoadBalancer
Configuration WebClient avec LoadBalancer :
@Configuration
public class WebClientConfig {
@Bean
@LoadBalanced
public WebClient.Builder loadBalancedWebClientBuilder() {
return WebClient.builder();
}
}
Service client avec Hystrix :
@Service
public class UserServiceClient {
private final WebClient webClient;
public UserServiceClient(@LoadBalanced WebClient.Builder webClientBuilder) {
this.webClient = webClientBuilder.build();
}
@HystrixCommand(
fallbackMethod = "getUserFallback",
commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "3000"),
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "5"),
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "40")
}
)
public Mono<User> getUser(Long userId) {
return webClient
.get()
.uri("lb://user-service/users/{id}", userId)
.retrieve()
.bodyToMono(User.class);
}
public Mono<User> getUserFallback(Long userId) {
logger.warn("Fallback activé pour l'utilisateur: " + userId);
User fallbackUser = new User(userId, "Utilisateur par défaut", "fallback@example.com");
return Mono.just(fallbackUser);
}
}
Flux d'Exécution
- Hystrix : Enveloppe l'appel dans une commande Hystrix
- LoadBalancer : Intercepte "lb://user-service" et choisit une instance
- WebClient : Effectue l'appel HTTP vers l'instance sélectionnée
- Monitoring : Hystrix surveille le succès/échec et la latence
- Fallback : Si échec, appelle la méthode de fallback
OpenFeign avec Hystrix et LoadBalancer
Interface Feign avec Hystrix :
@FeignClient(
name = "user-service",
configuration = UserServiceFeignConfig.class,
fallback = UserServiceFallback.class
)
public interface UserServiceClient {
@GetMapping("/users/{id}")
User getUser(@PathVariable("id") Long id);
@GetMapping("/users")
List<User> getAllUsers();
@PostMapping("/users")
User createUser(@RequestBody User user);
}
Configuration Feign :
@Configuration
public class UserServiceFeignConfig {
@Bean
public Logger.Level feignLoggerLevel() {
return Logger.Level.BASIC;
}
@Bean
public Retryer retryer() {
return new Retryer.Default(1000, 2000, 3);
}
@Bean
public Request.Options options() {
return new Request.Options(5000, 10000); // connectTimeout, readTimeout
}
}
Implémentation du Fallback :
Configuration application.properties :
# Activer Hystrix pour Feign
feign.hystrix.enabled=true
# Configuration des timeouts
feign.client.config.default.connect-timeout=5000
feign.client.config.default.read-timeout=10000
# Retry configuration
feign.retryer.max-attempts=3
# LoadBalancer
spring.cloud.loadbalancer.ribbon.enabled=false
spring.cloud.loadbalancer.retry.enabled=true
Gestion Avancée des Erreurs
Hystrix avec gestion d'exceptions spécifiques :
@Service
public class ResilientUserService {
@HystrixCommand(
fallbackMethod = "handleUserError",
ignoreExceptions = {UserNotFoundException.class},
commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "3000"),
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"),
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "50"),
@HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "10000")
}
)
public User getUser(Long userId) {
try {
return userServiceClient.getUser(userId);
} catch (UserNotFoundException e) {
// Cette exception est ignorée par Hystrix
throw e;
} catch (Exception e) {
// Les autres exceptions déclenchent le fallback
throw new RuntimeException(e);
}
}
public User handleUserError(Long userId) {
logger.error("Erreur critique lors de la récupération de l'utilisateur: " + userId);
// Notification d'alerte
alertService.sendAlert("Erreur service utilisateur pour ID: " + userId);
// Retourner un utilisateur par défaut
return new User(userId, "Utilisateur temporairement indisponible", "error@example.com");
}
}
LoadBalancer avec retry intelligent :
@Service
public class SmartUserServiceClient {
private final WebClient webClient;
private final LoadBalancerClient loadBalancerClient;
public SmartUserServiceClient(
@LoadBalanced WebClient.Builder webClientBuilder,
LoadBalancerClient loadBalancerClient) {
this.webClient = webClientBuilder.build();
this.loadBalancerClient = loadBalancerClient;
}
@HystrixCommand(fallbackMethod = "getUserFallback")
public Mono<User> getUserWithRetry(Long userId) {
return webClient
.get()
.uri("lb://user-service/users/{id}", userId)
.retrieve()
.onStatus(HttpStatus::is5xxServerError, response -> {
// Retry sur erreurs 5xx
return Mono.error(new ServiceUnavailableException("Service utilisateur indisponible"));
})
.bodyToMono(User.class)
.retryWhen(Retry.backoff(3, Duration.ofMillis(100))
.filter(throwable -> throwable instanceof ServiceUnavailableException));
}
public Mono<User> getUserFallback(Long userId) {
return Mono.just(new User(userId, "Utilisateur par défaut", "fallback@example.com"));
}
}
Monitoring et Métriques
Métriques de Performance
Activation des métriques :
# Configuration des métriques
management.endpoints.web.exposure.include=metrics,health,info,prometheus,hystrix.stream
management.endpoint.metrics.enabled=true
management.endpoint.prometheus.enabled=true
# Métriques spécifiques Hystrix
management.metrics.enable.hystrix=true
management.metrics.enable.loadbalancer=true
# Configuration des tags
management.metrics.tags.application=${spring.application.name}
management.metrics.tags.environment=${spring.profiles.active:default}
Métriques Clés à Surveiller
- hystrix.command.*.execution : Nombre d'exécutions par commande
- hystrix.command.*.fallback : Nombre de fallbacks activés
- hystrix.command.*.circuitBreaker : État du circuit breaker
- loadbalancer.choose.requests : Sélections d'instances
- http.client.requests : Performance des requêtes sortantes
Configuration Prometheus :
# Dépendance Prometheus
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
# Endpoint Prometheus
management.endpoints.web.exposure.include=prometheus
management.endpoint.prometheus.enabled=true
# Configuration avancée
management.metrics.distribution.percentiles-histogram.hystrix=true
management.metrics.distribution.percentiles.hystrix.command=0.5,0.9,0.95,0.99
management.metrics.distribution.sla.hystrix.command=1ms,5ms,10ms,50ms
Alerting et Notifications
Configuration des alertes :
# Configuration des seuils d'alerte
spring.cloud.loadbalancer.alerting.enabled=true
spring.cloud.loadbalancer.alerting.health-threshold=0.9
spring.cloud.loadbalancer.alerting.retry-threshold=0.1
spring.cloud.loadbalancer.alerting.latency-threshold=1000ms
# Notifications
spring.cloud.loadbalancer.alerting.notifications.enabled=true
spring.cloud.loadbalancer.alerting.notifications.channels=slack,email
spring.cloud.loadbalancer.alerting.notifications.slack.webhook-url=https://hooks.slack.com/services/YOUR/SLACK/WEBHOOK
spring.cloud.loadbalancer.alerting.notifications.email.to=admin@example.com
Implémentation d'alertes personnalisées :
@Component
public class ResilienceAlerting {
private static final Logger logger = LoggerFactory.getLogger(ResilienceAlerting.class);
@EventListener
public void handleHystrixMetrics(HystrixMetricsEvent event) {
HystrixCommandMetrics metrics = event.getMetrics();
// Vérifier les seuils d'alerte
if (metrics.getHealthCounts().getErrorPercentage() > 50) {
sendAlert("High error rate in Hystrix command: " +
metrics.getCommandKey().name() +
" - Error percentage: " + metrics.getHealthCounts().getErrorPercentage() + "%");
}
if (metrics.getCircuitBreaker().isOpen()) {
sendAlert("Circuit breaker OPEN for command: " + metrics.getCommandKey().name());
}
}
@EventListener
public void handleLoadBalancerStats(LoadBalancerStatsEvent event) {
LoadBalancerStats stats = event.getStats();
// Vérifier les seuils d'alerte
if (stats.getRetryRate() > 0.1) {
sendAlert("High retry rate detected: " + stats.getRetryRate());
}
if (stats.getAverageLatency() > 1000) {
sendAlert("High latency detected: " + stats.getAverageLatency() + "ms");
}
if (stats.getHealthyInstancesRatio() < 0.8) {
sendAlert("Low healthy instances ratio: " + stats.getHealthyInstancesRatio());
}
}
private void sendAlert(String message) {
logger.warn("RESILIENCE_ALERT: " + message);
// Implémentation d'envoi d'alerte (email, Slack, etc.)
notificationService.sendNotification(message);
}
}
Dashboard de Monitoring
Intégration avec Grafana :
# Exemple de dashboard Grafana pour Hystrix et LoadBalancer
# Panel 1: Taux de requêtes par service
rate(hystrix_command_execution_total[5m])
# Panel 2: Pourcentage d'erreurs
rate(hystrix_command_execution_total{status="failure"}[5m]) /
rate(hystrix_command_execution_total[5m]) * 100
# Panel 3: Latence 95th percentile
histogram_quantile(0.95, sum(rate(hystrix_command_latency_execute_seconds_bucket[5m])) by (le, command))
# Panel 4: État du circuit breaker
hystrix_circuitbreaker_open_total
# Panel 5: Taux de retry LoadBalancer
rate(loadbalancer_retry_requests_total[5m])
Indicateurs de Performance Clés (KPIs)
- Disponibilité : Pourcentage de temps où les services sont disponibles
- Latence : Temps moyen de réponse des requêtes load balanced
- Taux de retry : Pourcentage de requêtes qui nécessitent un retry
- Distribution d'instances : Équilibre de la charge entre les instances
- Taux d'erreurs : Pourcentage de requêtes échouées
- Taux de fallback : Pourcentage de fallbacks activés
- État des circuits : Nombre de circuits ouverts/fermés
Bonnes Pratiques Hystrix et Load Balancing
Configuration et Déploiement
Principes Fondamentaux
- Configuration externalisée : Utilisez des fichiers de configuration ou Config Server
- Environnements séparés : Différentes configurations pour dev, test, prod
- Versioning : Versionnez vos configurations avec le code
- Documentation : Documentez clairement les stratégies de load balancing
Configuration Recommandée
# Production
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=3000
hystrix.command.default.circuitBreaker.requestVolumeThreshold=20
hystrix.command.default.circuitBreaker.errorThresholdPercentage=50
hystrix.command.default.circuitBreaker.sleepWindowInMilliseconds=5000
spring.cloud.loadbalancer.cache.enabled=true
spring.cloud.loadbalancer.cache.ttl=30s
spring.cloud.loadbalancer.retry.enabled=true
spring.cloud.loadbalancer.retry.max-attempts=3
Performance et Optimisation
Optimisations Clés
- Cache approprié : Équilibre entre fraîcheur des données et performance
- Timeouts raisonnables : Ni trop courts (erreurs prématurées) ni trop longs (ressources bloquées)
- Retry stratégique : Retry seulement sur les erreurs récupérables
- Algorithmes adaptés : Choisissez l'algorithme selon vos besoins spécifiques
Paramètres de Performance Optimisés
| Paramètre | Valeur Recommandée | Raison |
|---|---|---|
| Cache TTL | 30-60 secondes | Équilibre fraîcheur/performance |
| Connect Timeout | 2-5 secondes | Détection rapide des pannes réseau |
| Read Timeout | 10-30 secondes | Temps suffisant pour les opérations longues |
| Max Retries | 2-3 tentatives | Évite la surcharge sans abandon prématuré |
Sécurité et Résilience
Considérations de Sécurité
- Validation des instances : Vérifiez que seules les instances autorisées sont load balanced
- Chiffrement des communications : Utilisez HTTPS entre les services
- Rate limiting : Protégez contre les abus et DDoS
- Monitoring des accès : Surveillez les patterns d'accès inhabituels
Pièges Courants à Éviter
- Timeouts trop courts : Causent des erreurs prématurées
- Trop de retries : Amplifient les problèmes de performance
- Pas de health checking : Envoient des requêtes à des services défaillants
- Cache trop long : Utilisent des instances dépréciées
- Pas de monitoring : Difficile de diagnostiquer les problèmes
Tests et Validation
Stratégies de Test
- Tests unitaires : Testez les algorithmes de load balancing
- Tests d'intégration : Vérifiez l'interaction avec Eureka
- Tests de charge : Simulez des scénarios de haute charge
- Tests de chaos : Simulez des pannes d'instances
- Tests de performance : Mesurez l'impact sur la latence
// Exemple de test de load balancing
@SpringBootTest
class LoadBalancerTest {
@Autowired
private UserServiceClient userServiceClient;
@Test
void testLoadBalancingDistribution() {
Set<String> hosts = new HashSet<>();
// Effectuer plusieurs appels
for (int i = 0; i < 100; i++) {
User user = userServiceClient.getUserById(1L);
hosts.add(user.getHost()); // Supposons que l'host est retourné
}
// Vérifier que plusieurs instances ont été utilisées
assertThat(hosts.size()).isGreaterThan(1);
}
@Test
void testFailover() {
// Simuler la panne d'une instance
// Vérifier que les requêtes sont routées vers d'autres instances
}
}
Résumé des Bonnes Pratiques
En suivant ces bonnes pratiques, vous obtiendrez :
- Haute disponibilité : Résilience face aux pannes
- Bonne performance : Distribution optimale de la charge
- Facilité de maintenance : Configuration claire et documentée
- Sécurité : Communications sécurisées et contrôlées
- Observabilité : Monitoring et alerting efficaces
Phase 1 : Création du Serveur Eureka
Étape 1 : Initialisation du Projet Spring Boot
Créez un nouveau projet Spring Boot via Spring Initializr ou votre IDE favori.
Sélectionnez les dépendances suivantes :
Fournit les fonctionnalités web nécessaires pour exposer les endpoints REST du serveur Eureka.
Contient l'implémentation du serveur Eureka pour la découverte de services.
Pourquoi ces dépendances ?
Spring Boot Web est nécessaire pour créer une application web, tandis que Eureka Server fournit toutes les fonctionnalités pour transformer votre application en registre de services.
Étape 2 : Configuration de la Classe Principale
Ouvrez la classe principale générée et ajoutez l'annotation @EnableEurekaServer :
package com.example.eurekaserver;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
Explication des Annotations
- @SpringBootApplication : Combinaison de @Configuration, @EnableAutoConfiguration et @ComponentScan
- @EnableEurekaServer : Active le serveur Eureka, transforme l'application en registre de services
Que fait @EnableEurekaServer ?
Cette annotation configure automatiquement tous les composants nécessaires pour faire fonctionner le serveur Eureka, y compris les contrôleurs REST, les services de découverte, et l'interface web d'administration.
Étape 3 : Configuration du Fichier application.properties
Modifiez le fichier src/main/resources/application.properties :
# Port du serveur Eureka
server.port=8761
# Ne pas s'enregistrer auprès d'un autre serveur Eureka
eureka.client.register-with-eureka=false
# Ne pas récupérer le registre d'autres serveurs
eureka.client.fetch-registry=false
# URL par défaut pour le service Eureka
eureka.client.service-url.defaultZone=http://localhost:8761/eureka/
# Durées de rafraîchissement (en millisecondes)
eureka.server.eviction-interval-timer-in-ms=30000
eureka.instance.lease-renewal-interval-in-seconds=30
eureka.instance.lease-expiration-duration-in-seconds=90
Explication des Propriétés
- server.port=8761 : Port standard pour le serveur Eureka
- eureka.client.register-with-eureka=false : Le serveur ne s'enregistre pas lui-même
- eureka.client.fetch-registry=false : Le serveur ne récupère pas d'autres registres
- eureka.client.service-url.defaultZone : URL du serveur Eureka pour l'enregistrement
- eureka.server.eviction-interval-timer-in-ms : Intervalle de nettoyage des services déconnectés
- eureka.instance.lease-renewal-interval-in-seconds : Intervalle d'envoi du heartbeat
- eureka.instance.lease-expiration-duration-in-seconds : Durée avant considération comme déconnecté
Pourquoi désactiver register-with-eureka et fetch-registry ?
Le serveur Eureka est le registre principal. Il ne doit pas s'enregistrer auprès d'un autre serveur ni récupérer d'autres registres, car il est lui-même le point central de découverte.
Vérifiez que le serveur fonctionne en accédant à :
http://localhost:8761
Validation
Vous devriez voir l'interface web d'Eureka avec "Instances currently registered with Eureka" vide (aucun service encore enregistré).
Phase 2 : Création des Microservices
Étape 1 : Création du Service Utilisateur
Créez un nouveau projet Spring Boot avec les dépendances suivantes :
Pour créer des endpoints REST
Pour s'enregistrer auprès du serveur Eureka
Pour le monitoring et la santé du service
Configurez la classe principale :
package com.example.userservice;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
@SpringBootApplication
@EnableEurekaClient
public class UserServiceApplication {
public static void main(String[] args) {
SpringApplication.run(UserServiceApplication.class, args);
}
}
Explication des Annotations
- @EnableEurekaClient : Active le client Eureka pour l'enregistrement automatique
Étape 2 : Configuration du Service Utilisateur
Configurez le fichier application.properties :
# Port du service utilisateur
server.port=8080
# Nom du service (doit être unique dans Eureka)
spring.application.name=user-service
# URL du serveur Eureka
eureka.client.service-url.defaultZone=http://localhost:8761/eureka/
# Configuration de l'instance
eureka.instance.prefer-ip-address=true
eureka.instance.instance-id=${spring.application.name}:${server.port}
# Actuator endpoints
management.endpoints.web.exposure.include=health,info
Explication des Propriétés
- server.port=8080 : Port d'écoute du service utilisateur
- spring.application.name=user-service : Nom unique du service dans Eureka
- eureka.client.service-url.defaultZone : URL du serveur Eureka pour s'enregistrer
- eureka.instance.prefer-ip-address=true : Utilise l'IP plutôt que le nom d'hôte
- eureka.instance.instance-id : Identifiant unique de l'instance
Étape 3 : Création d'un Contrôleur Simple
Créez une classe contrôleur de base :
package com.example.userservice.controller;
import org.springframework.web.bind.annotation.*;
import java.util.Arrays;
import java.util.List;
@RestController
@RequestMapping("/users")
public class UserController {
@GetMapping
public List<String> getAllUsers() {
return Arrays.asList("Jean Dupont", "Marie Martin", "Pierre Durand");
}
@GetMapping("/{id}")
public String getUserById(@PathVariable Long id) {
return "Utilisateur avec ID: " + id;
}
@PostMapping
public String createUser(@RequestBody String user) {
return "Utilisateur créé: " + user;
}
}
Structure du Contrôleur
- @RestController : Combine @Controller et @ResponseBody
- @RequestMapping("/users") : Base path pour tous les endpoints
- @GetMapping, @PostMapping : Mappings HTTP pour différentes méthodes
- @PathVariable, @RequestBody : Extraction des données de requête
Étape 4 : Création du Service de Commandes
Créez un deuxième service avec les mêmes dépendances de base :
Classe principale :
package com.example.orderservice;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
@SpringBootApplication
@EnableEurekaClient
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
}
Configuration application.properties :
# Port du service commande
server.port=8081
# Nom du service
spring.application.name=order-service
# URL du serveur Eureka
eureka.client.service-url.defaultZone=http://localhost:8761/eureka/
# Configuration de l'instance
eureka.instance.prefer-ip-address=true
eureka.instance.instance-id=${spring.application.name}:${server.port}
# Actuator endpoints
management.endpoints.web.exposure.include=health,info
Contrôleur de commandes :
package com.example.orderservice.controller;
import org.springframework.web.bind.annotation.*;
import java.util.Arrays;
import java.util.List;
@RestController
@RequestMapping("/orders")
public class OrderController {
@GetMapping
public List<String> getAllOrders() {
return Arrays.asList("Commande #123", "Commande #456", "Commande #789");
}
@GetMapping("/user/{userId}")
public List<String> getOrdersByUser(@PathVariable Long userId) {
return Arrays.asList("Commande #123 pour utilisateur " + userId);
}
@PostMapping
public String createOrder(@RequestBody String order) {
return "Commande créée: " + order;
}
}
Vérifiez l'enregistrement dans Eureka :
http://localhost:8761
Validation
Vous devriez maintenant voir "user-service" et "order-service" dans la section "Instances currently registered with Eureka".
Phase 3 : Configuration d'OpenFeign
Étape 1 : Ajout de la Dépendance OpenFeign
Ajoutez la dépendance OpenFeign à votre service utilisateur (pom.xml) :
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
Activez OpenFeign dans la classe principale :
package com.example.userservice;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
public class UserServiceApplication {
public static void main(String[] args) {
SpringApplication.run(UserServiceApplication.class, args);
}
}
Nouvelle Annotation
- @EnableFeignClients : Active le scanning des interfaces Feign dans le package
Étape 2 : Création du Client Feign
Créez une interface Feign pour communiquer avec le service de commandes :
package com.example.userservice.client;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import java.util.List;
@FeignClient(name = "order-service")
public interface OrderServiceClient {
@GetMapping("/orders/user/{userId}")
List<String> getOrdersByUserId(@PathVariable("userId") Long userId);
@GetMapping("/orders")
List<String> getAllOrders();
}
Comment ça marche ?
- @FeignClient(name = "order-service") : Nom du service cible défini dans Eureka
- @GetMapping : Mapping HTTP vers l'endpoint du service distant
- OpenFeign génère automatiquement l'implémentation de cette interface
- La communication passe par Eureka pour la découverte du service
Étape 3 : Utilisation du Client Feign
Injectez et utilisez le client Feign dans votre contrôleur :
package com.example.userservice.controller;
import com.example.userservice.client.OrderServiceClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.Arrays;
import java.util.List;
@RestController
@RequestMapping("/users")
public class UserController {
@Autowired
private OrderServiceClient orderServiceClient;
@GetMapping
public List<String> getAllUsers() {
return Arrays.asList("Jean Dupont", "Marie Martin", "Pierre Durand");
}
@GetMapping("/{id}")
public String getUserById(@PathVariable Long id) {
return "Utilisateur avec ID: " + id;
}
@GetMapping("/{userId}/orders")
public List<String> getUserOrders(@PathVariable Long userId) {
// Appel au service de commandes via Feign
return orderServiceClient.getOrdersByUserId(userId);
}
@GetMapping("/all-orders")
public List<String> getAllOrders() {
// Appel au service de commandes pour toutes les commandes
return orderServiceClient.getAllOrders();
}
@PostMapping
public String createUser(@RequestBody String user) {
return "Utilisateur créé: " + user;
}
}
Explication de l'Injection
- @Autowired : Injection automatique du client Feign généré
- orderServiceClient.getOrdersByUserId(userId) : Appel synchrone au service distant
- OpenFeign gère automatiquement la sérialisation, les headers, et la communication HTTP
Étape 4 : Configuration Avancée d'OpenFeign
Ajoutez des configurations de timeout dans application.properties :
# Configuration des timeouts pour OpenFeign
feign.client.config.default.connect-timeout=5000
feign.client.config.default.read-timeout=10000
# Activation de la compression
feign.compression.request.enabled=true
feign.compression.response.enabled=true
# Configuration du retry
ribbon.MaxAutoRetries=1
ribbon.MaxAutoRetriesNextServer=1
ribbon.ConnectTimeout=3000
ribbon.ReadTimeout=10000
Explication des Configurations
- connect-timeout=5000 : Timeout de connexion de 5 secondes
- read-timeout=10000 : Timeout de lecture de 10 secondes
- compression.* : Active la compression pour réduire la taille des données
- MaxAutoRetries=1 : 1 tentative de retry sur le même serveur
- MaxAutoRetriesNextServer=1 : 1 tentative sur un autre serveur
Étape 5 : Test de la Communication Inter-Services
Testez l'appel inter-services :
GET http://localhost:8080/users/1/orders
Vous devriez recevoir la réponse du service de commandes :
["Commande #123 pour utilisateur 1"]
Validation
La communication entre microservices fonctionne ! Le service utilisateur appelle le service de commandes via OpenFeign et Eureka.
Phase 4 : Tests et Déploiement
Étape 1 : Test de l'Architecture
Vérifiez l'état de santé des services :
GET http://localhost:8080/actuator/health
GET http://localhost:8081/actuator/health
Testez tous les endpoints :
GET http://localhost:8080/users
GET http://localhost:8080/users/1
GET http://localhost:8080/users/1/orders
GET http://localhost:8081/orders
Tous les tests doivent réussir
Chaque endpoint doit retourner une réponse valide, confirmant que l'architecture microservices fonctionne correctement.
Étape 2 : Surveillance dans Eureka
Accédez à l'interface web d'Eureka :
http://localhost:8761
Vérifiez que tous les services sont "UP" :
- EUREKA-SERVER : UP (port 8761)
- USER-SERVICE : UP (port 8080)
- ORDER-SERVICE : UP (port 8081)
Ce que vous voyez
L'interface montre les services enregistrés, leurs IPs, ports, et statuts. C'est votre tableau de bord pour la surveillance de l'architecture.
Étape 3 : Simulation de Panne de Service
Arrêtez le service de commandes :
Ctrl+C sur le terminal du service de commandes
Attendez 90 secondes (durée de lease) et vérifiez Eureka :
http://localhost:8761
Testez l'appel qui dépend du service arrêté :
GET http://localhost:8080/users/1/orders
Comportement attendu
Vous devriez recevoir une erreur 500 ou timeout, démontrant que la détection de panne fonctionne.
Étape 4 : Bonnes Pratiques de Déploiement
Structure de Déploiement Recommandée
- Eureka Server : Déployé en cluster (minimum 2 instances)
- Load Balancing : Utilisez un reverse proxy (NGINX, HAProxy)
- Configuration externe : Utilisez Spring Cloud Config
- Monitoring : Intégrez Prometheus/Grafana ou ELK
- Logging : Centralisez les logs avec ELK ou Fluentd
Variables d'Environnement
# Variables pour différents environnements
export SERVER_PORT=8080
export EUREKA_SERVER_URL=http://eureka1:8761/eureka/,http://eureka2:8761/eureka/
export JAVA_OPTS="-Xmx512m -Xms256m"
Script de Démarrage
#!/bin/bash
# start-services.sh
echo "Démarrage du serveur Eureka..."
nohup java -jar eureka-server.jar > eureka.log 2>&1 &
sleep 10
echo "Démarrage des microservices..."
nohup java -jar user-service.jar > user-service.log 2>&1 &
nohup java -jar order-service.jar > order-service.log 2>&1 &
echo "Tous les services sont démarrés"
Félicitations !
Vous avez maintenant une architecture microservices complète avec Eureka et OpenFeign. Vous pouvez étendre cette base en ajoutant plus de services, en implémentant des patterns de résilience (Hystrix), et en configurant un système de messagerie (RabbitMQ/Kafka).
Étape 2: Eureka Server Setup
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.0.9</version> <!-- Downgraded Spring Boot to a compatible version -->
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.emsieureka</groupId>
<artifactId>eurekaserver</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>eurekaserver</name>
<description>Demo project for Spring Boot Eureka Server</description>
<properties>
<java.version>17</java.version>
<spring-cloud.version>2022.0.1</spring-cloud.version> <!-- Spring Cloud 2022.x release train -->
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- Spring Boot Web Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Eureka Server -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<!-- Spring Boot DevTools -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<!-- Spring Boot Test Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Main Class (EurekaServerApplication.java)
package com.emsieureka.eurekaserver;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@SpringBootApplication // Indique que c'est une application Spring Boot, activant l'auto-configuration et le scan des composants
@EnableEurekaServer // Active cette application en tant que serveur Eureka, permettant la découverte de services
public class EurekaServerApplication { // Classe principale de l'application serveur Eureka
public static void main(String[] args) { // Point d'entrée de l'application Java
SpringApplication.run(EurekaServerApplication.class, args); // Lance l'application Spring
}
}
application.properties
spring.application.name=eurekaserver
server.port=8761
eureka.client.register-with-eureka=false
eureka.client.fetch-registry=false
2. Service A: Greeting Service
il faut créer un projet spring Boot appler Service1pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.emsi</groupId>
<artifactId>serviceA</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>serviceA</name>
<description>Demo project for Spring Boot</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>17</java.version>
<spring-cloud.version>2023.0.3</spring-cloud.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version> <!-- Use property for version -->
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Main Class (ServiceAApplication.java)
package com.emsi.serviceA;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@SpringBootApplication // Indique que c'est une application Spring Boot, activant l'auto-configuration et le scan des composants
@EnableDiscoveryClient
public class ServiceAApplication {
public static void main(String[] args) {
SpringApplication.run(ServiceAApplication.class, args);
}
}
@RestController
class GreetingController {
@GetMapping("/greeting")
public String getGreeting() {
return "Hello from Service A!";
}
}
application.properties
spring.application.name=serviceA
server.port=8081
eureka.client.service-url.defaultZone=http://localhost:8761/eureka/
3. Service B: Date Service
il faut créer un projet spring Boot appeler Service2pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.emsi</groupId>
<artifactId>serviceB</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>serviceB</name>
<description>Demo project for Spring Boot</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>17</java.version>
<spring-cloud.version>2023.0.3</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Main Class (ServiceBApplication.java)
package com.emsi.serviceB;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDate;
@SpringBootApplication // Indique que c'est une application Spring Boot, activant l'auto-configuration et le scan des composants
@EnableDiscoveryClient
public class ServiceBApplication {
public static void main(String[] args) {
SpringApplication.run(ServiceBApplication.class, args);
}
}
@RestController
class DateController {
@GetMapping("/date")
public String getCurrentDate() {
return "Current Date: " + LocalDate.now();
}
}
application.properties
server.port=8082
spring.application.name=service-b
eureka.client.service-url.defaultZone=http://localhost:8761/eureka/
4. Service C: Aggregator Service
il faut créer un projet spring Boot appeler Service3pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.emsi</groupId>
<artifactId>serviceC</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>serviceC</name>
<description>Demo project for Spring Boot</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>17</java.version>
<spring-cloud.version>2023.0.3</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<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-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Main Class (ServiceCApplication.java)
package com.emsi.serviceC;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.beans.factory.annotation.Autowired;
@SpringBootApplication // Indique que c'est une application Spring Boot, activant l'auto-configuration et le scan des composants
@EnableDiscoveryClient
@EnableFeignClients // Active l'utilisation des clients Feign pour effectuer des requêtes HTTP vers d'autres services
public class ServiceCApplication { // Classe principale de l'application
public static void main(String[] args) { // Point d'entrée de l'application Java
SpringApplication.run(ServiceCApplication.class, args); // Lance l'application Spring
}
}
@RestController // Indique que cette classe est un contrôleur Spring MVC avec des capacités RESTful
class AggregatorController { // Contrôleur pour gérer les requêtes entrantes et agréger les données
@Autowired // Injecte le bean GreetingClient
private GreetingClient greetingClient; // Client Feign pour appeler le Service A
@Autowired // Injecte le bean DateClient
private DateClient dateClient; // Client Feign pour appeler le Service B
@GetMapping("/aggregate") // Mappe les requêtes HTTP GET vers cette méthode à l'endpoint "/aggregate"
public String getAggregateData() { // Méthode pour agréger les données des deux services
String greeting = greetingClient.getGreeting(); // Appelle le Service A pour obtenir le message de salutation
String date = dateClient.getCurrentDate(); // Appelle le Service B pour obtenir la date actuelle
return greeting + " | " + date; // Retourne la réponse agrégée
}
}
@FeignClient(name = "service-a") // Déclare un client Feign pour le Service A avec le nom "service-a"
interface GreetingClient {
@GetMapping("/greeting") // Mappe la requête HTTP GET vers l'endpoint "/greeting" du Service A
String getGreeting(); // Méthode pour obtenir le message de salutation
}
@FeignClient(name = "service-b") // Déclare un client Feign pour le Service B avec le nom "service-b"
interface DateClient {
@GetMapping("/date") // Mappe la requête HTTP GET vers l'endpoint "/date" du Service B
String getCurrentDate(); // Méthode pour obtenir la date actuelle
}
application.properties
server.port=8083
spring.application.name=service-c
eureka.client.service-url.defaultZone=http://localhost:8761/eureka/
Run the Application
Start Eureka Server:
Run the Eureka Server application (EurekaServerApplication).
It will start on port 8761 and host the service registry.
Start Service A, Service B, and Service C:
Run each service on their respective ports (8081, 8082, 8083).
These services will automatically register themselves with the Eureka Server.
Test the Setup:
Access the Eureka Server dashboard at http://localhost:8761 to see the registered instances (service-a, service-b, and service-c).
Visit http://localhost:8083/aggregate to trigger Service C's aggregation. This will call Service A for a greeting and Service B for the current date and combine the results.