负载均衡
负载均衡是一种基础的网络服务,它的核心原理是按照指定的负载均衡算法,将请求分配到后端服务集群上,从而为系统提供并行处理和高可用的能力。提到负载均衡,你可能想到nginx。对于负载均衡,一般分为服务端负载均衡和客户端负载均衡。
- 服务端负载均衡:在消费者和服务提供方中间使用独立的代理方式进行负载,有硬件的负载均衡器,比如 F5,也有软件,比如 Nginx。
- 客户端负载均衡:所谓客户端负载均衡,就是客户端根据自己的请求情况做负载,本文介绍的Netflix Ribbon就是客户端负载均衡的组件。而Ribbon是一个客户端实现负载均衡的框架,主要用户客户端调用远端服务实现负载均衡。目前Ribbon使用的是spring-cloud-starter-netflix-ribbon这个依赖。
1.RestTemplate实现负载均衡
RestTemplate使用负载均衡,只需要给RestTemplate增加@LoadBalanced注解
@LoadBalanced
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
2.负载均衡策略配置
BestAvailableRule:在过滤掉故障服务后,它会基于过去30分钟的统计结果选取当前并发量最小的服务节点,也就是最“闲”的节点作为目标地址。如果统计结果尚未生成,则采用轮询的方式选定节点
ZoneAvoidanceRule: 包含了组合过滤条件,分别是Zone级别和可用性级别,Zone级别过滤为在Eureka注册中一个服务节点有Zone, Region和URL三个身份信息,其中Zone可以理解为机房大区(未指定则由Eureka给定默认值),而这里会对这个Zone的健康情况过滤其下面所有服务节点。可用性级别过滤和AvailabilityFilteringRule的验证非常像,会过滤掉当前并发量较大,或者处于熔断状态的服务节点
AvailabilityFilteringRule:这个规则底层依赖RandomRobinRule来选取节点,但并非来者不拒,必须要满足它的最低要求的节点才会被选中(节点处于非熔断状态和当前活跃请求数量不能超过阈值)。如果节点满足了要求,无论其响应时间或者当前并发量是什么,都会被选中
WeightedResponseTimeRule:这个Rule继承自RoundRibbonRule,他会根据服务节点的响应时间计算权重,响应时间越长权重就越低,响应越快则权重越高,权重的高低决定了机器被选中概率的高低。也就是说,响应时间越小的机器,被选中的概率越大
ResponseTimeWeightedRule:作用同 WeightedResponseTimeRule,ResponseTime-Weighted Rule 后来改名为 WeightedResponseTimeRule
RoundRobinRule:默认策略,轮询选择,轮询 index,选择 index 对应位置的 Server
RandomRule:随机选择一个 Server
RetryRule:对选定的负载均衡策略机上重试机制,也就是说当选定了某个策略进行请求负载时在一个配置时间段内若选择 Server 不成功,则一直尝试使用 subRule 的方式选择一个可用的 Server
2.1 配置负载均衡策略
方式1:代码方式
@Configuration
public class RibbonConfiguration {
@Bean
public IRule defaultLBStrategy() {
return new RandomRule();
}
方式2:配置文件方式
ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RoundRobinRule
2.2指定单个服务的负载均衡策略
方式1:代码方式
只需要在启动类或者配置类上增加注解
#这里的name指的是serviceId
@RibbonClient(name = "producer-one", configuration = com.netflix.loadbalancer.RoundRobinRule.class)
方式2:配置文件方式
producer-one.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RoundRobinRule
如果同时应用了以上两种方式去配置负载均衡,注解的优先级更高,因为配置文件的加载顺序在注解之前,后加载的配置会覆盖先前配置。
3.饥饿加载配置
Ribbon是在第一次方法调用的时候才去初始化LoadBalancer。这样看来,第一个方法请求不仅仅包含HTTP连接和方法的响应时间,还包括了LoadBalancer的创建耗时。假如你的方法本身就比较耗时的话,而且超时时间又设置的比较短,那么很大可能这第一次http调用就会失败。其实还有很多框架也实现了类似的懒加载功能,比如Hibernate的lazy-fetch,懒加载在大部分情况下可以节省系统资源开销,但某些情况下反而导致服务响应时间被延长。
#开启Ribbon的饥饿加载模式
ribbon.eager-load.enabled=true
#指定需要饥饿加载的服务名,若有多个则用逗号隔开
ribbon.eager-load.clients=producer-one
4.其他配置
4.1 脱离Eureka,单独使用ribbon
# 禁用 Eureka
ribbon.eureka.enabled=false
# 禁用 Eureka 后手动配置服务地址
producer-one.ribbon.listOfServers=localhost:50000,localhost:50001
禁用了 Eureka 之后,就不能使用服务名称去调用接口了,必须指定服务地址。
4.2 其他配置
#控制ribbon下面的配置是否生效,默认true
ribbon.http.client.enabled=false
# 请求连接的超时时间
ribbon.ConnectTimeout=2000
# 请求处理的超时时间
ribbon.ReadTimeout=2000
#不指定Ribbon默认使用轮询进行重试
ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RetryRule
# 在所有HTTP Method进行重试
ribbon.OkToRetryOnAllOperations=true
# 每台机器最大重试次数
ribbon.MaxAutoRetries=2
# 可以再重试几台机器
ribbon.MaxAutoRetriesNextServer=2
#也可以为每个Ribbon客户端设置不同的超时时间, 通过服务名称进行指定:
#producer-one.ribbon-config-demo.ribbon.ConnectTimeout=2000
#producer-one.ribbon-config-demo.ribbon.ReadTimeout=500
ConnectTimeout:创建会话的连接时间,注意,不是服务的响应时间,而是本机与服务建立会话的时间。
ReadTimeout:当连接建立好之后,如果对方服务没有在规定时间内返回,则直接进行重试。
最大超时时间计算公式:MAX(Response Time) = (ConnectTimeout + ReadTimeout) * (MaxAutoRetries + 1) * (MaxAutoRetriesNextServer + 1)
5@LoadBalanced 注解原理
Spring Cloud 给我们做了大量的底层工作,我们使用RestTemplate实现负载均衡只需要加一个@LoadBalanced 就可以了,主要的逻辑就是给 RestTemplate 增加拦截器,在请求之前对请求的地址进行替换,或者根据具体的负载策略选择服务地址,然后再去调用。(可以查看源码org.springframework.cloud.client.loadbalancer.LoadBalancerAutoConfiguration)。这里自定义一个@LoadBalanced来实现负责均衡。
1.新建一个拦截类MyLoadBalancerInterceptor
public class MyLoadBalancerInterceptor implements ClientHttpRequestInterceptor {
private LoadBalancerClient loadBalancer;
private LoadBalancerRequestFactory requestFactory;
public MyLoadBalancerInterceptor(LoadBalancerClient loadBalancer, LoadBalancerRequestFactory requestFactory) {
this.loadBalancer = loadBalancer;
this.requestFactory = requestFactory;
}
public MyLoadBalancerInterceptor(LoadBalancerClient loadBalancer) {
this(loadBalancer, new LoadBalancerRequestFactory(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();
System.out.println("进入自定义的请求拦截器中" + serviceName);
Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri);
return this.loadBalancer.execute(serviceName, requestFactory.createRequest(request, body, execution));
}
}
2.增加一个注解
@Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Qualifier
public @interface MyLoadBalanced {
}
3.增加一个自动配置类,将拦截器对象放入Spring中
@Configuration
public class MyLoadBalancerAutoConfiguration {
@MyLoadBalanced
@Autowired(required = false)
private List<RestTemplate> restTemplates = Collections.emptyList();
@Bean
public MyLoadBalancerInterceptor myLoadBalancerInterceptor(final LoadBalancerClient loadBalancer) {
return new MyLoadBalancerInterceptor(loadBalancer);
}
@Bean
public SmartInitializingSingleton myLoadBalancedRestTemplateInitializer(MyLoadBalancerInterceptor myLoadBalancerInterceptor) {
return new SmartInitializingSingleton() {
@Override
public void afterSingletonsInstantiated() {
for (RestTemplate restTemplateTemp : MyLoadBalancerAutoConfiguration.this.restTemplates) {
List<ClientHttpRequestInterceptor> list = new ArrayList<>(restTemplateTemp.getInterceptors());
list.add(myLoadBalancerInterceptor);
restTemplateTemp.setInterceptors(list);
}
}
};
}
}
4.只需要将原来的@LoadBalanced替换成@MyLoadBalanced就可以了