一. 基础概述
自己做的记录,推荐看大神的Ribbon的负载均衡策略及原理
- 什么是负载均衡: 简单来说就是根据算法指定将用户的请求平摊分片到多个服务上,或打到指定的服务上,从而达到服务的高可用,负载均衡分为软负载nginx,lvs与应负载F5等
- 在前面 SpringCloud 中使用注册中心,使用 RestTemplate 根据服务名称在注册中心获取指定服务的调用地址,使用 @LoadBalanced 修饰 RestTemplate ,默认是轮询算法(G版本前)
- SpringCloud 中有好多框架默认整合引入了 Ribbon 依赖,例如spring-cloud-starter-zookeeper-discovery 或者spring-cloud-starter-netflix-eureka-client等,也可以单独引入
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
二. 通过 @LoadBalanced 注解了解 Ribbon 负载均衡
- 点开 @LoadBalanced 注解源码,会发现该注解没做其它附加操作,只是用来修饰 RestTemplate 用来标识一下,是借助 Spring 装载机制完成的负载设置
- 查看 spring.factories 文件,找到项目启动时默认自动装载的类 RibbonAutoConfiguration
- RibbonAutoConfiguration 使用 @AutoConfigureBefore({LoadBalancerAutoConfiguration.class, AsyncLoadBalancerAutoConfiguration.class}) 修饰该类,表示在创建该类注入以前首先会创建 LoadBalancerAutoConfiguration 注入到容器中,通过 LoadBalancerAutoConfiguration 向 RestTemplate 添加了一个请求拦截器 LoadBalancerInterceptor
@Bean
@ConditionalOnMissingBean
public RestTemplateCustomizer restTemplateCustomizer(final LoadBalancerInterceptor loadBalancerInterceptor) {
return (restTemplate) -> {
List<ClientHttpRequestInterceptor> list = new ArrayList(restTemplate.getInterceptors());
//添加一个 LoadBalancerInterceptor 请求拦截器
list.add(loadBalancerInterceptor);
restTemplate.setInterceptors(list);
};
}
- 当发起请求时首先会执行该 LoadBalancerInterceptor 请求拦截器中的 intercept() 方法,在该方法中调用了 LoadBalancerClient 的 executr() 方法
public ClientHttpResponse intercept(final HttpRequest request, final byte[] body, final ClientHttpRequestExecution execution) throws IOException {
URI originalUri = request.getURI();
String serviceName = originalUri.getHost();
Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri);
//调用 RibbonLoadBalancerClient 中的 execute() 方法
return (ClientHttpResponse)this.loadBalancer.execute(serviceName, this.requestFactory.createRequest(request, body, execution));
}
- LoadBalancerClient 是父接口当前默认调用的是 RibbonLoadBalancerClient 子类中的 execute() 方法,在该方法中首先换气一个 ILoadBalancer 负载均衡器,通过 ILoadBalancer 去获取 Server 示例
public <T> T execute(String serviceId, LoadBalancerRequest<T> request, Object hint) throws IOException {
//1.首先获取一个 ILoadBalancer
ILoadBalancer loadBalancer = this.getLoadBalancer(serviceId);
//2.通过获取到的 ILoadBalancer 获取 Server
Server server = this.getServer(loadBalancer, hint);
if (server == null) {
throw new IllegalStateException("No instances available for " + serviceId);
} else {
RibbonLoadBalancerClient.RibbonServer ribbonServer = new RibbonLoadBalancerClient.RibbonServer(serviceId, server, this.isSecure(server, serviceId), this.serverIntrospector(serviceId).getMetadata(server));
return this.execute(serviceId, (ServiceInstance)ribbonServer, (LoadBalancerRequest)request);
}
}
//获取 Server 方法
protected Server getServer(ILoadBalancer loadBalancer, Object hint) {
//通过 loadBalancer 调用 chooseServer()
return loadBalancer == null ? null : loadBalancer.chooseServer(hint != null ? hint : "default");
}
- 查看 ILoadBalancer 中的 chooseServer() 方法(ILoadBalancer 是一个父接口,当前默认调用 BaseLoadBalancer 子类中的),在 chooseServer() 方法中会发现通过 IRule 调用 .choose() 方法返回的实例,而这个 IRule 就是 Ribbon 负载算法的父接口,根据算法对该接口进行了具体实现,当前默认使用 RoundRobinRule 轮询算法子类,并且ILoadBalance(BaseLoadBalancer是实现类)向 Eureka 注册中心获取服务注册列表,每10s一次向EurekaClient发送“ping”,来判断服务的可用性,如果服务的可用性发生了改变或者服务数量和之前的不一致,则从注册中心更新或者重新拉取。LoadBalancerClient有了这些服务注册列表,然后通过IRule来进行负载均衡。
public Server chooseServer(Object key) {
if (this.counter == null) {
this.counter = this.createCounter();
}
this.counter.increment();
if (this.rule == null) {
return null;
} else {
try {
//通过
return this.rule.choose(key);
} catch (Exception var3) {
logger.warn("LoadBalancer [{}]: Error choosing server for key {}", new Object[]{this.name, key, var3});
return null;
}
}
}
二. Ribbon 核心组件 IRule 与负载算法
以负载调用指定服务为例,Ribbon实现了 IRule 接口,该接口中提供了根据 key,操作在注册中心获取到的服务数据的方法,对于这些方法,Ribbon 根据算法不同,提供了具体实现,支持 轮询(默认),随机,一致性哈希,哈希,加权
- RoundRobinRule 轮询(默认的)策略: 取模方式每次都取下一个服务器,假设有两台服务器,第一次1%2=1,选择下标为1的,第二次2%2=0选择下标为0的,第三次3%2=1选择下标为1的…
- RandomRule 随机策略: 使用 JDK 内部的 random 随机获取
- WeightedResponseTimeRule 继承了 RoundRobinRule 轮询+权重: 第一次没有权重列表,采用父类的轮询方式,有一个默认每30秒更新一次权重列表的定时任务,定时任务根据实例的响应时间来更新权重列表,用一个(0,1)的随机 double 值乘以最大的权重得到 randomWeight,遍历权重列表,获取第一个比randomWeight大的实例下标
- RetryRule 重试: 先按照RoundRobinRule 轮询获取,如果指定时间内获取失败进行重试,获取可用的服务
- BestAvailableRule: 先过滤掉由于多次访问故障处于断路器跳闸状态的服务,然后选择请求数量最少的服务,如果没选上,使用轮询去获取
- AvailabilityFiteringRule: 先过滤掉故障示例,在选择并发量最小的
- ZoneAvoidanceRule: 默认规则,复合判断server所在的区域的性能和server的可用性选择服务器
三. 项目中如何修改负载策略
注意点: 在修改负载策略时,需要创建配置类,配置类不可以放在 @ComponentScan 扫描自动注入的当前包或子包路径下(@SpringBootApplication注解底层也是使用的 @ComponentScan),否则所有的 Ribbon 都会使用当前配置的这个负载策略
假设服务消费方A,调用服务提供方"CLOUD-PAYMENT-SERVICE" (注册到注册中心的名字),将默认的轮询算法修改为随机算法
- 服务消费方A中创建配置类(注意不要放到@ComponentScan扫描的包或子包下),将指定的负载算法对象注入到容器中,此处指定随机
import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.RandomRule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MySelfRule {
// RandomRule 随机负载,是IRule接口的实现类
@Bean
public IRule myRule(){
return new RandomRule();
}
}
- 还是使用以前配置注入的 RestTemplate 对象,根据服务名称向注册中心获取服务调用列表
- 启动类添加 @RibbonClient 注解修饰,指定当前服务消费方调用哪个服务提供方时,使用的指定的负载策略
import myrule.MySelfRule;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.ribbon.RibbonClient;
@SpringBootApplication
@EnableEurekaClient
//@EnableDiscoveryClient
//指定服务提供方"CLOUD-PAYMENT-SERVICE",使用 MySelfRule 配置类中配置的负载策略
@RibbonClient(name="CLOUD-PAYMENT-SERVICE", configuration = MySelfRule.class)
public class OrderMain80 {
public static void main(String[] args) {
SpringApplication.run(OrderMain80.class,args);
}
}