负载均衡调用——Ribbon
一、简介
1.开源代码库
https://github.com/Netflix/ribbon
2、Ribbon是什么
Ribbon是Netflix发布的开源项目,主要功能是在客户端的软件提供负载均衡和服务调用算法。Ribbon客户端组件提供一系列配置项如连接超时、重试等。简单的说,就是在配置文件中列出Load Balancer(负载均衡简称LB)后面所有的及其,Ribbon会自动的帮助你基于某种规则(如简单轮询,随机连接等)去连接这些机器。也可以使用Ribbon实现自定义的负载均衡算法。
3、Ribbon能干什么
Ribbon是部署在客户端的IPC库,提供以下功能:
- 客户端负载均衡
- 容错处理
- 支持多协议的异步通信。支持HTTP、TCP、UDP协议。
- 支持缓存和批量处理
Ribbon模块介绍:
Ribbon由多个组件组成,其中一些组件在内部用于生产,随着时间的推移,其中的一些组件已被非OSS解决方案所取代。这是因为Netflix开始着手于针对单一职责模块的RPC更加组件化的体系结构。因此,此时每个Ribbon功能区组件都会受到不同程度的关注。
- ribbon: Ribbon功能应用入口。使用Ribbon的功能可以通过初始化应用入口,调用接口实现。该模块依赖其他模版实现所需功能,比如容错处理ribbon依赖Histrix。
- ribbon-loadbalancer:负载均衡功能入口。如果仅需要负载均衡功能,可以使用单独使用该模块。
- ribbon-eureka:基于Eureka客户端实现可用服务列表管理
- ribbon-transport: 具备客户端负载均衡能力的,基于RxNetty框架能够支持HTTP、TCP、UDP协议的通信客户端。
- ribbon-httpclient: 具备客户端负载均衡能力的,基于Apache HttpClient,支持REST风格的HTTP请求客户端。
- ribbon-core: 客户端配置APIs和其他共享APIs。
- ribbon-example:使用例子。
从官网源码仓库得知,当前项目已进入维护状态,但有些功能还是在生产中大规模部署的,在将来会被 Spring Cloud Loadbalancer替代(参考:https://spring.io/blog/2018/12/12/spring-cloud-greenwich-rc1-available-now)
3、架构说明
Ribbon其实就是一个软负载均衡的客户端组件,他可以和其他所需请求的客户端结合使用,和eureka结合只是其中的一个实例。Ribbon在工作的时候分两步:
- 先选择EurekaServer,优选选择同一个区域内负载较少的Server;
- 根据用户指定的策略,从Server取到的服务注册列表中选择一个地址。
其中Ribbon提供了多种的负载均衡策略,如轮询、随机和根据响应时间加强等。
4、ribbon与nginx对比
Ribbon客户端负载均衡和Nginx服务端负载均衡的区别:
- Nginx是服务器负载均衡,客户端所有请求都会交给Nginx,然后由Nginx实现转发请求,即负载均衡是由服务端实现的。
- Ribbon是本地负载均衡,在调用微服务接口时候,会在注册中心上获取注册信息服务列表之后缓存到JVM本地,从而在本地实现RPC远程服务调用技术。
负载均衡又分为两类,分别可以对应于Nginx和Ribbon:
- 集中式LB:即在服务的消费方和提供方之间使用独立的LB设施(可以是硬件,如F5,也可以是软件,如Nginx),由该设施负责把访问请求通过某种策略转发至服务的提供方。
- 进程内LB:将LB逻辑集成到消费方,消费方从服务注册中心获知有哪些地址可用,然后自己再从这些地址中选择出一个合适的服务器,Ribbon就属于进程内LB,它只是一个类库,集成于消费方进程,消费方通过它来获取到服务提供方的地址。
二、springboot中使用ribbon
1、pom依赖
在服务消费端使用ribbon
ribbon的依赖为:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
<version>2.2.1.RELEASE</version>
<scope>compile</scope>
</dependency>
而eureka的依赖为:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
通过分析我们可以发现后者包含前者,所以通常引入后者即可
2、ribbon的负载均衡算法
- com.netflix.loadbalancer.RoundRobinRule:轮询,为默认的负载均衡算法。
- com.netflix.loadbalancer.RandomRule:随机。
- com.netflix.loadbalancer.RetryRule:先按照RoundRobinRule(轮询)的策略获取服务,如果获取服务失败则在指定时间内进行重试,获取可用的服务。
- com.netflix.loadbalancer.BestAvailableRule:先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,然后选择一个并发量最小的服务。
- com.netflix.loadbalancer.AvailabilityFilteringRule:先过滤掉故障实例,再选择并发较小的实例。
- com.netflix.loadbalancer.WeightedResponseTimeRule:对RoundRobinRule的扩展,响应速度越快的实例选择权重越大,越容易被选择。
- com.netflix.loadbalancer.ZoneAvoidanceRule:复合判断Server所在区域的性能和Server的可用性选择服务器。
这些负载均衡算法均是抽象类com.netflix.loadbalancer.AbstractLoadBalancerRule的实现,而抽象类实现了IRule接口。
3、ribbon算法的实现
官方文档明确给出了警告:自己自定义的轮询算法配置类不能放在@ComponentScan
注解所扫描的当前包下以及子包下,否则自定义的这个配置类就会被所有Ribbon客户端所共享,就打不到特殊化定制的目的了,换句话说,如果这个配置类我们能够被@ComponentScan
注解扫描到,那么访问所有的微服务提供方的具体实例时,我们都会采取配置类中的算法,如果要特殊化定制——即指定访问某些微服务提供方时采用配置的轮询算法,那么我们就应该使这个配置类让@ComponentScan
注解扫描不到,我们知道,在主启动类的@SpringBootApplication
注解中,其实这个注解包含了@SpringBootConfiguration
、@EnableAutoConfiguration
、@ComponentScan
这三个注解,所以我们写的轮询算法配置类不能和主启动类在同一个包下,所以我们需要建新的包,实现定制轮询算法的配置类
- 项目结构
- 主启动类
package org.jun;
import org.Rule.myRule;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.ribbon.RibbonClient;
/**
* @author junfeng.lin
* @date 2021/2/10 11:45
*/
@SpringBootApplication
@EnableEurekaClient
//访问的微服务名称,采用配置文件中的算法
@RibbonClient(name = "eureka-order" ,configuration = myRule.class)
public class RibbonClient80 {
public static void main(String[] args) {
SpringApplication.run(RibbonClient.class ,args);
}
}
4、负载均衡算法原理(轮询算法)
-
轮询负载均衡算法原理
实际调用的服务实例下标 = Rest接口第几次请求数 % 服务器集群总数量
//获取服务列表的信息(所有服务名) List<String> services = discoveryClient.getServices(); //根据微服务名称获取具体服务实例 List<ServiceInstance> instances = discoveryClient.getInstances("service-nae");
-
轮询负载均衡算法源码分析——choose()方法获取要选择的服务
private AtomicInteger nextServerCyclicCounter;
public RoundRobinRule() {
//初始化下标为0,是原子类
this.nextServerCyclicCounter = new AtomicInteger(0);
}
public Server choose(ILoadBalancer lb, Object key) {
//如果没有开启负载均衡则返回空
if (lb == null) {
log.warn("no load balancer");
return null;
} else {
Server server = null;
int count = 0;
while(true) {
if (server == null && count++ < 10) {
//获取所有存活的服务
List<Server> reachableServers = lb.getReachableServers();
//获取所有服务
List<Server> allServers = lb.getAllServers();
int upCount = reachableServers.size();
int serverCount = allServers.size();
if (upCount != 0 && serverCount != 0) {
//获取选中的服务列表的下标
int nextServerIndex = this.incrementAndGetModulo(serverCount);
//根据下标获取对应的服务
server = (Server)allServers.get(nextServerIndex);
if (server == null) {
Thread.yield();
} else {
if (server.isAlive() && server.isReadyToServe()) {
return server;
}
server = null;
}
continue;
}
log.warn("No up servers available from load balancer: " + lb);
return null;
}
//如果超过10次获取不到服务则返回空
if (count >= 10) {
log.warn("No available alive servers after 10 tries from load balancer: " + lb);
}
return server;
}
}
}
private int incrementAndGetModulo(int modulo) {
int current;
int next;
do {
current = this.nextServerCyclicCounter.get();
next = (current + 1) % modulo;
} while(!this.nextServerCyclicCounter.compareAndSet(current, next)); //通过自旋锁获取下标值
return next;
}
5、自定义负载均衡算法
- 由于使用了自定义的负载均衡算法,则RestTemplcate的负载均衡注解可以去掉
@Configuration
public class ApplicationContextConfig {
@Bean
// @LoadBalanced//使用该注解赋予RestTemplate负载均衡的能力
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
}
- 自定义负载均衡算法接口与实现类
import org.springframework.cloud.client.ServiceInstance;
import java.util.List;
/**
* @author junfeng.lin
* @date 2021/2/10 11:52
*/
public interface LoadBalancer
{
ServiceInstance instances(List<ServiceInstance> serviceInstances);
}
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @author junfeng.lin
* @date 2021/2/10 11:52
*/
@Component
public class MyLoadBalancer implements LoadBalancer
{
private AtomicInteger atomicInteger = new AtomicInteger(0);
public final int getAndIncrement()
{
int current;
int next;
do {
current = this.atomicInteger.get();
next = current >= 2147483647 ? 0 : current + 1;
}while(!this.atomicInteger.compareAndSet(current,next));
System.out.println("服务列表下标为: "+next);
return current;
}
//负载均衡算法:rest接口第几次请求数 % 服务器集群总数量 = 实际调用服务器位置下标 ,每次服务重启动后rest接口计数从0开始。
@Override
public ServiceInstance instances(List<ServiceInstance> serviceInstances)
{
int index = getAndIncrement() % serviceInstances.size();
return serviceInstances.get(index);
}
}
- 在Controller中使用
@Autowired
private RestTemplate restTemplate;
@Autowired
private DiscoveryClient discoveryClient;
@Autowired
private MyLoadBalancer myLoadBalancer;
@GetMapping("/myLoadBalancerGetPort")
public String myLoadBalancerGetPort() {
//根据微服务名称获取具体服务实例
List<ServiceInstance> instances = discoveryClient.getInstances("eureka-order");
if (instances == null || instances.size() <= 0) {
return null;
}
//调用自定义负载均衡类获取服务对象
ServiceInstance instance = myLoadBalancer.instances(instances);
//获取服务对象的统一资源标志符
URI uri = instance.getUri();
String port = restTemplate.getForObject(uri + "/getPort", String.class);
return port;
}
- 效果:通过debug模式查看,每次都是轮询的方式选择服务列表的服务