前言

在微服务中,对服务进行拆分之后,必然会带来微服务之间的通信需求,而每个微服务为了保证高可用性,又会去部署集群,那么面对一个集群微服务进行通信的时候,如何进行负载均衡也是必然需要考虑的问题。那么有需求自然就有供给,由此一大批优秀的开源的负载均衡组件应运而生,本文就让我们一起来分析一下 Spring Cloud Netflix 套件中的负载均衡组件 Ribbon

一个问题引发的思考

首先我们来看一个问题,假如说我们现在有两个微服务,一个 user-center,一个 user-order,我现在需要在 user-center 服务中调用 user-order 服务的一个接口。

这时候我们可以使用 HttpClientRestTemplate 等发起 http 请求,user-center 服务端口为 8001,如下图所示:

@RestController
@RequestMapping(value = "/user")
public class UserController {
@Autowired
private RestTemplate restTemplate; @Bean
public RestTemplate restTemplate(){
return new RestTemplate();
} @GetMapping("/order")
public String queryOrder(){
return restTemplate.getForObject("http://localhost:8002/order/query",String.class);
}
}

user-order 服务中只是简单的定义了一个接口,user-order 服务端口为 8002

@RestController
@RequestMapping(value = "/order")
public class UserOrderController { @GetMapping(value = "/query")
public String queryAllOrder(){
return "all orders";
}
}

这时候只需要将两个服务启动,访问 http://localhost:8001/user/order 就可以获取到所有的订单信息。

可以看到,这样是可以在两个微服务之间进行通讯的,但是,假如说我们的 user-order 服务是一个集群呢?这时候怎么访问呢?因为 user-order 服务已经是集群,所以必然需要一种算法来决定应该请求到哪个 user-order 服务中,最简单的那么自然就是随机或者轮询机制,轮询或者随机其实就是简单的负载均衡算法,而 Ribbon 就是用来实现负载均衡的一个组件,其内部支持轮询,等算法。

Ribbon的简单使用

接下来我们看看 Ribbon 的简单使用。

  • 首先改造 user-order 服务,在 user-order 服务中定义一个服务名配置:
spring.application.name=user-order-service
  • user-order 服务中的 UserOrderController 稍微改造一下,新增一个端口的输出来区分:
@RestController
@RequestMapping(value = "/order")
public class UserOrderController { @Value("${server.port}")
private int serverPort; @GetMapping(value = "/query")
public String queryAllOrder(){
return "订单来自:" + serverPort;
}
}
  • 通过 VM 参数 -Dserver.port=8002-Dserver.port=8003 分别来启动两个 user-order 服务。

  • 接下来改造 user-center 服务,在 user-center 服务中引入 Ribbon 的相关依赖:

 <dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
<version>2.2.3.RELEASE</version>
</dependency>
  • user-center 服务中新增一个 Ribbon 相关配置,列举出需要访问的所有服务:
user-order-service.ribbon.listOfServers=\
localhost:8002,localhost:8003
  • user-center 服务中的 UserController 进行改造:
@RestController
@RequestMapping(value = "/user")
public class UserController {
@Autowired
private RestTemplate restTemplate; @Autowired
private LoadBalancerClient loadBalancerClient; @Bean
public RestTemplate restTemplate(){
return new RestTemplate();
} @GetMapping("/order")
public String queryOrder(){
//获取一个 user-order 服务
ServiceInstance serviceInstance = loadBalancerClient.choose("user-order-service");
String url = String.format("http://%s:%s",serviceInstance.getHost(),serviceInstance.getPort()) + "/order/query";
return restTemplate.getForObject(url,String.class);
}
}

这时候我们再次访问 http://localhost:8001/user/order 就可以看到请求的 user-order 服务会在 80028003 之间进行切换。

Ribbon 原理分析

看了上面 Ribbon 的使用示例,会不会觉得有点麻烦,每次还需要自己去获取 ip 和端口,然后格式化 url,但是其实实际开发过程中我们并不会通过这么原始的方式来编写代码,接下来我们再对上面的示例进行一番改造:

@RestController
@RequestMapping(value = "/user")
public class UserController3 {
@Autowired
private RestTemplate restTemplate; @Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
} @GetMapping("/order")
public String queryOrder(){
return restTemplate.getForObject("http://user-order-service/order/query",String.class);
}
}

在这个示例中,主要就是一个关键主键起了作用:@LoadBalanced。

@LoadBalanced 注解

进入 @LoadBalanced 注解中,我们可以看到,这个注解其实没有任何逻辑,只是加了一个 @Qualifier 注解:

这个注解大家应该很熟悉了,常用语同一个 Bean 有多个不同名称注入的场景。

@Qualifier注解

下面我们通过一个例子来演示一下 Qualifier注解的用法。

新建一个空的 TestDemo 类,并新增一个 TestConfiguration 类来创建不同名称的 TestDemo

@Configuration
public class TestConfiguration {
@Bean("testDemo1")
public TestDemo testDemo(){
return new TestDemo();
} @Bean("testDemo2")
public TestDemo testDemo2(){
return new TestDemo();
}
}

这时候我们如果需要注入 TestDemo,那么有很多种办法,具体的使用就需要看业务需要来决定。

  • 方法一:直接使用 @Autowired,并使用 List 集合来接收 Bean,这样所有 TestDemo 类型的 Bean 都会被注入。
  • 方法二:通过使用 @Resource(name = "testDemo1") 注解来指定名称,这样就可以只注入一个 Bean
  • 方法三:通过使用 @Resource@Qualifier(value = "testDemo1") 来指定一个 Bean,其实这种方式和方法二的效果基本一致。
  • 方法四:使用 @Autowired@Qualifier 注解来注入,不指定任何名称,如下所示:
@RestController
@RequestMapping(value = "/test")
public class TestQualifierController { @Autowired(required = false)
@Qualifier
private List<TestDemo> testDemo = Collections.emptyList(); @GetMapping("/all")
public String allDemo(){
for (TestDemo testDemo : testDemos){
System.out.println(testDemo.toString());
}
return "succ";
}
}

这时候运行之后我们发现不会有任何 Bean 被注入到集合中,这是因为当使用这种方式来注入时,Spring 会认为当前只需要注入被 @Qualifier 注解标记的 Bean,而我们上面定义的两个 TestDemo 都没有被 @Qualifier 修饰。

这时候,我们只需要在 TestConfiguration 稍微改造,在 TestDemo 的定义上加上 @Qualifier 修饰即可:

@Configuration
public class TestConfiguration { @Bean("testDemo1")
@Qualifier
public TestDemo testDemo(){
return new TestDemo();
} @Bean("testDemo2")
@Qualifier
public TestDemo testDemo2(){
return new TestDemo();
}
}

这时候再去运行,就会发现,testDemo1testDemo2 都会被注入。

LoadBalancerAutoConfiguration 自动装配

SpringCloud 是基于 SpringBoot 实现的,所以我们常用的这些分布式组件都会基于 SpringBoot 自动装配来实现,我们进入 LoadBalancerAutoConfiguration 自动装配类可以看到,RestTemplate 的注入加上了 @LoadBalanced,这就是为什么我们前面的例子中加上了 @LoadBalanced 就能被自动注入的原因:

RestTemplateCustomizer

上面我们看到,RestTemplate 被包装成为了 RestTemplateCustomizer,而 RestTemplateCustomizer 的注入如下:

可以看到这里面加入了一个拦截器 LoadBalancerInterceptor,事实上即使不看这里,我们也可以猜测到,我们直接使用服务名就可以进行通讯的原因必然是底层有拦截器对其进行转换成 ip 形式,并在底层进行负载均衡选择合适的服务进行通讯。

LoadBalancerInterceptor

LoadBalancerInterceptorRibbon 中默认的一个拦截器,所以当我们调用 RestTemplategetObject 方法时,必然会调用拦截器中的方法。

从源码中可以看到,LoadBalancerInterceptor 中只有一个 intercept() 方法:

RibbonLoadBalancerClient#execute

继续跟进 execute 方法会进入到 RibbonLoadBalancerClient 类(由 RibbonAutoConfiguration 自动装配类初始化)中:

这个方法中也比较好理解,首先获取一个负载均衡器,然后再通过 getServer 方法获取一个指定的服务,也就是当我们有多个服务时,到这里就会选出一个服务进行通讯。

进入 getServer 方法:

我们看到,最终会调用 ILoadBalancer 中的 chooseServer 方法,而 ILoadBalancer 是一个顶层接口,这时候具体会调用哪个实现类那么就需要先来看一下类图:

这里直接看类图也无法看出到底会调用哪一个,但是不论调用哪一个,我们猜测他肯定会有一个地方去初始化这个类,而在 Spring 当中一般就是自动装配类中初始化或者 Configuration 中初始化,而 ILoadBalancer 正是在 RibbonClientConfiguration 类中被加载的:

ZoneAwareLoadBalancer 负载均衡器

ZoneAwareLoadBalancer 的初始化会调用其父类 DynamicServerListLoadBalancer 进行初始化,然后会调用 restOfInit 方法进行所有服务的初始化。

如何获取所有服务

使用 Ribbon 后,我们通讯时并没有指定某一个 ip 和端口,而是通过服务名来调用服务,那么这个服务名就可能对应多个真正的服务,那么我们就必然需要先获取到所有服务的 ip 和端口等信息,然后才能进行负载均衡处理。

获取所有服务有两种方式:

  • 从配置文件获取
  • Eureka 注册中心获取(需要引入注册中心)。

初始化服务的方式是通过启动一个 Scheduled 定时任务来实现的,默认就是 30s 更新一次,其实在很多源码中都是通过这种方式来定时更新的,因为源码要考虑的使用的简单性所以不太可能引入一个第三方中间件来实现定时器。

具体的源码如下所示:enableAndInitLearnNewServersFeature() 方法启动的定时任务最终仍然你是调用 updateListOfServers() 方法来更新服务。

最终在获取到服务之后会调用父类 BaseLoadBalancer 中的将所有服务设置到 allServerList 集合中(BaseLoadBalancer 类中维护了一些负载均衡需要使用到的服务相关信息)。

如何判断服务是否可用

当我们获取到配置文件(或者 Eureka 注册中心)中的所有服务,那么这时候能直接执行负载均衡策略进行服务分发吗?显然是不能的,因为已经配置好的服务可能会宕机(下线),从而导致服务不可用,所以在 BaseLoadBalancer 中除了有一个 allServerList 集合来维护所有服务器,还有一个集合 upServerList 用来维护可用服务集合,那么如何判断一个服务是否可用呢?答案就是通过心跳检测来判断一个服务是否可用。

心跳检测 Task

在讲心跳检测之前,我们先看一下 BaseLoadBalancer 中的 setServersList 方法,有一段逻辑比较重要:

这段逻辑我们看到,默认情况下,如果 Ping 的策略是 DummyPing,那么默认 upServerList = allServerList,而实际上,假如我们没有进行进行特殊配置,其实默认的就是 DummyPing,这也是在 RibbonClientConfiguration 类中被加载的:

BaseLoadBalancer 初始化过程中,也会启动一个 Scheduled 定时任务去定时更新任务,最终和 forceQuickPing() 方法一样,调用一个默认策略来触发心跳检测,而默认策略就是 DummyPing,也就是默认所有服务都是可用的。

虽然默认不执行真正的心跳检测操作,但是 Netflix 中提供了 PingUrl 等其他策略,PingUrl 其实就是发起一个 http 请求,如果有响应就认为服务可用,没响应就认为服务不可用。

修改心跳检测策略可以通过如下配置切换(user-order-service 为客户端的服务名),既然是可配置的,那么也可以自己实现一个策略,只需要实现 IPing 接口即可。

user-order-service.ribbon.NFLoadBalancerPingClassName=com.netflix.loadbalancer.PingUrl

Ribbon 的负载均衡算法

当获取到可用服务之后,那么最后应该选择哪一个服务呢?这就需要使用到负载均衡策略,在 Ribbon 中,可以通过配置修改,也可以自定义负载均衡策略(实现 IRule 接口)。

  • RandomRule:随机算法
  • RoundRobinRule:轮询算法
  • ZoneAvoidanceRule:结合分区统计信息筛选出合适的分区(默认的负载均衡算法)
  • RetryRule:在 deadline 时间内,如果请求不成功,则重新发起请求知道找到一个可用的服务。
  • WeightedResponseTimeRule:根据服务器的响应时间计算权重值,服务器响应时间越长,这个服务器的权重就越小,会有定时任务对权重值进行更新。
  • AvailabilityFilteringRule:过滤掉短路(连续 3 次连接失败)的服务和高并发的服务。
  • BestAvailableRule:选择并发数最低的服务器

负载均衡算法可通过以下配置进行修改:

user-order-service.ribbon.NFLoadBalancerRuleClassName=Rule规则的类名

总结

本文主要讲述了微服务体系下的 Spring Cloud Netflix 套件中 Ribbon 的使用,并结合部分源码讲述了 Ribbon 的底层原理,重点讲述了 Ribbon 中是如何获取服务以及如何判定一个服务是否可用,最后也介绍了 Ribbon 中默认提供的 7 种负载均衡策略。

Spring Cloud之负载均衡组件Ribbon原理分析的更多相关文章

  1. 客户端负载均衡Ribbon之一:Spring Cloud Netflix负载均衡组件Ribbon介绍

    Netflix:['netfliːks] ribbon:英[ˈrɪbən]美[ˈrɪbən]n. 带; 绶带; (打印机的) 色带; 带状物;v. 把…撕成条带; 用缎带装饰; 形成带状;     L ...

  2. 【Spring Cloud】客户端负载均衡组件——Ribbon(三)

    一.负载均衡 负载均衡技术是提高系统可用性.缓解网络压力和处理能力扩容的重要手段之一. 负载均衡可以分为服务器负载均衡和客户端负载均衡,服务器负载均衡由服务器实现,客户端只需正常访问:客户端负载均衡技 ...

  3. spring cloud 客户端负载均衡 - Ribbon

    Spring Cloud Ribbon是一个基于HTTP和TCP的客户端负载均衡工具,基于Netflix Ribbon实现的,Ribbon不像注册中心.网关那样需要单独部署,它是作为一个工具直接集成到 ...

  4. spring boot分布式技术,spring cloud,负载均衡,配置管理器

    spring boot分布式的实现,使用spring cloud技术. 下边是我理解的spring cloud的核心技术: 1.配置服务器 2.注册发现服务器eureka(spring boot默认使 ...

  5. Spring Cloud(十四):Ribbon实现客户端负载均衡及其实现原理介绍

    年后到现在一直很忙,都没什么时间记录东西了,其实之前工作中积累了很多知识点,一直都堆在备忘录里,只是因为近几个月经历了一些事情,没有太多的经历来写了,但是一些重要的东西,我还是希望能坚持记录下来.正好 ...

  6. Spring Cloud官方文档中文版-客户端负载均衡:Ribbon

    官方文档地址为:http://cloud.spring.io/spring-cloud-static/Dalston.SR2/#_spring_cloud_netflix 文中例子我做了一些测试在:h ...

  7. spring cloud 系列第3篇 —— ribbon 客户端负载均衡 (F版本)

    源码仓库地址:https://github.com/heibaiying/spring-samples-for-all 一.ribbon 简介 ribbon是Netfix公司开源的负载均衡组件,采用服 ...

  8. SpringCloud系列之客户端负载均衡Netflix Ribbon

    1. 什么是负载均衡? 负载均衡是一种基础的网络服务,它的核心原理是按照指定的负载均衡算法,将请求分配到后端服务集群上,从而为系统提供并行处理和高可用的能力.提到负载均衡,你可能想到nginx.对于负 ...

  9. spring-cloud-feign负载均衡组件

    Feign简介: Feign是一个声明式的Web服务客户端,使用Feign可使得Web服务客户端的写入更加方便.它具有可插拔注释支持,包括Feign注解和JAX-RS注解.Feign还支持可插拔编码器 ...

随机推荐

  1. C++设计模式 - 状态模式(State)

    状态变化模式 在组件构建过程中,某些对象的状态经常面临变化,如何对这些变化进行有效的管理?同时又维持高层模块的稳定?"状态变化"模式为这一问题提供了一种解决方案. 典型模式 Sta ...

  2. python3鸡兔同笼问题

    # 假设兔子有x只 for x in range(1,31): y = 30 - x if 4*x + 2*y == 90: print('兔子有%d只,鸡有%d只'%(x, y))

  3. CentOS 7 源码安装 Zabbix 6.0

    Zabbix 主要有以下几个组件组成: Zabbix Server:Zabbix 服务端,是 Zabbix 的核心组件.它负责接收监控数据并触发告警,还负责将监控数据持久化到数据库中. Zabbix ...

  4. RDMA相关的技术网站

    https://www.cnblogs.com/vlhn/p/7909893.html https://www.cnblogs.com/vlhn/ 这个家伙的博客写的还不错,可以参考.

  5. 12.19 file 与io流

    12.19 file与io流 File 1. 新建文件或者文件夹 新建文件 File F = new File(pathname:"路径+文件名");Boolean flag = ...

  6. 『现学现忘』Docker基础 — 33、Docker数据卷容器的说明与共享数据原理

    目录 1.数据卷容器的说明 2.数据卷容器共享数据原理 3.总结 4.练习:MySQL实现数据共享 1.数据卷容器的说明 (1)什么是数据卷容器 一个容器中已经创建好的数据卷,其它容器通过这个容器实现 ...

  7. leedCode

    https://blog.csdn.net/code_yilia/category_9851007.html https://blog.csdn.net/qq_17550379/article/det ...

  8. idea中将已有项目转变为git项目,并提交到git服务器上

    idea中将已有项目转变为git项目,并提交到git服务器上 前言 有时候,本地有个项目不错,想要分享到github或者码云上,我们就会有这样的需求:项目在本地已经存在(可能不是git项目),又想提交 ...

  9. String是基本数据类型吗?

    基本数据类型包括byte.short.int.long.char.float.double和boolean.String不是基本类型.String是引用类型. 而且java.lang.String类是 ...

  10. 什么是CopyOnWriteArrayList,它与ArrayList有何不同?

    CopyOnWriteArrayList是ArrayList的一个线程安全的变体,其中所有可变操作(add.set等等)都是通过对底层数组进行一次新的复制来实现的.相比较于ArrayList它的写操作 ...