Pular para o conteúdo principal

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​

ConceitoDescrição
RegisterServiço se registra na inicialização
Renew (Heartbeat)Ping a cada 30 segundos
Fetch RegistryCliente busca a lista de serviços
CancelServiço se desregistra ao parar
EvictionEureka 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​

EndpointMethodDescrição
/eureka/appsGETLista todos os serviços
/eureka/apps/\{appId\}GETDetalhes de um serviço
/eureka/apps/\{appId\}/\{instanceId\}GETDetalhes de uma instância
/eureka/apps/\{appId\}/\{instanceId\}DELETEDesregistrar
/eureka/apps/\{appId\}/\{instanceId\}/statusPUTAlterar 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: false para o servidor
  • spring.application.name definido 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:

  1. Intercepta chamadas com nome de serviço (ex: http://user-service/...)
  2. Consulta o Eureka para obter as instâncias
  3. Escolhe uma instância (round-robin por padrão)
  4. Substitui o nome pelo IP:porta real
  5. 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.


Próximo Capítulo: Configuração Centralizada →

Voltar ao Módulo 13