Chapitre 2: Service Discovery avec Eureka
đŻ Objectifs du Chapitreâ
- Comprendre le concept de Service Discovery
- Configurer un Eureka Server
- Enregistrer des services comme Eureka Clients
- Implémenter le Load Balancing client-side
- Gérer la haute disponibilité d'Eureka
1. Pourquoi Service Discovery?â
1.1 Le ProblĂšme Sans Service Discoveryâ
ProblĂšmes:
- URLs hardcodées dans la configuration
- Si une IP change â modifier tous les clients
- Si on ajoute une instance â modifier tous les clients
- Load balancing impossible
- Pas de détection des pannes
1.2 La Solution: Service Discoveryâ
Avantages:
- Services s'enregistrent dynamiquement
- Clients découvrent les services par leur nom
- Load balancing automatique
- Détection automatique des pannes
- Scaling transparent
2. Netflix Eurekaâ
2.1 Qu'est-ce qu'Eureka?â
Eureka est un service de découverte développé par Netflix, composé de:
- Eureka Server: Le registre central
- Eureka Client: BibliothĂšque dans chaque service
2.2 Architectureâ
2.3 Concepts ClĂ©sâ
| Concept | Description |
|---|---|
| Register | Service s'inscrit au démarrage |
| Renew (Heartbeat) | Ping toutes les 30 secondes |
| Fetch Registry | Client récupÚre la liste des services |
| Cancel | Service se dĂ©sinscrit Ă l'arrĂȘt |
| Eviction | Eureka supprime les services sans heartbeat (90s) |
3. Configuration Eureka Serverâ
3.1 CrĂ©er le Projetâ
Structure du projet:
eureka-server/
âââ pom.xml
âââ src/main/java/
âââ com/example/eurekaserver/
âââ EurekaServerApplication.java
âââ src/main/resources/
âââ application.yml
3.2 DĂ©pendances Mavenâ
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>eureka-server</artifactId>
<version>1.0.0</version>
<name>eureka-server</name>
<properties>
<java.version>17</java.version>
<spring-cloud.version>2023.0.0</spring-cloud.version>
</properties>
<dependencies>
<!-- Eureka Server -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<!-- Security (optionnel mais recommandé) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- Actuator -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
3.3 Application Principaleâ
package com.example.eurekaserver;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@SpringBootApplication
@EnableEurekaServer // Active le serveur Eureka
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
3.4 Configuration application.ymlâ
server:
port: 8761
spring:
application:
name: eureka-server
eureka:
instance:
hostname: localhost
client:
# Ne pas s'enregistrer soi-mĂȘme (c'est le serveur!)
register-with-eureka: false
# Ne pas récupérer le registre (c'est le serveur!)
fetch-registry: false
service-url:
defaultZone: http://localhost:8761/eureka/
server:
# Désactiver le mode de protection en dev
enable-self-preservation: false
# Intervalle de nettoyage des instances mortes
eviction-interval-timer-in-ms: 5000
# Logging
logging:
level:
com.netflix.eureka: INFO
com.netflix.discovery: INFO
3.5 SĂ©curiser Eureka (Optionnel)â
package com.example.eurekaserver.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/actuator/**").permitAll()
.anyRequest().authenticated()
)
.httpBasic(basic -> {});
return http.build();
}
@Bean
public UserDetailsService userDetailsService() {
var user = User.builder()
.username("eureka")
.password(passwordEncoder().encode("eureka123"))
.roles("ADMIN")
.build();
return new InMemoryUserDetailsManager(user);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
3.6 Dashboard Eurekaâ
AprÚs démarrage, accédez à : http://localhost:8761
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â EUREKA SERVER â
â Spring Cloud â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ€
â System Status â
â Environment: test â
â Data center: default â
â Current time: 2024-01-15 10:30:45 â
â Uptime: 00:15:30 â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ€
â Instances currently registered with Eureka â
â â
â Application AMIs Availability Zones Status â
â USER-SERVICE n/a (2) (2) UP (2) â
â PRODUCT-SERVICE n/a (1) (1) UP (1) â
â ORDER-SERVICE n/a (1) (1) UP (1) â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
4. Configuration Eureka Clientâ
4.1 DĂ©pendances Maven (Service)â
<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 health checks) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>2023.0.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
4.2 Application Principaleâ
package com.example.userservice;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
// @EnableDiscoveryClient est optionnel depuis Spring Cloud 2020
// La découverte est automatique si eureka-client est dans le classpath
public class UserServiceApplication {
public static void main(String[] args) {
SpringApplication.run(UserServiceApplication.class, args);
}
}
4.3 Configuration Clientâ
server:
port: 8081
spring:
application:
name: user-service # Nom du service dans Eureka
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
# Intervalle de récupération du registre
registry-fetch-interval-seconds: 5
instance:
# Utiliser l'IP au lieu du hostname
prefer-ip-address: true
# ID unique de l'instance
instance-id: ${spring.application.name}:${server.port}:${random.uuid}
# Intervalle des heartbeats
lease-renewal-interval-in-seconds: 10
# Temps avant expiration (3x le renouvellement)
lease-expiration-duration-in-seconds: 30
# Actuator pour health checks
management:
endpoints:
web:
exposure:
include: health,info
endpoint:
health:
show-details: always
4.4 Configuration avec SĂ©curitĂ©â
Si Eureka Server est sécurisé:
eureka:
client:
service-url:
# Format: http://user:password@host:port/eureka/
defaultZone: http://eureka:eureka123@localhost:8761/eureka/
5. Exemple Complet: User Serviceâ
5.1 Structure du Projetâ
user-service/
âââ pom.xml
âââ src/main/java/com/example/userservice/
âââ UserServiceApplication.java
âââ controller/
â âââ UserController.java
âââ model/
â âââ User.java
âââ service/
âââ UserService.java
5.2 Modelâ
package com.example.userservice.model;
public class User {
private Long id;
private String username;
private String email;
private String firstName;
private String lastName;
// Constructeurs
public User() {}
public User(Long id, String username, String email,
String firstName, String lastName) {
this.id = id;
this.username = username;
this.email = email;
this.firstName = firstName;
this.lastName = lastName;
}
// Getters et Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getFirstName() { return firstName; }
public void setFirstName(String firstName) { this.firstName = firstName; }
public String getLastName() { return lastName; }
public void setLastName(String lastName) { this.lastName = lastName; }
}
5.3 Serviceâ
package com.example.userservice.service;
import com.example.userservice.model.User;
import org.springframework.stereotype.Service;
import jakarta.annotation.PostConstruct;
import java.util.*;
@Service
public class UserService {
private final Map<Long, User> users = new HashMap<>();
@PostConstruct
public void init() {
users.put(1L, new User(1L, "john", "john@example.com", "John", "Doe"));
users.put(2L, new User(2L, "jane", "jane@example.com", "Jane", "Smith"));
users.put(3L, new User(3L, "bob", "bob@example.com", "Bob", "Wilson"));
}
public List<User> getAllUsers() {
return new ArrayList<>(users.values());
}
public Optional<User> getUserById(Long id) {
return Optional.ofNullable(users.get(id));
}
public User createUser(User user) {
Long newId = users.keySet().stream()
.max(Long::compareTo)
.orElse(0L) + 1;
user.setId(newId);
users.put(newId, user);
return user;
}
public boolean existsById(Long id) {
return users.containsKey(id);
}
}
5.4 Controllerâ
package com.example.userservice.controller;
import com.example.userservice.model.User;
import com.example.userservice.service.UserService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
@Value("${server.port}")
private String serverPort;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping
public ResponseEntity<List<User>> getAllUsers() {
return ResponseEntity.ok(userService.getAllUsers());
}
@GetMapping("/{id}")
public ResponseEntity<User> getUserById(@PathVariable Long id) {
return userService.getUserById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@GetMapping("/{id}/exists")
public ResponseEntity<Boolean> userExists(@PathVariable Long id) {
return ResponseEntity.ok(userService.existsById(id));
}
@PostMapping
public ResponseEntity<User> createUser(@RequestBody User user) {
return ResponseEntity.ok(userService.createUser(user));
}
// Endpoint pour voir quelle instance répond
@GetMapping("/info")
public ResponseEntity<String> getInfo() {
return ResponseEntity.ok("Response from User Service on port: " + serverPort);
}
}
6. Load Balancing avec Spring Cloud LoadBalancerâ
6.1 DĂ©pendanceâ
<!-- Inclus dans spring-cloud-starter-netflix-eureka-client -->
<!-- Mais peut ĂȘtre ajoutĂ© explicitement -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
6.2 RestTemplate avec Load Balancingâ
package com.example.orderservice.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
public class RestTemplateConfig {
@Bean
@LoadBalanced // Active le load balancing
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
6.3 Utilisation dans un Serviceâ
package com.example.orderservice.service;
import com.example.orderservice.model.User;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
@Service
public class UserServiceClient {
private final RestTemplate restTemplate;
public UserServiceClient(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
public User getUserById(Long userId) {
// Utilise le nom du service au lieu de l'URL!
// Spring Cloud LoadBalancer résout "user-service"
// en une des instances enregistrées dans Eureka
String url = "http://user-service/api/users/" + userId;
return restTemplate.getForObject(url, User.class);
}
public boolean userExists(Long userId) {
String url = "http://user-service/api/users/" + userId + "/exists";
return Boolean.TRUE.equals(
restTemplate.getForObject(url, Boolean.class)
);
}
}
6.4 WebClient avec Load Balancing (RĂ©actif)â
package com.example.orderservice.config;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.client.WebClient;
@Configuration
public class WebClientConfig {
@Bean
@LoadBalanced
public WebClient.Builder webClientBuilder() {
return WebClient.builder();
}
}
@Service
public class UserServiceClient {
private final WebClient.Builder webClientBuilder;
public UserServiceClient(WebClient.Builder webClientBuilder) {
this.webClientBuilder = webClientBuilder;
}
public Mono<User> getUserById(Long userId) {
return webClientBuilder.build()
.get()
.uri("http://user-service/api/users/" + userId)
.retrieve()
.bodyToMono(User.class);
}
}
7. Algorithmes de Load Balancingâ
7.1 Round Robin (Par dĂ©faut)â
RequĂȘte 1 â Instance A
RequĂȘte 2 â Instance B
RequĂȘte 3 â Instance C
RequĂȘte 4 â Instance A
...
7.2 Configuration Customâ
package com.example.orderservice.config;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.loadbalancer.core.RandomLoadBalancer;
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
public class LoadBalancerConfig {
@Bean
public ReactorLoadBalancer<ServiceInstance> randomLoadBalancer(
Environment environment,
LoadBalancerClientFactory loadBalancerClientFactory) {
String name = environment.getProperty(
LoadBalancerClientFactory.PROPERTY_NAME
);
return new RandomLoadBalancer(
loadBalancerClientFactory.getLazyProvider(
name,
ServiceInstanceListSupplier.class
),
name
);
}
}
7.3 Configuration par Serviceâ
@Configuration
@LoadBalancerClient(name = "user-service", configuration = UserServiceLBConfig.class)
public class LoadBalancerConfiguration {
}
8. Haute DisponibilitĂ© Eurekaâ
8.1 ProblĂšme: Single Point of Failureâ
Si Eureka Server tombe:
- Nouveaux services ne peuvent pas s'enregistrer
- Clients ne peuvent pas découvrir de nouveaux services
- MAIS: le cache local continue de fonctionner
8.2 Solution: Eureka Clusterâ
8.3 Configuration Eureka 1â
# eureka-server-1/application.yml
server:
port: 8761
spring:
application:
name: eureka-server
profiles:
active: peer1
eureka:
instance:
hostname: eureka1
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://eureka2:8762/eureka/,http://eureka3:8763/eureka/
8.4 Configuration Eureka 2â
# eureka-server-2/application.yml
server:
port: 8762
spring:
application:
name: eureka-server
profiles:
active: peer2
eureka:
instance:
hostname: eureka2
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://eureka1:8761/eureka/,http://eureka3:8763/eureka/
8.5 Configuration Client pour Clusterâ
eureka:
client:
service-url:
# Liste de tous les serveurs Eureka
defaultZone: http://eureka1:8761/eureka/,http://eureka2:8762/eureka/,http://eureka3:8763/eureka/
# Failover automatique
eureka-server-connect-timeout-seconds: 5
eureka-server-read-timeout-seconds: 8
9. Self-Preservation Modeâ
9.1 Qu'est-ce que c'est?â
Eureka entre en mode "self-preservation" quand il détecte une perte massive de heartbeats.
â ïž EMERGENCY! EUREKA MAY BE INCORRECTLY CLAIMING INSTANCES ARE UP
WHEN THEY'RE NOT. RENEWALS ARE LESSER THAN THRESHOLD AND HENCE THE
INSTANCES ARE NOT BEING EXPIRED JUST TO BE SAFE.
9.2 Pourquoi?â
Scénario: ProblÚme réseau entre Eureka et services
- Eureka ne reçoit plus de heartbeats
- Sans protection: Eureka supprime tous les services
- Résultat: Catastrophe!
Avec Self-Preservation:
- Eureka détecte la perte massive
- Il garde les services enregistrés
- Il attend que le réseau revienne
9.3 Configurationâ
eureka:
server:
# Activer/désactiver (défaut: true)
enable-self-preservation: true
# Seuil de renouvellement (défaut: 0.85 = 85%)
renewal-percent-threshold: 0.85
# Intervalle de mise Ă jour du seuil
renewal-threshold-update-interval-ms: 900000
# En développement, on peut désactiver
# enable-self-preservation: false
10. Eureka REST APIâ
10.1 Endpoints Principauxâ
| Endpoint | Method | Description |
|---|---|---|
/eureka/apps | GET | Liste tous les services |
/eureka/apps/\{appId\} | GET | Détails d'un service |
/eureka/apps/\{appId\}/\{instanceId\} | GET | Détails d'une instance |
/eureka/apps/\{appId\}/\{instanceId\} | DELETE | Désenregistrer |
/eureka/apps/\{appId\}/\{instanceId\}/status | PUT | Changer le statut |
10.2 Exemplesâ
# Lister tous les services
curl http://localhost:8761/eureka/apps
# Obtenir les instances de user-service
curl http://localhost:8761/eureka/apps/USER-SERVICE
# Vérifier le statut
curl http://localhost:8761/eureka/apps/USER-SERVICE/user-service:8081
# Mettre hors service (OUT_OF_SERVICE)
curl -X PUT "http://localhost:8761/eureka/apps/USER-SERVICE/user-service:8081/status?value=OUT_OF_SERVICE"
# Remettre en service (UP)
curl -X PUT "http://localhost:8761/eureka/apps/USER-SERVICE/user-service:8081/status?value=UP"
10.3 RĂ©ponse XMLâ
<applications>
<versions__delta>1</versions__delta>
<apps__hashcode>UP_3_</apps__hashcode>
<application>
<name>USER-SERVICE</name>
<instance>
<instanceId>user-service:8081</instanceId>
<hostName>192.168.1.10</hostName>
<app>USER-SERVICE</app>
<ipAddr>192.168.1.10</ipAddr>
<status>UP</status>
<port enabled="true">8081</port>
<healthCheckUrl>http://192.168.1.10:8081/actuator/health</healthCheckUrl>
</instance>
</application>
</applications>
11. Bonnes Pratiquesâ
11.1 Naming Conventionâ
# â
Bon
spring:
application:
name: user-service
# â Mauvais
spring:
application:
name: UserService
name: user_service
name: users
11.2 Health Checksâ
eureka:
instance:
# Activer les health checks
health-check-url-path: /actuator/health
status-page-url-path: /actuator/info
11.3 Graceful Shutdownâ
@Component
public class GracefulShutdown implements DisposableBean {
@Autowired
private EurekaClient eurekaClient;
@Override
public void destroy() {
// Se désenregistrer proprement
eurekaClient.shutdown();
}
}
Ou via configuration:
server:
shutdown: graceful
spring:
lifecycle:
timeout-per-shutdown-phase: 30s
11.4 Timeouts AppropriĂ©sâ
eureka:
instance:
# Heartbeat toutes les 10 secondes
lease-renewal-interval-in-seconds: 10
# Expiration aprĂšs 30 secondes sans heartbeat
lease-expiration-duration-in-seconds: 30
client:
# RafraĂźchir le registre toutes les 5 secondes
registry-fetch-interval-seconds: 5
12. RĂ©sumĂ©â
Points ClĂ©sâ
â
Eureka Server = Registre central des services
â
Eureka Client = S'enregistre et découvre les autres
â
Load Balancing = Automatique avec @LoadBalanced
â
Self-Preservation = Protection contre les faux positifs
â
Haute Disponibilité = Cluster de serveurs Eureka
Checklist Configurationâ
- Eureka Server sur port 8761
-
register-with-eureka: falsepour le serveur -
spring.application.namedéfini pour chaque client - Actuator pour les health checks
- Load Balancer configuré
- Timeouts appropriés en production
13. Quiz de Validationâ
Question 1â
Pourquoi @LoadBalanced est nécessaire sur RestTemplate?
Voir la réponse
@LoadBalanced injecte un intercepteur qui:
- Intercepte les appels avec un nom de service (ex:
http://user-service/...) - Consulte Eureka pour obtenir les instances
- Choisit une instance (round-robin par défaut)
- Remplace le nom par l'IP:port réel
- Effectue l'appel HTTP
Sans @LoadBalanced, RestTemplate essaierait de résoudre "user-service" comme un DNS normal et échouerait.
Question 2â
Que se passe-t-il si Eureka Server tombe?
Voir la réponse
Les clients Eureka gardent un cache local du registre:
- Les services déjà connus continuent de fonctionner
- Les nouveaux services ne peuvent pas s'enregistrer
- Les clients ne découvrent pas les nouvelles instances
- Le systÚme est en mode "dégradé" mais fonctionnel
C'est pourquoi la haute disponibilité (cluster) est importante en production.