springcloud中负载均衡工具ribbon用法及原理
1.ribbon用法
在这里我使用restTemplate和ribbon结合使用为例,前提是服务到注册到同一注册中心,本例使用的是nacos。
基本用法
-
一般情况下,我们使用restTemplate做服务调用,代码如下:
ResponseEntity<ProductInfo> responseEntity= restTemplate.getForEntity(uri+orderInfo.getProductNo(), ProductInfo.class); ProductInfo productInfo = responseEntity.getBody(); if(productInfo == null) { return "无数据"; }
当调用的服务有多个实例,需要做负载均衡是,则引入了ribbon,使用如下:

使用@LoadBalnaced注解,那么返回的restTemplate注解就是一个具有负载算法的对象;在调用服务时,会从注册中心获取被调用方的所有可调用实例列表,并依据负载均衡算法进行服务调用。
- 如果需要指定负载均衡算法,则在配置类中返回一个实现IRule接口的负载算法对象即可:

问题:那么ribbon是怎么让restTemplate具有负载均衡功能的呢?后面再分析原理
进阶用法
在一些业务场景中,我们希望对不同的微服务集群使用不同的负载均衡算法,甚至某些服务集群使用自定义的负载均衡算法。比如:我对订单服务集群使用轮询负载算法,对付费服务集群使用随机负载算法,对积分服务集群使用自定义的权重负载算法,这时我们有两种方式可以实现:
- 基于yml文件配置的方法(推荐的方式): 通过yml配置的方式,对不同的微服务集群采用不同的负载算法,ribbon中自带的负载算法均是实现IRule接口。
IRule接口是一个ribbon-balancer包下的一个上层接口,大致结构如下,使用的是模板方法模式,其实现类实现了不同的负载均衡算法:
类图如下:

我们可以基于此接口实现自己的负载均衡算法: 如下:
自定义负载算法类:TulingWeightedRule.class (自定义实现权重算法:nacos注册中心可以给集群中的实例设置权重)
package com.tuling.myrule;
import com.alibaba.cloud.nacos.NacosDiscoveryProperties;
import com.alibaba.cloud.nacos.ribbon.NacosServer;
import com.alibaba.nacos.api.exception.NacosException;
import com.alibaba.nacos.api.naming.NamingService;
import com.alibaba.nacos.api.naming.pojo.Instance;
import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.AbstractLoadBalancerRule;
import com.netflix.loadbalancer.BaseLoadBalancer;
import com.netflix.loadbalancer.Server;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
/**
*/
@Slf4j
public class TulingWeightedRule extends AbstractLoadBalancerRule {
@Autowired
private NacosDiscoveryProperties discoveryProperties;
@Override
public void initWithNiwsConfig(IClientConfig clientConfig) {
//读取配置文件并且初始化,ribbon内部的 几乎用不上
}
@Override
public Server choose(Object key) {
try {
log.info("key:{}",key);
BaseLoadBalancer baseLoadBalancer = (BaseLoadBalancer) this.getLoadBalancer();
log.info("baseLoadBalancer--->:{}",baseLoadBalancer);
//获取微服务的名称
String serviceName = baseLoadBalancer.getName();
//获取Nocas服务发现的相关组件API
NamingService namingService = discoveryProperties.namingServiceInstance();
//获取 一个基于nacos client 实现权重的负载均衡算法
Instance instance = namingService.selectOneHealthyInstance(serviceName);
//返回一个server
return new NacosServer(instance);
} catch (NacosException e) {
log.error("自定义负载均衡算法错误");
}
return null;
}
}
在yml配置中只需要指定对应的配置算法包位置即可,如下图所示:
这种配置方式不需要再添加其他的java代码,是最常用也是推荐的方式。
在真实开发中一般我们会使用feign代替restTemplate+ribbon,feign是对ribbon的封装,针对feign调用配置负载均衡算法,其实也是对ribbon的配置,配置方法和上图一致
- 通过javaConfig的方式配置 定义负载算法的配置类:跟基础用法的原理一样,通过配置类向容器中加入实现IRule接口的负载算法即可。
这种方式需要增加一个配置类。并使用@RibbonClients注解,代码如下:
package com.tuling.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 WebConfig {
@LoadBalanced
@Bean
public RestTemplate restTemplate( ) {
return new RestTemplate();
}
}
负载算法配置总类:
package com.tuling.config;
import com.ribbonconfig.GlobalRibbonConfig;
import com.ribbonconfig.PayCenterRibbonConfig;
import com.ribbonconfig.ProductCenterRibbonConfig;
import org.springframework.cloud.netflix.ribbon.RibbonClient;
import org.springframework.cloud.netflix.ribbon.RibbonClients;
import org.springframework.context.annotation.Configuration;
@Configuration
@RibbonClients(
defaultConfiguration = GlobalRibbonConfig.class, //全局默认算法
value = {
@RibbonClient(name = "product-center",configuration ProductCenterRibbonConfig.class), //服务集群product-center使用的负载算法
@RibbonClient(name = "pay-center",configuration = PayCenterRibbonConfig.class) //服务集群pay-center使用的负载算法
})
public class CustomRibbonConfig {
}
服务集群product-center对应的算法配置:ProductCenterRibbonConfig.class:
package com.ribbonconfig;
import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.RandomRule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ProductCenterRibbonConfig {
@Bean
public IRule randomRule() {
return new RandomRule();
}
}
服务集群pay-center对应的算法配置:PayCenterRibbonConfig.class:
package com.ribbonconfig;
import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.RoundRobinRule;
import org.springframework.context.annotation.Configuration;
@Configuration
public class PayCenterRibbonConfig {
public IRule roundRobinRule() {
return new RoundRobinRule();
}
}
全局默认使用自定义的负载算法(比如需要实现权重算法:nacos注册中心可以给集群中的实例设置权重):
全局默认算法对应配置: GlobalRibbonConfig.class
package com.ribbonconfig;
import com.netflix.loadbalancer.IRule;
import com.tuling.myrule.TheSameClusterPriorityRule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class GlobalRibbonConfig {
@Bean
public IRule theSameClusterPriorityRule() {
return new TulingWeightedRule(); //上文中自定义的负载算法
}
}
- 综上,就通过javaConfig的方式,实现了指定负载均衡算法,并且可以指定使用自定义的负载算法。
2.ribbon的原理
- 先通过一个例子来说明,也可以理解为手写实现一个简单的ribbon嵌入到restTemplate中:
首先还是基础的restTemplate配置类:
package com.tuling.config;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class WebConfig {
@Bean
public RestTemplate restTemplate(DiscoveryClient discoveryClient) {
return new TulingRestTemplate(discoveryClient);
}
}
可以看到这次我不是直接返回一个springcloud包中的RestTemplate的实例,而是一个自定义的TulingRestTemplate,该类代码如下:
package com.tuling.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.http.HttpMethod;
import org.springframework.http.client.ClientHttpRequest;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.web.client.*;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
import java.util.Random;
/**
* 根据RestTemplate特性自己改造
*/
@Slf4j
public class TulingRestTemplate extends RestTemplate {
private DiscoveryClient discoveryClient;
public TulingRestTemplate (DiscoveryClient discoveryClient) {
this.discoveryClient = discoveryClient;
}
protected <T> T doExecute(URI url, @Nullable HttpMethod method, @Nullable RequestCallback requestCallback,
@Nullable ResponseExtractor<T> responseExtractor) throws RestClientException {
Assert.notNull(url, "URI is required");
Assert.notNull(method, "HttpMethod is required");
ClientHttpResponse response = null;
try {
log.info("请求的url路径为:{}",url);
//把服务名 替换成我们的IP
url = replaceUrl(url);
log.info("替换后的路径:{}",url);
ClientHttpRequest request = createRequest(url, method);
if (requestCallback != null) {
requestCallback.doWithRequest(request);
}
response = request.execute();
handleResponse(url, method, response);
return (responseExtractor != null ? responseExtractor.extractData(response) : null);
}
catch (IOException ex) {
String resource = url.toString();
String query = url.getRawQuery();
resource = (query != null ? resource.substring(0, resource.indexOf('?')) : resource);
throw new ResourceAccessException("I/O error on " + method.name() +
" request for \"" + resource + "\": " + ex.getMessage(), ex);
} finally {
if (response != null) {
response.close();
}
}
}
/**
* 方法实现说明:把微服务名称 去注册中心拉取对应IP进行调用
* http://product-center/selectProductInfoById/1
* @param url:请求的url
* @return:
* @exception:
*/
private URI replaceUrl(URI url){
//1:从URI中解析调用的调用的serviceName=product-center
String serviceName = url.getHost();
log.info("调用微服务的名称:{}",serviceName);
//2:解析我们的请求路径 reqPath= /selectProductInfoById/1
String reqPath = url.getPath();
log.info("请求path:{}",reqPath);
//通过微服务的名称去nacos服务端获取 对应的实例列表
List<ServiceInstance> serviceInstanceList = discoveryClient.getInstances(serviceName);
if(serviceInstanceList.isEmpty()) {
throw new RuntimeException("没有可用的微服务实例列表:"+serviceName);
}
String serviceIp = chooseTargetIp(serviceInstanceList);
String source = serviceIp+reqPath;
try {
return new URI(source);
} catch (URISyntaxException e) {
log.error("根据source:{}构建URI异常",source);
}
return url;
}
/**
* 方法实现说明:从服务列表中 随机选举一个ip
* @param serviceInstanceList 服务列表
* @return: 调用的ip
* @exception:
*/
private String chooseTargetIp(List<ServiceInstance> serviceInstanceList) {
//采取随机的获取一个
Random random = new Random();
Integer randomIndex = random.nextInt(serviceInstanceList.size());
String serviceIp = serviceInstanceList.get(randomIndex).getUri().toString();
log.info("随机选举的服务IP:{}",serviceIp);
return serviceIp;
}
}
代码很多,思路就是通过继承RestTemplate类,并复写doExecute方法(在RestTemplate源码中可以看到,所有的调用方法最后都会走doExecute方法)。
大体意思是在调用时,去拉取注册中心该服务的服务实例列表,然后通过random随机算法来选出一个服务实例来进行调用。这样就相当于对RestTemplate进行了一个扩展,让它在调用服务时,拥有了负载均衡的功能。
- 那么ribbon也是通过扩展restTemplate来实现负载均衡的。
跟踪源码可以看到,ribbon是通过在spring容器启动时,向restTemplate中加入interceptor拦截器的方式来为restTemplate添加负载均衡功能,截图如下:

大体的思想就是这样!