@LoadBalanced y cliente HTTP
En el contexto de microservicios, uno de los desafíos más comunes es distribuir las peticiones entre múltiples instancias del mismo servicio. Spring Cloud LoadBalancer ofrece una solución elegante mediante la anotación @LoadBalanced
, que permite interceptar automáticamente las llamadas HTTP realizadas por clientes como RestTemplate
o RestClient
para aplicar balanceo de carga del lado del cliente.
Configuración de clientes HTTP con @LoadBalanced
La anotación @LoadBalanced
actúa como un interceptor que modifica el comportamiento de los clientes HTTP configurados. Cuando un cliente está marcado con esta anotación, Spring Cloud LoadBalancer intercepta las peticiones antes de que se envíen y resuelve automáticamente el nombre del servicio por la dirección IP y puerto de una instancia específica.
Configuración con RestClient (recomendado):
@Configuration
public class LoadBalancerConfig {
@Bean
@LoadBalanced
public RestClient.Builder restClientBuilder() {
return RestClient.builder();
}
}
Configuración con RestTemplate (modo mantenimiento):
@Configuration
public class LoadBalancerConfig {
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
RestClient vs RestTemplate: el cambio hacia la modernidad
Spring Framework ha evolucionado sus clientes HTTP, y RestClient se ha convertido en la opción recomendada para aplicaciones síncronas en Spring Boot 3. A diferencia de RestTemplate, que está en modo mantenimiento, RestClient ofrece una API más fluida y moderna, manteniendo compatibilidad total con @LoadBalanced
.
Ejemplo de uso con RestClient:
@Service
public class ProductService {
private final RestClient restClient;
public ProductService(RestClient.Builder restClientBuilder) {
this.restClient = restClientBuilder.build();
}
public String getProductInfo(Long productId) {
// El nombre 'product-service' se resuelve automáticamente
return restClient.get()
.uri("http://product-service/api/products/{id}", productId)
.retrieve()
.body(String.class);
}
}
Comparación con RestTemplate:
@Service
public class ProductService {
@Autowired
private RestTemplate restTemplate;
public String getProductInfo(Long productId) {
// Sintaxis más verbosa pero funcional
String url = "http://product-service/api/products/" + productId;
return restTemplate.getForObject(url, String.class);
}
}
Integración automática con Eureka
Cuando utilizas @LoadBalanced
junto con Eureka Discovery Client, la integración es completamente transparente. El cliente HTTP no necesita conocer las direcciones IP específicas de los servicios; simplemente utiliza el nombre del servicio registrado en Eureka.
@RestController
public class OrderController {
private final RestClient restClient;
public OrderController(RestClient.Builder restClientBuilder) {
this.restClient = restClientBuilder.build();
}
@GetMapping("/orders/{orderId}/user")
public UserResponse getUserForOrder(@PathVariable String orderId) {
// Spring Cloud LoadBalancer resuelve 'user-service' automáticamente
// consultando Eureka y aplicando balanceo de carga
return restClient.get()
.uri("http://user-service/api/users/{userId}", getUserIdFromOrder(orderId))
.retrieve()
.body(UserResponse.class);
}
private String getUserIdFromOrder(String orderId) {
// Lógica para obtener el userId del pedido
return "user123";
}
}
Dependencias necesarias
Para utilizar Spring Cloud LoadBalancer con clientes HTTP, necesitas incluir la dependencia apropiada en tu pom.xml
:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
Si ya tienes configurado Eureka Client, Spring Cloud LoadBalancer se incluye automáticamente como dependencia transitiva, por lo que no es necesario agregarlo explícitamente.
Manejo de errores y tolerancia a fallos
Los clientes configurados con @LoadBalanced
mantienen su comportamiento estándar de manejo de errores, pero ahora con la capacidad adicional de intentar con diferentes instancias si una falla:
@Service
public class NotificationService {
private final RestClient restClient;
public NotificationService(RestClient.Builder restClientBuilder) {
this.restClient = restClientBuilder.build();
}
public void sendNotification(String message) {
try {
String response = restClient.post()
.uri("http://notification-service/api/notifications")
.contentType(MediaType.APPLICATION_JSON)
.body(new NotificationRequest(message))
.retrieve()
.body(String.class);
log.info("Notification sent successfully: {}", response);
} catch (Exception e) {
log.error("Failed to send notification: {}", e.getMessage());
// El balanceador intentará con otra instancia automáticamente
}
}
}
Esta configuración básica con @LoadBalanced
proporciona la base para el balanceo de carga automático. En la siguiente sección exploraremos cómo Spring Cloud LoadBalancer implementa el algoritmo round-robin por defecto y cómo puedes observar su comportamiento en acción.
Round-robin básico
El algoritmo round-robin es la estrategia de balanceo de carga que utiliza Spring Cloud LoadBalancer por defecto. Este algoritmo distribuye las peticiones de manera secuencial y cíclica entre todas las instancias disponibles de un servicio, garantizando una distribución equitativa de la carga sin necesidad de configuración adicional.
Funcionamiento del algoritmo round-robin
Cuando Spring Cloud LoadBalancer recibe una petición hacia un servicio con múltiples instancias, el algoritmo round-robin selecciona la siguiente instancia siguiendo un orden predeterminado. Una vez que se ha enviado una petición a la última instancia disponible, el ciclo vuelve a empezar desde la primera.
El comportamiento es el siguiente:
- Primera petición → Instancia A
- Segunda petición → Instancia B
- Tercera petición → Instancia C
- Cuarta petición → Instancia A (reinicia el ciclo)
Esta distribución cíclica asegura que ninguna instancia reciba una carga desproporcionada, siempre que las peticiones lleguen de manera constante.
Demostración práctica con múltiples instancias
Para observar el comportamiento del round-robin en acción, necesitamos crear un escenario con múltiples instancias del mismo servicio. Aquí te mostramos cómo configurar y probar esta funcionalidad:
Paso 1: Crear el servicio proveedor
Primero, desarrollemos un servicio simple que nos permita identificar qué instancia está respondiendo:
@RestController
public class ProductController {
@Value("${server.port}")
private String port;
@Value("${spring.application.name}")
private String serviceName;
@GetMapping("/api/products/{id}")
public Map<String, Object> getProduct(@PathVariable String id) {
Map<String, Object> response = new HashMap<>();
response.put("productId", id);
response.put("productName", "Producto " + id);
response.put("price", Math.random() * 100);
response.put("instance", serviceName + ":" + port);
response.put("timestamp", LocalDateTime.now());
return response;
}
@GetMapping("/health")
public Map<String, String> health() {
Map<String, String> status = new HashMap<>();
status.put("status", "UP");
status.put("instance", serviceName + ":" + port);
return status;
}
}
Paso 2: Configurar múltiples instancias
Para ejecutar múltiples instancias del mismo servicio, configura diferentes archivos application.yml
o utiliza argumentos de línea de comandos:
application-instance1.yml:
spring:
application:
name: product-service
server:
port: 8081
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka
instance:
instance-id: ${spring.application.name}:${server.port}
application-instance2.yml:
spring:
application:
name: product-service
server:
port: 8082
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka
instance:
instance-id: ${spring.application.name}:${server.port}
Paso 3: Crear el cliente consumidor
Desarrolla un servicio cliente que utilice @LoadBalanced
para distribuir las peticiones:
@RestController
public class OrderController {
private final RestClient restClient;
public OrderController(RestClient.Builder restClientBuilder) {
this.restClient = restClientBuilder.build();
}
@GetMapping("/orders/{orderId}/product")
public Map<String, Object> getOrderProduct(@PathVariable String orderId) {
// Esta llamada será balanceada automáticamente entre instancias
return restClient.get()
.uri("http://product-service/api/products/{productId}", "P" + orderId)
.retrieve()
.body(Map.class);
}
@GetMapping("/test-load-balancing")
public List<Map<String, Object>> testLoadBalancing() {
List<Map<String, Object>> responses = new ArrayList<>();
// Realizar múltiples peticiones para observar el round-robin
for (int i = 1; i <= 6; i++) {
Map<String, Object> response = restClient.get()
.uri("http://product-service/api/products/{productId}", "TEST" + i)
.retrieve()
.body(Map.class);
responses.add(response);
}
return responses;
}
}
Observando el comportamiento round-robin
Cuando ejecutes el endpoint /test-load-balancing
, observarás una distribución alternada de las respuestas entre las instancias disponibles:
[
{
"productId": "TEST1",
"instance": "product-service:8081",
"timestamp": "2024-01-15T10:30:01"
},
{
"productId": "TEST2",
"instance": "product-service:8082",
"timestamp": "2024-01-15T10:30:02"
},
{
"productId": "TEST3",
"instance": "product-service:8081",
"timestamp": "2024-01-15T10:30:03"
},
{
"productId": "TEST4",
"instance": "product-service:8082",
"timestamp": "2024-01-15T10:30:04"
}
]
Configuración del balanceador (opcional)
Aunque round-robin es el algoritmo por defecto, puedes configurarlo explícitamente en tu application.yml
:
spring:
cloud:
loadbalancer:
hint:
default: round-robin
Ventajas y limitaciones del round-robin
Ventajas:
- Simplicidad: No requiere configuración adicional ni métricas complejas
- Distribución equitativa: Garantiza que todas las instancias reciban el mismo número de peticiones
- Rendimiento: Overhead mínimo en la selección de instancias
- Predictibilidad: El comportamiento es determinístico y fácil de debugar
Limitaciones:
- No considera la carga real: Una instancia lenta puede degradar el rendimiento general
- Capacidad uniforme: Asume que todas las instancias tienen la misma capacidad de procesamiento
- Estado de las instancias: No tiene en cuenta si una instancia está experimentando problemas
Monitorización del balanceo
Para verificar que el round-robin funciona correctamente, puedes añadir logging en tu aplicación cliente:
@Service
public class LoadBalancerMonitorService {
private static final Logger logger = LoggerFactory.getLogger(LoadBalancerMonitorService.class);
private final RestClient restClient;
private final Map<String, Integer> instanceCalls = new ConcurrentHashMap<>();
public LoadBalancerMonitorService(RestClient.Builder restClientBuilder) {
this.restClient = restClientBuilder.build();
}
public Map<String, Object> callWithMonitoring(String productId) {
Map<String, Object> response = restClient.get()
.uri("http://product-service/api/products/{id}", productId)
.retrieve()
.body(Map.class);
String instance = (String) response.get("instance");
instanceCalls.merge(instance, 1, Integer::sum);
logger.info("Petición enviada a: {} - Total llamadas por instancia: {}",
instance, instanceCalls);
return response;
}
@EventListener(ApplicationReadyEvent.class)
@Scheduled(fixedRate = 30000)
public void logStatistics() {
logger.info("Estadísticas de distribución: {}", instanceCalls);
}
}
Este enfoque te permite confirmar visualmente que las peticiones se distribuyen de manera equitativa entre todas las instancias registradas en Eureka, demostrando la efectividad del algoritmo round-robin predeterminado de Spring Cloud LoadBalancer.
Fuentes y referencias
Documentación oficial y recursos externos para profundizar en SpringBoot
Documentación oficial de SpringBoot
Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, SpringBoot es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de SpringBoot
Explora más contenido relacionado con SpringBoot y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
- Comprender la función de la anotación @LoadBalanced en clientes HTTP.
- Configurar RestClient y RestTemplate para balanceo de carga en Spring Cloud.
- Diferenciar entre RestClient y RestTemplate y su integración con Eureka.
- Entender el algoritmo round-robin y su funcionamiento en la distribución de peticiones.
- Implementar y monitorizar un escenario práctico con múltiples instancias y balanceo de carga.
Cursos que incluyen esta lección
Esta lección forma parte de los siguientes cursos estructurados con rutas de aprendizaje