这是SpringCloud实战系列中第三篇文章,了解前面第两篇文章更有助于更好理解本文内容:

①SpringCloud 实战:引入Eureka组件,完善服务治理

②SpringCloud 实战:引入Feign组件,发起服务间调用

简介

Ribbon 是由 Netflix 发布的一个客户端负载均衡器,它提供了对 HTTP 和 TCP 客户端行为的大量控制。Ribbon 可以基于某些负载均衡的算法,自动为客户端选择发起理论最优的网络请求。常见的负载均衡算法有:轮询,随机,哈希,加权轮询,加权随机等。

客户端负载均衡的意思就是发起网络请求的端根据自己的网络请求情况来做相应的负载均衡策略,与之相对的非客户端负载均衡就有比如硬件F5、软件Nginx,它们更多是介于消费者和提供者之间的,并非客户端。

改造eureka-provider项目

在使用之前我们先把第二节里面的 eureka-provider 项目改造一下,在HelloController 里面新增一个接口,输出自己项目的端口信息,用于区别验证待会儿客户端负载均衡时所调用的服务。

  1. 新增接口方法,返回自己的端口号信息:

    @Controller
    public class HelloController{
    @Value("${server.port}")
    private int serverPort;
    ... @ResponseBody
    @GetMapping("queryPort")
    public String queryPort(){
    return "hei, jinglingwang, my server port is:"+serverPort;
    }
    }
  2. 分别以8082,8083,8084端口启动该项目:eureka-provider

    下图是 IDEA 快速启动三个不同端口项目方法截图,当然你也可以用其他办法

  3. 然后启动,访问三个接口测试一下是否正常返回了对应端口

至此,服务提供者的接口准备工作就做好了。

新建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:

  1. 新建一个Feign,命名为:ProviderTempFeign

    @FeignClient(value = "eureka-provider-temp")
    public interface ProviderTempFeign{ @RequestMapping("/queryPort")
    String queryPort();
    }
  2. 使用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();
    }
    }
  3. 使用注解@RibbonClient 配置负载均衡客户端:

    @RibbonClient(name = "eureka-provider-temp",configuration = ProviderTempConfiguration.class)
    public class ProviderTempRibbonClient{ }
  4. 在Controller新增一个接口,来调用新增Feign(eureka-provider-temp)的方法

    @GetMapping("queryTempPort")
    public String queryTempPort(){
    return providerTempFeign.queryPort();
    }
  5. 再为另一个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{ }
  6. 启动服务之后分别访问两个接口(http://localhost:7071/queryPorthttp://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/queryPorthttp://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();
}
...
}

再说下排查这个问题的思路:

  1. 通过查看异常输出栈的错误日志信息,定位到抛出异常的地方

  2. 之后继续往前面找相关的逻辑,加断点,慢慢调试,发现有一个字段(autowiredBeanName)为空,才会进入到后面抛异常的逻辑

  3. 断点也显示matchingBeans里面有两条数据,说明确实是匹配到了2个bean

  4. 然后我们进入到determineAutowireCandidate方法,发现里面有个看起来很不一般的字段:primaryCandidate,如果这个字段不为空,会直接返回,那这个字段的值是怎么确认的呢?

  5. 继续进入到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;
    }
  6. 进入到isPrimary(candidateBeanName, beanInstance)方法,最后实际就是返回的以下逻辑:

    @Override
    public boolean isPrimary() {
    return this.primary;
    }
  7. 所以解决上面的问题,只需要在我们的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总结

  1. Ribbon 没有类似@EnableRibbon这样的注解
  2. 新版的SpringCloud已经不使用Ribbon作为默认的负载均衡器了
  3. 可以使用@RibbonClients@RibbonClient 注解来负载均衡相关策略的配置
  4. 实现对应的接口就可以完成自定义负载均衡策略
  5. Ribbon 配置的所有key都可以在CommonClientConfigKey类中查看

③SpringCloud 实战:使用 Ribbon 客户端负载均衡的更多相关文章

  1. springcloud(十三):Ribbon客户端负载均衡实例

    一.采用默认的负载均衡策略:RoundRobinRule 轮询策略 1.修改提供者原的控制类 在之前的eureka-client-provider项目的CenterController.java中加入 ...

  2. springcloud(十二):Ribbon客户端负载均衡介绍

    springcloud(十二):Ribbon客户端负载均衡介绍 Ribbon简介 使用分布式微服务脚骨的应用系统,在部署的时候通常会为部分或者全部微服务搭建集群环境,通过提供多个实例来提高系统的稳定型 ...

  3. spring cloud --- Ribbon 客户端负载均衡 + RestTemplate + Hystrix 熔断器 [服务保护] ---心得

    spring boot      1.5.9.RELEASE spring cloud    Dalston.SR1 1.前言 当超大并发量并发访问一个服务接口时,服务器会崩溃 ,不仅导致这个接口无法 ...

  4. spring cloud --- Ribbon 客户端负载均衡 + RestTemplate ---心得【无熔断器】

    spring boot      1.5.9.RELEASE spring cloud    Dalston.SR1 1.前言 了解了 eureka 服务注册与发现 的3大角色 ,会使用RestTem ...

  5. SpringBoot(三) - Ribbon客户端负载均衡,Zuul网关,Config配置中心

    1.Ribbon客户端负载均衡 1.1 依赖 1.2 配置信息 # feign默认加载了ribbon负载均衡,默认负载均衡机制是:轮询 # 负载均衡机制是添加在消费端(客户端)的,如果改为随机,指定服 ...

  6. SpringCloud实战-Ribbon客户端负载均衡

    前面我们已经完成了注册中心和服务提供者两个基础组件.接着介绍使用Spring Cloud Ribbon在客户端负载均衡的调用服务. ribbon 是一个客户端负载均衡器,可以简单的理解成类似于 ngi ...

  7. springcloud 之Ribbon客户端负载均衡配置使用

    pom.xml添加配置说明:这里服务注册与发现用的是Eureka,所以消费者端需要引入eureka,使用EurekaClient来调用服务 <dependency> <groupId ...

  8. 笔记:Spring Cloud Ribbon 客户端负载均衡

    Spring Cloud Ribbon 是一个基于 HTTP 和 TCP 的客户端负载均衡工具,基于 Netflix Ribbon 实现,通过Spring Cloud 的封装,可以让我们轻松的将面向服 ...

  9. Spring Cloud Ribbon——客户端负载均衡

    一.负载均衡负载均衡(Load Balance): 建立在现有网络结构之上,它提供了一种廉价有效透明的方法扩展网络设备和服务器的带宽.增加吞吐量.加强网络数据处理能力.提高网络的灵活性和可用性.其意思 ...

随机推荐

  1. Appium常用操作之「元素定位、swipe 滑屏操作」

    坚持原创输出,点击蓝字关注我吧 作者:清菡 博客:oschina.云+社区.知乎等各大平台都有. 目录 一.打开 uiautomatorviewer 二.Appium 常用操作 1.用 layui 做 ...

  2. JAVA学习第一阶段(1)

    java入门第一阶段 1.在java中接受并保存用户输入的值: (1)import java.util.Scanner//引入包 (2)Scanner input=new Scanner (Syste ...

  3. 最简单的基于FFmpeg的直播系统开发移动端例子:IOS 视频解码器

    本文记录IOS平台下基于FFmpeg的视频解码器.该示例C语言的源代码来自于<最简单的基于FFMPEG+SDL的视频播放器>.相关的概念就不再重复记录了. 源代码 项目的目录结构如图所示. ...

  4. php 获取时间相差的月份和天数

    function getMonthAndDay($date1,$date2){ $datestart= date('Y-m-d',strtotime($date1)); if(strtotime($d ...

  5. 10before_request钩子函数

    1,什么是钩子函数? 就是运行别人前都得先运行他: from flask import Flask app = Flask(__name__) @app.route('/') def hello_wo ...

  6. 【RabbitMQ-7】RabbitMQ—交换机标识符

    死信队列概念 死信队列(Dead Letter Exchange),死信交换器.当业务队列中的消息被拒绝或者过期或者超过队列的最大长度时,消息会被丢弃,但若是配置了死信队列,那么消息可以被重新发布到另 ...

  7. 3. Hive相关知识点

    以下是阅读<Hive编程指南>后整理的一些零散知识点: 1. 有时候用户需要频繁执行一些命令,例如设置系统属性,或增加对于Hadoop的分布式内存,加入自定的Hive扩展的Jave包(JA ...

  8. vue-count-to(简单好用的一个数字滚动插件)

    vue-count-to是一个无依赖,轻量级的vue组件,可覆盖easingFn. 1. 你可以设置两个属性startVal和endVal,它会自动判断计数或倒计时.支持vue-ssr.vue-cou ...

  9. 【JVM第五篇--运行时数据区】方法区

    写在前面的话:本文是在观看尚硅谷JVM教程后,整理的学习笔记.其观看地址如下:尚硅谷2020最新版宋红康JVM教程 一.栈.堆.方法区的关系 虚拟机运行时的数据区如下所示: 即方法区是属于线程共享的内 ...

  10. C++ Split string into vector<string> by space(转)

    c++中没有这么方便的实现,但也有很多的方法能实现这个功能,下面列出五种常用的实现的方法,请根据需要选择,个人觉得前三种使用起来比较方便,参见代码如下: #include <vector> ...