Hystrix
Tolérance aux pannes avec Circuit Breaker (Déprécié - )
Introduction à Hystrix
Qu'est-ce que Hystrix ?
⚠️ ATTENTION - PRODUIT DÉPRÉCIÉ
Hystrix est officiellement déprécié depuis novembre 2018. Cette documentation est fournie à titre historique et éducatif. Pour les nouveaux projets, utilisez Resilience4j à la place.
Hystrix était une bibliothèque de tolérance aux pannes développée par Netflix. Elle permettait de contrôler les points de latence et d'échec entre les services distribués, empêchant les cascades d'échecs et permettant de donner aux services le temps de récupérer.
Service A] --> B[Hystrix] B --> C[Circuit Breaker] C --> D[Service B] C --> E[Fallback] style A fill:#FF9800,stroke:#E65100 style B fill:#4CAF50,stroke:#388E3C style C fill:#2196F3,stroke:#0D47A1 style D fill:#9C27B0,stroke:#4A148C style E fill:#FF5722,stroke:#E64A19
Fonctionnalités principales de Hystrix :
- Circuit Breaker : Empêche les appels répétés vers un service défaillant
- Fallback : Fournit des réponses alternatives en cas de panne
- Timeouts : Limite la durée d'attente pour les appels
- Monitoring : Dashboard en temps réel des performances
- Isolation : Isole les appels pour éviter la propagation des pannes
- Métriques : Collecte détaillée des statistiques
Architecture avec Hystrix
Structure de l'application
Port: 8761] --> B[Service A
Client - Port: 8080] A --> C[Service B
Users - Port: 8081] B -- Hystrix --> D[Hystrix Dashboard
Port: 9000] B -- Hystrix Commands --> C style A fill:#2196F3,stroke:#0D47A1 style B fill:#FF9800,stroke:#E65100 style C fill:#4CAF50,stroke:#388E3C style D fill:#9C27B0,stroke:#4A148C
Rôles des composants :
- Service A : Client qui utilise Hystrix pour communiquer
- Service B : Service distant pouvant tomber en panne
- Eureka Server : Serveur de découverte de services
- Hystrix Dashboard : Interface de monitoring en temps réel
- Hystrix Commands : Commandes encapsulant les appels distants
Service B - Service Utilisateurs
1 Structure du projet
Arborescence du projet :
serviceB/
├── pom.xml
└── src/main/
├── java/com/example/users/
│ ├── UsersMicroserviceApplication.java
│ ├── controller/
│ │ └── UserController.java
│ ├── model/
│ │ └── User.java
└── resources/
└── application.properties
2 Fichier pom.xml
Dépendances Maven :
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Eureka Client -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- Actuator pour monitoring -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
3 Configuration application.properties
Fichier de configuration :
# === CONFIGURATION SERVEUR ===
server.port=8081
spring.application.name=service-b
# === CONFIGURATION EUREKA ===
eureka.client.service-url.defaultZone=http://localhost:8761/eureka/
eureka.client.register-with-eureka=true
eureka.client.fetch-registry=true
eureka.instance.prefer-ip-address=true
# === CONFIGURATION ACTUATOR ===
management.endpoints.web.exposure.include=health,info,metrics
management.endpoint.health.show-details=always
4 Modèle User
Modèle User :
package com.example.users.model;
/**
* Modèle représentant un utilisateur
*/
public class User {
private Long id;
private String name;
private String email;
private String department;
// Constructeurs
// Getters et Setters
5 Contrôleur REST
Contrôleur REST :
package com.example.users.controller;
import com.example.users.model.User;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
/**
* Contrôleur REST pour les opérations sur les utilisateurs
*/
@RestController
@RequestMapping("/api/users")
@CrossOrigin(origins = "*")
public class UserController {
// Stockage en mémoire pour la démonstration
private final ConcurrentHashMap<Long, User> users = new ConcurrentHashMap<>();
private final AtomicLong counter = new AtomicLong();
// Initialisation avec quelques utilisateurs
public UserController() {
users.put(counter.incrementAndGet(), new User(1L, "John Doe", "john@example.com", "IT"));
users.put(counter.incrementAndGet(), new User(2L, "Jane Smith", "jane@example.com", "HR"));
users.put(counter.incrementAndGet(), new User(3L, "Bob Johnson", "bob@example.com", "Finance"));
counter.set(3);
}
/**
* Récupère tous les utilisateurs
* @return Liste d'utilisateurs
*/
@GetMapping
public List<User> getAllUsers() {
return new ArrayList<>(users.values());
}
/**
* Récupère un utilisateur par son ID
* @param id ID de l'utilisateur
* @return Utilisateur trouvé
*/
@GetMapping("/{id}")
public User getUserById(@PathVariable Long id) {
return users.get(id);
}
/**
* Crée un nouvel utilisateur
* @param user Utilisateur à créer
* @return Utilisateur créé
*/
@PostMapping
public User createUser(@RequestBody User user) {
Long id = counter.incrementAndGet();
user.setId(id);
users.put(id, user);
return user;
}
/**
* Met à jour un utilisateur existant
* @param id ID de l'utilisateur
* @param user Données de mise à jour
* @return Utilisateur mis à jour
*/
@PutMapping("/{id}")
public User updateUser(@PathVariable Long id, @RequestBody User user) {
user.setId(id);
users.put(id, user);
return user;
}
/**
* Supprime un utilisateur
* @param id ID de l'utilisateur à supprimer
*/
@DeleteMapping("/{id}")
public void deleteUser(@PathVariable Long id) {
users.remove(id);
}
/**
* Recherche par département
*/
@GetMapping(params = "department")
public List<User> getUsersByDepartment(@RequestParam String department) {
List<User> result = new ArrayList<>();
for (User user : users.values()) {
if (user.getDepartment().equals(department)) {
result.add(user);
}
}
return result;
}
/**
* Endpoint qui simule une erreur pour tester Hystrix
*/
@GetMapping("/error")
public User getErrorUser() {
// Simule une erreur 50% du temps
if (Math.random() > 0.5) {
throw new RuntimeException("Erreur simulée pour tester Hystrix");
}
return new User(999L, "Error Test", "error@test.com", "TEST");
}
/**
* Endpoint qui simule un timeout pour tester Hystrix
*/
@GetMapping("/slow")
public User getSlowUser() throws InterruptedException {
// Simule un traitement lent
Thread.sleep(3000); // 3 secondes
return new User(888L, "Slow Test", "slow@test.com", "TEST");
}
}
6 Classe principale
Application Spring Boot :
package com.example.users;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
/**
* Classe principale du microservice de gestion des utilisateurs
*/
@SpringBootApplication
@EnableDiscoveryClient
public class UsersMicroserviceApplication {
public static void main(String[] args) {
SpringApplication.run(UsersMicroserviceApplication.class, args);
}
}
Service A - Client avec Hystrix
1 Configuration de Base de Hystrix
⚠️ RAPPEL - Hystrix est déprécié
Pour les nouveaux projets, utilisez Resilience4j à la place.
Dépendances Maven :
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Eureka Client -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- Hystrix Core -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<!-- Hystrix Dashboard -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>
<!-- Actuator pour monitoring -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- RestTemplate -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- OpenFeign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
</dependencies>
Activation des fonctionnalités :
package com.example.client;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.hystrix.dashboard.EnableHystrixDashboard;
import org.springframework.cloud.openfeign.EnableFeignClients;
/**
* Classe principale du microservice client avec Hystrix
*
* @EnableCircuitBreaker - Active Hystrix Circuit Breaker
* @EnableHystrixDashboard - Active le dashboard Hystrix
*/
@SpringBootApplication
@EnableDiscoveryClient
@EnableCircuitBreaker // Active Hystrix
@EnableHystrixDashboard // Active le dashboard Hystrix
@EnableFeignClients // Active OpenFeign
public class ClientMicroserviceApplication {
public static void main(String[] args) {
SpringApplication.run(ClientMicroserviceApplication.class, args);
}
}
2 Modèle de Données
Modèle User :
package com.example.client.model;
/**
* Modèle représentant un utilisateur
*/
public class User {
private Long id;
private String name;
private String email;
private String department;
// Constructeurs
// Getters et Setters
3 Hystrix Commands avec RestTemplate
Commandes Hystrix :
package com.example.client.hystrix;
import com.example.client.model.User;
import com.netflix.hystrix.HystrixCommand;
import com.netflix.hystrix.HystrixCommandGroupKey;
import com.netflix.hystrix.HystrixCommandKey;
import com.netflix.hystrix.HystrixCommandProperties;
import org.springframework.web.client.RestTemplate;
/**
* Commande Hystrix pour récupérer un utilisateur
*
* HystrixCommand - Classe de base pour les commandes Hystrix
* Permet de gérer le circuit breaker, les timeouts et les fallbacks
*/
public class GetUserCommand extends HystrixCommand<User> {
private final RestTemplate restTemplate;
private final Long userId;
private final String serviceUrl;
public GetUserCommand(RestTemplate restTemplate, Long userId, String serviceUrl) {
super(Setter
.withGroupKey(HystrixCommandGroupKey.Factory.asKey("UserServiceGroup"))
.andCommandKey(HystrixCommandKey.Factory.asKey("GetUserCommand"))
.andCommandPropertiesDefaults(
HystrixCommandProperties.Setter()
.withExecutionTimeoutInMilliseconds(2000) // Timeout de 2 secondes
.withCircuitBreakerRequestVolumeThreshold(5) // 5 requêtes minimum
.withCircuitBreakerErrorThresholdPercentage(50) // 50% d'erreurs
.withCircuitBreakerSleepWindowInMilliseconds(5000) // 5 secondes d'attente
)
);
this.restTemplate = restTemplate;
this.userId = userId;
this.serviceUrl = serviceUrl;
}
/**
* Méthode principale - exécutée dans un thread séparé
* Contient la logique d'appel distant
*/
@Override
protected User run() throws Exception {
String url = serviceUrl + "/api/users/" + userId;
return restTemplate.getForObject(url, User.class);
}
/**
* Méthode de fallback - appelée en cas d'erreur, timeout ou circuit breaker ouvert
*/
@Override
protected User getFallback() {
System.err.println("Fallback appelé pour GetUserCommand - userId: " + userId);
// Retourne un utilisateur par défaut
return new User(userId, "Utilisateur par défaut", "default@example.com", "DEFAULT");
}
}
package com.example.client.hystrix;
import com.example.client.model.User;
import com.netflix.hystrix.HystrixCommand;
import com.netflix.hystrix.HystrixCommandGroupKey;
import com.netflix.hystrix.HystrixCommandKey;
import com.netflix.hystrix.HystrixCommandProperties;
import org.springframework.web.client.RestTemplate;
import java.util.Arrays;
import java.util.List;
/**
* Commande Hystrix pour récupérer tous les utilisateurs
*/
public class GetAllUsersCommand extends HystrixCommand<List<User>> {
private final RestTemplate restTemplate;
private final String serviceUrl;
public GetAllUsersCommand(RestTemplate restTemplate, String serviceUrl) {
super(Setter
.withGroupKey(HystrixCommandGroupKey.Factory.asKey("UserServiceGroup"))
.andCommandKey(HystrixCommandKey.Factory.asKey("GetAllUsersCommand"))
.andCommandPropertiesDefaults(
HystrixCommandProperties.Setter()
.withExecutionTimeoutInMilliseconds(3000) // Timeout de 3 secondes
.withCircuitBreakerRequestVolumeThreshold(5)
.withCircuitBreakerErrorThresholdPercentage(50)
.withCircuitBreakerSleepWindowInMilliseconds(5000)
)
);
this.restTemplate = restTemplate;
this.serviceUrl = serviceUrl;
}
@Override
protected List<User> run() throws Exception {
String url = serviceUrl + "/api/users";
User[] usersArray = restTemplate.getForObject(url, User[].class);
return Arrays.asList(usersArray);
}
@Override
protected List<User> getFallback() {
System.err.println("Fallback appelé pour GetAllUsersCommand");
// Retourne une liste vide ou une liste par défaut
return Arrays.asList(
new User(0L, "Utilisateur par défaut", "default@example.com", "DEFAULT")
);
}
}
package com.example.client.hystrix;
import com.example.client.model.User;
import com.netflix.hystrix.HystrixCommand;
import com.netflix.hystrix.HystrixCommandGroupKey;
import com.netflix.hystrix.HystrixCommandKey;
import com.netflix.hystrix.HystrixCommandProperties;
import org.springframework.web.client.RestTemplate;
/**
* Commande Hystrix pour créer un utilisateur
*/
public class CreateUserCommand extends HystrixCommand<User> {
private final RestTemplate restTemplate;
private final User user;
private final String serviceUrl;
public CreateUserCommand(RestTemplate restTemplate, User user, String serviceUrl) {
super(Setter
.withGroupKey(HystrixCommandGroupKey.Factory.asKey("UserServiceGroup"))
.andCommandKey(HystrixCommandKey.Factory.asKey("CreateUserCommand"))
.andCommandPropertiesDefaults(
HystrixCommandProperties.Setter()
.withExecutionTimeoutInMilliseconds(2000)
.withCircuitBreakerRequestVolumeThreshold(5)
.withCircuitBreakerErrorThresholdPercentage(50)
.withCircuitBreakerSleepWindowInMilliseconds(5000)
)
);
this.restTemplate = restTemplate;
this.user = user;
this.serviceUrl = serviceUrl;
}
@Override
protected User run() throws Exception {
String url = serviceUrl + "/api/users";
return restTemplate.postForObject(url, user, User.class);
}
@Override
protected User getFallback() {
System.err.println("Fallback appelé pour CreateUserCommand");
// Retourne l'utilisateur avec un ID par défaut
User fallbackUser = new User(0L, user.getName(), user.getEmail(), user.getDepartment());
return fallbackUser;
}
}
4 Services utilisant Hystrix Commands
Service avec RestTemplate et Hystrix :
package com.example.client.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
/**
* Configuration de Hystrix et RestTemplate
*/
@Configuration
public class HystrixConfig {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
package com.example.client.service;
import com.example.client.hystrix.*;
import com.example.client.model.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.util.List;
/**
* Service utilisant Hystrix Commands avec RestTemplate
*/
@Service
public class HystrixRestTemplateService {
@Autowired
private RestTemplate restTemplate;
private static final String SERVICE_URL = "http://service-b";
/**
* Récupère un utilisateur avec Hystrix Command
*/
public User getUserById(Long id) {
GetUserCommand command = new GetUserCommand(restTemplate, id, SERVICE_URL);
return command.execute(); // Exécute la commande Hystrix
}
/**
* Récupère tous les utilisateurs avec Hystrix Command
*/
public List<User> getAllUsers() {
GetAllUsersCommand command = new GetAllUsersCommand(restTemplate, SERVICE_URL);
return command.execute();
}
/**
* Crée un utilisateur avec Hystrix Command
*/
public User createUser(User user) {
CreateUserCommand command = new CreateUserCommand(restTemplate, user, SERVICE_URL);
return command.execute();
}
/**
* Mise à jour d'utilisateur avec Hystrix Command
*/
public User updateUser(Long id, User user) {
UpdateUserCommand command = new UpdateUserCommand(restTemplate, id, user, SERVICE_URL);
return command.execute();
}
/**
* Suppression d'utilisateur avec Hystrix Command
*/
public void deleteUser(Long id) {
DeleteUserCommand command = new DeleteUserCommand(restTemplate, id, SERVICE_URL);
command.execute();
}
// === TESTS DE TOLÉRANCE AUX PANNES ===
public User getErrorUser() {
GetErrorUserCommand command = new GetErrorUserCommand(restTemplate, SERVICE_URL);
return command.execute();
}
public User getSlowUser() {
GetSlowUserCommand command = new GetSlowUserCommand(restTemplate, SERVICE_URL);
return command.execute();
}
}
package com.example.client.hystrix;
import com.example.client.model.User;
import com.netflix.hystrix.HystrixCommand;
import com.netflix.hystrix.HystrixCommandGroupKey;
import com.netflix.hystrix.HystrixCommandKey;
import com.netflix.hystrix.HystrixCommandProperties;
import org.springframework.web.client.RestTemplate;
/**
* Commande Hystrix pour mettre à jour un utilisateur
*/
public class UpdateUserCommand extends HystrixCommand<User> {
private final RestTemplate restTemplate;
private final Long userId;
private final User user;
private final String serviceUrl;
public UpdateUserCommand(RestTemplate restTemplate, Long userId, User user, String serviceUrl) {
super(Setter
.withGroupKey(HystrixCommandGroupKey.Factory.asKey("UserServiceGroup"))
.andCommandKey(HystrixCommandKey.Factory.asKey("UpdateUserCommand"))
.andCommandPropertiesDefaults(
HystrixCommandProperties.Setter()
.withExecutionTimeoutInMilliseconds(2000)
.withCircuitBreakerRequestVolumeThreshold(5)
.withCircuitBreakerErrorThresholdPercentage(50)
.withCircuitBreakerSleepWindowInMilliseconds(5000)
)
);
this.restTemplate = restTemplate;
this.userId = userId;
this.user = user;
this.serviceUrl = serviceUrl;
}
@Override
protected User run() throws Exception {
String url = serviceUrl + "/api/users/" + userId;
restTemplate.put(url, user);
return user; // Retourne l'utilisateur mis à jour
}
@Override
protected User getFallback() {
System.err.println("Fallback appelé pour UpdateUserCommand - userId: " + userId);
return new User(userId, user.getName(), user.getEmail(), user.getDepartment());
}
}
package com.example.client.hystrix;
import com.netflix.hystrix.HystrixCommand;
import com.netflix.hystrix.HystrixCommandGroupKey;
import com.netflix.hystrix.HystrixCommandKey;
import com.netflix.hystrix.HystrixCommandProperties;
import org.springframework.web.client.RestTemplate;
/**
* Commande Hystrix pour supprimer un utilisateur
*/
public class DeleteUserCommand extends HystrixCommand<Void> {
private final RestTemplate restTemplate;
private final Long userId;
private final String serviceUrl;
public DeleteUserCommand(RestTemplate restTemplate, Long userId, String serviceUrl) {
super(Setter
.withGroupKey(HystrixCommandGroupKey.Factory.asKey("UserServiceGroup"))
.andCommandKey(HystrixCommandKey.Factory.asKey("DeleteUserCommand"))
.andCommandPropertiesDefaults(
HystrixCommandProperties.Setter()
.withExecutionTimeoutInMilliseconds(2000)
.withCircuitBreakerRequestVolumeThreshold(5)
.withCircuitBreakerErrorThresholdPercentage(50)
.withCircuitBreakerSleepWindowInMilliseconds(5000)
)
);
this.restTemplate = restTemplate;
this.userId = userId;
this.serviceUrl = serviceUrl;
}
@Override
protected Void run() throws Exception {
String url = serviceUrl + "/api/users/" + userId;
restTemplate.delete(url);
return null;
}
@Override
protected Void getFallback() {
System.err.println("Fallback appelé pour DeleteUserCommand - userId: " + userId);
return null;
}
}
5 Hystrix avec OpenFeign
Client Feign avec Hystrix :
package com.example.client.feign;
import com.example.client.model.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* Client Feign pour le service utilisateurs avec Hystrix
*
* fallback - Classe de fallback implémentant l'interface
*/
@FeignClient(
name = "service-b",
fallback = UserServiceClientFallback.class // Fallback pour Hystrix
)
public interface UserServiceClient {
@GetMapping("/api/users")
List<User> getAllUsers();
@GetMapping("/api/users/{id}")
User getUserById(@PathVariable("id") Long id);
@PostMapping("/api/users")
User createUser(@RequestBody User user);
@PutMapping("/api/users/{id}")
User updateUser(@PathVariable("id") Long id, @RequestBody User user);
@DeleteMapping("/api/users/{id}")
void deleteUser(@PathVariable("id") Long id);
@GetMapping("/api/users")
List<User> getUsersByDepartment(@RequestParam("department") String department);
// Endpoints pour tests Hystrix
@GetMapping("/api/users/error")
User getErrorUser();
@GetMapping("/api/users/slow")
User getSlowUser();
}
package com.example.client.feign;
import com.example.client.model.User;
import org.springframework.stereotype.Component;
import java.util.Collections;
import java.util.List;
/**
* Fallback pour le client Feign UserService
* Implémente l'interface avec des réponses par défaut
*/
@Component
public class UserServiceClientFallback implements UserServiceClient {
@Override
public List<User> getAllUsers() {
System.err.println("Fallback: getAllUsers");
return Collections.singletonList(
new User(0L, "Utilisateur par défaut", "default@example.com", "DEFAULT")
);
}
@Override
public User getUserById(Long id) {
System.err.println("Fallback: getUserById - " + id);
return new User(id, "Utilisateur par défaut", "default@example.com", "DEFAULT");
}
@Override
public User createUser(User user) {
System.err.println("Fallback: createUser");
return new User(0L, user.getName(), user.getEmail(), user.getDepartment());
}
@Override
public User updateUser(Long id, User user) {
System.err.println("Fallback: updateUser - " + id);
return new User(id, user.getName(), user.getEmail(), user.getDepartment());
}
@Override
public void deleteUser(Long id) {
System.err.println("Fallback: deleteUser - " + id);
}
@Override
public List<User> getUsersByDepartment(String department) {
System.err.println("Fallback: getUsersByDepartment - " + department);
return Collections.singletonList(
new User(0L, "Utilisateur par défaut", "default@example.com", "DEFAULT")
);
}
@Override
public User getErrorUser() {
System.err.println("Fallback: getErrorUser");
return new User(999L, "Utilisateur d'erreur", "error@example.com", "ERROR");
}
@Override
public User getSlowUser() {
System.err.println("Fallback: getSlowUser");
return new User(888L, "Utilisateur lent", "slow@example.com", "SLOW");
}
}
6 Configuration de Hystrix
Configuration application.properties :
# === CONFIGURATION SERVEUR ===
server.port=8080
spring.application.name=client-service
# === CONFIGURATION EUREKA ===
eureka.client.service-url.defaultZone=http://localhost:8761/eureka/
eureka.client.register-with-eureka=true
eureka.client.fetch-registry=true
eureka.instance.prefer-ip-address=true
# === CONFIGURATION HYSTRIX ===
# Activation de Hystrix
feign.hystrix.enabled=true
# Configuration globale de Hystrix
hystrix.command.default.execution.timeout.enabled=true
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=3000
# Configuration du circuit breaker
hystrix.command.default.circuitBreaker.requestVolumeThreshold=5
hystrix.command.default.circuitBreaker.errorThresholdPercentage=50
hystrix.command.default.circuitBreaker.sleepWindowInMilliseconds=5000
# Configuration du thread pool
hystrix.threadpool.default.coreSize=10
hystrix.threadpool.default.maximumSize=20
hystrix.threadpool.default.maxQueueSize=100
hystrix.threadpool.default.queueSizeRejectionThreshold=80
# === CONFIGURATION DES COMMANDES SPÉCIFIQUES ===
# GetUserCommand
hystrix.command.GetUserCommand.execution.isolation.thread.timeoutInMilliseconds=2000
hystrix.command.GetUserCommand.circuitBreaker.requestVolumeThreshold=5
# GetAllUsersCommand
hystrix.command.GetAllUsersCommand.execution.isolation.thread.timeoutInMilliseconds=3000
hystrix.command.GetAllUsersCommand.circuitBreaker.requestVolumeThreshold=5
# CreateUserCommand
hystrix.command.CreateUserCommand.execution.isolation.thread.timeoutInMilliseconds=2000
hystrix.command.CreateUserCommand.circuitBreaker.requestVolumeThreshold=5
# === CONFIGURATION ACTUATOR ===
management.endpoints.web.exposure.include=health,info,metrics,hystrix.stream
management.endpoint.health.show-details=always
# === CONFIGURATION DU DASHBOARD HYSTRIX ===
hystrix.dashboard.proxy-stream-allow-list=*
7 Services Métier
Services utilisant Hystrix :
package com.example.client.service;
import com.example.client.feign.UserServiceClient;
import com.example.client.model.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* Service métier pour les utilisateurs avec Hystrix
*/
@Service
public class UserService {
@Autowired
private HystrixRestTemplateService hystrixRestTemplateService;
@Autowired
private UserServiceClient userServiceClient;
// === MÉTHODES AVEC HYSTRIX COMMANDS (RestTemplate) ===
public User getUserByIdHystrix(Long id) {
return hystrixRestTemplateService.getUserById(id);
}
public List<User> getAllUsersHystrix() {
return hystrixRestTemplateService.getAllUsers();
}
public User createUserHystrix(User user) {
return hystrixRestTemplateService.createUser(user);
}
public User updateUserHystrix(Long id, User user) {
return hystrixRestTemplateService.updateUser(id, user);
}
public void deleteUserHystrix(Long id) {
hystrixRestTemplateService.deleteUser(id);
}
public User getErrorUserHystrix() {
return hystrixRestTemplateService.getErrorUser();
}
public User getSlowUserHystrix() {
return hystrixRestTemplateService.getSlowUser();
}
// === MÉTHODES AVEC FEIGN ET HYSTRIX ===
public List<User> getAllUsersFeign() {
return userServiceClient.getAllUsers();
}
public User getUserByIdFeign(Long id) {
return userServiceClient.getUserById(id);
}
public User createUserFeign(User user) {
return userServiceClient.createUser(user);
}
public User updateUserFeign(Long id, User user) {
return userServiceClient.updateUser(id, user);
}
public void deleteUserFeign(Long id) {
userServiceClient.deleteUser(id);
}
public List<User> getUsersByDepartmentFeign(String department) {
return userServiceClient.getUsersByDepartment(department);
}
public User getErrorUserFeign() {
return userServiceClient.getErrorUser();
}
public User getSlowUserFeign() {
return userServiceClient.getSlowUser();
}
}
8 Contrôleur REST Client
Contrôleur exposant les fonctionnalités :
package com.example.client.controller;
import com.example.client.model.User;
import com.example.client.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* Contrôleur REST pour exposer les fonctionnalités Hystrix
*/
@RestController
@RequestMapping("/api/hystrix")
@CrossOrigin(origins = "*")
public class HystrixController {
@Autowired
private UserService userService;
// === ENDPOINTS AVEC HYSTRIX COMMANDS ===
@GetMapping("/users/hystrix/{id}")
public User getUserByIdHystrix(@PathVariable Long id) {
return userService.getUserByIdHystrix(id);
}
@GetMapping("/users/hystrix")
public List<User> getAllUsersHystrix() {
return userService.getAllUsersHystrix();
}
@PostMapping("/users/hystrix")
public User createUserHystrix(@RequestBody User user) {
return userService.createUserHystrix(user);
}
@PutMapping("/users/hystrix/{id}")
public User updateUserHystrix(@PathVariable Long id, @RequestBody User user) {
return userService.updateUserHystrix(id, user);
}
@DeleteMapping("/users/hystrix/{id}")
public ResponseEntity<Void> deleteUserHystrix(@PathVariable Long id) {
userService.deleteUserHystrix(id);
return ResponseEntity.ok().build();
}
@GetMapping("/users/hystrix/error")
public User getErrorUserHystrix() {
return userService.getErrorUserHystrix();
}
@GetMapping("/users/hystrix/slow")
public User getSlowUserHystrix() {
return userService.getSlowUserHystrix();
}
// === ENDPOINTS AVEC FEIGN ET HYSTRIX ===
@GetMapping("/users/feign")
public List<User> getAllUsersFeign() {
return userService.getAllUsersFeign();
}
@GetMapping("/users/feign/{id}")
public User getUserByIdFeign(@PathVariable Long id) {
return userService.getUserByIdFeign(id);
}
@PostMapping("/users/feign")
public User createUserFeign(@RequestBody User user) {
return userService.createUserFeign(user);
}
@PutMapping("/users/feign/{id}")
public User updateUserFeign(@PathVariable Long id, @RequestBody User user) {
return userService.updateUserFeign(id, user);
}
@DeleteMapping("/users/feign/{id}")
public ResponseEntity<Void> deleteUserFeign(@PathVariable Long id) {
userService.deleteUserFeign(id);
return ResponseEntity.ok().build();
}
@GetMapping("/users/feign/department/{department}")
public List<User> getUsersByDepartmentFeign(@PathVariable String department) {
return userService.getUsersByDepartmentFeign(department);
}
@GetMapping("/users/feign/error")
public User getErrorUserFeign() {
return userService.getErrorUserFeign();
}
@GetMapping("/users/feign/slow")
public User getSlowUserFeign() {
return userService.getSlowUserFeign();
}
}
Hystrix Dashboard
1 Configuration du Dashboard
Microservice Dashboard Hystrix :
<dependencies>
<!-- Hystrix Dashboard -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>
<!-- Actuator -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
package com.example.dashboard;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.hystrix.dashboard.EnableHystrixDashboard;
/**
* Application Hystrix Dashboard
*/
@SpringBootApplication
@EnableHystrixDashboard
public class HystrixDashboardApplication {
public static void main(String[] args) {
SpringApplication.run(HystrixDashboardApplication.class, args);
}
}
# === CONFIGURATION SERVEUR ===
server.port=9000
spring.application.name=hystrix-dashboard
# === CONFIGURATION DASHBOARD ===
hystrix.dashboard.proxy-stream-allow-list=*
# === CONFIGURATION ACTUATOR ===
management.endpoints.web.exposure.include=*
management.endpoint.health.show-details=always
2 Utilisation du Dashboard
Accès au Dashboard Hystrix :
Une fois démarré, le dashboard est accessible à l'URL : http://localhost:9000/hystrix
Pour monitorer un service, entrez l'URL du stream Hystrix : http://localhost:8080/actuator/hystrix.stream
Fonctionnalités du Dashboard :
- Circuit Status : État des circuits (Ouvert/Fermé)
- Error Rate : Taux d'erreurs en temps réel
- Request Volume : Volume de requêtes
- Latency : Temps de réponse
- Fallbacks : Nombre d'appels fallback
- Thread Pools : Utilisation des pools de threads
Configuration Avancée de Hystrix
1 Configuration Programmatique
Configuration via code :
package com.example.client.config;
import com.netflix.hystrix.HystrixCommandProperties;
import com.netflix.hystrix.HystrixThreadPoolProperties;
import org.springframework.context.annotation.Configuration;
/**
* Configuration programmatique de Hystrix
*/
@Configuration
public class HystrixProgrammaticConfig {
/**
* Configuration des propriétés Hystrix par défaut
*/
static {
// Configuration du circuit breaker
HystrixCommandProperties.Setter()
.withCircuitBreakerEnabled(true)
.withCircuitBreakerRequestVolumeThreshold(10)
.withCircuitBreakerErrorThresholdPercentage(50)
.withCircuitBreakerSleepWindowInMilliseconds(5000);
// Configuration du thread pool
HystrixThreadPoolProperties.Setter()
.withCoreSize(10)
.withMaximumSize(20)
.withMaxQueueSize(100)
.withQueueSizeRejectionThreshold(80);
// Configuration des timeouts
HystrixCommandProperties.Setter()
.withExecutionTimeoutEnabled(true)
.withExecutionTimeoutInMilliseconds(3000);
}
}
2 Métriques et Monitoring
Collecte de métriques :
package com.example.client.metrics;
import com.netflix.hystrix.HystrixCommandMetrics;
import com.netflix.hystrix.HystrixThreadPoolMetrics;
import org.springframework.stereotype.Service;
import java.util.Collection;
/**
* Service pour collecter les métriques Hystrix
*/
@Service
public class HystrixMetricsService {
/**
* Récupère les métriques des commandes Hystrix
*/
public void logCommandMetrics() {
Collection<HystrixCommandMetrics> commandMetrics = HystrixCommandMetrics.getInstances();
for (HystrixCommandMetrics metrics : commandMetrics) {
System.out.println("=== Métriques pour la commande: " + metrics.getCommandKey().name() + " ===");
System.out.println("Requêtes totales: " + metrics.getHealthCounts().getTotalRequests());
System.out.println("Erreurs: " + metrics.getHealthCounts().getErrorCount());
System.out.println("Taux d'erreurs: " + metrics.getHealthCounts().getErrorPercentage() + "%");
System.out.println("Temps moyen: " + metrics.getExecutionTimeMean() + "ms");
System.out.println("99ème percentile: " + metrics.getExecutionTimePercentile(99) + "ms");
System.out.println();
}
}
/**
* Récupère les métriques des pools de threads
*/
public void logThreadPoolMetrics() {
Collection<HystrixThreadPoolMetrics> threadPoolMetrics = HystrixThreadPoolMetrics.getInstances();
for (HystrixThreadPoolMetrics metrics : threadPoolMetrics) {
System.out.println("=== Métriques du pool de threads: " + metrics.getThreadPoolKey().name() + " ===");
System.out.println("Threads actifs: " + metrics.getCurrentActiveCount().intValue());
System.out.println("Taille du pool: " + metrics.getCurrentPoolSize().intValue());
System.out.println("Files d'attente: " + metrics.getCurrentQueueSize().intValue());
System.out.println("Rejets de requêtes: " + metrics.getRollingCountThreadsRejected().intValue());
System.out.println();
}
}
/**
* Vérifie l'état des circuits
*/
public void logCircuitStates() {
Collection<HystrixCommandMetrics> commandMetrics = HystrixCommandMetrics.getInstances();
for (HystrixCommandMetrics metrics : commandMetrics) {
System.out.println("=== État du circuit: " + metrics.getCommandKey().name() + " ===");
System.out.println("État: " + (metrics.isCircuitBreakerOpen() ? "OUVERT" : "FERMÉ"));
System.out.println("Requêtes dans la fenêtre: " + metrics.getRollingCount(HystrixCommandMetrics.HealthCounts.class).getTotalRequests());
System.out.println();
}
}
}
Tests et Validation
1 Tests Unitaires
Tests de Hystrix :
package com.example.client.hystrix;
import com.example.client.model.User;
import com.netflix.hystrix.HystrixCommand;
import com.netflix.hystrix.HystrixCommandGroupKey;
import com.netflix.hystrix.exception.HystrixRuntimeException;
import org.junit.jupiter.api.Test;
import org.springframework.web.client.RestTemplate;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
class GetUserCommandTest {
@Test
void testSuccessfulExecution() {
// Given
RestTemplate restTemplate = mock(RestTemplate.class);
Long userId = 1L;
String serviceUrl = "http://service-b";
User expectedUser = new User(userId, "Test User", "test@example.com", "TEST");
when(restTemplate.getForObject(anyString(), eq(User.class)))
.thenReturn(expectedUser);
GetUserCommand command = new GetUserCommand(restTemplate, userId, serviceUrl);
// When
User result = command.execute();
// Then
assertNotNull(result);
assertEquals(userId, result.getId());
assertEquals("Test User", result.getName());
verify(restTemplate).getForObject(anyString(), eq(User.class));
}
@Test
void testFallbackExecution() {
// Given
RestTemplate restTemplate = mock(RestTemplate.class);
Long userId = 1L;
String serviceUrl = "http://service-b";
// Simuler une exception
when(restTemplate.getForObject(anyString(), eq(User.class)))
.thenThrow(new RuntimeException("Service indisponible"));
GetUserCommand command = new GetUserCommand(restTemplate, userId, serviceUrl);
// When
User result = command.execute();
// Then
assertNotNull(result);
assertEquals(userId, result.getId());
assertEquals("Utilisateur par défaut", result.getName());
assertEquals("default@example.com", result.getEmail());
}
@Test
void testCircuitBreakerOpens() {
// Given
RestTemplate restTemplate = mock(RestTemplate.class);
String serviceUrl = "http://service-b";
// Simuler plusieurs échecs
when(restTemplate.getForObject(anyString(), eq(User.class)))
.thenThrow(new RuntimeException("Service indisponible"));
// When & Then - Exécuter plusieurs fois pour ouvrir le circuit
for (int i = 0; i < 10; i++) {
GetUserCommand command = new GetUserCommand(restTemplate, (long)i, serviceUrl);
try {
command.execute();
} catch (HystrixRuntimeException e) {
// Circuit breaker ouvert
if (i >= 5) { // Après 5 erreurs, le circuit devrait s'ouvrir
System.out.println("Circuit breaker ouvert à l'itération " + i);
}
}
}
}
}
2 Tests d'Intégration
Tests avec Spring Boot :
package com.example.client.integration;
import com.example.client.model.User;
import com.example.client.service.UserService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.TestPropertySource;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
@TestPropertySource(properties = {
"eureka.client.enabled=false"
})
class HystrixIntegrationTest {
@Autowired
private UserService userService;
@Test
void testHystrixCommandWithFallback() {
// Test avec un service qui tombe en panne
// Hystrix devrait appeler le fallback
User result = userService.getErrorUserHystrix();
assertNotNull(result);
// Vérifier que c'est l'utilisateur par défaut (fallback)
assertEquals("Utilisateur par défaut", result.getName());
assertEquals("default@example.com", result.getEmail());
}
@Test
void testHystrixFeignWithFallback() {
// Test avec Feign + Hystrix fallback
User result = userService.getErrorUserFeign();
assertNotNull(result);
// Vérifier que c'est l'utilisateur d'erreur (fallback de Feign)
assertEquals("Utilisateur d'erreur", result.getName());
assertEquals("error@example.com", result.getEmail());
}
@Test
void testHystrixTimeoutHandling() {
// Test de la gestion des timeouts
User result = userService.getSlowUserHystrix();
assertNotNull(result);
// Le fallback devrait être appelé en cas de timeout
assertTrue(result.getName().contains("par défaut") || result.getName().contains("lent"));
}
}
Tableau Récapitulatif des Fonctionnalités
| Fonctionnalité | Hystrix | Resilience4j | État |
|---|---|---|---|
| Circuit Breaker | @HystrixCommand | @CircuitBreaker | ✅ Disponible |
| Retry | Limité | @Retry | ✅ Amélioré |
| Rate Limiter | Non | @RateLimiter | ✅ Nouveau |
| Bulkhead | Thread Pools | @Bulkhead | ✅ Amélioré |
| Time Limiter | Timeouts | @TimeLimiter | ✅ Nouveau |
| Monitoring | Hystrix Dashboard | Actuator + Métriques | ✅ Évolué |
| Maintenance | Déprécié (2018) | Actif | ⚠️ Hystrix / ✅ Resilience4j |
Bonnes Pratiques et Conseils
1 Configuration Optimale
Configuration recommandée pour Hystrix :
# === CONFIGURATION HYSTRIX OPTIMALE ===
# Timeout raisonnable
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=3000
# Circuit breaker sensible
hystrix.command.default.circuitBreaker.requestVolumeThreshold=10
hystrix.command.default.circuitBreaker.errorThresholdPercentage=50
hystrix.command.default.circuitBreaker.sleepWindowInMilliseconds=10000
# Pool de threads adapté
hystrix.threadpool.default.coreSize=20
hystrix.threadpool.default.maximumSize=50
hystrix.threadpool.default.maxQueueSize=100
# === CONFIGURATION SPÉCIFIQUE PAR COMMANDE ===
hystrix.command.GetUserCommand.execution.isolation.thread.timeoutInMilliseconds=2000
hystrix.command.CreateUserCommand.execution.isolation.thread.timeoutInMilliseconds=5000
2 Gestion des Fallbacks
Fallbacks robustes :
public class RobustUserCommand extends HystrixCommand<User> {
@Override
protected User run() throws Exception {
// Logique normale
return userService.getUserById(userId);
}
@Override
protected User getFallback() {
// Logging détaillé
log.error("Fallback activé pour l'utilisateur {}: {}", userId, getFailedExecutionException().getMessage());
// Métriques personnalisées
metricsService.incrementFallbackCounter("getUserById");
// Cache fallback
User cachedUser = cacheService.getUserFromCache(userId);
if (cachedUser != null) {
return cachedUser;
}
// Données par défaut avec contexte
return User.builder()
.id(userId)
.name("Utilisateur temporairement indisponible")
.email("unavailable@example.com")
.department("UNKNOWN")
.build();
}
}
3 Monitoring et Alertes
Alertes importantes :
- Taux d'erreurs élevé : > 50% sur 10 requêtes consécutives
- Circuit breaker ouvert : Indique un service en difficulté
- Timeouts fréquents : Problème de performance
- Fallbacks élevés : Impact sur l'expérience utilisateur
- Saturation thread pool : Risque de blocage
Configuration des alertes :
@Component
public class HystrixAlertService {
@EventListener
public void onHystrixEvent(HystrixCommandExecutionEvent event) {
if (event.getEventType() == HystrixEventType.FAILURE) {
alertService.sendAlert("Hystrix failure detected: " + event.getCommandKey());
}
}
@EventListener
public void onCircuitBreakerEvent(HystrixCircuitBreakerEvent event) {
if (event.getCircuitBreakerState() == CircuitBreakerState.OPEN) {
alertService.sendAlert("Circuit breaker opened: " + event.getCommandKey());
}
}
}
Conclusion
Points Clés à Retenir
⚠️ Hystrix est DÉPRÉCIÉ
Utilisez Resilience4j pour les nouveaux projets !
Résumé historique :
- Hystrix était la solution Netflix pour la tolérance aux pannes
- Utilisait HystrixCommand pour encapsuler les appels distants
- Fournissait des fallbacks pour gérer les échecs gracieusement
- Incluait un Dashboard pour le monitoring en temps réel
- Déprécié en 2018 au profit de solutions plus légères