Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 126 additions & 7 deletions docs/modules/ROOT/pages/spring-cloud-netflix.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -389,15 +389,134 @@ the ability to refresh Eureka clients. To do this set `eureka.client.refresh.en

=== Using Eureka with Spring Cloud LoadBalancer

We offer support for the Spring Cloud LoadBalancer `ZonePreferenceServiceInstanceListSupplier`.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did you rewrite the documentation? Please revert to focus on one thing

The `zone` value from the Eureka instance metadata (`eureka.instance.metadataMap.zone`) is used for setting the
value of `spring-cloud-loadbalancer-zone` property that is used to filter service instances by zone.
Spring Cloud Netflix integrates with Spring Cloud LoadBalancer to provide
client-side load balancing for services registered with Eureka.

If that is missing and if the `spring.cloud.loadbalancer.eureka.approximateZoneFromHostname` flag is set to `true`,
it can use the domain name from the server hostname as a proxy for the zone.
When a client makes a request to another service using a logical service ID,
Spring Cloud LoadBalancer selects an appropriate service instance based on
available instances retrieved from Eureka.

If there is no other source of zone data, then a guess is made, based on the client configuration (as opposed to the instance configuration).
We take `eureka.client.availabilityZones`, which is a map from region name to a list of zones, and pull out the first zone for the instance's own region (that is, the `eureka.client.region`, which defaults to "us-east-1", for compatibility with native Netflix).
==== How It Works

The load balancing process follows these steps:

1. A client makes a request using a service ID (for example, `http://user-service`).
2. The `DiscoveryClient` retrieves all available instances of the service from Eureka.
3. Spring Cloud LoadBalancer selects one instance using a load balancing strategy.
4. The request is routed to the selected service instance.

This mechanism enables service discovery and client-side load balancing across multiple instances.

==== Configuration Example

The following example shows a minimal configuration for using Eureka with Spring Cloud LoadBalancer:

.application.yml
[source,yaml]
----
spring:
application:
name: user-service
cloud:
loadbalancer:
ribbon:
enabled: false

eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
----

NOTE: Ribbon is deprecated and replaced by Spring Cloud LoadBalancer.

==== Using Load-Balanced Clients

Spring Cloud LoadBalancer can be used with `RestTemplate` or `WebClient`.

.Example using RestTemplate
[source,java,indent=0]
----
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
----

.Example using WebClient
[source,java,indent=0]
----
@Bean
@LoadBalanced
public WebClient.Builder webClientBuilder() {
return WebClient.builder();
}
----

With these configurations, requests made using a service name are automatically resolved
to an actual service instance registered in Eureka.

==== Zone-Based Load Balancing

Spring Cloud LoadBalancer supports zone-based routing by using metadata provided
by Eureka instances.

To define a zone for a service instance, use the following configuration:

.application.yml
[source,yaml]
----
eureka:
instance:
metadataMap:
zone: zone1
----

When multiple instances exist across zones, the load balancer can prioritize instances
in the same zone as the client.

The `ZonePreferenceServiceInstanceListSupplier` is used to filter instances based on zone.

==== Zone Resolution

The zone used by the load balancer can be determined in multiple ways:

- Explicitly via `eureka.instance.metadataMap.zone`
- From the hostname if the following property is enabled:

[source]
----
spring.cloud.loadbalancer.eureka.approximateZoneFromHostname=true
----

When this property is enabled, the system attempts to infer the zone from the domain name
of the service instance hostname.

If no zone information is available, the system falls back to the client configuration,
such as `eureka.client.availabilityZones`.

==== Troubleshooting

If load balancing does not behave as expected, consider the following:

- Ensure that all service instances are properly registered in Eureka
- Verify that `metadataMap.zone` is correctly configured
- Check that the Eureka client is able to fetch registry data
- Enable debug logs to inspect load balancer decisions

==== Best Practices

- Always define zone metadata for service instances in multi-zone deployments
- Use consistent naming conventions for zones
- Avoid relying solely on hostname-based zone approximation in production
- Monitor service instance health and registration status

==== Summary

Spring Cloud LoadBalancer, when combined with Eureka, provides a flexible and dynamic
approach to client-side load balancing. It enables services to discover and communicate
with each other efficiently while distributing traffic across available instances.

=== AOT and Native Image Support

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright 2013-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.cloud.netflix.eureka;

import java.util.HashMap;
import java.util.Map;

import com.netflix.appinfo.InstanceInfo.InstanceStatus;

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties("eureka.client.status.mapping")
public class EurekaClientStatusMappingProperties {

private Map<String, InstanceStatus> mapping = new HashMap<>();

public Map<String, InstanceStatus> getMapping() {
return mapping;
}

public void setMapping(Map<String, InstanceStatus> mapping) {
this.mapping = mapping;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import java.util.HashMap;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

Expand Down Expand Up @@ -83,6 +84,8 @@ public class EurekaHealthCheckHandler

private final Map<String, HealthContributor> healthContributors = new HashMap<>();

private final EurekaClientStatusMappingProperties properties;

/**
* {@code true} until the context is stopped.
*/
Expand All @@ -91,9 +94,14 @@ public class EurekaHealthCheckHandler
private final Map<String, ReactiveHealthContributor> reactiveHealthContributors = new HashMap<>();

public EurekaHealthCheckHandler(StatusAggregator statusAggregator) {
this(statusAggregator, new EurekaClientStatusMappingProperties());
}

public EurekaHealthCheckHandler(StatusAggregator statusAggregator, EurekaClientStatusMappingProperties properties) {

this.statusAggregator = statusAggregator;
this.properties = properties;
Assert.notNull(statusAggregator, "StatusAggregator must not be null");

}

@Override
Expand Down Expand Up @@ -179,9 +187,27 @@ else if (contributor instanceof ReactiveHealthIndicator) {
}

protected InstanceStatus mapToInstanceStatus(Status status) {

if (status == null) {
return InstanceStatus.UNKNOWN;
}

String statusCode = status.getCode().toLowerCase(Locale.ROOT);

// 🔥 Custom mapping (case-insensitive)
if (properties != null && properties.getMapping() != null) {
for (Map.Entry<String, InstanceStatus> entry : properties.getMapping().entrySet()) {
if (entry.getKey().equalsIgnoreCase(statusCode)) {
return entry.getValue();
}
}
}

// 🔁 Default mapping
if (!STATUS_MAPPING.containsKey(status)) {
return InstanceStatus.UNKNOWN;
}

return STATUS_MAPPING.get(status);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import org.springframework.boot.health.contributor.HealthContributors;
import org.springframework.boot.health.contributor.HealthIndicator;
import org.springframework.boot.health.contributor.ReactiveHealthIndicator;
import org.springframework.boot.health.contributor.Status;
import org.springframework.cloud.client.discovery.health.DiscoveryClientHealthIndicator;
import org.springframework.cloud.client.discovery.health.DiscoveryCompositeHealthContributor;
import org.springframework.cloud.client.discovery.health.DiscoveryHealthIndicator;
Expand Down Expand Up @@ -161,6 +162,31 @@ void testCompositeComponentsOneDown() {
assertThat(status).isEqualTo(InstanceStatus.DOWN);
}

@Test
void testCustomStatusMapping() {

EurekaClientStatusMappingProperties props = new EurekaClientStatusMappingProperties();
props.getMapping().put("fatal", InstanceStatus.OUT_OF_SERVICE);

EurekaHealthCheckHandler handler = new EurekaHealthCheckHandler(new SimpleStatusAggregator(), props);

Status status = new Status("fatal");

InstanceStatus result = handler.mapToInstanceStatus(status);

assertThat(result).isEqualTo(InstanceStatus.OUT_OF_SERVICE);
}

@Test
void testNullStatusReturnsUnknown() {

EurekaHealthCheckHandler handler = new EurekaHealthCheckHandler(new SimpleStatusAggregator());

InstanceStatus result = handler.mapToInstanceStatus(null);

assertThat(result).isEqualTo(InstanceStatus.UNKNOWN);
}

private void initialize(Class<?>... configurations) {
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(configurations);
healthCheckHandler.setApplicationContext(applicationContext);
Expand Down
Loading