了解Ribbon

本书使用的Spring Cloud 默认支持的Riibbon 版本是2.3.0


1.负载均衡介绍

负载均衡是分布式项目架构的重点,负载均衡决定着整个分布式服务集群的性能和稳定,前面讲解了Eureka集群的使用,本节我们讲解Spring Cloud 如何为我们的微服务提供负载均衡的方案。

2.Ribbon 介绍

Ribbon是Netflix下的负载均衡项目,通过Spring Cloud的封装的工具Spring Cloud Ribbon,可以让我们轻松地将面向服务的REST模版请求自动转换成客户端负载均衡的服务调用。主要提供了以下特性:

  • 负载均衡器,可支持插拔式的负载均衡原则
  • 对多种协议提供支持,例如HTTP、TCP、UDP等
  • 集成了负载均衡功能的客户端

它不需要独立部署,但是它几乎存在于每一个Spring Cloud构建的微服务中。因为微服务间的调用,API网关的请求转发等内容,实际上都是通过Ribbon来实现的,包括后续我们将要介绍的Feign,它也是基于Ribbon实现的工具。所以,对Spring Cloud Ribbon的理解和使用,对于我们使用Spring Cloud来构建微服务非常重要

2.1 Ribbon子模块

  • ribbon-core:ribbon的核心,主要包含负载均衡器、负载均衡接口、客户端接口、内置负载均衡实现API
  • ribbon-httpclient:为负载均衡提供了REST客户端
  • ribbon-loadbalancer:负载均衡模块,可以独立使用,也可和其它模块一起使用
  • ribbon:在 ribbon 模块和 Hystrix 基础之上,集成了 负载均衡、容错处理、缓存/批处理 的 API

    2.2 Ribbon 负载均衡器主键

    Ribbon 的负载均衡器主要与集群中的各个服务进行通信,负载均衡器主要提供已下功能:

  • 维护服务器的IP、DNS的信息

  • 根据特定的逻辑在服务列表中循环
    为了实现负载均衡的基础功能,RIbbon的负载均衡器提供了已下三大模块:
  • Rule: Ribbon提供的逻辑主键,决定了服从服务器列表中返回那个服务器实例
  • Ping: 主要使用定时器来确保服务器的网络可以连接
  • ServerList: 服务器列表,可以静态的配置服务器列表,也可以动态指定服务器列表

    2.3 Ribbon提供的负载均衡算法

  • RoundRobinRule(轮询算法)

  • RandomRule(随机算法)
  • AvailabilityFilteringRule():会先过滤由于多次访问故障而处于断路器跳闸状态的服务,还有并发的连接数量超过阈值的服务,然后对剩余的服务列表按照轮询策略进行访问
  • WeightedResponseTimeRule():根据平均响应的时间计算所有服务的权重,响应时间越快服务权重越大被选中的概率越高,刚启动时如果统计信息不足,则使用RoundRobinRule策略,等统计信息足够会切换到WeightedResponseTimeRule
  • RetryRule():先按照RoundRobinRule的策略获取服务,如果获取失败则在制定时间内进行重试,获取可用的服务。
  • BestAviableRule():会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,然后选择一个并发量最小的服务

    3. 总结

    通过上面的介绍,我们对Ribbon 已经有了了解,知道Ribbon 在微服务中是干什么的。下面我们通过程序让大家对Ribbon 有更进一步的认识。

    Ribbon 服务提供者


本节代码地址

GitHub: https://github.com/xuyisu/fw-sping-cloud/tree/master/fw-cloud-ribbon/fw-cloud-ribbon-server


本节将通过一个简单的程序展示Ribbon API 的使用,本节的程序结构图如下:
负载均衡Ribbon - 图1
先建服务端fw-cloud-ribbon-server提供一个RESTFUL的接口,然后在创建Ribbon客户端调动,并将负载的效果显示出来。接下来开始

1. ribbon服务端模块

新建模块fw-cloud-ribbon-server
负载均衡Ribbon - 图2

1.1 maven 包引入

  1. <dependencies>
  2. <dependency>
  3. <groupId>org.springframework.boot</groupId>
  4. <artifactId>spring-boot-starter-web</artifactId>
  5. </dependency>
  6. <dependency>
  7. <groupId>org.springframework.cloud</groupId>
  8. <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
  9. </dependency>
  10. <dependency>
  11. <groupId>org.springframework.boot</groupId>
  12. <artifactId>spring-boot-starter-actuator</artifactId>
  13. </dependency>
  14. </dependencies>

spring-boot-starter-actuator包不清楚的可以看下,前面章节“Eureka服务端健康检查”,使用spring-cloud-starter-netflix-eureka-client是想通过用Eureka的注册中心发现注册的服务列表。

1.2 新建启动类

  1. /**
  2. *
  3. * @Author xuyisu
  4. * @Date 2019/12/7
  5. */
  6. @EnableDiscoveryClient
  7. @SpringBootApplication
  8. public class FwRibbonServerApplication {
  9. public static void main(String[] args) {
  10. SpringApplication.run(FwRibbonServerApplication.class, args);
  11. }
  12. }

1.3 添加配置

  1. server:
  2. port: 8773
  3. spring:
  4. application:
  5. name: fw-cloud-ribbon-server
  6. eureka:
  7. client:
  8. service-url:
  9. defaultZone: http://localhost:8761/eureka

这里为了更好的演示RESTFUL接口,我创建了一个User类

1.4 新建实体

  1. /**
  2. * @author xuyisu
  3. * @description 用户
  4. * @date 2019/12/11
  5. */
  6. @Data
  7. @AllArgsConstructor
  8. @NoArgsConstructor
  9. public class User implements Serializable {
  10. /**
  11. * 主键
  12. */
  13. private long id;
  14. /**
  15. * 用户名
  16. */
  17. private String username;
  18. /**
  19. * 真实姓名
  20. */
  21. private String realname;
  22. /**
  23. * 邮箱
  24. */
  25. private String email;
  26. /**
  27. * 备注
  28. */
  29. private String remark;
  30. }

1.5 新建Service 接口

  1. /**
  2. * @author xuyisu
  3. * @description 用户service
  4. * @date 2019/12/11
  5. */
  6. public interface UserService {
  7. /**
  8. * 模拟数据库获取所有用户
  9. * @return
  10. */
  11. List<User> getUsers();
  12. /**
  13. * 模拟数据库根据id获取用户
  14. * @return
  15. */
  16. User getUserById(long id);
  17. }

1.6 新建 UserServiceImpl 实现类

这里我模拟了数据库构造数据,同时使用了java8 新特性lambda表达式查询用户信息,想要熟悉更多的java8 特性,可以看一本“Java 8 实战”的书。

  1. /**
  2. * @author xuyisu
  3. * @description User接口实现
  4. * @date 2019/12/11
  5. */
  6. @Service
  7. public class UserServiceImpl implements UserService {
  8. @Override
  9. public List<User> getUsers() {
  10. return initUser();
  11. }
  12. @Override
  13. public User getUserById(long id) {
  14. List<User> userList = getUsers().stream().filter(user -> user.getId() == id).collect(Collectors.toList());
  15. if(CollectionUtils.isEmpty(userList)){
  16. return new User(0,null,null,null,"这位顾客先拿一下腰牌!");
  17. }
  18. return userList.get(0);
  19. }
  20. /**
  21. * 模拟数据库初始化数据
  22. * @return
  23. */
  24. private List<User> initUser(){
  25. List<User> userList =new ArrayList<>();
  26. User user1=new User(1,"113445","刘备","liubei@gmail.com","汉室刘皇叔,蜀国大佬");
  27. User user2=new User(2,"123456","关羽","guanyu@gmail.com","人称关二爷,蜀国五虎上将");
  28. User user3=new User(3,"147258","张飞","zhangfei@gmail.com","此人性格暴躁,蜀国五虎上将");
  29. userList.add(user1);
  30. userList.add(user2);
  31. userList.add(user3);
  32. return userList;
  33. }
  34. }

1.7 接下来编写RESTFUL 接口

这里我们增加了一个log日志,我们将当前提供服务的是url地址打印出来,这样方便我们看到演示效果。

  1. /**
  2. * @author xuyisu
  3. * @description ribbon demo接口演示
  4. * @date 2019/12/11
  5. */
  6. @RestController
  7. @RequestMapping("user")
  8. public class RibbonController {
  9. @Autowired
  10. private UserService userService;
  11. @GetMapping("/{id:\\d+}")
  12. public User getUserById(@PathVariable Long id, HttpServletRequest req){
  13. String url = req.getRequestURL().toString();
  14. User user = userService.getUserById(id);
  15. user.setRemark(user.getRemark()+":提供服务的是:"+url);
  16. return user;
  17. }
  18. }

接下来我们需要启动项目验证RESTFUL 接口

1.8 启动项目

启动之前记得先启动Eureka服务端
通过postman 验证没问题
负载均衡Ribbon - 图3
下面我们启动8774 端口的服务,跟之前一样,我们需要修改一下配置,允许并行启动
负载均衡Ribbon - 图4
然后修改端口为8774之后再启动

  1. server:
  2. port: 8774

全部启动成功
负载均衡Ribbon - 图5

2.总结

本节我们了解了Ribbon 的请求结构及服务端的搭建,下面我们再创建Ribbon客户端,看看Ribbon 客户端如何跟服务端发生亲密的接触。

Ribbon客户端

本节代码地址

GitHub: https://github.com/xuyisu/fw-sping-cloud/tree/master/fw-cloud-ribbon/fw-cloud-ribbon-client


下面我们开始创建Ribbon客户端的项目,本节我们会讲到Ribbon调用的原理和自定义规则。

1.新建客户端

新建模块fw-cloud-ribbon,注意是New Module ,我们创建的是一个聚合工程
负载均衡Ribbon - 图6

1.1 maven 配置

这里我们使用Spring Cloud 官方提供的spring-cloud-starter-netflix-ribbon

  1. <dependencies>
  2. <dependency>
  3. <groupId>org.springframework.boot</groupId>
  4. <artifactId>spring-boot-starter-web</artifactId>
  5. </dependency>
  6. <dependency>
  7. <groupId>org.springframework.cloud</groupId>
  8. <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
  9. </dependency>
  10. <dependency>
  11. <groupId>org.springframework.boot</groupId>
  12. <artifactId>spring-boot-starter-actuator</artifactId>
  13. </dependency>
  14. <dependency>
  15. <groupId>org.springframework.cloud</groupId>
  16. <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
  17. </dependency>
  18. </dependencies>

1.2 新建启动类

  1. /**
  2. *
  3. * @Author xuyisu
  4. * @Date 2019/12/6
  5. */
  6. @EnableDiscoveryClient
  7. @SpringBootApplication
  8. public class FwRibbonApplication {
  9. public static void main(String[] args) {
  10. SpringApplication.run(FwRibbonApplication.class, args);
  11. }
  12. }

1.3 项目配置

  1. server:
  2. port: 8772
  3. spring:
  4. application:
  5. name: fw-feign
  6. eureka:
  7. client:
  8. service-url:
  9. defaultZone: http://localhost:8761/eureka

因为服务端是接口返回的User 类,所以接收端也需要同样的User 类

1.4 新建User 类

注意@NoArgsConstructor如果不添加反序列化的时候回报错

  1. /**
  2. * @author xuyisu
  3. * @description 用户
  4. * @date 2019/12/11
  5. */
  6. @Data
  7. @AllArgsConstructor
  8. @NoArgsConstructor
  9. public class User implements Serializable {
  10. /**
  11. * 主键
  12. */
  13. private long id;
  14. /**
  15. * 用户名
  16. */
  17. private String username;
  18. /**
  19. * 真实姓名
  20. */
  21. private String realname;
  22. /**
  23. * 邮箱
  24. */
  25. private String email;
  26. /**
  27. * 备注
  28. */
  29. private String remark;
  30. }

1.5 添加 Ribbon 的配置

  1. /**
  2. * @author xuyisu
  3. * @description ribbon配置
  4. * @date 2019/12/10
  5. */
  6. @Configuration
  7. public class EurekaRibbonConfig {
  8. @Bean
  9. @LoadBalanced // 实现负载均衡
  10. public RestTemplate restTemplate() {
  11. return new RestTemplate();
  12. }
  13. }

1.5.1 @LoadBalanced 原理

在 RestTemplate 上加了一个 @LoadBalanced 注解就可以负载均衡。这是因为 Spring Cloud 做了大量的底层封装,做了很多简化。
内部的主要逻辑就是给 RestTemplate 增加拦截器,在请求之前对请求的地址进行了替换,或者根据具体的负载策略选择服务地址,然后去调用,这就是 @LoadBalanced 的原理。
Spring Web 为 HttpClient 提供了 Request 拦载器 ClientHttpRequestInterceptor,位于 spring-web jar 包下。
在 spring-cloud-commons 包中提供了负载均衡自动配置类 LoadBalancerAutoConfiguration ,里面维护了一个 @LoadBalanced 注解的 RestTemplate 列表,里面的静态类 LoadBalancerInterceptorConfig 注册了 负载均衡拦截器 LoadBalancerInterceptor,RestTemplateCustomizer 来添加拦截器列表。
负载均衡拦截器 LoadBalancerInterceptor 实现了 ClientHttpRequestInterceptor,主要逻辑在 intercept() 方法中,执行交给了 LoadBalancerClient,通过 LoadBalancerRequestFactory 来构建一个 LoadBalancerRequest 对象,createRequest 方法中通过 ServiceRequestWrapper 来执行替换 URI 的逻辑,核心是通过 reconstructURI() 方法实现,该方法的业务实现是在 RibbonLoadBalancerClient 类中 。

1.6 创建Ribbon Service

  1. /**
  2. * @author xuyisu
  3. * @description
  4. * @date 2019/12/10
  5. */
  6. @Service
  7. @Slf4j
  8. public class EurekaRibbonService {
  9. @Autowired
  10. RestTemplate restTemplate;
  11. public User findUserById(Long id) {
  12. // http://服务提供者的serviceId/url
  13. return restTemplate.getForObject("http://fw-cloud-ribbon-server/user/" + id, User.class);
  14. }
  15. }

restTemplate.getForObject("http://fw-cloud-feign-server/user/" + id, User.class)这里需要添加正确的服务名称,RestTemplate 这样请求的好处就是不需要关心应用的IP,通过Eureka 注册中心发现是此服务名的服务提供者就会去请求。非常方便应用的水平拓展。

1.7 Ribbon RESTFUL 请求层

  1. /**
  2. * @author xuyisu
  3. * @description controller
  4. * @date 2019/12/10
  5. */
  6. @RestController
  7. public class EurekaRibbonController {
  8. @Resource
  9. private EurekaRibbonService eurekaRibbonService;
  10. /**
  11. * 根据id获取用户信息
  12. * @param id
  13. * @return
  14. */
  15. @GetMapping("/user/{id:\\d+}")
  16. public User findUserById(@PathVariable long id){
  17. return eurekaRibbonService.findUserById(id);
  18. }
  19. }

\\d+是一个正则表达式,这个正则表达式的含义就是请求的id只能是正整数

1.8 启动应用

通过postman 验证发现,分别输入localhost:8771/user/1
结果如下
负载均衡Ribbon - 图7
负载均衡Ribbon - 图8
通过返回结果可以看到8773和8774一直在变化,轮询着变化,原因是什么呢?下面我们说一下负载均衡的策略。

1.9 负载均衡策略

Ribbon 默认的负载策略是轮询,同时也提供了很多其他的策略能够让用户根据业务需求来选择。负载均衡策略的根接口是 com.netflix.loadbalancer.IRule
负载均衡Ribbon - 图9

  • BestAvailableRule
    选择最小并发请求的服务器,每个客户端都会获得一个随机的服务器列表,如果服务器被标记为错误,则跳过。
  • AvailabilityFilteringRule
    用于过滤连接一直失败或读取失败而被标记为 circuit breaker tripped 状态的服务;或持有超过可配置限制的活动连接(默认值为 Integer.MAX_VALUE),即过滤掉高并发的后端服务。实际就是检查获取到的服务列表里,各个 Server 的 Status 。
  • ZoneAvoidanceRule
    根据区域和可用性来过滤服务器。使用 ZoneAvoidancePredicate 来判断 Zone 的使用是否达到阀值,过滤出最差 Zone 中的所有服务器; AvailabilityPredicate 用于过滤出并发连接过多的服务。
  • RandomRule
    随机分配流量,即随机选择一个 Server。
  • RetryRule
    向现有负载均衡策略添加重试机制。
  • ResponseTimeWeightedRule
    该策略已过期,同见 WeightedResponseTimeRule。
  • WeightedResponseTimeRule
    根据响应时间为每个服务器动态分配权重(Weight)分,然后台加权循环的方式使用该策略。响应时间越长,权重越低,被选中可能性越低。

以上是Ribbon 自带的负载策略,如果我们想改变策略怎么改?我们可以自定义策略吗? 请见下一节 Ribbon 自定义负载规则

Ribbon自定义操作

1. Ribbon修改请求策略

本节代码地址

GitHub: https://github.com/xuyisu/fw-sping-cloud/tree/master/fw-cloud-ribbon/fw-cloud-ribbon-client


前面已经说,Ribbon 默认的请求策略是轮询,如何调整为其他策略呢?

1.1 修改EurekaRibbonConfig

路径在fw-cloud-ribbon中的com.yisu.ribbon.config.EurekaRibbonConfig
我们只需要定义一个bean,比如替换Ribbon 自带的随机策略

  1. @Bean
  2. public IRule ribbonRule() {
  3. //自定义成随机
  4. return new RandomRule();
  5. }

1.2 重启客户端

修改好之后启动客户端,postman 重新请求localhost:8771/user/1
多请求几次,会发现会有连续请求几次的接口是8773或8774服务提供的。

2. 如何自定义请求策略

下面我们自定义负载请求的策略规则,新定义规则需要实现IRule并实现里面的方法,其中规则策略的实现是在choose(Object o)中。比如我们现在实现一个只取第一个注册到Eureka服务中的提供者实例。

本节代码地址

GitHub: https://github.com/xuyisu/fw-sping-cloud/tree/master/fw-cloud-ribbon/fw-cloud-ribbon-client


2.1 新建规则类

  1. /**
  2. * @author xuyisu
  3. * @description 自定义负载规则
  4. * @date 2019/12/22
  5. */
  6. @Slf4j
  7. public class MyRule implements IRule {
  8. private ILoadBalancer loadBalancer;
  9. @Override
  10. public Server choose(Object o) {
  11. List<Server> allServers = loadBalancer.getAllServers();
  12. //输出一遍提供者实例
  13. allServers.stream().forEach(server -> System.out.println(server.getHostPort()));
  14. if(CollectionUtils.isEmpty(allServers)){
  15. log.info("当前不存在负载调用的提供者实例");
  16. return null;
  17. }
  18. return allServers.get(0);
  19. }
  20. @Override
  21. public void setLoadBalancer(ILoadBalancer iLoadBalancer) {
  22. this.loadBalancer=iLoadBalancer;
  23. }
  24. @Override
  25. public ILoadBalancer getLoadBalancer() {
  26. return this.loadBalancer;
  27. }
  28. }

2.2 修改EurekaRibbonConfig

路径在fw-cloud-ribbon中的com.yisu.ribbon.config.EurekaRibbonConfig
我们只需要替换我们自己定义的负载规则

  1. @Bean
  2. public IRule ribbonRule() {
  3. //自定义规则
  4. return new MyRule();
  5. }

2.3 重启客户端

通过postman 重新请求localhost:8771/user/1
多请求几次,会发现返回的结构永远是第一次注册到Eureka服务上的提供者
负载均衡Ribbon - 图10

3. 如何不再Eureka环境下运行

本节代码地址

GitHub: https://github.com/xuyisu/fw-sping-cloud/tree/master/fw-cloud-ribbon/fw-cloud-ribbon-client


3.1 修改配置

在无Eureka 环境下如何运行Spring Cloud RIbbon 提供的负载均衡?修改一下application.yml配置

  1. ribbon:
  2. eureka:
  3. enabled: false #禁用eurake
  4. # 禁用 Eureka 后手动配置服务地址
  5. fw-cloud-ribbon-server: #服务名称
  6. ribbon:
  7. listOfServers: localhost:8773,localhost:8774

这里需要将Eureka设置为false,就是不启用从Eureka获取服务实例,但是需要将已经启动的服务端实例的地址设置在调用服务的后面,规则如下

  1. 服务名称(spring.application.name):
  2. ribbon:
  3. listOfServers: ip:port,ip:port

3.2 重新启动

通过postman 重新请求localhost:8771/user/1
负载均衡Ribbon - 图11
并且控制台会把配置的实例地址输出来

  1. 2019-12-22 15:03:55.411 INFO 19300 --- [nio-8772-exec-2] c.n.l.DynamicServerListLoadBalancer : DynamicServerListLoadBalancer for client fw-cloud-ribbon-server initialized: DynamicServerListLoadBalancer:{NFLoadBalancer:name=fw-cloud-ribbon-server,current list of Servers=[localhost:8773, localhost:8774],Load balancer stats=Zone stats: {unknown=[Zone:unknown; Instance count:2; Active connections count: 0; Circuit breaker tripped count: 0; Active connections per server: 0.0;]
  2. },Server stats: [[Server:localhost:8774; Zone:UNKNOWN; Total Requests:0; Successive connection failure:0; Total blackout seconds:0; Last connection made:Thu Jan 01 08:00:00 CST 1970; First connection made: Thu Jan 01 08:00:00 CST 1970; Active Connections:0; total failure count in last (1000) msecs:0; average resp time:0.0; 90 percentile resp time:0.0; 95 percentile resp time:0.0; min resp time:0.0; max resp time:0.0; stddev resp time:0.0]
  3. , [Server:localhost:8773; Zone:UNKNOWN; Total Requests:0; Successive connection failure:0; Total blackout seconds:0; Last connection made:Thu Jan 01 08:00:00 CST 1970; First connection made: Thu Jan 01 08:00:00 CST 1970; Active Connections:0; total failure count in last (1000) msecs:0; average resp time:0.0; 90 percentile resp time:0.0; 95 percentile resp time:0.0; min resp time:0.0; max resp time:0.0; stddev resp time:0.0]
  4. ]}ServerList:com.netflix.loadbalancer.ConfigurationBasedServerList@d0606e7
  5. localhost:8773
  6. localhost:8774