负载均衡介绍
负载均衡(Load Balance),其含义就是指将负载(工作任务)进行平衡、分摊到多个操作单元上进行运行,例如FTP服务器、Web服务器、企业核心应用服务器和其它主要任务服务器等,从而协同完成工作任务。
思考: 如果有多个provider实例,consumer应该如何调用呢?
目前主流的负载均衡方案分为以下两种:
- 服务端的负载均衡:集中式负载均衡,在消费者和服务提供方中间使用独立的代理方式进行负载,有基于硬件的(比如F5),也有基于软件的(比如Nginx、LVS)。
- 客户端的负载均衡:客户端根据自己的请求情况做负载均衡,Ribbon就属于客户端自己做负载均衡。
客户端的负载均衡
例如spring cloud中的ribbon,客户端会有一个服务器地址列表,在发送请求前通过负载均衡算法选择一个服务器,然后进行访问,这是客户端负载均衡;即在客户端就进行负载均衡算法分配。
服务端的负载均衡
例如Nginx,通过Nginx进行负载均衡,先发送请求,然后通过负载均衡算法,在多个服务器之间选择一个进行访问;即在服务器端再进行负载均衡算法分配。
常见负载均衡算法
- 随机:通过随机选择一个服务进行执行,一般这种方式使用较少;
- 轮询:负载均衡默认实现方式,请求来之后排队处理;
- 加权轮询:通过对服务器性能的分型,给高配置,低负载的服务器分配更高的权重,均衡各个服务器的压力;
- 地址hash:通过客户端请求的地址的hash值取模映射进行服务器调度,可尽量保证同一个客户的请求都到同一个服务器
- 最小连接数:即使请求均衡了,压力不一定会均衡,最小连接数法就是根据服务器的情况,比如请求积压数等参数,将请求分配到当前压力最小的服务器上。
Spring Cloud Alibaba整合Ribbon快速开始
Spring Cloud Ribbon是基于Netflix Ribbon实现的一套客户端的负载均衡工具,Ribbon客户端组件提供一系列的完善的配置,如超时,重试等。通过Load Balancer获取到服务提供的所有机器实例,Ribbon会自动基于某种规则(轮询,随机)去调用这些服务。Ribbon也可以实现我们自己的负载均衡算法。
由于spring-cloud-starter-alibaba-nacos-discovery
依赖了ribbon的依赖,所以我们不再需要单独引入ribbon的依赖了,如下面:
<!--添加ribbon的依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
注入RestTemplate时需要添加@LoadBalanced注解,让RestTemplate在请求时拥有客户端负载均衡的能力
package com.morris.user.config;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class RestConfig {
/**
* 默认的RestTemplate,不带负载均衡
* @return
*/
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
/**
* 有负责均衡能力的RestTemplate
* @return
*/
@Bean
@LoadBalanced
public RestTemplate restTemplate2() {
return new RestTemplate();
}
}
RestTemplate的使用:
package com.morris.user.controller;
import com.morris.user.entity.Order;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import javax.annotation.Resource;
import java.util.Arrays;
import java.util.List;
@RestController
@RequestMapping("user")
public class RestTemplateController {
@Resource
private RestTemplate restTemplate;
@Resource
private RestTemplate restTemplate2;
@GetMapping("findOrderByUserId")
public List<Order> findOrderByUserId(Long userId) {
Order[] orders = restTemplate.getForObject("http://127.0.0.1:8020/order/findOrderByUserId?userId=", Order[].class, userId);
return Arrays.asList(orders);
}
@GetMapping("findOrderByUserId2")
public List<Order> findOrderByUserId2(Long userId) {
Order[] orders = restTemplate2.getForObject("http://order-service/order/findOrderByUserId?userId=", Order[].class, userId);
return Arrays.asList(orders);
}
}
RestTemplate使用时,如果不带负载均衡时URL中的host直接填的是IP或者域名,使用负载均衡时host填的服务提供者在注册中心注册的微服务名。
模拟Ribbon实现负载均衡
为RestTemplate添加ClientHttpRequestInterceptor,拦截器负责从注册中心注册的微服务中轮询选取一个服务,改写URL中的服务名为具体的IP和端口。
package com.morris.user.config;
import lombok.SneakyThrows;
import org.checkerframework.checker.units.qual.A;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.client.support.HttpRequestWrapper;
import org.springframework.web.client.RestTemplate;
import javax.annotation.RegEx;
import javax.annotation.Resource;
import java.io.IOException;
import java.net.URI;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
@Configuration
public class MyRibbonConfig {
@Resource
private DiscoveryClient discoveryClient;
private final AtomicInteger counter = new AtomicInteger();
/**
* 默认的RestTemplate,不带负载均衡
* @return
*/
@Bean
public RestTemplate restTemplate3() {
RestTemplate restTemplate = new RestTemplate();
restTemplate.setInterceptors(Collections.singletonList((request, body, execution) -> {
String host = request.getURI().getHost();
List<ServiceInstance> instances = discoveryClient.getInstances(host);
int count = counter.incrementAndGet();
int index = count % instances.size();
ServiceInstance serviceInstance = instances.get(index);
RequestWrapper newRequest = new RequestWrapper(request, serviceInstance.getHost(), serviceInstance.getPort());
return execution.execute(newRequest, body);
}));
return restTemplate;
}
private static class RequestWrapper extends HttpRequestWrapper {
private final String host;
private final int port;
public RequestWrapper(HttpRequest request, String host, int port) {
super(request);
this.host = host;
this.port = port;
}
@SneakyThrows
@Override
public URI getURI() {
URI oldUri = super.getURI();
return new URI(oldUri.getScheme(), oldUri.getUserInfo(), host, port, oldUri.getPath(), oldUri.getQuery(), oldUri.getFragment());
}
}
}
Ribbon内核源码分析
@LoadBalanced注解原理
主要关注@LoadBalanced注解上有个@Qualifier注解,使用了Spring中限定符。
@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Qualifier
public @interface LoadBalanced {
}
LoadBalancerAutoConfiguration
LoadBalancerAutoConfiguration注入了2个重要的Bean:
- SmartInitializingSingleton
- LoadBalancerInterceptor
@LoadBalanced使用了@Qualifier,Spring中@Qualifier用于筛选限定注入Bean。@LoadBalanced利用@Qualifier作为restTemplates注入的筛选条件,筛选出具有负载均衡标识的RestTemplate。
被@LoadBalanced注解的restTemplate会被定制,添加LoadBalancerInterceptor拦截器。
SmartInitializingSingleton是在所有的bean都实例化完成之后才会调用的,所以在bean的实例化期间使用@LoadBalanced修饰的restTemplate是不具备负载均衡作用的,比如在项目的启动过程中要使用调用某个微服务,此时RestTemplate是不具备负载均衡作用的。
public class LoadBalancerAutoConfiguration {
@LoadBalanced
@Autowired(required = false)
private List<RestTemplate> restTemplates = Collections.emptyList();
@Autowired(required = false)
private List<LoadBalancerRequestTransformer> transformers = Collections.emptyList();
@Bean
public SmartInitializingSingleton loadBalancedRestTemplateInitializerDeprecated(
final ObjectProvider<List<RestTemplateCustomizer>> restTemplateCustomizers) {
return () -> restTemplateCustomizers.ifAvailable(customizers -> {
for (RestTemplate restTemplate : LoadBalancerAutoConfiguration.this.restTemplates) {
for (RestTemplateCustomizer customizer : customizers) {
customizer.customize(restTemplate);
}
}
});
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingClass("org.springframework.retry.support.RetryTemplate")
static class LoadBalancerInterceptorConfig {
@Bean
public LoadBalancerInterceptor loadBalancerInterceptor(
LoadBalancerClient loadBalancerClient,
LoadBalancerRequestFactory requestFactory) {
return new LoadBalancerInterceptor(loadBalancerClient, requestFactory);
}
@Bean
@ConditionalOnMissingBean
public RestTemplateCustomizer restTemplateCustomizer(
final LoadBalancerInterceptor loadBalancerInterceptor) {
return restTemplate -> {
List<ClientHttpRequestInterceptor> list = new ArrayList<>(
restTemplate.getInterceptors());
list.add(loadBalancerInterceptor);
restTemplate.setInterceptors(list);
};
}
}
}
SmartInitializingSingleton会获取容器中所有加了@LoadBalanced注解的RestTemplate,然后为这些RestTemplate逐个添加LoadBalancerInterceptor。
如果不使用@LoadBalanced注解,也可以通过添加LoadBalancerInterceptor拦截器让restTemplate起到负载均衡器的作用,这种方式就可以在启动的过程中使用RestTemplate的负载均衡功能。
@Bean
public RestTemplate restTemplate(LoadBalancerInterceptor loadBalancerInterceptor) {
RestTemplate restTemplate = new RestTemplate();
//注入loadBalancerInterceptor拦截器
restTemplate.setInterceptors(Arrays.asList(loadBalancerInterceptor));
return restTemplate;
}
LoadBalancerInterceptor拦截器
LoadBalancerInterceptor主要使用RibbonLoadBalancerClient来根据不同的负载均衡算法选择实例,然后使用LoadBalancerRequestFactory来对请求进行改写,与我们手写的Ribbon功能类似。
org.springframework.cloud.client.loadbalancer.LoadBalancerInterceptor#intercept
public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
final ClientHttpRequestExecution execution) throws IOException {
final URI originalUri = request.getURI();
String serviceName = originalUri.getHost();
Assert.state(serviceName != null,
"Request URI does not contain a valid hostname: " + originalUri);
return this.loadBalancer.execute(serviceName,
this.requestFactory.createRequest(request, body, execution));
}