Aller au contenu principal

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​

ConceptDescription
RegisterService s'inscrit au démarrage
Renew (Heartbeat)Ping toutes les 30 secondes
Fetch RegistryClient récupÚre la liste des services
CancelService se dĂ©sinscrit Ă  l'arrĂȘt
EvictionEureka 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​

EndpointMethodDescription
/eureka/appsGETListe tous les services
/eureka/apps/\{appId\}GETDétails d'un service
/eureka/apps/\{appId\}/\{instanceId\}GETDétails d'une instance
/eureka/apps/\{appId\}/\{instanceId\}DELETEDésenregistrer
/eureka/apps/\{appId\}/\{instanceId\}/statusPUTChanger 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: false pour le serveur
  • spring.application.name dĂ©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:

  1. Intercepte les appels avec un nom de service (ex: http://user-service/...)
  2. Consulte Eureka pour obtenir les instances
  3. Choisit une instance (round-robin par défaut)
  4. Remplace le nom par l'IP:port réel
  5. 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.


Chapitre Suivant: Configuration CentralisĂ©e →

Retour au Module 13