Capítulo 2: Service Discovery com Eureka
🎯 Objetivos do Capítulo
- Compreender o conceito de Service Discovery
- Configurar um Eureka Server
- Registrar serviços como Eureka Clients
- Implementar o Load Balancing client-side
- Gerenciar a alta disponibilidade do Eureka
1. Por que Service Discovery?
1.1 O Problema Sem Service Discovery
Problemas:
- URLs hardcoded na configuração
- Se um IP mudar → precisa alterar todos os clientes
- Se adicionar uma instância → precisa alterar todos os clientes
- Load balancing impossível
- Sem detecção de falhas
1.2 A Solução: Service Discovery
Vantagens:
- Serviços se registram dinamicamente
- Clientes descobrem os serviços pelo nome
- Load balancing automático
- Detecção automática de falhas
- Escalabilidade transparente
2. Netflix Eureka
2.1 O que é Eureka?
Eureka é um serviço de descoberta desenvolvido pela Netflix, composto por:
- Eureka Server: O registro central
- Eureka Client: Biblioteca em cada serviço
2.2 Arquitetura
2.3 Conceitos-Chave
| Conceito | Descrição |
|---|---|
| Register | Serviço se registra na inicialização |
| Renew (Heartbeat) | Ping a cada 30 segundos |
| Fetch Registry | Cliente busca a lista de serviços |
| Cancel | Serviço se desregistra ao parar |
| Eviction | Eureka remove serviços sem heartbeat (90s) |
3. Configuração do Eureka Server
3.1 Criar o Projeto
Estrutura do projeto:
eureka-server/
├── pom.xml
└── src/main/java/
└── com/example/eurekaserver/
└── EurekaServerApplication.java
└── src/main/resources/
└── application.yml
3.2 Dependências 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 mas 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 Aplicação 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 // Ativa o servidor Eureka
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
3.4 Configuração application.yml
server:
port: 8761
spring:
application:
name: eureka-server
eureka:
instance:
hostname: localhost
client:
# Não registrar a si mesmo (é o servidor!)
register-with-eureka: false
# Não buscar o registro (é o servidor!)
fetch-registry: false
service-url:
defaultZone: http://localhost:8761/eureka/
server:
# Desativar o modo de proteção em dev
enable-self-preservation: false
# Intervalo de limpeza das instâncias mortas
eviction-interval-timer-in-ms: 5000
# Logging
logging:
level:
com.netflix.eureka: INFO
com.netflix.discovery: INFO
3.5 Proteger o 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 do Eureka
Após iniciar, acesse: 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. Configuração do Eureka Client
4.1 Dependências 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 (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 Aplicação Principal
package com.example.userservice;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
// @EnableDiscoveryClient é opcional desde o Spring Cloud 2020
// A descoberta é automática se eureka-client está no classpath
public class UserServiceApplication {
public static void main(String[] args) {
SpringApplication.run(UserServiceApplication.class, args);
}
}
4.3 Configuração do Client
server:
port: 8081
spring:
application:
name: user-service # Nome do serviço no Eureka
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
# Intervalo de busca do registro
registry-fetch-interval-seconds: 5
instance:
# Usar IP ao invés do hostname
prefer-ip-address: true
# ID único da instância
instance-id: ${spring.application.name}:${server.port}:${random.uuid}
# Intervalo dos heartbeats
lease-renewal-interval-in-seconds: 10
# Tempo antes de expirar (3x o renewal)
lease-expiration-duration-in-seconds: 30
# Actuator para health checks
management:
endpoints:
web:
exposure:
include: health,info
endpoint:
health:
show-details: always
4.4 Configuração com Segurança
Se o Eureka Server estiver protegido:
eureka:
client:
service-url:
# Formato: http://user:password@host:port/eureka/
defaultZone: http://eureka:eureka123@localhost:8761/eureka/
5. Exemplo Completo: User Service
5.1 Estrutura do Projeto
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;
// Construtores
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 e 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 para ver qual instância está respondendo
@GetMapping("/info")
public ResponseEntity<String> getInfo() {
return ResponseEntity.ok("Response from User Service on port: " + serverPort);
}
}
6. Load Balancing com Spring Cloud LoadBalancer
6.1 Dependência
<!-- Incluído em spring-cloud-starter-netflix-eureka-client -->
<!-- Mas pode ser adicionado explicitamente -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
6.2 RestTemplate com 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 // Ativa o load balancing
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
6.3 Uso em um 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) {
// Usa o nome do serviço ao invés da URL!
// O Spring Cloud LoadBalancer resolve "user-service"
// para uma das instâncias registradas no 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 com Load Balancing (Reativo)
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 (Padrão)
Requisição 1 → Instância A
Requisição 2 → Instância B
Requisição 3 → Instância C
Requisição 4 → Instância A
...
7.2 Configuração Customizada
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 Configuração por Serviço
@Configuration
@LoadBalancerClient(name = "user-service", configuration = UserServiceLBConfig.class)
public class LoadBalancerConfiguration {
}
8. Alta Disponibilidade Eureka
8.1 Problema: Single Point of Failure
Se o Eureka Server cair:
- Novos serviços não conseguem se registrar
- Clientes não conseguem descobrir novos serviços
- MAS: o cache local continua funcionando
8.2 Solução: Eureka Cluster
8.3 Configuração 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 Configuração 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 Configuração do Client para Cluster
eureka:
client:
service-url:
# Lista de todos os 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 O que é?
O Eureka entra em modo "self-preservation" quando detecta uma perda massiva 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ê?
Cenário: Problema de rede entre Eureka e serviços
- O Eureka não recebe mais heartbeats
- Sem proteção: o Eureka remove todos os serviços
- Resultado: Catástrofe!
Com Self-Preservation:
- O Eureka detecta a perda massiva
- Ele mantém os serviços registrados
- Ele espera a rede voltar
9.3 Configuração
eureka:
server:
# Ativar/desativar (padrão: true)
enable-self-preservation: true
# Threshold de renovação (padrão: 0.85 = 85%)
renewal-percent-threshold: 0.85
# Intervalo de atualização do threshold
renewal-threshold-update-interval-ms: 900000
# Em desenvolvimento, pode desativar
# enable-self-preservation: false
10. Eureka REST API
10.1 Endpoints Principais
| Endpoint | Method | Descrição |
|---|---|---|
/eureka/apps | GET | Lista todos os serviços |
/eureka/apps/\{appId\} | GET | Detalhes de um serviço |
/eureka/apps/\{appId\}/\{instanceId\} | GET | Detalhes de uma instância |
/eureka/apps/\{appId\}/\{instanceId\} | DELETE | Desregistrar |
/eureka/apps/\{appId\}/\{instanceId\}/status | PUT | Alterar status |
10.2 Exemplos
# Listar todos os serviços
curl http://localhost:8761/eureka/apps
# Obter as instâncias do user-service
curl http://localhost:8761/eureka/apps/USER-SERVICE
# Verificar o status
curl http://localhost:8761/eureka/apps/USER-SERVICE/user-service:8081
# Colocar fora de serviço (OUT_OF_SERVICE)
curl -X PUT "http://localhost:8761/eureka/apps/USER-SERVICE/user-service:8081/status?value=OUT_OF_SERVICE"
# Colocar em serviço novamente (UP)
curl -X PUT "http://localhost:8761/eureka/apps/USER-SERVICE/user-service:8081/status?value=UP"
10.3 Resposta 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. Boas Práticas
11.1 Naming Convention
# ✅ Correto
spring:
application:
name: user-service
# ❌ Incorreto
spring:
application:
name: UserService
name: user_service
name: users
11.2 Health Checks
eureka:
instance:
# Ativar 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() {
// Desregistrar de forma limpa
eurekaClient.shutdown();
}
}
Ou via configuração:
server:
shutdown: graceful
spring:
lifecycle:
timeout-per-shutdown-phase: 30s
11.4 Timeouts Apropriados
eureka:
instance:
# Heartbeat a cada 10 segundos
lease-renewal-interval-in-seconds: 10
# Expira após 30 segundos sem heartbeat
lease-expiration-duration-in-seconds: 30
client:
# Atualizar o registro a cada 5 segundos
registry-fetch-interval-seconds: 5
12. Resumo
Pontos-Chave
✅ Eureka Server = Registro central dos serviços
✅ Eureka Client = Se registra e descobre os outros
✅ Load Balancing = Automático com @LoadBalanced
✅ Self-Preservation = Proteção contra falsos positivos
✅ Alta Disponibilidade = Cluster de servidores Eureka
Checklist de Configuração
- Eureka Server na porta 8761
-
register-with-eureka: falsepara o servidor -
spring.application.namedefinido para cada client - Actuator para health checks
- Load Balancer configurado
- Timeouts apropriados em produção
13. Quiz de Validação
Pergunta 1
Por que @LoadBalanced é necessário no RestTemplate?
Ver resposta
@LoadBalanced injeta um interceptor que:
- Intercepta chamadas com nome de serviço (ex:
http://user-service/...) - Consulta o Eureka para obter as instâncias
- Escolhe uma instância (round-robin por padrão)
- Substitui o nome pelo IP:porta real
- Realiza a chamada HTTP
Sem @LoadBalanced, o RestTemplate tentaria resolver "user-service" como um DNS normal e falharia.
Pergunta 2
O que acontece se o Eureka Server cair?
Ver resposta
Os clientes Eureka mantêm um cache local do registro:
- Os serviços já conhecidos continuam funcionando
- Novos serviços não conseguem se registrar
- Clientes não descobrem novas instâncias
- O sistema fica em "modo degradado", mas funcional
Por isso a alta disponibilidade (cluster) é importante em produção.