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

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

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