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.
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
RestTemplatethat uses the Ribbon strategy:
<code>@Configuration
public class RestTemplateConfig {
@LoadBalanced
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
</code>Local code can then use the
RestTemplateto 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
InterceptingHttpAccessorand receives
HttpRequestInterceptorinstances via the
interceptorsfield. The Ribbon auto‑configuration class is
RibbonAutoConfigurationin
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
RestTemplateclass and a
LoadBalancerClientbean in the Spring container.
LoadBalancerClienthas a single implementation in
spring-cloud-netflix-ribbon:
RibbonLoadBalancerClient.
Spring’s
SmartInitializingSingletonextension point adds a
LoadBalancerInterceptorto all
RestTemplateinstances 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
choosemethod 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
executemethod 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
ZoneAwareLoadBalancershows 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.chooseServerRibbon Summary
Key points for using
@LoadBalanced RestTemplate:
The URL string must be an absolute path (e.g.,
http://...); otherwise an
IllegalArgumentExceptionis thrown.
serviceIdis case‑insensitive (e.g.,
http://order-service/works the same as
http://ORDER‑SERVICE/).
Do not append a port number to
serviceId.
A
@LoadBalanced RestTemplatecan only use
serviceId; it cannot use an IP address or domain name. If both scenarios are needed, define separate
RestTemplatebeans.
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
selectInstancesmethod is invoked, which eventually calls
hostReactor.getServiceInfoto 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.getServiceInfochecks the local cache, creates a
ServiceInfoif absent, and triggers an update by calling the Nacos
/instance/listendpoint.
<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/listrequests, 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#doSrvIpxtwhich 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.srvIPsto retrieve all service instance information.
Cluster#allIPswrites all registration information into the service list.
Reference Links
https://nacos.io
https://zhuanlan.zhihu.com
https://blog.csdn.net/f641385712/article/details/100788040
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.