Saltar al contenido principal

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​

ConceptoDescripción
RegisterEl servicio se registra al iniciar
Renew (Heartbeat)Ping cada 30 segundos
Fetch RegistryEl cliente obtiene la lista de servicios
CancelEl servicio se da de baja al apagarse
EvictionEureka 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​

EndpointMethodDescripción
/eureka/appsGETLista todos los servicios
/eureka/apps/\{appId\}GETDetalles de un servicio
/eureka/apps/\{appId\}/\{instanceId\}GETDetalles de una instancia
/eureka/apps/\{appId\}/\{instanceId\}DELETEDar de baja
/eureka/apps/\{appId\}/\{instanceId\}/statusPUTCambiar 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: false para el servidor
  • spring.application.name definido 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:

  1. Intercepta las llamadas con un nombre de servicio (ej: http://user-service/...)
  2. Consulta Eureka para obtener las instancias
  3. Elige una instancia (round-robin por defecto)
  4. Sustituye el nombre por la IP:puerto real
  5. 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.


Capítulo Siguiente: Configuración Centralizada →

Volver al Módulo 13