Capítulo 2: Descubrimiento de Servicios con Eureka
🎯 Objetivos del Capítulo
- Comprender el concepto de Service Discovery
- Configurar un Eureka Server
- Registrar servicios como Eureka Clients
- Implementar el Load Balancing del lado del cliente
- Gestionar la alta disponibilidad de Eureka
1. ¿Por qué Service Discovery?
1.1 El Problema Sin Service Discovery
Problemas:
- URLs codificadas en la configuración
- Si una IP cambia → hay que modificar todos los clientes
- Si se añade una instancia → hay que modificar todos los clientes
- Imposible hacer load balancing
- No hay detección de fallos
1.2 La Solución: Service Discovery
Ventajas:
- Los servicios se registran dinámicamente
- Los clientes descubren los servicios por su nombre
- Load balancing automático
- Detección automática de fallos
- Escalado transparente
2. Netflix Eureka
2.1 ¿Qué es Eureka?
Eureka es un servicio de descubrimiento desarrollado por Netflix, compuesto por:
- Eureka Server: El registro central
- Eureka Client: Librería en cada servicio
2.2 Arquitectura
2.3 Conceptos Clave
| Concepto | Descripción |
|---|---|
| Register | El servicio se registra al iniciar |
| Renew (Heartbeat) | Ping cada 30 segundos |
| Fetch Registry | El cliente obtiene la lista de servicios |
| Cancel | El servicio se da de baja al apagarse |
| Eviction | Eureka elimina los servicios sin heartbeat (90s) |
3. Configuración de Eureka Server
3.1 Crear el Proyecto
Estructura del proyecto:
eureka-server/
├── pom.xml
└── src/main/java/
└── com/example/eurekaserver/
└── EurekaServerApplication.java
└── src/main/resources/
└── application.yml
3.2 Dependencias 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 (opcional pero recomendado) -->
<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 Aplicación Principal
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 // Activa el servidor Eureka
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
3.4 Configuración application.yml
server:
port: 8761
spring:
application:
name: eureka-server
eureka:
instance:
hostname: localhost
client:
# No registrarse a sí mismo (¡es el servidor!)
register-with-eureka: false
# No obtener el registro (¡es el servidor!)
fetch-registry: false
service-url:
defaultZone: http://localhost:8761/eureka/
server:
# Desactivar el modo de protección en desarrollo
enable-self-preservation: false
# Intervalo de limpieza de instancias muertas
eviction-interval-timer-in-ms: 5000
# Logging
logging:
level:
com.netflix.eureka: INFO
com.netflix.discovery: INFO
3.5 Proteger Eureka (Opcional)
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
Después de iniciar, acceda a: 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. Configuración de Eureka Client
4.1 Dependencias Maven (Servicio)
<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 (para 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 Aplicación Principal
package com.example.userservice;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
// @EnableDiscoveryClient es opcional desde Spring Cloud 2020
// El descubrimiento es automático si eureka-client está en el classpath
public class UserServiceApplication {
public static void main(String[] args) {
SpringApplication.run(UserServiceApplication.class, args);
}
}
4.3 Configuración del Cliente
server:
port: 8081
spring:
application:
name: user-service # Nombre del servicio en Eureka
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
# Intervalo de obtención del registro
registry-fetch-interval-seconds: 5
instance:
# Usar la IP en vez del hostname
prefer-ip-address: true
# ID único de la instancia
instance-id: ${spring.application.name}:${server.port}:${random.uuid}
# Intervalo de heartbeats
lease-renewal-interval-in-seconds: 10
# Tiempo antes de expiración (3x el intervalo de renovación)
lease-expiration-duration-in-seconds: 30
# Actuator para health checks
management:
endpoints:
web:
exposure:
include: health,info
endpoint:
health:
show-details: always
4.4 Configuración con Seguridad
Si Eureka Server está protegido:
eureka:
client:
service-url:
# Formato: http://user:password@host:port/eureka/
defaultZone: http://eureka:eureka123@localhost:8761/eureka/
5. Ejemplo Completo: User Service
5.1 Estructura del Proyecto
user-service/
├── pom.xml
└── src/main/java/com/example/userservice/
├── UserServiceApplication.java
├── controller/
│ └── UserController.java
├── model/
│ └── User.java
└── service/
└── UserService.java
5.2 Modelo
package com.example.userservice.model;
public class User {
private Long id;
private String username;
private String email;
private String firstName;
private String lastName;
// Constructores
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 y 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 Servicio
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 Controlador
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 para ver qué instancia responde
@GetMapping("/info")
public ResponseEntity<String> getInfo() {
return ResponseEntity.ok("Response from User Service on port: " + serverPort);
}
}
6. Load Balancing con Spring Cloud LoadBalancer
6.1 Dependencia
<!-- Incluido en spring-cloud-starter-netflix-eureka-client -->
<!-- Pero se puede añadir explícitamente -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
6.2 RestTemplate con 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 // Activa el load balancing
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
6.3 Uso en un Servicio
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) {
// ¡Usa el nombre del servicio en vez de la URL!
// Spring Cloud LoadBalancer resuelve "user-service"
// en una de las instancias registradas en 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 con Load Balancing (Reactivo)
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. Algoritmos de Load Balancing
7.1 Round Robin (Por defecto)
Petición 1 → Instancia A
Petición 2 → Instancia B
Petición 3 → Instancia C
Petición 4 → Instancia A
...
7.2 Configuración Personalizada
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 Configuración por Servicio
@Configuration
@LoadBalancerClient(name = "user-service", configuration = UserServiceLBConfig.class)
public class LoadBalancerConfiguration {
}
8. Alta Disponibilidad Eureka
8.1 Problema: Single Point of Failure
Si Eureka Server cae:
- Los nuevos servicios no pueden registrarse
- Los clientes no pueden descubrir nuevos servicios
- PERO: la caché local sigue funcionando
8.2 Solución: Eureka Cluster
8.3 Configuración 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 Configuración 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 Configuración del Cliente para Cluster
eureka:
client:
service-url:
# Lista de todos los servidores Eureka
defaultZone: http://eureka1:8761/eureka/,http://eureka2:8762/eureka/,http://eureka3:8763/eureka/
# Failover automático
eureka-server-connect-timeout-seconds: 5
eureka-server-read-timeout-seconds: 8
9. Self-Preservation Mode
9.1 ¿Qué es?
Eureka entra en modo "self-preservation" cuando detecta una pérdida masiva 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 ¿Por qué?
Escenario: Problema de red entre Eureka y los servicios
- Eureka ya no recibe heartbeats
- Sin protección: Eureka elimina todos los servicios
- Resultado: ¡Catástrofe!
Con Self-Preservation:
- Eureka detecta la pérdida masiva
- Mantiene los servicios registrados
- Espera a que la red se recupere
9.3 Configuración
eureka:
server:
# Activar/desactivar (por defecto: true)
enable-self-preservation: true
# Umbral de renovación (por defecto: 0.85 = 85%)
renewal-percent-threshold: 0.85
# Intervalo de actualización del umbral
renewal-threshold-update-interval-ms: 900000
# En desarrollo, se puede desactivar
# enable-self-preservation: false
10. Eureka REST API
10.1 Endpoints Principales
| Endpoint | Method | Descripción |
|---|---|---|
/eureka/apps | GET | Lista todos los servicios |
/eureka/apps/\{appId\} | GET | Detalles de un servicio |
/eureka/apps/\{appId\}/\{instanceId\} | GET | Detalles de una instancia |
/eureka/apps/\{appId\}/\{instanceId\} | DELETE | Dar de baja |
/eureka/apps/\{appId\}/\{instanceId\}/status | PUT | Cambiar el estado |
10.2 Ejemplos
# Listar todos los servicios
curl http://localhost:8761/eureka/apps
# Obtener las instancias de user-service
curl http://localhost:8761/eureka/apps/USER-SERVICE
# Verificar el estado
curl http://localhost:8761/eureka/apps/USER-SERVICE/user-service:8081
# Poner fuera de servicio (OUT_OF_SERVICE)
curl -X PUT "http://localhost:8761/eureka/apps/USER-SERVICE/user-service:8081/status?value=OUT_OF_SERVICE"
# Volver a poner en servicio (UP)
curl -X PUT "http://localhost:8761/eureka/apps/USER-SERVICE/user-service:8081/status?value=UP"
10.3 Respuesta 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. Buenas Prácticas
11.1 Naming Convention
# ✅ Correcto
spring:
application:
name: user-service
# ❌ Incorrecto
spring:
application:
name: UserService
name: user_service
name: users
11.2 Health Checks
eureka:
instance:
# Activar los 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() {
// Dar de baja correctamente
eurekaClient.shutdown();
}
}
O vía configuración:
server:
shutdown: graceful
spring:
lifecycle:
timeout-per-shutdown-phase: 30s
11.4 Timeouts Apropiados
eureka:
instance:
# Heartbeat cada 10 segundos
lease-renewal-interval-in-seconds: 10
# Expira después de 30 segundos sin heartbeat
lease-expiration-duration-in-seconds: 30
client:
# Refrescar el registro cada 5 segundos
registry-fetch-interval-seconds: 5
12. Resumen
Puntos Clave
✅ Eureka Server = Registro central de servicios
✅ Eureka Client = Se registra y descubre a los demás
✅ Load Balancing = Automático con @LoadBalanced
✅ Self-Preservation = Protección contra falsos positivos
✅ Alta Disponibilidad = Cluster de servidores Eureka
Checklist de Configuración
- Eureka Server en el puerto 8761
-
register-with-eureka: falsepara el servidor -
spring.application.namedefinido para cada cliente - Actuator para los health checks
- Load Balancer configurado
- Timeouts apropiados en producción
13. Quiz de Validación
Pregunta 1
¿Por qué @LoadBalanced es necesario en RestTemplate?
Ver la respuesta
@LoadBalanced inyecta un interceptor que:
- Intercepta las llamadas con un nombre de servicio (ej:
http://user-service/...) - Consulta Eureka para obtener las instancias
- Elige una instancia (round-robin por defecto)
- Sustituye el nombre por la IP:puerto real
- Realiza la llamada HTTP
Sin @LoadBalanced, RestTemplate intentaría resolver "user-service" como un DNS normal y fallaría.
Pregunta 2
¿Qué sucede si Eureka Server cae?
Ver la respuesta
Los clientes Eureka mantienen una caché local del registro:
- Los servicios ya conocidos siguen funcionando
- Los nuevos servicios no pueden registrarse
- Los clientes no descubren nuevas instancias
- El sistema está en modo "degradado" pero funcional
Por eso la alta disponibilidad (cluster) es importante en producción.