Spring Cloud LoadBalancer
Équilibrage de charge client-side pour microservices
Introduction à Spring Cloud LoadBalancer
Qu'est-ce que Spring Cloud LoadBalancer ?
Spring Cloud LoadBalancer est un équilibreur de charge client-side qui remplace Ribbon dans l'écosystème Spring Cloud. Il permet de distribuer les requêtes entre plusieurs instances d'un même service de manière intelligente, améliorant ainsi la disponibilité et la performance des applications distribuées.
graph LR
A[Client
Service A] --> B[LoadBalancer] B --> C[Service B - Instance 1] B --> D[Service B - Instance 2] B --> E[Service B - Instance 3] style A fill:#FF9800,stroke:#E65100 style B fill:#4CAF50,stroke:#388E3C style C fill:#2196F3,stroke:#0D47A1 style D fill:#2196F3,stroke:#0D47A1 style E fill:#2196F3,stroke:#0D47A1
Service A] --> B[LoadBalancer] B --> C[Service B - Instance 1] B --> D[Service B - Instance 2] B --> E[Service B - Instance 3] style A fill:#FF9800,stroke:#E65100 style B fill:#4CAF50,stroke:#388E3C style C fill:#2196F3,stroke:#0D47A1 style D fill:#2196F3,stroke:#0D47A1 style E fill:#2196F3,stroke:#0D47A1
Avantages de Spring Cloud LoadBalancer :
- Client-Side : L'équilibrage se fait côté client
- Intégration Spring : Fonctionne parfaitement avec Spring Cloud
- Stratégies d'équilibrage : Round Robin, Random, Weighted
- Health Checks : Vérification de la santé des instances
- Extensible : Personnalisation des règles d'équilibrage
- Reactive : Support natif de Spring WebFlux
Architecture avec LoadBalancer
Structure de l'application
graph TD
A[Eureka Server
Port: 8761] --> B[Service A
Client - Port: 8080] A --> C1[Service B - Instance 1
Port: 8081] A --> C2[Service B - Instance 2
Port: 8082] A --> C3[Service B - Instance 3
Port: 8083] B -- LoadBalancer --> C1 B -- LoadBalancer --> C2 B -- LoadBalancer --> C3 style A fill:#2196F3,stroke:#0D47A1 style B fill:#FF9800,stroke:#E65100 style C1 fill:#4CAF50,stroke:#388E3C style C2 fill:#4CAF50,stroke:#388E3C style C3 fill:#4CAF50,stroke:#388E3C
Port: 8761] --> B[Service A
Client - Port: 8080] A --> C1[Service B - Instance 1
Port: 8081] A --> C2[Service B - Instance 2
Port: 8082] A --> C3[Service B - Instance 3
Port: 8083] B -- LoadBalancer --> C1 B -- LoadBalancer --> C2 B -- LoadBalancer --> C3 style A fill:#2196F3,stroke:#0D47A1 style B fill:#FF9800,stroke:#E65100 style C1 fill:#4CAF50,stroke:#388E3C style C2 fill:#4CAF50,stroke:#388E3C style C3 fill:#4CAF50,stroke:#388E3C
Rôles des composants :
- Service A : Client qui utilise LoadBalancer pour communiquer
- Service B : Service avec plusieurs instances (1, 2, 3)
- Eureka Server : Serveur de découverte de services
- LoadBalancer : Équilibreur de charge client-side
Service B - Service Utilisateurs (Multiples Instances)
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 :
serviceB/pom.xml
<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 :
serviceB/src/main/resources/application.properties (Instance 1 - Port 8081)
# === 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
# === IDENTIFICATION DE L'INSTANCE ===
eureka.instance.instance-id=${spring.application.name}:${server.port}
# === CONFIGURATION ACTUATOR ===
management.endpoints.web.exposure.include=health,info,metrics
management.endpoint.health.show-details=always
Instance 1
This one runs normally because it uses application.properties:
mvn spring-boot:run
serviceB/src/main/resources/application-instance2.properties (Instance 2 - Port 8082)
# === CONFIGURATION SERVEUR ===
server.port=8082
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
# === IDENTIFICATION DE L'INSTANCE ===
eureka.instance.instance-id=${spring.application.name}:${server.port}
# === CONFIGURATION ACTUATOR ===
management.endpoints.web.exposure.include=health,info,metrics
management.endpoint.health.show-details=always
Instance 2
mvn spring-boot:run -Dspring-boot.run.arguments="--spring.config.name=application-instance2"
serviceB/src/main/resources/application-instance3.properties (Instance 3 - Port 8083)
# === CONFIGURATION SERVEUR ===
server.port=8083
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
# === IDENTIFICATION DE L'INSTANCE ===
eureka.instance.instance-id=${spring.application.name}:${server.port}
# === CONFIGURATION ACTUATOR ===
management.endpoints.web.exposure.include=health,info,metrics
management.endpoint.health.show-details=always
4 Modèle User
Modèle User :
serviceB/src/main/java/com/example/users/model/User.java
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;
private String instanceInfo; // Information sur l'instance qui a traité la requête
// Constructeurs
// Getters et Setters
5 Contrôleur REST
Contrôleur REST :
serviceB/src/main/java/com/example/users/controller/UserController.java
package com.example.users.controller;
import com.example.users.model.User;
import org.springframework.beans.factory.annotation.Value;
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 {
@Value("${server.port}")
private int serverPort;
@Value("${spring.application.name}")
private String applicationName;
// 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 avec info d'instance
*/
@GetMapping
public List<User> getAllUsers() {
String instanceInfo = applicationName + ":" + serverPort;
List<User> userList = new ArrayList<>();
for (User user : users.values()) {
User userWithInstance = new User(
user.getId(),
user.getName(),
user.getEmail(),
user.getDepartment(),
instanceInfo
);
userList.add(userWithInstance);
}
return userList;
}
/**
* Récupère un utilisateur par son ID
* @param id ID de l'utilisateur
* @return Utilisateur trouvé avec info d'instance
*/
@GetMapping("/{id}")
public User getUserById(@PathVariable Long id) {
String instanceInfo = applicationName + ":" + serverPort;
User user = users.get(id);
if (user != null) {
return new User(
user.getId(),
user.getName(),
user.getEmail(),
user.getDepartment(),
instanceInfo
);
}
return null;
}
/**
* Crée un nouvel utilisateur
* @param user Utilisateur à créer
* @return Utilisateur créé avec info d'instance
*/
@PostMapping
public User createUser(@RequestBody User user) {
String instanceInfo = applicationName + ":" + serverPort;
Long id = counter.incrementAndGet();
user.setId(id);
user.setInstanceInfo(instanceInfo);
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 avec info d'instance
*/
@PutMapping("/{id}")
public User updateUser(@PathVariable Long id, @RequestBody User user) {
String instanceInfo = applicationName + ":" + serverPort;
user.setId(id);
user.setInstanceInfo(instanceInfo);
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) {
String instanceInfo = applicationName + ":" + serverPort;
List<User> result = new ArrayList<>();
for (User user : users.values()) {
if (user.getDepartment().equals(department)) {
User userWithInstance = new User(
user.getId(),
user.getName(),
user.getEmail(),
user.getDepartment(),
instanceInfo
);
result.add(userWithInstance);
}
}
return result;
}
/**
* Endpoint pour tester la distribution de charge
*/
@GetMapping("/instance-info")
public String getInstanceInfo() {
return "Service B - Instance " + serverPort;
}
/**
* Endpoint qui simule une erreur pour tester LoadBalancer
*/
@GetMapping("/error")
public User getErrorUser() {
// Simule une erreur 30% du temps
if (Math.random() > 0.7) {
throw new RuntimeException("Erreur simulée sur instance " + serverPort);
}
String instanceInfo = applicationName + ":" + serverPort;
return new User(999L, "Error Test", "error@test.com", "TEST", instanceInfo);
}
/**
* Endpoint qui simule un timeout pour tester LoadBalancer
*/
@GetMapping("/slow")
public User getSlowUser() throws InterruptedException {
// Simule un traitement lent 20% du temps
if (Math.random() > 0.8) {
Thread.sleep(3000); // 3 secondes
}
String instanceInfo = applicationName + ":" + serverPort;
return new User(888L, "Slow Test", "slow@test.com", "TEST", instanceInfo);
}
}
6 Classe principale
Application Spring Boot :
serviceB/src/main/java/com/example/users/UsersMicroserviceApplication.java
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 LoadBalancer
1 Configuration de Base de LoadBalancer
Dépendances Maven :
serviceA/pom.xml
<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>
<!-- Spring Cloud LoadBalancer -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</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>
<!-- Actuator pour monitoring -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
Activation des fonctionnalités :
serviceA/src/main/java/com/example/client/ClientMicroserviceApplication.java
package com.example.client;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
/**
* Classe principale du microservice client avec LoadBalancer
*/
@SpringBootApplication
@EnableDiscoveryClient
@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 :
serviceA/src/main/java/com/example/client/model/User.java
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;
private String instanceInfo; // Information sur l'instance qui a traité la requête
// Constructeurs
// Getters et Setters
3 Configuration de LoadBalancer avec RestTemplate
Configuration de RestTemplate :
serviceA/src/main/java/com/example/client/config/LoadBalancerConfig.java
package com.example.client.config;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
/**
* Configuration de LoadBalancer avec RestTemplate
*/
@Configuration
public class LoadBalancerConfig {
/**
* @LoadBalanced - Active le LoadBalancer pour ce RestTemplate
* Spring Cloud LoadBalancer interceptera automatiquement les appels
* vers des services enregistrés dans Eureka
*/
@LoadBalanced
@Bean
public RestTemplate loadBalancedRestTemplate() {
return new RestTemplate();
}
/**
* RestTemplate non load-balancé pour les appels externes
*/
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
Service utilisant RestTemplate avec LoadBalancer :
serviceA/src/main/java/com/example/client/service/LoadBalancerRestTemplateService.java
package com.example.client.service;
import com.example.client.model.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.util.Arrays;
import java.util.List;
/**
* Service utilisant RestTemplate avec LoadBalancer
*/
@Service
public class LoadBalancerRestTemplateService {
/**
* RestTemplate load-balancé - injecté avec @LoadBalanced
@Qualifier tells Spring exactly which bean to inject by its name.
In this case, it is looking for a bean with the name "loadBalancedRestTemplate".
*/
@Autowired
@Qualifier("loadBalancedRestTemplate")
private RestTemplate loadBalancedRestTemplate;
/**
* RestTemplate non load-balancé - pour les appels externes
*/
@Autowired
@Qualifier("restTemplate")
private RestTemplate restTemplate;
// === UTILISATION DU LOADBALANCER ===
/**
* Récupère tous les utilisateurs en utilisant le LoadBalancer
* L'URL utilise le nom du service (service-b) au lieu d'une IP/port spécifique
*/
public List<User> getAllUsers() {
// URL avec nom du service - LoadBalancer choisira l'instance
String url = "http://service-b/api/users";
User[] usersArray = loadBalancedRestTemplate.getForObject(url, User[].class);
return Arrays.asList(usersArray);
}
/**
* Récupère un utilisateur par ID avec LoadBalancer
*/
public User getUserById(Long id) {
String url = "http://service-b/api/users/" + id;
return loadBalancedRestTemplate.getForObject(url, User.class);
}
/**
* Crée un utilisateur avec LoadBalancer
*/
public User createUser(User user) {
String url = "http://service-b/api/users";
return loadBalancedRestTemplate.postForObject(url, user, User.class);
}
/**
* Met à jour un utilisateur avec LoadBalancer
*/
public User updateUser(Long id, User user) {
String url = "http://service-b/api/users/" + id;
loadBalancedRestTemplate.put(url, user);
return getUserById(id); // Retourne l'utilisateur mis à jour
}
/**
* Supprime un utilisateur avec LoadBalancer
*/
public void deleteUser(Long id) {
String url = "http://service-b/api/users/" + id;
loadBalancedRestTemplate.delete(url);
}
/**
* Recherche par département avec LoadBalancer
*/
public List<User> getUsersByDepartment(String department) {
String url = "http://service-b/api/users?department=" + department;
User[] usersArray = loadBalancedRestTemplate.getForObject(url, User[].class);
return Arrays.asList(usersArray);
}
// === UTILISATION SANS LOADBALANCER ===
/**
* Appel à un service externe sans LoadBalancer
*/
public String callExternalService() {
String url = "https://jsonplaceholder.typicode.com/posts/1";
return restTemplate.getForObject(url, String.class);
}
/**
* Appel direct à une instance spécifique (sans LoadBalancer)
*/
public User callSpecificInstance(String host, int port, Long id) {
String url = "http://" + host + ":" + port + "/api/users/" + id;
return restTemplate.getForObject(url, User.class);
}
// === TESTS DE LOADBALANCING ===
/**
* Test de distribution de charge
*/
public String getInstanceInfo() {
String url = "http://service-b/api/users/instance-info";
return loadBalancedRestTemplate.getForObject(url, String.class);
}
/**
* Test avec gestion d'erreurs
*/
public User getErrorUser() {
String url = "http://service-b/api/users/error";
return loadBalancedRestTemplate.getForObject(url, User.class);
}
/**
* Test avec timeout
*/
public User getSlowUser() {
String url = "http://service-b/api/users/slow";
return loadBalancedRestTemplate.getForObject(url, User.class);
}
}
4 Configuration de LoadBalancer avec OpenFeign
Client Feign avec LoadBalancer :
serviceA/src/main/java/com/example/client/feign/UserServiceClient.java
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 LoadBalancer
*
* @FeignClient - Le nom du service active automatiquement le LoadBalancer
* Spring Cloud LoadBalancer est intégré nativement avec OpenFeign
*/
@FeignClient(
name = "service-b" // Nom du service - LoadBalancer activé automatiquement
// url = "http://localhost:8081" // Ne pas spécifier pour utiliser LoadBalancer
)
public interface UserServiceClient {
/**
* Toutes les méthodes utilisent automatiquement le LoadBalancer
* grâce à l'annotation @FeignClient(name = "service-b")
*/
@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 LoadBalancer
@GetMapping("/api/users/instance-info")
String getInstanceInfo();
@GetMapping("/api/users/error")
User getErrorUser();
@GetMapping("/api/users/slow")
User getSlowUser();
}
Service utilisant Feign avec LoadBalancer :
serviceA/src/main/java/com/example/client/service/LoadBalancerFeignService.java
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 utilisant Feign avec LoadBalancer intégré
*/
@Service
public class LoadBalancerFeignService {
@Autowired
private UserServiceClient userServiceClient;
// === MÉTHODES AVEC LOADBALANCER (via Feign) ===
/**
* Toutes les méthodes utilisent automatiquement le LoadBalancer
* OpenFeign + Spring Cloud LoadBalancer fonctionnent ensemble nativement
*/
public List<User> getAllUsers() {
return userServiceClient.getAllUsers();
}
public User getUserById(Long id) {
return userServiceClient.getUserById(id);
}
public User createUser(User user) {
return userServiceClient.createUser(user);
}
public User updateUser(Long id, User user) {
return userServiceClient.updateUser(id, user);
}
public void deleteUser(Long id) {
userServiceClient.deleteUser(id);
}
public List<User> getUsersByDepartment(String department) {
return userServiceClient.getUsersByDepartment(department);
}
// === TESTS DE LOADBALANCING ===
public String getInstanceInfo() {
return userServiceClient.getInstanceInfo();
}
public User getErrorUser() {
return userServiceClient.getErrorUser();
}
public User getSlowUser() {
return userServiceClient.getSlowUser();
}
}
5 Configuration Avancée de LoadBalancer
Configuration application.properties :
serviceA/src/main/resources/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 LOADBALANCER ===
# Activation du LoadBalancer Reactif (par défaut depuis 2020)
spring.cloud.loadbalancer.ribbon.enabled=false
# Configuration du LoadBalancer
spring.cloud.loadbalancer.configurations.default.enabled=true
# === CONFIGURATION DES STRATÉGIES D'ÉQUILIBRAGE ===
# Stratégie par défaut - Round Robin
spring.cloud.loadbalancer.algorithm.default=round_robin
# Configuration spécifique pour service-b
spring.cloud.loadbalancer.algorithm.service-b=round_robin
# === CONFIGURATION DES HEALTH CHECKS ===
# Activation des health checks
spring.cloud.loadbalancer.health-check.enabled=true
# Intervalle des health checks
spring.cloud.loadbalancer.health-check.interval=30s
# Timeout des health checks
spring.cloud.loadbalancer.health-check.timeout=5s
# Chemin du health check
spring.cloud.loadbalancer.health-check.path=/actuator/health
# === CONFIGURATION DU CACHE ===
# Durée de vie du cache des instances
spring.cloud.loadbalancer.cache.ttl=30s
# Taille maximale du cache
spring.cloud.loadbalancer.cache.max-size=1000
# === CONFIGURATION ACTUATOR ===
management.endpoints.web.exposure.include=health,info,metrics,httptrace
management.endpoint.health.show-details=always
# === CONFIGURATION DES RETRIES ===
# (Pour les clients non réactifs)
spring.cloud.loadbalancer.retry.enabled=true
spring.cloud.loadbalancer.retry.max-attempts-on-next-service-instance=3
spring.cloud.loadbalancer.retry.max-attempts-on-same-service-instance=2
6 Stratégies d'Équilibrage Personnalisées
Implémentation de stratégies personnalisées :
serviceA/src/main/java/com/example/client/loadbalancer/WeightedLoadBalancer.java
package com.example.client.loadbalancer;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.reactive.ReactiveLoadBalancer;
import org.springframework.cloud.loadbalancer.core.ReactorLoadBalancer;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import reactor.core.publisher.Mono;
import java.util.List;
import java.util.Random;
/**
* Implémentation personnalisée d'un LoadBalancer pondéré
*/
@Configuration
public class WeightedLoadBalancerConfig {
@Bean
public ReactorLoadBalancer<ServiceInstance> weightedLoadBalancer(
Environment environment,
LoadBalancerClientFactory loadBalancerClientFactory) {
String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
return new WeightedLoadBalancer(
loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class),
name
);
}
/**
* LoadBalancer pondéré - attribue des poids aux instances
*/
static class WeightedLoadBalancer implements ReactorLoadBalancer<ServiceInstance> {
private final ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;
private final String serviceId;
private final Random random = new Random();
WeightedLoadBalancer(
ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider,
String serviceId) {
this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider;
this.serviceId = serviceId;
}
@Override
public Mono<ServiceInstance> choose(Request request) {
ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider
.getIfAvailable();
return supplier.get()
.next()
.map(this::chooseInstance);
}
private ServiceInstance chooseInstance(List<ServiceInstance> instances) {
if (instances.isEmpty()) {
return null;
}
if (instances.size() == 1) {
return instances.get(0);
}
// Implémentation simple de pondération
// Ici, on pourrait utiliser des métriques ou des configurations
int totalWeight = instances.size() * 10; // Poids par défaut
int randomWeight = random.nextInt(totalWeight);
int currentWeight = 0;
for (ServiceInstance instance : instances) {
currentWeight += 10; // Poids fixe pour l'exemple
if (randomWeight <= currentWeight) {
return instance;
}
}
return instances.get(0);
}
@Override
public String getServiceId() {
return serviceId;
}
}
}
serviceA/src/main/java/com/example/client/loadbalancer/CustomLoadBalancerConfiguration.java
package com.example.client.loadbalancer;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.loadbalancer.core.ReactorLoadBalancer;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
/**
* Configuration personnalisée du LoadBalancer
*/
@Configuration
public class CustomLoadBalancerConfiguration {
@Bean
@ConditionalOnMissingBean
public ReactorLoadBalancer<ServiceInstance> reactorServiceInstanceLoadBalancer(
Environment environment,
LoadBalancerClientFactory loadBalancerClientFactory) {
String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
return new CustomLoadBalancer(
loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class),
name
);
}
/**
* LoadBalancer personnalisé - exemple de sélection basée sur des critères
*/
static class CustomLoadBalancer implements ReactorLoadBalancer<ServiceInstance> {
private final org.springframework.beans.factory.ObjectProvider<ServiceInstanceListSupplier>
serviceInstanceListSupplierProvider;
private final String serviceId;
CustomLoadBalancer(
org.springframework.beans.factory.ObjectProvider<ServiceInstanceListSupplier>
serviceInstanceListSupplierProvider,
String serviceId) {
this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider;
this.serviceId = serviceId;
}
@Override
public reactor.core.publisher.Mono<ServiceInstance> choose(
org.springframework.cloud.client.loadbalancer.reactive.Request<?> request) {
org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier supplier =
serviceInstanceListSupplierProvider.getIfAvailable();
return supplier.get()
.next()
.map(this::selectBestInstance);
}
private ServiceInstance selectBestInstance(List<ServiceInstance> instances) {
if (instances.isEmpty()) {
return null;
}
// Sélection de l'instance avec le port le plus bas (exemple simple)
return instances.stream()
.min((i1, i2) -> Integer.compare(i1.getPort(), i2.getPort()))
.orElse(instances.get(0));
}
@Override
public String getServiceId() {
return serviceId;
}
}
}
7 Health Checks et Monitoring
Configuration des health checks :
serviceA/src/main/java/com/example/client/health/CustomHealthIndicator.java
package com.example.client.health;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;
/**
* Indicateur de santé personnalisé pour le LoadBalancer
*/
@Component
public class CustomHealthIndicator implements HealthIndicator {
@Override
public Health health() {
// Logique personnalisée de vérification de santé
boolean isLoadBalancerHealthy = checkLoadBalancerHealth();
if (isLoadBalancerHealthy) {
return Health.up()
.withDetail("LoadBalancer", "Fonctionne correctement")
.withDetail("Instances disponibles", getAvailableInstances())
.build();
} else {
return Health.down()
.withDetail("LoadBalancer", "Problème détecté")
.withDetail("Erreur", "Impossible de joindre les instances")
.build();
}
}
private boolean checkLoadBalancerHealth() {
// Implémentation de la vérification de santé
// Par exemple : vérifier la connectivité aux instances
try {
// Logique de vérification
return true;
} catch (Exception e) {
return false;
}
}
private int getAvailableInstances() {
// Retourner le nombre d'instances disponibles
return 3; // Exemple
}
}
serviceA/src/main/java/com/example/client/config/LoadBalancerHealthConfig.java
package com.example.client.config;
import org.springframework.cloud.client.loadbalancer.LoadBalancerHealthIndicator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Configuration des health checks pour le LoadBalancer
*/
@Configuration
public class LoadBalancerHealthConfig {
@Bean
public LoadBalancerHealthIndicator loadBalancerHealthIndicator() {
return new LoadBalancerHealthIndicator();
}
}
8 Services Métier
Services utilisant LoadBalancer :
serviceA/src/main/java/com/example/client/service/UserService.java
package com.example.client.service;
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 LoadBalancer
*/
@Service
public class UserService {
@Autowired
private LoadBalancerRestTemplateService restTemplateService;
@Autowired
private LoadBalancerFeignService feignService;
// === MÉTHODES AVEC RESTTEMPLATE ET LOADBALANCER ===
public List<User> getAllUsersRest() {
return restTemplateService.getAllUsers();
}
public User getUserByIdRest(Long id) {
return restTemplateService.getUserById(id);
}
public User createUserRest(User user) {
return restTemplateService.createUser(user);
}
public User updateUserRest(Long id, User user) {
return restTemplateService.updateUser(id, user);
}
public void deleteUserRest(Long id) {
restTemplateService.deleteUser(id);
}
public List<User> getUsersByDepartmentRest(String department) {
return restTemplateService.getUsersByDepartment(department);
}
public String getInstanceInfoRest() {
return restTemplateService.getInstanceInfo();
}
public User getErrorUserRest() {
return restTemplateService.getErrorUser();
}
public User getSlowUserRest() {
return restTemplateService.getSlowUser();
}
// === MÉTHODES AVEC FEIGN ET LOADBALANCER ===
public List<User> getAllUsersFeign() {
return feignService.getAllUsers();
}
public User getUserByIdFeign(Long id) {
return feignService.getUserById(id);
}
public User createUserFeign(User user) {
return feignService.createUser(user);
}
public User updateUserFeign(Long id, User user) {
return feignService.updateUser(id, user);
}
public void deleteUserFeign(Long id) {
feignService.deleteUser(id);
}
public List<User> getUsersByDepartmentFeign(String department) {
return feignService.getUsersByDepartment(department);
}
public String getInstanceInfoFeign() {
return feignService.getInstanceInfo();
}
public User getErrorUserFeign() {
return feignService.getErrorUser();
}
public User getSlowUserFeign() {
return feignService.getSlowUser();
}
}
9 Contrôleur REST Client
Contrôleur exposant les fonctionnalités :
serviceA/src/main/java/com/example/client/controller/LoadBalancerController.java
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 LoadBalancer
*/
@RestController
@RequestMapping("/api/loadbalancer")
@CrossOrigin(origins = "*")
public class LoadBalancerController {
@Autowired
private UserService userService;
// === ENDPOINTS AVEC RESTTEMPLATE ===
@GetMapping("/users/rest")
public List<User> getAllUsersRest() {
return userService.getAllUsersRest();
}
@GetMapping("/users/rest/{id}")
public User getUserByIdRest(@PathVariable Long id) {
return userService.getUserByIdRest(id);
}
@PostMapping("/users/rest")
public User createUserRest(@RequestBody User user) {
return userService.createUserRest(user);
}
@PutMapping("/users/rest/{id}")
public User updateUserRest(@PathVariable Long id, @RequestBody User user) {
return userService.updateUserRest(id, user);
}
@DeleteMapping("/users/rest/{id}")
public ResponseEntity<Void> deleteUserRest(@PathVariable Long id) {
userService.deleteUserRest(id);
return ResponseEntity.ok().build();
}
@GetMapping("/users/rest/department/{department}")
public List<User> getUsersByDepartmentRest(@PathVariable String department) {
return userService.getUsersByDepartmentRest(department);
}
@GetMapping("/users/rest/instance-info")
public String getInstanceInfoRest() {
return userService.getInstanceInfoRest();
}
@GetMapping("/users/rest/error")
public User getErrorUserRest() {
return userService.getErrorUserRest();
}
@GetMapping("/users/rest/slow")
public User getSlowUserRest() {
return userService.getSlowUserRest();
}
// === ENDPOINTS AVEC FEIGN ===
@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/instance-info")
public String getInstanceInfoFeign() {
return userService.getInstanceInfoFeign();
}
@GetMapping("/users/feign/error")
public User getErrorUserFeign() {
return userService.getErrorUserFeign();
}
@GetMapping("/users/feign/slow")
public User getSlowUserFeign() {
return userService.getSlowUserFeign();
}
// === ENDPOINTS DE TEST DE DISTRIBUTION ===
@GetMapping("/test-distribution")
public List<String> testLoadDistribution() {
List<String> results = new ArrayList<>();
for (int i = 0; i < 10; i++) {
results.add(userService.getInstanceInfoFeign());
}
return results;
}
}
Configuration Avancée et Personnalisation
1 Stratégies d'Équilibrage Avancées
Implémentation de stratégies complexes :
serviceA/src/main/java/com/example/client/loadbalancer/PerformanceBasedLoadBalancer.java
package com.example.client.loadbalancer;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.loadbalancer.core.ReactorLoadBalancer;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import reactor.core.publisher.Mono;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
/**
* LoadBalancer basé sur les performances
*/
@Configuration
public class PerformanceBasedLoadBalancerConfig {
@Bean
public ReactorLoadBalancer<ServiceInstance> performanceBasedLoadBalancer(
Environment environment,
LoadBalancerClientFactory loadBalancerClientFactory) {
String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
return new PerformanceBasedLoadBalancer(
loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class),
name
);
}
/**
* LoadBalancer basé sur les performances - sélectionne l'instance la plus performante
*/
static class PerformanceBasedLoadBalancer implements ReactorLoadBalancer<ServiceInstance> {
private final ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;
private final String serviceId;
// Stockage des métriques de performance
private final ConcurrentHashMap<String, AtomicLong> responseTimes = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, AtomicLong> requestCounts = new ConcurrentHashMap<>();
PerformanceBasedLoadBalancer(
ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider,
String serviceId) {
this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider;
this.serviceId = serviceId;
}
@Override
public Mono<ServiceInstance> choose(org.springframework.cloud.client.loadbalancer.reactive.Request request) {
ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider.getIfAvailable();
return supplier.get()
.next()
.map(this::selectBestPerformingInstance);
}
private ServiceInstance selectBestPerformingInstance(List<ServiceInstance> instances) {
if (instances.isEmpty()) {
return null;
}
if (instances.size() == 1) {
return instances.get(0);
}
ServiceInstance bestInstance = instances.get(0);
double bestScore = Double.MAX_VALUE;
for (ServiceInstance instance : instances) {
String instanceKey = getInstanceKey(instance);
double score = calculatePerformanceScore(instanceKey);
if (score < bestScore) {
bestScore = score;
bestInstance = instance;
}
}
return bestInstance;
}
private String getInstanceKey(ServiceInstance instance) {
return instance.getHost() + ":" + instance.getPort();
}
private double calculatePerformanceScore(String instanceKey) {
AtomicLong responseTime = responseTimes.get(instanceKey);
AtomicLong requestCount = requestCounts.get(instanceKey);
if (responseTime == null || requestCount == null || requestCount.get() == 0) {
return 1000.0; // Score par défaut
}
// Score basé sur le temps de réponse moyen
return (double) responseTime.get() / requestCount.get();
}
// Méthodes pour mettre à jour les métriques (appelées par l'intercepteur)
public void recordResponseTime(String instanceKey, long responseTimeMs) {
responseTimes.computeIfAbsent(instanceKey, k -> new AtomicLong(0))
.addAndGet(responseTimeMs);
requestCounts.computeIfAbsent(instanceKey, k -> new AtomicLong(0))
.incrementAndGet();
}
@Override
public String getServiceId() {
return serviceId;
}
}
}
2 Intercepteurs pour Monitoring
Intercepteurs pour collecte de métriques :
serviceA/src/main/java/com/example/client/interceptor/LoadBalancerMetricsInterceptor.java
package com.example.client.interceptor;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
/**
* Intercepteur pour collecter les métriques du LoadBalancer
*/
@Component
public class LoadBalancerMetricsInterceptor implements ClientHttpRequestInterceptor {
private final ConcurrentHashMap<String, AtomicLong> responseTimes = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, AtomicLong> requestCounts = new ConcurrentHashMap<>();
@Override
public ClientHttpResponse intercept(
HttpRequest request,
byte[] body,
ClientHttpRequestExecution execution) throws IOException {
long startTime = System.currentTimeMillis();
try {
ClientHttpResponse response = execution.execute(request, body);
long endTime = System.currentTimeMillis();
// Calcul du temps de réponse
long responseTime = endTime - startTime;
// Extraction de l'instance cible
String instanceKey = extractInstanceKey(request);
// Mise à jour des métriques
recordMetrics(instanceKey, responseTime);
return response;
} catch (IOException e) {
long endTime = System.currentTimeMillis();
long responseTime = endTime - startTime;
String instanceKey = extractInstanceKey(request);
recordMetrics(instanceKey, responseTime);
throw e;
}
}
private String extractInstanceKey(HttpRequest request) {
// Extraction de l'hôte et du port de la requête
String host = request.getURI().getHost();
int port = request.getURI().getPort();
return host + ":" + (port == -1 ? 80 : port);
}
private void recordMetrics(String instanceKey, long responseTime) {
responseTimes.computeIfAbsent(instanceKey, k -> new AtomicLong(0))
.addAndGet(responseTime);
requestCounts.computeIfAbsent(instanceKey, k -> new AtomicLong(0))
.incrementAndGet();
}
// Méthodes pour accéder aux métriques
public long getTotalResponseTime(String instanceKey) {
AtomicLong responseTime = responseTimes.get(instanceKey);
return responseTime != null ? responseTime.get() : 0;
}
public long getRequestCount(String instanceKey) {
AtomicLong requestCount = requestCounts.get(instanceKey);
return requestCount != null ? requestCount.get() : 0;
}
public double getAverageResponseTime(String instanceKey) {
long totalResponseTime = getTotalResponseTime(instanceKey);
long totalRequests = getRequestCount(instanceKey);
if (totalRequests == 0) {
return 0.0;
}
return (double) totalResponseTime / totalRequests;
}
}
Tests et Validation
1 Tests Unitaires
Tests de l'équilibrage de charge :
serviceA/src/test/java/com/example/client/service/LoadBalancerServiceTest.java
package com.example.client.service;
import com.example.client.model.User;
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 java.util.List;
import java.util.Set;
import java.util.HashSet;
import java.util.stream.Collectors;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
@TestPropertySource(properties = {
"eureka.client.enabled=false",
"spring.cloud.loadbalancer.retry.enabled=false"
})
class LoadBalancerServiceTest {
@Autowired
private UserService userService;
@Test
void testLoadBalancingDistribution() {
// Test de la distribution de charge
Set<String> instancesUsed = new HashSet<>();
// Effectuer plusieurs appels
for (int i = 0; i < 20; i++) {
String instanceInfo = userService.getInstanceInfoFeign();
instancesUsed.add(instanceInfo);
System.out.println("Instance utilisée: " + instanceInfo);
}
// Vérifier que plusieurs instances ont été utilisées
assertTrue(instancesUsed.size() > 1,
"Le LoadBalancer devrait distribuer les requêtes sur plusieurs instances");
System.out.println("Instances utilisées: " + instancesUsed);
}
@Test
void testGetAllUsersLoadBalanced() {
// Test de récupération des utilisateurs avec LoadBalancer
List<User> users = userService.getAllUsersFeign();
assertNotNull(users);
assertFalse(users.isEmpty());
// Vérifier que les informations d'instance sont présentes
for (User user : users) {
assertNotNull(user.getInstanceInfo());
assertFalse(user.getInstanceInfo().isEmpty());
}
System.out.println("Utilisateurs récupérés: " + users.size());
users.forEach(user -> System.out.println(" - " + user.getName() + " (" + user.getInstanceInfo() + ")"));
}
@Test
void testCreateUserLoadBalanced() {
// Test de création d'utilisateur avec LoadBalancer
User newUser = new User(null, "Test User", "test@example.com", "TEST");
User createdUser = userService.createUserFeign(newUser);
assertNotNull(createdUser);
assertNotNull(createdUser.getId());
assertEquals("Test User", createdUser.getName());
assertEquals("test@example.com", createdUser.getEmail());
assertNotNull(createdUser.getInstanceInfo());
System.out.println("Utilisateur créé sur: " + createdUser.getInstanceInfo());
}
@Test
void testErrorHandlingWithLoadBalancer() {
// Test de la gestion d'erreurs avec LoadBalancer
// Le LoadBalancer devrait réessayer sur une autre instance en cas d'erreur
try {
User errorUser = userService.getErrorUserFeign();
assertNotNull(errorUser);
System.out.println("Utilisateur récupéré malgré les erreurs: " + errorUser.getInstanceInfo());
} catch (Exception e) {
// Cela peut arriver si toutes les instances échouent
System.out.println("Erreur attrapée: " + e.getMessage());
}
}
}
2 Tests d'Intégration
Tests avec plusieurs instances :
serviceA/src/test/java/com/example/client/integration/LoadBalancerIntegrationTest.java
package com.example.client.integration;
import com.example.client.model.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.http.ResponseEntity;
import java.util.List;
import java.util.Map;
import java.util.HashMap;
import java.util.stream.Collectors;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class LoadBalancerIntegrationTest {
@LocalServerPort
private int port;
@Autowired
private TestRestTemplate restTemplate;
@Test
void testLoadDistributionAcrossInstances() {
String baseUrl = "http://localhost:" + port + "/api/loadbalancer/test-distribution";
ResponseEntity<List<String>> response = restTemplate.getForEntity(baseUrl, List.class);
assertTrue(response.getStatusCode().is2xxSuccessful());
assertNotNull(response.getBody());
List<String> instances = response.getBody();
assertEquals(10, instances.size());
// Compter les occurrences de chaque instance
Map<String, Long> instanceCounts = instances.stream()
.collect(Collectors.groupingBy(
instance -> instance,
Collectors.counting()
));
System.out.println("Distribution des requêtes:");
instanceCounts.forEach((instance, count) ->
System.out.println(" " + instance + ": " + count + " requêtes")
);
// Vérifier que plusieurs instances ont été utilisées
assertTrue(instanceCounts.size() > 1,
"Les requêtes devraient être distribuées sur plusieurs instances");
}
@Test
void testInstanceInfoContainsLoadBalancerInfo() {
String baseUrl = "http://localhost:" + port + "/api/loadbalancer/users/feign/instance-info";
ResponseEntity<String> response = restTemplate.getForEntity(baseUrl, String.class);
assertTrue(response.getStatusCode().is2xxSuccessful());
assertNotNull(response.getBody());
String instanceInfo = response.getBody();
System.out.println("Info d'instance: " + instanceInfo);
// Vérifier que l'info contient le nom du service et le port
assertTrue(instanceInfo.contains("Service B"));
assertTrue(instanceInfo.matches(".*Service B - Instance \\d+.*"));
}
}
Tableau Récapitulatif des Fonctionnalités
| Fonctionnalité | RestTemplate | OpenFeign | Configuration |
|---|---|---|---|
| Activation LoadBalancer | @LoadBalanced | @FeignClient(name) | Automatique |
| Stratégie d'équilibrage | Round Robin par défaut | Round Robin par défaut | Configurable |
| Health Checks | Intégré | Intégré | Activable |
| Retry Mechanism | Supporté | Supporté | Configurable |
| Personnalisation | Classes personnalisées | Annotations | Properties |
| Monitoring | Actuator endpoints | Actuator endpoints | Métriques intégrées |
Bonnes Pratiques et Conseils
1 Configuration Optimale
Configuration recommandée :
# === CONFIGURATION LOADBALANCER OPTIMALE ===
# Désactiver Ribbon (obsolète)
spring.cloud.loadbalancer.ribbon.enabled=false
# Configuration des health checks
spring.cloud.loadbalancer.health-check.enabled=true
spring.cloud.loadbalancer.health-check.interval=30s
spring.cloud.loadbalancer.health-check.timeout=5s
# Cache des instances
spring.cloud.loadbalancer.cache.ttl=30s
spring.cloud.loadbalancer.cache.refresh-interval=15s
# Retry configuration
spring.cloud.loadbalancer.retry.enabled=true
spring.cloud.loadbalancer.retry.max-attempts-on-next-service-instance=3
# === CONFIGURATION DES TIMEOUTS ===
# Pour RestTemplate
spring.cloud.loadbalancer.retry.backoff.enabled=true
spring.cloud.loadbalancer.retry.backoff.min-backoff=1s
spring.cloud.loadbalancer.retry.backoff.max-backoff=10s
2 Gestion des Erreurs
Gestion robuste des erreurs :
@Service
public class RobustLoadBalancerService {
@Autowired
private UserServiceClient userServiceClient;
public User getUserById(Long id) {
try {
return userServiceClient.getUserById(id);
} catch (Exception e) {
// Le LoadBalancer a déjà réessayé sur d'autres instances
// Mais si toutes échouent, on peut fournir une réponse par défaut
log.warn("Impossible de récupérer l'utilisateur {}: {}", id, e.getMessage());
// Retourner un utilisateur par défaut
return User.builder()
.id(id)
.name("Utilisateur temporairement indisponible")
.email("unavailable@example.com")
.department("UNKNOWN")
.instanceInfo("DEFAULT")
.build();
}
}
public List<User> getAllUsers() {
try {
return userServiceClient.getAllUsers();
} catch (Exception e) {
log.error("Erreur lors de la récupération des utilisateurs", e);
// Retourner une liste vide plutôt que de faire échouer l'application
return Collections.emptyList();
}
}
}
3 Monitoring et Métriques
Monitoring avancé :
@Component
public class LoadBalancerMetricsService {
private final MeterRegistry meterRegistry;
public LoadBalancerMetricsService(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
}
@EventListener
public void handleLoadBalancerStats(LoadBalancerStatsEvent event) {
// Enregistrement des métriques
Gauge.builder("loadbalancer.instances.available")
.description("Nombre d'instances disponibles")
.register(meterRegistry, this, s -> getAvailableInstancesCount());
Counter.builder("loadbalancer.requests.total")
.description("Nombre total de requêtes")
.register(meterRegistry)
.increment();
}
private int getAvailableInstancesCount() {
// Logique pour obtenir le nombre d'instances disponibles
return 3; // Exemple
}
}
4 Tests de Performance
Tests de charge :
@SpringBootTest
@ActiveProfiles("test")
class LoadBalancerPerformanceTest {
@Autowired
private UserService userService;
@Test
@Timeout(value = 30, unit = TimeUnit.SECONDS)
void testHighLoadDistribution() {
ExecutorService executor = Executors.newFixedThreadPool(10);
List<Future<String>> futures = new ArrayList<>();
// Simuler une charge élevée
for (int i = 0; i < 100; i++) {
final int requestId = i;
Future<String> future = executor.submit(() -> {
try {
return userService.getInstanceInfoFeign();
} catch (Exception e) {
return "ERROR";
}
});
futures.add(future);
}
// Collecter les résultats
Map<String, Integer> instanceDistribution = new HashMap<>();
for (Future<String> future : futures) {
try {
String instance = future.get(5, TimeUnit.SECONDS);
instanceDistribution.merge(instance, 1, Integer::sum);
} catch (Exception e) {
instanceDistribution.merge("ERROR", 1, Integer::sum);
}
}
executor.shutdown();
// Afficher la distribution
instanceDistribution.forEach((instance, count) ->
System.out.println(instance + ": " + count + " requêtes")
);
// Vérifier que la distribution est raisonnablement équilibrée
int totalRequests = instanceDistribution.values().stream().mapToInt(Integer::intValue).sum();
int expectedPerInstance = totalRequests / instanceDistribution.size();
for (Map.Entry<String, Integer> entry : instanceDistribution.entrySet()) {
int count = entry.getValue();
// Accepter une variation de ±30%
assertTrue(count > expectedPerInstance * 0.7 && count < expectedPerInstance * 1.3,
"Distribution déséquilibrée pour " + entry.getKey());
}
}
}
Conclusion
Points Clés à Retenir
graph TB
A[Spring Cloud LoadBalancer] --> B[Fonctionnalités de Base]
A --> C[Intégration]
A --> D[Configuration]
A --> E[Stratégies Avancées]
B --> B1[@LoadBalanced]
B --> B2[@FeignClient]
B --> B3[Health Checks]
B --> B4[Retry]
C --> C1[RestTemplate]
C --> C2[OpenFeign]
C --> C3[Eureka]
C --> C4[Actuator]
D --> D1[Properties]
D --> D2[Personnalisation]
D --> D3[Métriques]
E --> E1[Round Robin]
E --> E2[Random]
E3[Pondéré]
E4[Performance-based]
style A fill:#4CAF50,stroke:#388E3C
style B fill:#2196F3,stroke:#0D47A1
style C fill:#FF9800,stroke:#E65100
style D fill:#9C27B0,stroke:#4A148C
style E fill:#FF5722,stroke:#E64A19
Résumé des concepts importants :
- Spring Cloud LoadBalancer est l'équilibreur de charge client-side moderne
- Utilisez @LoadBalanced pour activer LoadBalancer avec RestTemplate
- @FeignClient(name) active automatiquement LoadBalancer avec OpenFeign
- Les health checks assurent que seules les instances saines reçoivent du trafic
- Les stratégies d'équilibrage peuvent être personnalisées (Round Robin, Random, etc.)
- Le retry mechanism améliore la résilience en cas d'échec d'instance
- Le monitoring via Actuator permet de suivre la distribution de charge