
- 出 处
- 微软
- 内 容
- 收藏命令按钮和图示
- 中文名
- 功能区
- 外文名
- Ribbon
-
Ribbon
2020-04-07 07:46:03Ribbon1 负载均衡的两种方式
@Autowired private DiscoveryClient discoveryClient; //-------------------------------------------手写负载均衡----------------------------// @GetMapping("/hello") public String hello() { // 1.拿到用户中心所有的实例信息 List<ServiceInstance> userInstances = discoveryClient.getInstances("user-center"); // 2.所有用户中心实例的请求地址 List<String> targetUrls = userInstances.stream() .map((instance) -> instance.getUri().toString() + "/user/hello") .collect(Collectors.toList()); //随机请求 int i = ThreadLocalRandom.current().nextInt(targetUrls.size()); log.info("请求的目标地址:{}", targetUrls.get(i)); return new RestTemplate().getForObject(targetUrls.get(i), String.class); }
2 整合Ribbon
2.1 加依赖
spring-cloud-starter-alibaba-nacos-discovery 已经依赖ribbon了所以无需单独引入
使用ribbon无需写注解,无需写配置(需要修改另说)
2.2 hello world
@Configuration public class RestTemplageConfig { @Bean @LoadBalanced public RestTemplate restTemplate(){ return new RestTemplate(); } }
@Autowired private RestTemplate restTemplate; @GetMapping("/hello") public String hello() { return restTemplate.getForObject("http://user-center/user/hello", String.class); }
3 Ribbon组成
4 Ribbon内置的负载均衡规则
5 Ribbon细粒度配置
5.1 Java代码配置
Spring中的父子容器:https://yuanyu.blog.csdn.net/article/details/105294435
16.2 Customizing the Ribbon Client
The
CustomConfiguration
clas must be a@Configuration
class, but take care that it is not in a@ComponentScan
for the main application context. Otherwise, it is shared by all the@RibbonClients
. If you use@ComponentScan
(or@SpringBootApplication
), you need to take steps to avoid it being included (for instance, you can put it in a separate, non-overlapping package or specify the packages to scan explicitly in the@ComponentScan
).注意: UserCenterRibbonConfiguration需要被扫描到,RibbonConfiguration不能被扫描到,否则会成为全局配置。@SpringBootApplication会扫描当前包和子包
@Configuration @RibbonClient(name = "user-center",configuration = RibbonConfiguration.class) public class UserCenterRibbonConfiguration {}
@Configuration public class RibbonConfiguration { @Bean public IRule ribbonRule() { return new RandomRule(); } }
5.2 用配置属性配置
<clientName>.ribbon.NFLoadBalancerRuleClassName
- com.netflix.loadbalancer.RandomRule
- com.nobug.client.config.NacosSameClusterWeightedRule
user-center: ribbon: NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
5.3 最佳实践总结
代码配置方式 vs 属性配置方式 配置方式 优点 缺点 代码配置 基于代码,更加灵活 有小坑(父子上下文)
线上修改得重新打包、发布属性配置【优先级更高】 易上手
配置更加直观
线上修改无需重新打包、发布
优先级更高极端场景下没有代码配置方式灵活 - 尽量使用属性配置,属性方式实现不了的情况下再考虑用代码配置
- 在同一个微服务内尽量保持单一性,比如统一使用属性配置,不要两种方式混用,增加定位代码的复杂性
6 Ribbon全局配置
方式一 : 让ComponentScan上下文重叠(强烈不建议使用)- 方式二【唯一正确的途径】 : @RibbonClients(defaultConfiguration=xxx.class)
RibbonConfiguration需要被@SpringBootApplication扫描
@Configuration @RibbonClients(defaultConfiguration = RibbonConfiguration.class) public class RibbonConfiguration { }
7 Ribbon支持的配置项
@Configuration public class RibbonConfiguration { @Bean public IRule ribbonRule() { return new RandomRule(); } @Bean public IPing ping(){ return new PingUrl(); } //... }
16.4 Customizing the Ribbon Client by Setting Properties
<clientName>.ribbon.NFLoadBalancerClassName
: Should implementILoadBalancer
<clientName>.ribbon.NFLoadBalancerRuleClassName
: Should implementIRule
<clientName>.ribbon.NFLoadBalancerPingClassName
: Should implementIPing
<clientName>.ribbon.NIWSServerListClassName
: Should implementServerList
<clientName>.ribbon.NIWSServerListFilterClassName
: Should implementServerListFilter
xxx-center: ribbon: NFLoadBalancerClassName: ILoadBalancer实现类 NFLoadBalancerRuleClassName: IRule实现类 NFLoadBalancerPingClassName: IPing实现类 NIWSServerListClassName: ServerList实现类 NIWSServerListFilterClassName: ServerListFilter实现类
8 Ribbon的饥饿加载
开启饥饿加载
ribbon: eager-load: enabled: true clients: user-service ,xxx # 细粒度指定哪些具体的服务【多个使用逗号分隔,不在这个名单上的依然使用懒加载】
9 nacos && Ribbon
9.1 扩展Ribbon支持Nacos权重
http://www.imooc.com/article/288660
/** * 扩展Ribbon支持Nacos权重 */ //IRule @Slf4j public class NacosWeightedRule extends AbstractLoadBalancerRule { @Autowired private NacosDiscoveryProperties nacosDiscoveryProperties; @Override public void initWithNiwsConfig(IClientConfig iClientConfig) { //读取配置文件,并初始化NacosWeightedRule } @Override public Server choose(Object o) { try { BaseLoadBalancer loadBalancer = (BaseLoadBalancer) this.getLoadBalancer(); log.info("lb={}",loadBalancer); //想要请求的微服务的名称 String name = loadBalancer.getName(); //拿到服务发现的相关API NamingService namingService = nacosDiscoveryProperties.namingServiceInstance(); //nacos client自动通过基于权重的负载均衡算法,给我们选择一个实例 Instance instance = namingService.selectOneHealthyInstance(name); log.info("选择的实例是: port={},instance={}", instance.getPort(), instance); return new NacosServer(instance); } catch (NacosException e) { //e.printStackTrace(); log.error(e.getErrMsg()); } return null; } }
9.2 扩展Ribbon同一集群优先调用
/** * 扩展Ribbon-同一集群优先调用 */ @Slf4j public class NacosSameClusterWeightedRule extends AbstractLoadBalancerRule { @Autowired private NacosDiscoveryProperties nacosDiscoveryProperties; @Override public void initWithNiwsConfig(IClientConfig iClientConfig) {} @Override public Server choose(Object o) { //1.找到指定服务的所有实例 A //2.过滤出相同集群下的所有实例 B //3.如果B是空,就用A //4.基于权重的负载均衡算法,返回1个实例 try { //获取到配置中的集群名称 SH //spring.cloud.nacos.discovery.cluster-name=SH String clusterName = nacosDiscoveryProperties.getClusterName(); BaseLoadBalancer loadBalancer = (BaseLoadBalancer) this.getLoadBalancer(); //想要请求的微服务的名称 String name = loadBalancer.getName(); //拿到服务发现的相关API NamingService namingService = nacosDiscoveryProperties.namingServiceInstance(); //1.找到指定服务的所有实例 List<Instance> instances = namingService.selectInstances(name, true); //2.过滤出相同集群下的所有实例 List<Instance> sameClusterInstances = instances.stream() .filter(instance -> Objects.equals(instance.getClusterName(), clusterName)) .collect(Collectors.toList()); // 3.如果上述2为空,就用1 List<Instance> instancesToBeChosen; if (CollectionUtils.isEmpty(sameClusterInstances)) { instancesToBeChosen = instances; log.warn("发生了跨集群的调用,name = {}, clusterName = {}, instances = {}", name, clusterName, instances); } else { instancesToBeChosen = sameClusterInstances; log.info("同一集群调用,name = {}, clusterName = {}, instances = {}", name, clusterName, instances); } //4.基于权重的负载均衡算法,返回一个实例 Instance instance = ExtendsBalancer.getHostByRandomWeightExtends(instancesToBeChosen); log.info("选择的实例是 instance = {}", instance); return new NacosServer(instance); } catch (Exception e) { //e.printStackTrace(); log.error(e.getMessage()); return null; } } } /** * */ class ExtendsBalancer extends Balancer { public static Instance getHostByRandomWeightExtends(List<Instance> hosts) { return getHostByRandomWeight(hosts); } }
扩展Ribbon支持基于元数据的版本管理:http://www.imooc.com/article/288674
-
ribbon
2019-12-21 23:58:36ribbon 是一个负载均衡客户端,可以很好的控制htt和tcp的一些行为。Feign默认集成了ribbon。 项目名称service-ribbon; 在它的pom.xml文件分别引入起步依赖spring-cloud-starter-eureka、spring-cloud-starter-...ribbon
- 是一个负载均衡客户端,可以很好的控制htt和tcp的一些行为。Feign默认集成了ribbon。
- 项目名称service-ribbon; 在它的pom.xml文件分别引入起步依赖spring-cloud-starter-eureka、spring-cloud-starter-ribbon、spring-boot-starter-web。
配置文件
-
yml文件,注册到注册中心
server: port: 8764 spring: application: name: server-ribbon eureka: client: eureka-server-u-r-l-context: http://localhost:8761/eureka/
-
启动多个服务提供者eureka-client -Dserver.port=xxxx
-
启动类,通过@EnableDiscoveryClient向服务中心注册
@SpringBootApplication @EnableDiscoveryClient public class ServiceRibbonApplication { public static void main(String[] args) { SpringApplication.run(ServiceRibbonApplication.class, args); } @Bean @LoadBalanced RestTemplate restTemplate() { return new RestTemplate(); } } @Service public class HelloService { @Autowired RestTemplate restTemplate; public String hiService(String name) { return restTemplate.getForObject("http://SERVICE-HI/hi?name="+name,String.class); //注册的服务名称 } } @RestController public class HelloControler { @Autowired HelloService helloService; @RequestMapping(value = "/hi") public String hi(@RequestParam String name){ return helloService.hiService(name); } }
-
访问 http://localhost:8764/hi?name=tom
-
史上最简单的SpringCloud教程 | 第二篇: 服务消费者(rest+ribbon)
2017-04-08 23:25:26转载请标明出处: http://blog.csdn.net/forezp/article/details/69788938 本文出自方志朋的博客 在上一篇文章,讲了服务的注册和...Spring cloud有两种调用方式,一种是ribbon+restTemplate,另一种是feign。在这转载请标明出处:
https://www.fangzhipeng.com/springcloud/2017/06/02/sc02-rest-ribbon.html
本文出自方志朋的博客个人博客纯净版:https://www.fangzhipeng.com/springcloud/2017/06/02/sc02-rest-ribbon.html
最新Finchley版本:
https://www.fangzhipeng.com/springcloud/2018/08/02/sc-f2-ribbon.html
或者
http://blog.csdn.net/forezp/article/details/81040946在上一篇文章,讲了服务的注册和发现。在微服务架构中,业务都会被拆分成一个独立的服务,服务与服务的通讯是基于http restful的。Spring cloud有两种服务调用方式,一种是ribbon+restTemplate,另一种是feign。在这一篇文章首先讲解下基于ribbon+rest。
一、ribbon简介
Ribbon is a client side load balancer which gives you a lot of control over the behaviour of HTTP and TCP clients. Feign already uses Ribbon, so if you are using @FeignClient then this section also applies.
-----摘自官网
ribbon是一个负载均衡客户端,可以很好的控制htt和tcp的一些行为。Feign默认集成了ribbon。
ribbon 已经默认实现了这些配置bean:
-
IClientConfig ribbonClientConfig: DefaultClientConfigImpl
-
IRule ribbonRule: ZoneAvoidanceRule
-
IPing ribbonPing: NoOpPing
-
ServerList ribbonServerList: ConfigurationBasedServerList
-
ServerListFilter ribbonServerListFilter: ZonePreferenceServerListFilter
-
ILoadBalancer ribbonLoadBalancer: ZoneAwareLoadBalancer
二、准备工作
这一篇文章基于上一篇文章的工程,启动eureka-server 工程;启动service-hi工程,它的端口为8762;将service-hi的配置文件的端口改为8763,并启动,这时你会发现:service-hi在eureka-server注册了2个实例,这就相当于一个小的集群。访问localhost:8761如图所示:
如何一个工程启动多个实例,请看这篇文章:https://blog.csdn.net/forezp/article/details/76408139
三、建一个服务消费者
重新新建一个spring-boot工程,取名为:service-ribbon;
在它的pom.xml文件分别引入起步依赖spring-cloud-starter-eureka、spring-cloud-starter-ribbon、spring-boot-starter-web,代码如下:<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.forezp</groupId> <artifactId>service-ribbon</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>service-ribbon</name> <description>Demo project for Spring Boot</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.2.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-ribbon</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Dalston.RC1</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> <repositories> <repository> <id>spring-milestones</id> <name>Spring Milestones</name> <url>https://repo.spring.io/milestone</url> <snapshots> <enabled>false</enabled> </snapshots> </repository> </repositories> </project>
在工程的配置文件指定服务的注册中心地址为http://localhost:8761/eureka/,程序名称为 service-ribbon,程序端口为8764。配置文件application.yml如下:
eureka: client: serviceUrl: defaultZone: http://localhost:8761/eureka/ server: port: 8764 spring: application: name: service-ribbon
在工程的启动类中,通过@EnableDiscoveryClient向服务中心注册;并且向程序的ioc注入一个bean: restTemplate;并通过@LoadBalanced注解表明这个restRemplate开启负载均衡的功能。
@SpringBootApplication @EnableDiscoveryClient public class ServiceRibbonApplication { public static void main(String[] args) { SpringApplication.run(ServiceRibbonApplication.class, args); } @Bean @LoadBalanced RestTemplate restTemplate() { return new RestTemplate(); } }
写一个测试类HelloService,通过之前注入ioc容器的restTemplate来消费service-hi服务的“/hi”接口,在这里我们直接用的程序名替代了具体的url地址,在ribbon中它会根据服务名来选择具体的服务实例,根据服务实例在请求的时候会用具体的url替换掉服务名,代码如下:
@Service public class HelloService { @Autowired RestTemplate restTemplate; public String hiService(String name) { return restTemplate.getForObject("http://SERVICE-HI/hi?name="+name,String.class); } }
写一个controller,在controller中用调用HelloService 的方法,代码如下:
/** * Created by fangzhipeng on 2017/4/6. */ @RestController public class HelloControler { @Autowired HelloService helloService; @RequestMapping(value = "/hi") public String hi(@RequestParam String name){ return helloService.hiService(name); } }
在浏览器上多次访问http://localhost:8764/hi?name=forezp,浏览器交替显示:
hi forezp,i am from port:8762
hi forezp,i am from port:8763
这说明当我们通过调用restTemplate.getForObject(“http://SERVICE-HI/hi?name=”+name,String.class)方法时,已经做了负载均衡,访问了不同的端口的服务实例。
四、此时的架构
- 一个服务注册中心,eureka server,端口为8761
- service-hi工程跑了两个实例,端口分别为8762,8763,分别向服务注册中心注册
- sercvice-ribbon端口为8764,向服务注册中心注册
- 当sercvice-ribbon通过restTemplate调用service-hi的hi接口时,因为用ribbon进行了负载均衡,会轮流的调用service-hi:8762和8763 两个端口的hi接口;
源码下载:https://github.com/forezp/SpringCloudLearning/tree/master/chapter2
更多阅读
五、参考资料
本文参考了以下:
springcloud ribbon with eureka
扫码关注公众号有惊喜(转载本站文章请注明作者和出处 方志朋的博客)
-
-
微服务架构-实现技术之具体实现工具与框架5:Spring Cloud Feign与Ribbon原理与注意事项
2019-02-14 14:32:18(三)Feign请求超时问题 方法一 方法二 方法三 三、Spring Cloud Ribbon概述与核心工作原理 (一)Ribbon与负载均衡 (二)Ribbon核心工作原理 1.Ribbon 服务配置方式 2.和Feign的集成模式 (三)LoadBalancer–...目录
二、FeignClent注解剖析+Spring Cloud Feign基本功能配置解读
三、Spring Cloud Ribbon概述与核心工作原理
注:以上所有只做理论性的总结与分析,相关实战代码会在后面的博客中和github中逐步增加。
一、Spring Cloud Feign概述与工作原理解读
(一)服务间调用的几种方式
使用Spring Cloud开发微服务时,在服务消费者调用服务提供者时,底层通过HTTP Client 的方式访问。但实际上在服务调用时,有主要以下来实现:
使用JDK原生的URLConnection;
Apache提供的HTTP Client;
Netty提供的异步HTTP Client;
Spring提供的RestTemplate。
Spring Cloud的Spring Cloud Open Feign相对是最方便与最优雅的,使Feign支持Spring MVC注解的同时并整合了Ribbon。
(二)Feign 概述
Feign 是一个声明式的 Web Service 客户端。它的出现使开发 Web Service 客户端变得很简单。使用 Feign 只需要创建一个接口加上对应的注解,比如:@FeignClient 注解。 Feign 有可插拔的注解,包括 Feign 注解和 AX-RS 注解。Feign 也支持编码器和解码器,Spring Cloud Open Feign 对 Feign 进行增强支持 Spring Mvc 注解,可以像 Spring Web 一样使用 HttpMessageConverters 等。
Feign 是一种声明式、模板化的 HTTP 客户端。在 Spring Cloud 中使用 Feign,可以做到使用 HTTP 请求访问远程服务,就像调用本地方法一样的,开发者完全感知不到这是在调用远程方法,更感知不到在访问 HTTP 请求。接下来介绍一下 Feign 的特性,具体如下:
- 可插拔的注解支持,包括 Feign 注解和AX-RS注解。
- 支持可插拔的 HTTP 编码器和解码器。
- 支持 Hystrix 和它的 Fallback。
- 支持 Ribbon 的负载均衡。
- 支持 HTTP 请求和响应的压缩。Feign 是一个声明式的 WebService 客户端,它的目的就是让 Web Service 调用更加简单。它整合了 Ribbon 和 Hystrix,从而不需要开发者针对 Feign 对其进行整合。Feign 还提供了 HTTP 请求的模板,通过编写简单的接口和注解,就可以定义好 HTTP 请求的参数、格式、地址等信息。Feign 会完全代理 HTTP 的请求,在使用过程中我们只需要依赖注入 Bean,然后调用对应的方法传递参数即可。
(三)Feign 工作原理
- 在开发微服务应用时,我们会在主程序入口添加 @EnableFeignClients 注解开启对 Feign Client 扫描加载处理。根据 Feign Client 的开发规范,定义接口并加 @FeignClients 注解。
- 当程序启动时,会进行包扫描,扫描所有 @FeignClients 的注解的类,并将这些信息注入 Spring IOC 容器中。当定义的 Feign 接口中的方法被调用时,通过JDK的代理的方式,来生成具体的 RequestTemplate。当生成代理时,Feign 会为每个接口方法创建一个 RequetTemplate 对象,该对象封装了 HTTP 请求需要的全部信息,如请求参数名、请求方法等信息都是在这个过程中确定的。
- 然后由 RequestTemplate 生成 Request,然后把 Request 交给 Client 去处理,这里指的 Client 可以是 JDK 原生的 URLConnection、Apache 的 Http Client 也可以是 Okhttp。最后 Client 被封装到 LoadBalanceclient 类,这个类结合 Ribbon 负载均衡发起服务之间的调用。
二、FeignClent注解剖析+Spring Cloud Feign基本功能配置解读
(一)@FeignClient 注解剖析
FeignClient注解被@Target(ElementType.TYPE)修饰,表示FeignClient注解的作用目标在接口上。
声明接口之后,在代码中通过@Resource注入之后即可使用。@FeignClient标签的常用属性如下:
- name:指定FeignClient的名称,如果项目使用了Ribbon,name属性会作为微服务的名称,用于服务发现
- url: url一般用于调试,可以手动指定@FeignClient调用的地址
- decode404:当发生http 404错误时,如果该字段位true,会调用decoder进行解码,否则抛出FeignException
- configuration: Feign配置类,可以自定义Feign的Encoder、Decoder、LogLevel、Contract
- fallback: 定义容错的处理类,当调用远程接口失败或超时时,会调用对应接口的容错逻辑,fallback指定的类必须实现@FeignClient标记的接口
- fallbackFactory: 工厂类,用于生成fallback类示例,通过这个属性我们可以实现每个接口通用的容错逻辑,减少重复的代码
- path: 定义当前FeignClient的统一前缀
(二)Spring Cloud Feign基本功能配置
所谓的基本功能配置主要是指可以自定义Feign的配置,相关代码会在后续博客和github中更新。
- 日志配置(后续博客和github中更新)
- 契约配置(后续博客和github中更新)
- Basic认证配置(后续博客和github中更新)
- 超时时间配置(后续博客和github中更新)
- 客户端组件配置(后续博客和github中更新)
- GZIP压缩配置(后续博客和github中更新)
- 编码器解码器配置(后续博客和github中更新)
- Feign默认Client的替换配置(后续博客和github中更新)
- Feign返回图片流处理方式(后续博客和github中更新)
- Feign调用传递Token(后续博客和github中更新)
- venus-cloud-feign的设计和使用(后续博客和github中更新)
(三)Feign请求超时问题
Hystrix默认的超时时间是1秒,如果超过这个时间尚未响应,将会进入fallback代码。而首次请求往往会比较慢(因为Spring的懒加载机制,要实例化一些类),这个响应时间可能就大于1秒了
解决方案有三种,以feign为例。方法一
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds: 5000
该配置是让Hystrix的超时时间改为5秒方法二
hystrix.command.default.execution.timeout.enabled: false
该配置,用于禁用Hystrix的超时时间方法三
feign.hystrix.enabled: false
该配置,用于索性禁用feign的hystrix。该做法除非一些特殊场景,不推荐使用。三、Spring Cloud Ribbon概述与核心工作原理
Ribbon是2013年1月Netflix公司开发的一个组件,它在云服务体系中起着至关重要的作用,一直是Netflix最活跃的项目。
Pivotal公司将其整入了Spring Cloud生态,正式命名为Spring Cloud Ribbon作为微服务弹性扩展的组件,与其他组件结合发挥强大作用。(Pivotal中国研发中心于2013.04成立)
(一)Ribbon与负载均衡
负载均衡在业界有不少分类:(基本可见https://blog.csdn.net/xiaofeng10330111/article/details/85682513)
最常见的有软负载和硬负载,代表产品为nginx和F5.
另外一组分类为集中式负载和进程内负载,即服务端负载均衡和客户端负载均衡。这种分类下,nginx和F5都为集中式负载,Ribbon为进程内负载。
Ribbon是Spring Cloud框架中相当核心的模块,负责着服务负载调用,Ribbon也可以脱离SpringCloud单独使用。
另外Ribbon是客户端的负载均衡框架,即每个客户端上,独立维护着自身的调用信息统计,相互隔离;也就是说:Ribbon的负载均衡表现在各个机器上变现并不完全一致
Ribbon 也是整个组件框架中最复杂的一环,控制流程上为了保证服务的高可用性,有很多比较细节的参数控制,在使用的过程中,需要深入理清每个环节的处理机制,这样在问题定位上会高效很多。(二)Ribbon核心工作原理
Spring Cloud集成模式下的Ribbon有以下几个特征:
1.Ribbon 服务配置方式
每一个服务配置都有一个Spring ApplicationContext上下文,用于加载各自服务的实例。
比如,当前Spring Cloud 系统内,有如下几个服务:服务名称 角色 依赖服务 order 订单模块 user user 用户模块 无 mobile-bff 移动端BFF order,user mobile-bff服务在实际使用中,会用到order和user模块,那么在mobile-bff服务的Spring上下文中,会为order 和user 分别创建一个子ApplicationContext,用于加载各自服务模块的配置。也就是说,各个客户端的配置相互独立,彼此不收影响
2.和Feign的集成模式
在使用Feign作为客户端时,最终请求会转发成 http://<服务名称>/<relative-path-to-service>的格式,通过LoadBalancerFeignClient, 提取出服务标识<服务名称>,然后根据服务名称在上下文中查找对应服务的负载均衡器FeignLoadBalancer,负载均衡器负责根据既有的服务实例的统计信息,挑选出最合适的服务实例。
(三)LoadBalancer–负载均衡器的核心
LoadBalancer 的职能主要有三个:
- 维护Sever列表的数量(新增、更新、删除等)
- 维护Server列表的状态(状态更新)
- 当请求Server实例时,能否返回最合适的Server实例
1.负载均衡器的内部基本实现原理
- Server
Server 作为服务实例的表示,会记录服务实例的相关信息,如:服务地址,所属zone,服务名称,实例ID等
- ServerList
维护着一组Server实例列表,在应用运行的过程中,Ribbon通过ServerList中的服务实例供负载均衡器选择。ServerList维护列表可能在运行的过程中动态改变
- ServerStats
作为对应Server 的运行情况统计,一般是服务调用过程中的Server平均响应时间,累计请求失败计数,熔断时间控制等。一个ServerStats实例唯一对应一个Server实例
- LoadBalancerStats
作为 ServerStats实例列表的容器,统一维护
- ServerListUpdater
负载均衡器通过ServerListUpdater来更新ServerList,比如实现一个定时任务,每隔一段时间获取最新的Server实例列表
- Pinger
服务状态检验器,负责维护ServerList列表中的服务状态注意:Pinger仅仅负责Server的状态,没有能力决定是否删除
- PingerStrategy
定义以何种方式还检验服务是否有效,比如是按照顺序的方式还是并行的方式
- IPing
Ping,检验服务是否可用的方法,常见的是通过HTTP,或者TCP/IP的方式看服务有无认为正常的请求
2.如何维护Server列表?(新增、更新、删除)
Server列表的维护从实现方法上分为两类:
- 基于配置的服务列表
这种方式一般是通过配置文件,静态地配置服务器列表,这种方式相对而言比较简单,但并不是意味着在机器运行的时候就一直不变。netflix 在做Spring cloud 套件时,使用了分布式配置框架netflix archaius ,archaius 框架有一个特点是会动态的监控配置文件的变化,将变化刷新到各个应用上。也就是说,当我们在不关闭服务的情况下,如果修改了基于配置的服务列表时, 服务列表可以直接刷新
- 结合服务发现组件(如Eureka)的服务注册信息动态维护服务列表
基于Spring Cloud框架下,服务注册和发现是一个分布式服务集群必不可少的一个组件,它负责维护不同的服务实例(注册、续约、取消注册)。
3.负载均衡器如何维护服务实例的状态?
Ribbon负载均衡器将服务实例的状态维护托交给
Pinger
、PingerStrategy
、IPing
来维护,具体交互模式如下所示:4.如何从服务列表中挑选一个合适的服务实例?
(1)服务实例容器:ServerList的维护
负载均衡器通过 ServerList来统一维护服务实例,具体模式如上图,基础的接口定义如下:
/** * Interface that defines the methods sed to obtain the List of Servers * @author stonse * * @param <T> */ public interface ServerList<T extends Server> { //获取初始化的服务列表 public List<T> getInitialListOfServers(); /** * Return updated list of servers. This is called say every 30 secs * (configurable) by the Loadbalancer's Ping cycle * 获取更新后的的服务列表 */ public List<T> getUpdatedListOfServers(); }
在Ribbon的实现中,在
ServerList(
负责维护服务实例,并使用ServerListFilter
过滤器过滤出符合要求的服务实例列表List<Server>)
中,维护着Server的实例,并返回最新的List<Server>
集合,供LoadBalancer
使用 。(2) 服务实例列表过滤器
ServerListFilter
传入一个服务实例列表,过滤出满足过滤条件的服务列表
public interface ServerListFilter<T extends Server> { public List<T> getFilteredListOfServers(List<T> servers); }
- Ribbon 的默认
ServerListFilter
实现1:ZoneAffinityServerListFilter
Ribbon默认采取了
区域
优先的过滤策略,即当Server列表中,过滤出和当前实例所在的区域(zone)
一致的server
列表- Ribbon 的
ServerListFilter
实现2:ZonePreferenceServerListFilter
ZonePreferenceServerListFilter
集成自ZoneAffinityServerListFilter
,在此基础上做了拓展,在 返回结果的基础上,再过滤出和本地服务相同区域(zone
)的服务列表。当指定了当前服务的所在Zone,并且
ZoneAffinityServerListFilter
没有起到过滤效果时,ZonePreferenceServerListFilter
会返回当前Zone的Server列表。- Ribbon 的
ServerListFilter
实现3:ServerListSubsetFilter
这个过滤器作用于当Server数量列表特别庞大时(比如有上百个Server实例),这时,长时间保持Http链接也不太合适,可以适当地保留部分服务,舍弃其中一些服务,这样可使释放没必要的链接。
此过滤器也是继承自 ZoneAffinityServerListFilter,在此基础上做了拓展,在实际使用中不太常见。(3)LoadBalancer选择
服务实例
的流程LoadBalancer的核心功能是根据负载情况,从服务列表中挑选最合适的
服务实例
。LoadBalancer内部采用了如下图所示的组件完成:LoadBalancer 选择服务实例的流程
- 通过ServerList获取当前可用的服务实例列表;
- 通过ServerListFilter将步骤1 得到的服务列表进行一次过滤,返回满足过滤器条件的服务实例列表;
- 应用Rule规则,结合服务实例的统计信息,返回满足规则的某一个服务实例;
通过上述的流程可以看到,实际上,在服务实例列表选择的过程中,有两次过滤的机会:第一次是首先通过ServerListFilter过滤器,另外一次是用过IRule 的选择规则进行过滤。
四、Spring Cloud Ribbon源码解读
既然是在restTemplate加了@LoadBalanced注解,那就进去这个注解里面看下吧。
/** * Annotation to mark a RestTemplate bean to be configured to use a LoadBalancerClient * @author Spencer Gibb */ @Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @Qualifier public @interface LoadBalanced { }
从注释中可以知道,这个注解是用来给RestTemplate做标记,以使用负载均衡客户端(LoadBalancerClient)来配置它。所以,我们在生成的RestTemplate的bean上添加这么一个注解,这个bean就会配置LoadBalancerClient。LoadBalancerClient的代码如下:
/** * Represents a client side load balancer * @author Spencer Gibb */ public interface LoadBalancerClient { /** * Choose a ServiceInstance from the LoadBalancer for the specified service * @param serviceId the service id to look up the LoadBalancer * @return a ServiceInstance that matches the serviceId */ ServiceInstance choose(String serviceId); /** * execute request using a ServiceInstance from the LoadBalancer for the specified * service * @param serviceId the service id to look up the LoadBalancer * @param request allows implementations to execute pre and post actions such as * incrementing metrics * @return the result of the LoadBalancerRequest callback on the selected * ServiceInstance */ <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException; /** * Create a proper URI with a real host and port for systems to utilize. * Some systems use a URI with the logical serivce name as the host, * such as http://myservice/path/to/service. This will replace the * service name with the host:port from the ServiceInstance. * @param instance * @param original a URI with the host as a logical service name * @return a reconstructed URI */ URI reconstructURI(ServiceInstance instance, URI original); }
LoadBalancerClient是一个接口,里面有三个方法。
第一个,ServiceInstance choose(String serviceId);从方法名上就可以看出,是根据传入的serviceId(服务名),从负载均衡器中选择一个服务实例,服务实例通过ServiceInstance类来表示。
第二个,execute方法,使用从负载均衡器中选择的服务实例来执行请求内容。
第三个,URI reconstructURI(ServiceInstance instance, URI original);方法,是重新构建一个URI的,还记得我们在代码中,通过RestTemplate请求服务时,写的是服务名吧,这个方法就会把这个请求的URI进行转换,返回host+port,通过host+port的形式去请求服务。
从工程中搜索LoadBalancerClient接口的实现类,可以找到RibbonLoadBalancerClient类实现了这个接口,并且实现了接口中定义的方法。再梳理一下逻辑,我们在RestTemplate上添加了@LoadBalanced注解,RibbonLoadBalancerClient就会配置到这个RestTemplate实例上。
在LoadBalancerClient接口的同一个包路径下,还会看到另一个LoadBalancerAutoConfiguration类,看名字就感觉这是一个自动配置LoadBalancer的,进去这个类看一下。
/** * Auto configuration for Ribbon (client side load balancing). * * @author Spencer Gibb * @author Dave Syer */ @Configuration @ConditionalOnClass(RestTemplate.class) @ConditionalOnBean(LoadBalancerClient.class) public class LoadBalancerAutoConfiguration { @LoadBalanced @Autowired(required = false) private List<RestTemplate> restTemplates = Collections.emptyList(); @Bean public SmartInitializingSingleton loadBalancedRestTemplateInitializer( final List<RestTemplateCustomizer> customizers) { return new SmartInitializingSingleton() { @Override public void afterSingletonsInstantiated() { for (RestTemplate restTemplate : LoadBalancerAutoConfiguration.this.restTemplates) { for (RestTemplateCustomizer customizer : customizers) { customizer.customize(restTemplate); } } } }; } @Bean @ConditionalOnMissingBean public RestTemplateCustomizer restTemplateCustomizer( final LoadBalancerInterceptor loadBalancerInterceptor) { return new RestTemplateCustomizer() { @Override public void customize(RestTemplate restTemplate) { List<ClientHttpRequestInterceptor> list = new ArrayList<>( restTemplate.getInterceptors()); list.add(loadBalancerInterceptor); restTemplate.setInterceptors(list); } }; } @Bean public LoadBalancerInterceptor ribbonInterceptor( LoadBalancerClient loadBalancerClient) { return new LoadBalancerInterceptor(loadBalancerClient); } }
注释中说明这个类是为Ribbon做自动配置的,类上的@Configuration说明这是一个配置类,在当前项目中存在RestTemplate类、并且存在LoadBalancerClient接口的实现类时,就满足了自动化配置的条件。
在LoadBalancerAutoConfiguration类中,创建了一个LoadBalancerInterceptor拦截器,还维护了一个被@LoadBalanced修饰的RestTemplate列表,在初始化的时候,会为每个restTemplate实例添加LoadBalancerInterceptor拦截器。
我们自己实现的项目,就定义了RestTemplate的一个对象,并且引入了spring-cloud相关的包,存在RibbonLoadBalancerClient作为LoadBalancerClient的实现类,所以,满足自动化配置的条件。接下来就看下,在restTemplate实例添加的LoadBalancerInterceptor拦截器的逻辑。public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor { private LoadBalancerClient loadBalancer; public LoadBalancerInterceptor(LoadBalancerClient loadBalancer) { this.loadBalancer = loadBalancer; } @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, new LoadBalancerRequest<ClientHttpResponse>() { @Override public ClientHttpResponse apply(final ServiceInstance instance) throws Exception { HttpRequest serviceRequest = new ServiceRequestWrapper(request, instance); return execution.execute(serviceRequest, body); } }); } private class ServiceRequestWrapper extends HttpRequestWrapper { private final ServiceInstance instance; public ServiceRequestWrapper(HttpRequest request, ServiceInstance instance) { super(request); this.instance = instance; } @Override public URI getURI() { URI uri = LoadBalancerInterceptor.this.loadBalancer.reconstructURI( this.instance, getRequest().getURI()); return uri; } } }
由于在自动配置类中,对restTemplate实例添加了LoadBalancerInterceptor拦截器,所以,当用restTemplate发送http请求时,就会执行这个拦截器的intercept方法。
intercept方法中,会根据request.getURI(),获取请求的uri,再获取host,我们在发送http请求的时候,是用的服务名作为host,所以,这里就会拿到服务名,再调用具体LoadBalancerClient实例的execute方法,发送请求。
LoadBalancerClient的实现类为RibbonLoadBalancerClient,最终的负载均衡请求由它来执行,所以,还需要再梳理下RibbonLoadBalancerClient的逻辑。先看下RibbonLoadBalancerClient中的execute方法:
@Override public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException { ILoadBalancer loadBalancer = getLoadBalancer(serviceId); Server server = getServer(loadBalancer); if (server == null) { throw new IllegalStateException("No instances available for " + serviceId); } RibbonServer ribbonServer = new RibbonServer(serviceId, server, isSecure(server, serviceId), serverIntrospector(serviceId).getMetadata(server)); RibbonLoadBalancerContext context = this.clientFactory .getLoadBalancerContext(serviceId); RibbonStatsRecorder statsRecorder = new RibbonStatsRecorder(context, server); try { T returnVal = request.apply(ribbonServer); statsRecorder.recordStats(returnVal); return returnVal; } // catch IOException and rethrow so RestTemplate behaves correctly catch (IOException ex) { statsRecorder.recordStats(ex); throw ex; } catch (Exception ex) { statsRecorder.recordStats(ex); ReflectionUtils.rethrowRuntimeException(ex); } return null; }
服务名作为serviceId字段传进来,先通过getLoadBalancer获取loadBalancer,再根据loadBalancer获取server,下面是getServer的代码:
protected Server getServer(ILoadBalancer loadBalancer) { if (loadBalancer == null) { return null; } return loadBalancer.chooseServer("default"); // TODO: better handling of key }
如果loadBalancer为空,就直接返回空,否则就调用loadBalancer的chooseServer方法,获取相应的server。
看一下ILoadBalancer是一个接口,里面声明了一系列负载均衡实现的方法:public interface ILoadBalancer { public void addServers(List<Server> newServers); public Server chooseServer(Object key); public void markServerDown(Server server); public List<Server> getReachableServers(); public List<Server> getAllServers(); }
这里面还有一个getServerList方法,不过已经标记为Deprecated,所以就没有列出。
这些方法名比较直观,很容易就能猜出是干啥的,addServers是用来添加一个server集合,chooseServer是选择一个server,markServerDown用来标记某个服务下线,getReachableServers获取可用的Server集合,getAllServers是获取所有的server集合。
ILoadBalancer有很多实现,那具体是用的哪个类呢,可以通过RibbonClientConfiguration类看到,这个配置类在初始化的时候,返回了ZoneAwareLoadBalancer作为负载均衡器。@Bean @ConditionalOnMissingBean public ILoadBalancer ribbonLoadBalancer(IClientConfig config, ServerList<Server> serverList, ServerListFilter<Server> serverListFilter, IRule rule, IPing ping) { ZoneAwareLoadBalancer<Server> balancer = LoadBalancerBuilder.newBuilder() .withClientConfig(config).withRule(rule).withPing(ping) .withServerListFilter(serverListFilter).withDynamicServerList(serverList) .buildDynamicServerListLoadBalancer(); return balancer; }
ZoneAwareLoadBalancer从名字中可以看出来,这个负载均衡器和zone是有关系的。下面看下ZoneAwareLoadBalancer中的chooseServer方法:
@Override public Server chooseServer(Object key) { if (!ENABLED.get() || getLoadBalancerStats().getAvailableZones().size() <= 1) { logger.debug("Zone aware logic disabled or there is only one zone"); return super.chooseServer(key); } Server server = null; try { LoadBalancerStats lbStats = getLoadBalancerStats(); Map<String, ZoneSnapshot> zoneSnapshot = ZoneAvoidanceRule.createSnapshot(lbStats); logger.debug("Zone snapshots: {}", zoneSnapshot); if (triggeringLoad == null) { triggeringLoad = DynamicPropertyFactory.getInstance().getDoubleProperty( "ZoneAwareNIWSDiscoveryLoadBalancer." + this.getName() + ".triggeringLoadPerServerThreshold", 0.2d); } if (triggeringBlackoutPercentage == null) { triggeringBlackoutPercentage = DynamicPropertyFactory.getInstance().getDoubleProperty( "ZoneAwareNIWSDiscoveryLoadBalancer." + this.getName() + ".avoidZoneWithBlackoutPercetage", 0.99999d); } Set<String> availableZones = ZoneAvoidanceRule.getAvailableZones(zoneSnapshot, triggeringLoad.get(), triggeringBlackoutPercentage.get()); logger.debug("Available zones: {}", availableZones); if (availableZones != null && availableZones.size() < zoneSnapshot.keySet().size()) { String zone = ZoneAvoidanceRule.randomChooseZone(zoneSnapshot, availableZones); logger.debug("Zone chosen: {}", zone); if (zone != null) { BaseLoadBalancer zoneLoadBalancer = getLoadBalancer(zone); server = zoneLoadBalancer.chooseServer(key); } } } catch (Throwable e) { logger.error("Unexpected exception when choosing server using zone aware logic", e); } if (server != null) { return server; } else { logger.debug("Zone avoidance logic is not invoked."); return super.chooseServer(key); } }
这个方法会根据server的zone和可用性来选择具体的实例,返回一个Server对象。
通过ZoneAwareLoadBalancer选择具体的Server之后,再包装成RibbonServer对象,之前返回的server是该对象中的一个字段,除此之外,还有服务名serviceId,是否需要使用https等信息。最后,通过LoadBalancerRequest的apply方法,向具体的server发请求,从而实现了负载均衡。
下面是apply方法的定义:public interface LoadBalancerRequest<T> { public T apply(ServiceInstance instance) throws Exception; }
在请求时,传入的ribbonServer对象,被当成ServiceInstance类型的对象进行接收。ServiceInstance是一个接口,定义了服务治理系统中,每个实例需要提供的信息,比如serviceId,host,port等。
LoadBalancerRequest是一个接口,最终会通过实现类的apply方法去执行,实现类是在LoadBalancerInterceptor中调用RibbonLoadBalancerClient的execute方法时,传进来的一个匿名类,可以通过查看LoadBalancerInterceptor的代码看到。
创建LoadBalancerRequest匿名类的时候,就重写了apply方法,apply方法中,还新建了一个ServiceRequestWrapper的内部类,这个类中,就重写了getURI方法,getURI方法会调用loadBalancer的reconstructURI方法来构建uri。看到这里,已经可以大体知道Ribbon实现负载均衡的流程了,我们在RestTemplate上添加注解,就会有LoadBalancerClient的对象来配置它,也就是RibbonLoadBalancerClient。同时,LoadBalancerAutoConfiguration会进行配置,创建一个LoadBalancerInterceptor,并且拿到我们声明的所有restTemplate,在这些restTemplate中添加LoadBalancerInterceptor拦截器。
当通过restTemplate发送请求时,就会经过这个拦截器,在拦截器中,就会调用RibbonLoadBalancerClient中的方法,获取到根据服务名,通过负载均衡方法获取到服务实例,然后去请求这个实例。
上面说的这些,是如何对请求进行负载均衡的,但是还有个问题,我们请求的实例,是从Eureka Server上获取到的,那这个实例列表是如何获取的呢?怎么保证这个实例列表中的实例是可用的呢?在RibbonLoadBalancerClient选择实例的时候,是通过ILoadBalancer的实现类根据负载均衡算法选择服务实例的,也就是ZoneAwareLoadBalancer的chooseServer中的逻辑,那就在这里找线索。查看ZoneAwareLoadBalancer的继承关系,可以看到如下图所示。
可以看到,最上面是ILoadBalancer接口,AbstractLoadBalancer类继承了这个接口,BaseLoadBalancer继承了AbstractLoadBalancer类,DynamicServerListLoadBalancer继承了BaseLoadBalancer,ZoneAwareLoadBalancer继承了DynamicServerListLoadBalancer。
ILoadBalancer接口的代码已经看过了,现在看下AbstractLoadBalancer的代码:
public abstract class AbstractLoadBalancer implements ILoadBalancer { public enum ServerGroup{ ALL, STATUS_UP, STATUS_NOT_UP } /** * delegate to {@link #chooseServer(Object)} with parameter null. */ public Server chooseServer() { return chooseServer(null); } /** * List of servers that this Loadbalancer knows about * * @param serverGroup Servers grouped by status, e.g., {@link ServerGroup#STATUS_UP} */ public abstract List<Server> getServerList(ServerGroup serverGroup); /** * Obtain LoadBalancer related Statistics */ public abstract LoadBalancerStats getLoadBalancerStats(); }
这是一个抽象类,里面加了一个枚举,增加了两个抽象方法。定义的chooseServer方法。
下面再看BaseLoadBalancer类,BaseLoadBalancer类就算是负载均衡器的一个基础实现类,在里面可以看到定义了两个list:
@Monitor(name = PREFIX + "AllServerList", type = DataSourceType.INFORMATIONAL) protected volatile List<Server> allServerList = Collections .synchronizedList(new ArrayList<Server>()); @Monitor(name = PREFIX + "UpServerList", type = DataSourceType.INFORMATIONAL) protected volatile List<Server> upServerList = Collections .synchronizedList(new ArrayList<Server>());
从名字上看,这就是维护所有服务的实例列表,和维护状态为up的实例列表。
而且还可以看到BaseLoadBalancer中实现的ILoadBalancer接口中的方法,比如下面这两个,获取可用的服务列表,就会把upServerList返回,获取所有的服务列表,就会把allServerList返回。@Override public List<Server> getReachableServers() { return Collections.unmodifiableList(upServerList); } @Override public List<Server> getAllServers() { return Collections.unmodifiableList(allServerList); }
接下来,再看DynamicServerListLoadBalancer类。从类头上的注释可以知道,这个类可以动态的获取服务列表,并且利用filter对服务列表进行过滤。
在DynamicServerListLoadBalancer类中,能看到定义了一个ServerList类型的serverListImpl字段,ServerList是一个接口,里面有两个方法:
public interface ServerList<T extends Server> { public List<T> getInitialListOfServers(); /** * Return updated list of servers. This is called say every 30 secs * (configurable) by the Loadbalancer's Ping cycle * */ public List<T> getUpdatedListOfServers(); }
getInitialListOfServers是获取初始化的服务列表。
getUpdatedListOfServers是获取更新的服务列表。
ServerList有多个实现类,具体用的哪个呢,可以在EurekaRibbonClientConfiguration类中找到,这是Ribbon和Eureka结合的自动配置类,这里面有个方法:@Bean @ConditionalOnMissingBean public ServerList<?> ribbonServerList(IClientConfig config) { DiscoveryEnabledNIWSServerList discoveryServerList = new DiscoveryEnabledNIWSServerList( config); DomainExtractingServerList serverList = new DomainExtractingServerList( discoveryServerList, config, this.approximateZoneFromHostname); return serverList; }
方法中先新建了一个DiscoveryEnabledNIWSServerList类型的对象,又把这个对象作为一个参数,创建了DomainExtractingServerList类型的对象,最终返回的是DomainExtractingServerList的实例对象。
查看DomainExtractingServerList中重写的这两个方法,发现还是调用的DiscoveryEnabledNIWSServerList中的方法。然后,进到DiscoveryEnabledNIWSServerList类中,看这两个方法的定义:@Override public List<DiscoveryEnabledServer> getInitialListOfServers(){ return obtainServersViaDiscovery(); } @Override public List<DiscoveryEnabledServer> getUpdatedListOfServers(){ return obtainServersViaDiscovery(); }
这两个方法,都是调用了obtainServersViaDiscovery这个方法:
private List<DiscoveryEnabledServer> obtainServersViaDiscovery() { List<DiscoveryEnabledServer> serverList = new ArrayList<DiscoveryEnabledServer>(); if (eurekaClientProvider == null || eurekaClientProvider.get() == null) { logger.warn("EurekaClient has not been initialized yet, returning an empty list"); return new ArrayList<DiscoveryEnabledServer>(); } EurekaClient eurekaClient = eurekaClientProvider.get(); if (vipAddresses!=null){ for (String vipAddress : vipAddresses.split(",")) { // if targetRegion is null, it will be interpreted as the same region of client List<InstanceInfo> listOfInstanceInfo = eurekaClient.getInstancesByVipAddress(vipAddress, isSecure, targetRegion); for (InstanceInfo ii : listOfInstanceInfo) { if (ii.getStatus().equals(InstanceStatus.UP)) { if(shouldUseOverridePort){ if(logger.isDebugEnabled()){ logger.debug("Overriding port on client name: " + clientName + " to " + overridePort); } // copy is necessary since the InstanceInfo builder just uses the original reference, // and we don't want to corrupt the global eureka copy of the object which may be // used by other clients in our system InstanceInfo copy = new InstanceInfo(ii); if(isSecure){ ii = new InstanceInfo.Builder(copy).setSecurePort(overridePort).build(); }else{ ii = new InstanceInfo.Builder(copy).setPort(overridePort).build(); } } DiscoveryEnabledServer des = new DiscoveryEnabledServer(ii, isSecure, shouldUseIpAddr); des.setZone(DiscoveryClient.getZone(ii)); serverList.add(des); } } if (serverList.size()>0 && prioritizeVipAddressBasedServers){ break; // if the current vipAddress has servers, we dont use subsequent vipAddress based servers } } } return serverList; }
在这个方法中,就是通过eurekaClient去注册中心获取服务,将状态为up的服务实例封装成DiscoveryEnabledServer对象,然后放入列表返回,这就是获取服务列表的流程。
获取服务列表的流程知道了,那是如何触发去获取,如何更新服务列表的呢?再看DynamicServerListLoadBalancer类中的代码,有一段:protected final ServerListUpdater.UpdateAction updateAction = new ServerListUpdater.UpdateAction() { @Override public void doUpdate() { updateListOfServers(); } };
ServerListUpdater是一个接口,在DynamicServerListLoadBalancer的构造函数中,创建了一个PollingServerListUpdater类的对象,为ServerListUpdater字段赋值。进入PollingServerListUpdater类看一下:
@Override public synchronized void start(final UpdateAction updateAction) { if (isActive.compareAndSet(false, true)) { final Runnable wrapperRunnable = new Runnable() { @Override public void run() { if (!isActive.get()) { if (scheduledFuture != null) { scheduledFuture.cancel(true); } return; } try { updateAction.doUpdate(); lastUpdated = System.currentTimeMillis(); } catch (Exception e) { logger.warn("Failed one update cycle", e); } } }; scheduledFuture = getRefreshExecutor().scheduleWithFixedDelay( wrapperRunnable, initialDelayMs, refreshIntervalMs, TimeUnit.MILLISECONDS ); } else { logger.info("Already active, no-op"); } }
里面有个start方法,实现了Runnable接口,run方法里调用UpdateAction的doUpdate,之后再启动一个定时任务,执行这个方法。定时任务传入的两个时间参数:initialDelayMs和refreshIntervalMs,任务启动后一秒开始执行,并且每隔三十秒执行一次,用于刷新列表。
看到这里,就可以大体了解了,构造DynamicServerListLoadBalancer实例的时候,就会启动一个定时任务了,一开始先获取服务列表,之后每隔三十秒获取一次。负载均衡时,就是通过负载均衡算法在实例列表中选择一个,发送请求。
以上这就是Ribbon源码大体的流程。
注:以上所有只做理论性的总结与分析,相关实战代码会在后面的博客中和github中逐步增加。
参考书籍、文献和资料:
【1】郑天民. 微服务设计原理与架构. 北京:人民邮电出版社,2018.
【2】徐进,叶志远,钟尊发,蔡波斯等. 重新定义Spring Cloud. 北京:机械工业出版社. 2018.
【3】https://blog.csdn.net/wo18237095579/article/details/83343915.
【4】https://blog.csdn.net/a15514920226/article/details/78924483.
【5】https://blog.csdn.net/xiaofeng10330111/article/details/85682513.
【6】https://blog.csdn.net/luanlouis/article/details/83060310.
【7】https://blog.csdn.net/chayangdz/article/details/82177917.
-
Spring Cloud Alibaba 02_使用 ribbon 实现负载均衡
2020-10-20 14:23:39Spring Cloud Alibaba 02_使用 ribbon 实现负载均衡 注:Ribbon 是 Netflix 提供的组件,不是 Alibaba 的组件 修改 ConsumerConfiguration,添加 @LoadBalanced 注解 package com.blu.configuration; import org.... -
史上最简单的SpringCloud教程 | 第二篇: 服务消费者(rest+ribbon)(Finchley版本)
2018-07-14 10:05:50转载请标明出处: http://blog.csdn.net/forezp/article/details/69788938 本文出自方志朋的博客 在上一篇文章,讲了服务的注册和发现...Spring cloud有两种服务调用方式,一种是ribbon+restTemplate,另一种... -
深入理解Ribbon之源码解析
2017-07-08 14:48:05Ribbon是Netflix公司开源的一个负载均衡的项目,它属于上述的第二种,是一个客户端负载均衡器,运行在客户端上。它是一个经过了云端测试的IPC库,可以很好地控制HTTP和TCP客户端的一些行为。 Feign已经默认使用. -
Ribbon Ribbon is a client side IPC library that is battle-tested in cloud. It provides the following features Load balancing Fault tolerance Multiple protocol (HTTP, TCP, UDP) support in an ...
-
无线计算卸载的能量和时间优化
-
Arduino云植物自动浇水
-
MaxScale 实现 MySQL 读写分离与负载均衡
-
Unity 热更新技术-ILRuntime
-
linux基础入门和项目实战部署系列课程
-
工程制图 AutoCAD 2012 从二维到三维
-
2019年下半年 多媒体应用设计师 上午试卷 综合知识 软考真题【含答案和答案解析】
-
【硬核】一线Python程序员实战经验分享(1)
-
基于杜鹃搜索的磷虾群算法解决工程优化问题
-
第1关上 将错就错.mp4
-
元素周期表-three.js实战详解
-
物联网之mqtt实现(emqx+springboot+mqtt附源码)
-
2016年上半年 信息系统管理工程师 上午试卷 综合知识 软考真题【含答案和答案解析】
-
2013年上半年 多媒体应用设计师 上午试卷 综合知识 软考真题【含答案和答案解析】
-
iptables 企业级防火墙配置(四表五链)
-
基于Flink+Hudi构建企业亿级云上实时数据湖教程(PC、移动、小
-
2012年上半年 数据库系统工程师 上午试卷 综合知识 软考真题【含答案和答案解析】
-
2012年下半年 嵌入式系统设计师 上午试卷 综合知识 软考真题【含答案和答案解析】
-
2019年下半年 嵌入式系统设计师 上午试卷 综合知识 软考真题【含答案和答案解析】
-
会话管理漏洞检测