Backend Development 21 min read

How Nacos and Ribbon Enable Client‑Side Load Balancing in Spring Cloud

This article explains how Nacos provides service discovery via its open API, how Spring Cloud Ribbon implements client‑side load balancing, and walks through the key source code of Ribbon, LoadBalancerInterceptor, RibbonLoadBalancerClient, ZoneAwareLoadBalancer, and Nacos server‑side handling, offering practical usage tips.

Ops Development Stories
Ops Development Stories
Ops Development Stories
How Nacos and Ribbon Enable Client‑Side Load Balancing in Spring Cloud

Nacos Service List Management

Nacos provides an open API that can retrieve the service list via

/nacos/v1/ns/instance/list

. When using Spring Cloud to obtain services, the request ultimately goes through Nacos Client + LoadBalancer, achieving client‑side load balancing.

Ribbon Source Code Analysis

Ribbon Overview

Spring Cloud Ribbon is the Netflix Ribbon implementation for client‑side load balancing. It offers configuration options such as timeout and retry, and automatically selects a server based on rules like round‑robin or random.

Ribbon Usage

First define a

RestTemplate

that uses the Ribbon strategy:

<code>@Configuration
public class RestTemplateConfig {
    @LoadBalanced
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}
</code>

Local code can then use the

RestTemplate

to call a remote interface:

<code>@Autowired
private RestTemplate restTemplate;

@RequestMapping(value = "/echo/{id}", method = RequestMethod.GET)
public String echo(@PathVariable Long id) {
    return restTemplate.getForObject("http://member-service/member/get/" + id, String.class);
}
</code>

Ribbon Source Code Analysis

RestTemplate extends

InterceptingHttpAccessor

and receives

HttpRequestInterceptor

instances via the

interceptors

field. The Ribbon auto‑configuration class is

RibbonAutoConfiguration

in

spring-cloud-netflix-ribbon

, which itself is loaded by

spring-cloud-common

. The relevant code is:

<code>@Configuration(proxyBeanMethods = false)
// The project must contain a RestTemplate class
@ConditionalOnClass(RestTemplate.class)
// The container must contain a LoadBalancerClient bean instance
@ConditionalOnBean(LoadBalancerClient.class)
@EnableConfigurationProperties(LoadBalancerRetryProperties.class)
public class LoadBalancerAutoConfiguration {

    // Get all RestTemplate instances from the Spring container
    @LoadBalanced
    @Autowired(required = false)
    private List<RestTemplate> restTemplates = Collections.emptyList();

    // Get LoadBalancerRequestTransformer instances from the container
    @Autowired(required = false)
    private List<LoadBalancerRequestTransformer> transformers = Collections.emptyList();

    // After bean initialization, set a RestTemplateCustomizer for each RestTemplate
    @Bean
    public SmartInitializingSingleton loadBalancedRestTemplateInitializerDeprecated(final ObjectProvider<List<RestTemplateCustomizer>> restTemplateCustomizers) {
        return () -> restTemplateCustomizers.ifAvailable(customizers -> {
            for (RestTemplate restTemplate : LoadBalancerAutoConfiguration.this.restTemplates) {
                for (RestTemplateCustomizer customizer : customizers) {
                    customizer.customize(restTemplate);
                }
            }
        });
    }

    // LoadBalancerRequestFactory bean
    @Bean
    @ConditionalOnMissingBean
    public LoadBalancerRequestFactory loadBalancerRequestFactory(LoadBalancerClient loadBalancerClient) {
        return new LoadBalancerRequestFactory(loadBalancerClient, this.transformers);
    }

    // LoadBalancerInterceptor configuration
    @Configuration(proxyBeanMethods = false)
    @ConditionalOnMissingClass("org.springframework.retry.support.RetryTemplate")
    static class LoadBalancerInterceptorConfig {

        // Create the default LoadBalancerInterceptor instance
        @Bean
        public LoadBalancerInterceptor ribbonInterceptor(LoadBalancerClient loadBalancerClient, LoadBalancerRequestFactory requestFactory) {
            return new LoadBalancerInterceptor(loadBalancerClient, requestFactory);
        }

        // If no RestTemplateCustomizer exists, add the interceptor to all RestTemplate instances
        @Bean
        @ConditionalOnMissingBean
        public RestTemplateCustomizer restTemplateCustomizer(final LoadBalancerInterceptor loadBalancerInterceptor) {
            return restTemplate -> {
                List<ClientHttpRequestInterceptor> list = new ArrayList<>(restTemplate.getInterceptors());
                list.add(loadBalancerInterceptor);
                restTemplate.setInterceptors(list);
            };
        }
    }
    // ...
}
</code>

Key takeaways from the code:

If load balancing is required, the project must contain a

RestTemplate

class and a

LoadBalancerClient

bean in the Spring container.

LoadBalancerClient

has a single implementation in

spring-cloud-netflix-ribbon

:

RibbonLoadBalancerClient

.

Spring’s

SmartInitializingSingleton

extension point adds a

LoadBalancerInterceptor

to all

RestTemplate

instances via

restTemplateCustomizer()

.

Load balancing essentially works by intercepting requests and delegating them to the

LoadBalancerClient

.

LoadBalancerInterceptor

The interceptor forwards the request to

LoadBalancerClient

, which selects a server based on the service name and the configured load‑balancing algorithm.

<code>public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor {

    // Load balancer
    private LoadBalancerClient loadBalancer;

    // Build request
    private LoadBalancerRequestFactory requestFactory;

    @Override
    public ClientHttpResponse intercept(final HttpRequest request, final byte[] body, final ClientHttpRequestExecution execution) throws IOException {
        final URI originalUri = request.getURI();
        String serviceName = originalUri.getHost();
        return this.loadBalancer.execute(serviceName, this.requestFactory.createRequest(request, body, execution));
    }
}
</code>

RibbonLoadBalancerClient

All requests eventually reach

RibbonLoadBalancerClient

, which implements

LoadBalancerClient

:

<code>public interface ServiceInstanceChooser {
    // Choose a specific service instance by serviceId
    ServiceInstance choose(String serviceId);
}

public interface LoadBalancerClient extends ServiceInstanceChooser {
    <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException;
    <T> T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest<T> request) throws IOException;
    URI reconstructURI(ServiceInstance instance, URI original);
}
</code>

The

choose

method selects a server:

<code>@Override
public ServiceInstance choose(String serviceId) {
    return choose(serviceId, null);
}

public ServiceInstance choose(String serviceId, Object hint) {
    Server server = getServer(getLoadBalancer(serviceId), hint);
    if (server == null) {
        return null;
    }
    return new RibbonServer(serviceId, server, isSecure(server, serviceId), serverIntrospector(serviceId).getMetadata(server));
}

protected ILoadBalancer getLoadBalancer(String serviceId) {
    return this.clientFactory.getLoadBalancer(serviceId);
}

protected Server getServer(ILoadBalancer loadBalancer, Object hint) {
    if (loadBalancer == null) {
        return null;
    }
    return loadBalancer.chooseServer(hint != null ? hint : "default");
}
</code>

The

execute

method builds the request, selects a server, and handles the response:

<code>@Override
public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {
    return execute(serviceId, request, null);
}

public <T> T execute(String serviceId, LoadBalancerRequest<T> request, Object hint) throws IOException {
    ILoadBalancer loadBalancer = getLoadBalancer(serviceId);
    Server server = getServer(loadBalancer, hint);
    if (server == null) {
        throw new IllegalStateException("No instances available for " + serviceId);
    }
    RibbonServer ribbonServer = new RibbonServer(serviceId, server, isSecure(server, serviceId), serverIntrospector(serviceId).getMetadata(server));
    return execute(serviceId, ribbonServer, request);
}

@Override
public <T> T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest<T> request) throws IOException {
    Server server = null;
    if (serviceInstance instanceof RibbonServer) {
        server = ((RibbonServer) serviceInstance).getServer();
    }
    if (server == null) {
        throw new IllegalStateException("No instances available for " + serviceId);
    }
    RibbonLoadBalancerContext context = this.clientFactory.getLoadBalancerContext(serviceId);
    // ... request execution and response handling ...
    T returnVal = request.apply(serviceInstance);
    return returnVal;
}
</code>

ZoneAwareLoadBalancer

The class diagram of

ZoneAwareLoadBalancer

shows it extends

DynamicServerListLoadBalancer

. Core methods include updating and initializing the server list,

updateAllServerList()

, setting the list with

setServersList()

, choosing a server via

chooseServer()

, obtaining the load balancer with

getLoadBalancer()

, and selecting a zone‑aware server with

zoneLoadBalancer.chooseServer

.

Update all service list:

updateAllServerList();

Set all service list:

setServersList();

Choose service instance:

chooseServer();

Choose load balancer:

getLoadBalancer();

Choose zone‑internal service instance:

zoneLoadBalancer.chooseServer

Ribbon Summary

Key points for using

@LoadBalanced
RestTemplate

:

The URL string must be an absolute path (e.g.,

http://...

); otherwise an

IllegalArgumentException

is thrown.

serviceId

is case‑insensitive (e.g.,

http://order-service/

works the same as

http://ORDER‑SERVICE/

).

Do not append a port number to

serviceId

.

A

@LoadBalanced
RestTemplate

can only use

serviceId

; it cannot use an IP address or domain name. If both scenarios are needed, define separate

RestTemplate

beans.

Nacos Service Query

Client Query

When using the default Nacos client, the service list is obtained via

NacosServerList#getUpdatedListOfServers()

:

<code>public class NacosServerList extends AbstractServerList<NacosServer> {

    private NacosDiscoveryProperties discoveryProperties;

    @Override
    public List<NacosServer> getUpdatedListOfServers() {
        return getServers();
    }

    private List<NacosServer> getServers() {
        try {
            String group = discoveryProperties.getGroup();
            List<Instance> instances = discoveryProperties.namingServiceInstance()
                .selectInstances(serviceId, group, true);
            return instancesToServerList(instances);
        } catch (Exception e) {
            throw new IllegalStateException("Can not get service instances from nacos, serviceId=" + serviceId, e);
        }
    }
}
</code>

The

selectInstances

method is invoked, which eventually calls

hostReactor.getServiceInfo

to retrieve service information from the local cache or, if missing, from Nacos via

/instance/list

:

<code>@Override
public List<Instance> selectInstances(String serviceName, String groupName, List<String> clusters, boolean healthy, boolean subscribe) throws NacosException {
    ServiceInfo serviceInfo;
    if (subscribe) {
        serviceInfo = hostReactor.getServiceInfo(NamingUtils.getGroupedName(serviceName, groupName),
                StringUtils.join(clusters, ","));
    } else {
        serviceInfo = hostReactor.getServiceInfoDirectlyFromServer(NamingUtils.getGroupedName(serviceName, groupName),
                StringUtils.join(clusters, ","));
    }
    return selectInstances(serviceInfo, healthy);
}
</code>

Core logic in

hostReactor.getServiceInfo

checks the local cache, creates a

ServiceInfo

if absent, and triggers an update by calling the Nacos

/instance/list

endpoint.

<code>public ServiceInfo getServiceInfo(final String serviceName, final String clusters) {
    String key = ServiceInfo.getKey(serviceName, clusters);
    if (failoverReactor.isFailoverSwitch()) {
        return failoverReactor.getService(key);
    }
    ServiceInfo serviceObj = getServiceInfo0(serviceName, clusters);
    if (serviceObj == null) {
        serviceObj = new ServiceInfo(serviceName, clusters);
        serviceInfoMap.put(serviceObj.getKey(), serviceObj);
        updatingMap.put(serviceName, new Object());
        updateServiceNow(serviceName, clusters);
        updatingMap.remove(serviceName);
    } else if (updatingMap.containsKey(serviceName)) {
        if (UPDATE_HOLD_INTERVAL > 0) {
            synchronized (serviceObj) {
                try {
                    serviceObj.wait(UPDATE_HOLD_INTERVAL);
                } catch (InterruptedException e) {
                    NAMING_LOGGER.error("[getServiceInfo] serviceName:" + serviceName + ", clusters:" + clusters, e);
                }
            }
        }
    }
    scheduleUpdateIfAbsent(serviceName, clusters);
    return serviceInfoMap.get(serviceObj.getKey());
}
</code>

This explains why the first Ribbon call is slower—it must initialize the service list by querying Nacos.

Server‑Side Processing

The server handles

/instance/list

requests, storing instances in a

ConcurrentHashMap

:

<code>/**
 * Map(namespace, Map(group::serviceName, Service)).
 */
private final Map<String, Map<String, Service>> serviceMap = new ConcurrentHashMap<>();
</code>

The entry point is

InstanceController#doSrvIpxt

which retrieves the service, filters instances, and builds the JSON response:

<code>public ObjectNode doSrvIpxt(String namespaceId, String serviceName, String agent, String clusters, String clientIP,
        int udpPort, String env, boolean isCheck, String app, String tid, boolean healthyOnly) throws Exception {
    ClientInfo clientInfo = new ClientInfo(agent);
    ObjectNode result = JacksonUtils.createEmptyJsonNode();
    Service service = serviceManager.getService(namespaceId, serviceName);
    long cacheMillis = switchDomain.getDefaultCacheMillis();
    // ... push client handling omitted ...
    if (service == null) {
        result.put("name", serviceName);
        result.put("clusters", clusters);
        result.put("cacheMillis", cacheMillis);
        result.replace("hosts", JacksonUtils.createEmptyArrayNode());
        return result;
    }
    // ... instance retrieval and filtering ...
    List<Instance> servedIPs = service.srvIPs(Arrays.asList(StringUtils.split(clusters, ",")));
    // ... health check, protect threshold, building hosts array ...
    ArrayNode hosts = JacksonUtils.createEmptyArrayNode();
    for (Instance instance : servedIPs) {
        if (!instance.isEnabled()) {
            continue;
        }
        ObjectNode ipObj = JacksonUtils.createEmptyJsonNode();
        ipObj.put("ip", instance.getIp());
        ipObj.put("port", instance.getPort());
        ipObj.put("valid", entry.getKey());
        ipObj.put("healthy", entry.getKey());
        ipObj.put("marked", instance.isMarked());
        ipObj.put("instanceId", instance.getInstanceId());
        ipObj.set("metadata", JacksonUtils.transferToJsonNode(instance.getMetadata()));
        ipObj.put("enabled", instance.isEnabled());
        ipObj.put("weight", instance.getWeight());
        ipObj.put("clusterName", instance.getClusterName());
        ipObj.put("serviceName", clientInfo.type == ClientInfo.ClientType.JAVA && clientInfo.version.compareTo(VersionUtil.parseVersion("1.0.0")) >= 0 ? instance.getServiceName() : NamingUtils.getServiceName(instance.getServiceName()));
        ipObj.put("ephemeral", instance.isEphemeral());
        hosts.add(ipObj);
    }
    result.replace("hosts", hosts);
    result.put("dom", clientInfo.type == ClientInfo.ClientType.JAVA && clientInfo.version.compareTo(VersionUtil.parseVersion("1.0.0")) >= 0 ? serviceName : NamingUtils.getServiceName(serviceName));
    result.put("name", serviceName);
    result.put("cacheMillis", cacheMillis);
    result.put("lastRefTime", System.currentTimeMillis());
    result.put("checksum", service.getChecksum());
    result.put("useSpecifiedURL", false);
    result.put("clusters", clusters);
    result.put("env", env);
    result.replace("metadata", JacksonUtils.transferToJsonNode(service.getMetadata()));
    return result;
}
</code>

Core steps:

Call

service.srvIPs

to retrieve all service instance information.

Cluster#allIPs

writes all registration information into the service list.

Reference Links

https://nacos.io

https://zhuanlan.zhihu.com

https://blog.csdn.net/f641385712/article/details/100788040

MicroservicesLoad BalancingNacosSpring CloudRestTemplateRibbon
Ops Development Stories
Written by

Ops Development Stories

Maintained by a like‑minded team, covering both operations and development. Topics span Linux ops, DevOps toolchain, Kubernetes containerization, monitoring, log collection, network security, and Python or Go development. Team members: Qiao Ke, wanger, Dong Ge, Su Xin, Hua Zai, Zheng Ge, Teacher Xia.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.