③SpringCloud 实战:使用 Ribbon 客户端负载均衡
这是SpringCloud实战系列中第三篇文章,了解前面第两篇文章更有助于更好理解本文内容:
①SpringCloud 实战:引入Eureka组件,完善服务治理
②SpringCloud 实战:引入Feign组件,发起服务间调用
简介
Ribbon 是由 Netflix 发布的一个客户端负载均衡器,它提供了对 HTTP 和 TCP 客户端行为的大量控制。Ribbon 可以基于某些负载均衡的算法,自动为客户端选择发起理论最优的网络请求。常见的负载均衡算法有:轮询,随机,哈希,加权轮询,加权随机等。
客户端负载均衡的意思就是发起网络请求的端根据自己的网络请求情况来做相应的负载均衡策略,与之相对的非客户端负载均衡就有比如硬件F5、软件Nginx,它们更多是介于消费者和提供者之间的,并非客户端。
改造eureka-provider项目
在使用之前我们先把第二节里面的 eureka-provider 项目改造一下,在HelloController 里面新增一个接口,输出自己项目的端口信息,用于区别验证待会儿客户端负载均衡时所调用的服务。
新增接口方法,返回自己的端口号信息:
@Controller
public class HelloController{
@Value("${server.port}")
private int serverPort;
... @ResponseBody
@GetMapping("queryPort")
public String queryPort(){
return "hei, jinglingwang, my server port is:"+serverPort;
}
}分别以8082,8083,8084端口启动该项目:eureka-provider
下图是 IDEA 快速启动三个不同端口项目方法截图,当然你也可以用其他办法
然后启动,访问三个接口测试一下是否正常返回了对应端口

至此,服务提供者的接口准备工作就做好了。
新建Ribbon-Client 项目
我们使用 Spring Initializr 生成SpringCloud项目基础框架,然后修改pom.xml里面的SpringBoot和SpringCloud的版本,对应版本修改请求如下:
...
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.3.RELEASE</version> <!--修改了版本jinglingwang.cn-->
<relativePath/> <!-- lookup parent from repository -->
</parent>
... 略
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Finchley.SR4</spring-cloud.version><!--修改了版本-->
</properties>
... 略
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
为什么要单独修改版本呢?因为从 Spring Cloud Hoxton.M2 版本开始,Spring Cloud 已经不再默认使用Ribbon来做负载均衡了,而是使用 spring-cloud-starter-loadbalancer替代。所以我们在使用 Spring Initializr 生成项目框架的时,如果使用最新版本Spring Cloud将不再提供Ribbon相关的组件。需要我们自己引入或者使用低一点的版本。
之后就是在ribbon-client项目引入eureka-client依赖和openfeign的依赖,这个过程省略,如果不会的话请看前两篇文章。
Ribbon 的三种使用方式
我们在新建的ribbon-client项目里面来使用三种方式调用eureka-provider的queryPort接口,因为eureka-provider服务启动了三个节点,到时候只要观察三种方式的响应结果,就可以判断负载均衡是否有生效。
一、使用原生API
直接使用LoadBalancerClient来获得对应的实例,然后发起URL请求,编写对应的RibbonController:
@RestController
public class RibbonController{
@Autowired
private LoadBalancerClient loadBalancer;
@GetMapping("ribbonApi")
public String ribbonApi() throws Exception{
ServiceInstance instance = loadBalancer.choose("eureka-provider");
System.out.println(instance.getUri());
URL url = new URL("http://localhost:" + instance.getPort() + "/queryPort");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
InputStream inputStream = conn.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
String line = null;
StringBuffer buffer = new StringBuffer();
while ((line = reader.readLine()) != null) {
buffer.append(line);
}
reader.close();
return Observable.just(buffer.toString()).toBlocking().first();
}
}
启动Ribbon-Client服务,访问http://localhost:7071/ribbonApi 接口,多次刷新接口发现采用的是轮询方式,运行效果图如下:

二、结合RestTemplate使用
使用 RestTemplate 的话,我们只需要再结合@LoadBalanced注解一起使用即可:
@Configuration
public class RestTemplateConfig{
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
编写RibbonController:
@RestController
public class RibbonController{
@Autowired
private RestTemplate restTemplate;
@GetMapping("queryPortByRest")
public String queryPortByRest(){
return restTemplate.getForEntity("http://eureka-provider/queryPort",String.class).getBody();
}
}
启动ribbon-client服务,访问http://localhost:7071/queryPortByRest 接口,多次刷新接口发现采用的也是轮询方式,运行效果图如下:

三、结合Feign使用
新建一个Feign:
@FeignClient(value = "eureka-provider")
public interface ProviderFeign{
/**
* 调用服务提供方,其中会返回服务提供者的端口信息
* @return jinglingwang.cn
*/
@RequestMapping("/queryPort")
String queryPort();
}
编写调用接口:
@RestController
public class RibbonController{
...略
@Autowired
private ProviderFeign providerFeign;
...
@GetMapping("queryPort")
public String queryPort(){
// 通过feign ribbon-client 调用 eureka-provider
return providerFeign.queryPort();
}
}
启动ribbon-client服务,访问 http://localhost:7071/queryPort 接口,多次刷新接口发现采用的也是轮询方式,运行效果图如下:

自定义Ribbon配置
为指定的客户端自定义负载均衡规则
在配置之前先做一点准备工作,我们把之前的服务eureka-provider再起3个节点,启动之前把端口改为8085、8086、8087,三个节点的服务名改为eureka-provider-temp。这样做的目的是等会儿我们新建一个Feign,但是名字和之前的区分开,相当于两个不同的服务,并且都是多节点的。
以上准备工作做完之后你会在IDEA中看到如下图的6个服务:

在注册中心也可以观察到2个不同的服务,一共6个节点:

eureka-provide 和 eureka-provide-temp 他们唯一的区别就是服务名不一样、端口不一样。
JavaBean的配置方式
现在开始为Feign配置ribbon:
新建一个Feign,命名为:ProviderTempFeign
@FeignClient(value = "eureka-provider-temp")
public interface ProviderTempFeign{ @RequestMapping("/queryPort")
String queryPort();
}
使用JAVA Bean的方式定义配置项
public class ProviderTempConfiguration{
@Bean
public IRule ribbonRule(){
System.out.println("new ProviderTempConfiguration RandomRule");
return new RandomRule(); // 定义一个随机的算法
}
@Bean
public IPing ribbonPing() {
// return new PingUrl();
return new NoOpPing();
}
}
使用注解
@RibbonClient配置负载均衡客户端:@RibbonClient(name = "eureka-provider-temp",configuration = ProviderTempConfiguration.class)
public class ProviderTempRibbonClient{ }
在Controller新增一个接口,来调用新增Feign(eureka-provider-temp)的方法
@GetMapping("queryTempPort")
public String queryTempPort(){
return providerTempFeign.queryPort();
}
再为另一个Feign(
eureka-provider)也配置一下ribbon,对外接口还是上面已经写好了public class ProviderConfiguration{
@Bean
public IRule ribbonRule(){
System.out.println("new ProviderConfiguration BestAvailableRule");
return new BestAvailableRule(); // 选择的最佳策略
}
@Bean
public IPing ribbonPing() {
// return new PingUrl();
return new NoOpPing();
}
} @RibbonClient(name = "eureka-provider",configuration = ProviderConfiguration.class)
public class ProviderRibbonClient{ }
启动服务之后分别访问两个接口(http://localhost:7071/queryPort 和 http://localhost:7071/queryTempPort),观察接口的端口返回情况
如果以上过程顺利的话,你访问queryPort接口的时候返回的端口不是随机的,几乎没怎么变化,访问queryTempPort接口的时候,接口返回的端口是随机的,说明我们以上配置是可行的。而且第一次访问接口的时候,我们在控制台打印了出对应的算法规则,你可以观察一下。
配置文件的配置方式
以上的配置也可以写到配置文件中,效果是一样的:
# 通过配置文件 分别为每个客户端配置
eureka-provider.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.BestAvailableRule
eureka-provider.ribbon.NFLoadBalancerPingClassName=com.netflix.loadbalancer.NoOpPing
eureka-provider-temp.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RandomRule
eureka-provider-temp.ribbon.NFLoadBalancerPingClassName=com.netflix.loadbalancer.NoOpPing
配置的规则是:.ribbo. = xxxXXX,其中configKey可以在 CommonClientConfigKey.class类中查看。常用的有:
NFLoadBalancerClassName
NFLoadBalancerRuleClassName
NFLoadBalancerPingClassName
NIWSServerListClassName
NIWSServerListFilterClassName
为所有的客户端自定义默认的配置
这里需要用到的注解是@RibbonClients
@Configuration()
public class DefaultRibbonConfiguration{
@Bean
public IRule iRule() {
// 轮询
return new RoundRobinRule();
}
@Bean
public IPing ribbonPing() {
return new DummyPing();
}
}
@RibbonClients(defaultConfiguration = DefaultRibbonConfiguration.class)
public class DefaultRibbonClient{
****}
启动我们的ribbon-client服务,测试访问下我们的http://localhost:7071/queryPort 接口,发现返回的数据每次都不一样,变为轮询的方式返回接口信息了。
测试到这里的时候,配置文件中的相关配置我并没有注释掉,Java Bean方式的@RibbonClient被注释掉了,也就是说测试的时候同时配置了配置文件和@RibbonClients,最后测试下来是@RibbonClients配置生效了,配置文件中配置的策略没有生效。
测试下来,@RibbonClients 的优先级最高,之后是配置文件,再是@RibbonClient,最后是Spring Cloud Netflix 默认值。
同时使用@RibbonClients和@RibbonClient
如果同时使用@RibbonClients和@RibbonClient,全局默认配置和自定义单个ribbon配置,会按照哪个配置生效呢?
我把配置文件中的相关配置都注释,然后把两个配置 @RibbonClient 的地方都放开,然后重启项目,访问http://localhost:7071/queryPort 和 http://localhost:7071/queryTempPort
测试结果是都报错,报错信息如下:
org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'com.netflix.loadbalancer.IRule' available: expected single matching bean but found 2: providerRule,iRule
报错信息的意思是预期需要一个bean,但是结果找到了两个(providerRule 和 iRule),结果不知道该用哪一个了,所以抛出异常。
那这个问题怎么解决呢?
首先直接说结论吧,就是给你想要生效的那个bean加@Primary注解,代码如下所示,如果eureka-provider 不加还是会继续报错:
public class ProviderTempConfiguration{
@Primary
@Bean("providerTempRule")
public IRule ribbonRule(){
System.out.println("new ProviderTempConfiguration RandomRule");
return new RandomRule();
}
...
}
再说下排查这个问题的思路:
通过查看异常输出栈的错误日志信息,定位到抛出异常的地方

之后继续往前面找相关的逻辑,加断点,慢慢调试,发现有一个字段(
autowiredBeanName)为空,才会进入到后面抛异常的逻辑
断点也显示matchingBeans里面有两条数据,说明确实是匹配到了2个bean

然后我们进入到
determineAutowireCandidate方法,发现里面有个看起来很不一般的字段:primaryCandidate,如果这个字段不为空,会直接返回,那这个字段的值是怎么确认的呢?继续进入到
determinePrimaryCandidate方法,发现这个方法的主要功能就是从给定的多个bean中确定一个主要的候选对象bean,说白了就是选一个bean,那这个方法是怎么选的呢?上源代码:@Nullable
protected String determinePrimaryCandidate(Map<String, Object> candidates, Class<?> requiredType) {
String primaryBeanName = null;
// candidates 是匹配到的多个bean
// requiredType 是要匹配的目标依赖类型
for (Map.Entry<String, Object> entry : candidates.entrySet()) { // 遍历map
String candidateBeanName = entry.getKey();
Object beanInstance = entry.getValue();
if (isPrimary(candidateBeanName, beanInstance)) { // 最重要的逻辑,看是不是主要的bean,看到这有经验的其实都知道要加@Primary注解了
if (primaryBeanName != null) {
boolean candidateLocal = containsBeanDefinition(candidateBeanName);
boolean primaryLocal = containsBeanDefinition(primaryBeanName);
if (candidateLocal && primaryLocal) {
throw new NoUniqueBeanDefinitionException(requiredType, candidates.size(),
"more than one 'primary' bean found among candidates: " + candidates.keySet());
}
else if (candidateLocal) {
primaryBeanName = candidateBeanName;
}
}
else {
primaryBeanName = candidateBeanName;
}
}
}
return primaryBeanName;
}
进入到
isPrimary(candidateBeanName, beanInstance)方法,最后实际就是返回的以下逻辑:@Override
public boolean isPrimary() {
return this.primary;
}
所以解决上面的问题,只需要在我们的ProviderTempConfiguration类里面为bean 再添加一个
@Primary注解
Ribbon超时时间
全局默认配置
# 全局ribbon超时时间
#读超时
ribbon.ReadTimeout=3000
#连接超时
ribbon.ConnectTimeout=3000
#同一台实例最大重试次数,不包括首次调用
ribbon.MaxAutoRetries=0
#重试负载均衡其他的实例最大重试次数,不包括首次调用
ribbon.MaxAutoRetriesNextServer=1
为每个client单独配置
# 为每个服务单独配置超时时间
eureka-provider.ribbon.ReadTimeout=4000
eureka-provider.ribbon.ConnectTimeout=4000
eureka-provider.ribbon.MaxAutoRetries=0
eureka-provider.ribbon.MaxAutoRetriesNextServer=1
自定义Ribbon负载均衡策略
Ribbon定义了以下几个属性支持自定义配置:
<clientName>.ribbon.NFLoadBalancerClassName: Should implement ILoadBalancer
<clientName>.ribbon.NFLoadBalancerRuleClassName: Should implement IRule
<clientName>.ribbon.NFLoadBalancerPingClassName: Should implement IPing
<clientName>.ribbon.NIWSServerListClassName: Should implement ServerList
<clientName>.ribbon.NIWSServerListFilterClassName: Should implement ServerListFilter
这里以自定义负载均衡策略规则为例,只需要实现IRule接口或者继承AbstractLoadBalancerRule:
public class MyRule implements IRule{
private static Logger log = LoggerFactory.getLogger(MyRule.class);
private ILoadBalancer lb;
@Override
public Server choose(Object key){
if (lb == null) {
return null;
}
Server server = null;
while (server == null) {
if (Thread.interrupted()) {
return null;
}
List<Server> allList = lb.getAllServers();
int serverCount = allList.size();
if (serverCount == 0) {
log.warn("No up servers available from load balancer: " + lb);
return null;
}
// 是轮询、随机、加权、hash?自己实现从server list中选择一个server
// 这里写简单点,总是请求第一台服务,这样的逻辑是不会用到真实的环境的
server = allList.get(0);
}
return server;
}
@Override
public void setLoadBalancer(ILoadBalancer lb){
this.lb = lb;
}
@Override
public ILoadBalancer getLoadBalancer(){
return lb;
}
}
然后就可以用Java Bean的方式或者配置文件的方式进行配置了,其他像自定义ping的策略也差不多。
Ribbon总结
- Ribbon 没有类似@EnableRibbon这样的注解
- 新版的SpringCloud已经不使用Ribbon作为默认的负载均衡器了
- 可以使用
@RibbonClients或@RibbonClient注解来负载均衡相关策略的配置 - 实现对应的接口就可以完成自定义负载均衡策略
- Ribbon 配置的所有key都可以在
CommonClientConfigKey类中查看
③SpringCloud 实战:使用 Ribbon 客户端负载均衡的更多相关文章
- springcloud(十三):Ribbon客户端负载均衡实例
一.采用默认的负载均衡策略:RoundRobinRule 轮询策略 1.修改提供者原的控制类 在之前的eureka-client-provider项目的CenterController.java中加入 ...
- springcloud(十二):Ribbon客户端负载均衡介绍
springcloud(十二):Ribbon客户端负载均衡介绍 Ribbon简介 使用分布式微服务脚骨的应用系统,在部署的时候通常会为部分或者全部微服务搭建集群环境,通过提供多个实例来提高系统的稳定型 ...
- spring cloud --- Ribbon 客户端负载均衡 + RestTemplate + Hystrix 熔断器 [服务保护] ---心得
spring boot 1.5.9.RELEASE spring cloud Dalston.SR1 1.前言 当超大并发量并发访问一个服务接口时,服务器会崩溃 ,不仅导致这个接口无法 ...
- spring cloud --- Ribbon 客户端负载均衡 + RestTemplate ---心得【无熔断器】
spring boot 1.5.9.RELEASE spring cloud Dalston.SR1 1.前言 了解了 eureka 服务注册与发现 的3大角色 ,会使用RestTem ...
- SpringBoot(三) - Ribbon客户端负载均衡,Zuul网关,Config配置中心
1.Ribbon客户端负载均衡 1.1 依赖 1.2 配置信息 # feign默认加载了ribbon负载均衡,默认负载均衡机制是:轮询 # 负载均衡机制是添加在消费端(客户端)的,如果改为随机,指定服 ...
- SpringCloud实战-Ribbon客户端负载均衡
前面我们已经完成了注册中心和服务提供者两个基础组件.接着介绍使用Spring Cloud Ribbon在客户端负载均衡的调用服务. ribbon 是一个客户端负载均衡器,可以简单的理解成类似于 ngi ...
- springcloud 之Ribbon客户端负载均衡配置使用
pom.xml添加配置说明:这里服务注册与发现用的是Eureka,所以消费者端需要引入eureka,使用EurekaClient来调用服务 <dependency> <groupId ...
- 笔记:Spring Cloud Ribbon 客户端负载均衡
Spring Cloud Ribbon 是一个基于 HTTP 和 TCP 的客户端负载均衡工具,基于 Netflix Ribbon 实现,通过Spring Cloud 的封装,可以让我们轻松的将面向服 ...
- Spring Cloud Ribbon——客户端负载均衡
一.负载均衡负载均衡(Load Balance): 建立在现有网络结构之上,它提供了一种廉价有效透明的方法扩展网络设备和服务器的带宽.增加吞吐量.加强网络数据处理能力.提高网络的灵活性和可用性.其意思 ...
随机推荐
- 学了元件作用域,我终于对JMeter开窍了
引子 先看一下这个例子,测试计划"进入考场"下面有一个线程组,线程组下面有 3 个 HTTP 请求,分别是学生登录.考场 token和进入房间: 它们的处理逻辑是: 学生登录后,在 ...
- FloodFill算法详解及应用
啥是 FloodFill 算法呢,最直接的一个应用就是「颜色填充」,就是 Windows 绘画本中那个小油漆桶的标志,可以把一块被圈起来的区域全部染色. 这种算法思想还在许多其他地方有应用.比如说扫雷 ...
- 抓紧下载了!2020最新版《神经网络与深度学习》中文版,PDF免费开放下载
介绍<神经⽹络和深度学习>是⼀本免费的在线书,对读者数学知识需求适度,兼顾理论和动手实践.⽬前给出了在图像识别.语⾳识别和⾃然语⾔处理领域中很多问题的最好解决⽅案,教读者在神经⽹络和深度学 ...
- Docker(3)- Centos 7.x 下 Docker 镜像加速配置
如果你还想从头学起 Docker,可以看看这个系列的文章哦! https://www.cnblogs.com/poloyy/category/1870863.html 前言 默认情况下,Docker ...
- 【Kata Daily 190923】Odder Than the Rest(找出奇数)
题目: Create a method that takes an array/list as an input, and outputs the index at which the sole od ...
- 3.2spring源码系列----循环依赖源码分析
首先,我们在3.1 spring5源码系列--循环依赖 之 手写代码模拟spring循环依赖 中手写了循环依赖的实现. 这个实现就是模拟的spring的循环依赖. 目的是为了更容易理解spring源码 ...
- 09线程隔离的g对象
1,g是global的意思. g对象再一次请求中的所有的代码的地方,都是可以使用的. 同一次请求,那么在这个项目的所有地方都可以用了. from flask import Flask,request, ...
- [MIT6.006] 5. Binary Search Trees, BST Sort 二分搜索树,BST排序
第5节课主要讲述了二分搜索树概念和BST排序.讲师提出一个关于"跑道预订系统"的问题,假设飞机场只有一个跑道,飞机需要为未来降落时间t进行预订,如果时间集合R中,在t时间前后k分钟 ...
- 性能工具-CPU
- python 迭代器(转)
迭代器 迭代器是在python2.2中被加入的,它为类序列对象提供了一个类序列的接口.有了迭代器可以迭代一个不是序列的对象,因为他表现出了序列的行为.当在python中使用for循环迭代一个对象时,调 ...