负载均衡这个东西,昂相信每个软件开发者应该都接触过。spring cloud ribbon是spring cloud 对Netflix Ribbon的封装,可以让我们轻松的把REST请求转换成客户端负载均衡的服务调用。spring cloud ribbon虽然只是一个工具类的框架,但她几乎存在于每一个基于spring cloud构建的微服务里。
要想通过spring cloud ribbon实现负载均衡,非常简单,只需要两步:
- 服务提供者启动多个实例注册到注册中心
- 服务消费者直接调用被@LoadBalanced注解修饰过的RestTemplate来实现服务的调用。
非常简单,这样就能将高可用的服务提供者配合负载均衡一起实现了。
下面进入主题,RestTemplate的分析
一.API提供
- GET请求
GET请求主要有两种方式:
1.1 getForEntity 返回的是ResponseEntity<T>。该对象是Spring对HTTP请求的封装,存储了HTTP的几个重要元素,如请求状态码的枚举HttpStatus,Http请求头HttpHeaders以及泛型的请求体对象。拥有3种重载方法,具体的就不展开了
1.2 getForObject 放回的是<T>泛型,可以理解为是对getForEntity的进一步封装。通过HttpMessageConverterExtractor对HTTP的请求响应体body进行对象转换,直接返回包装好的对象。 - POST请求
POST请求有三种方法进行调用:
2.1 postForEntity 返回的是ResponseEntity<T>,同getForEntity类似
2.2 postForObject 放回的是<T>泛型,同getForObject类似
3.3 postForLocation 返回的是URI,该方法实现了以POST请求提交资源,然后返回资源的URI,有3种重载方法 - PUT请求
即对put方法进行调用,3种重载方法 ,都没有返回值,都没有返回值
put(String url, @Nullable Object request, Object... uriVariables)
put(String url, @Nullable Object request, Map<String, ?> uriVariables)
put(URI url, @Nullable Object request) - DELETE请求
对delete方法进行调用,3种重载方法,都没有返回值
delete(String url, Object... uriVariables)
delete(String url, Map<String, ?> uriVariables)
delete(URI url)
------------------------------------------------------------------------------------------------------------------------------------------------------
二.源码简单分析
首先 查看@LoadBalanced的代码
/**
* Annotation to mark a RestTemplate bean to be configured to use a LoadBalancerClient
* @author Spencer Gibb
*/
@Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Qualifier
public @interface LoadBalanced {
}
从注释里可以看到,这个注解使用来给RestTemplate做标记,使LoadBalancerClient来配置它。
接下来,搜索LoadBalancerClient可以发现,是spring cloud定义的一个接口
public interface LoadBalancerClient {
ServiceInstance choose(String serviceId);
<T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException;
URI reconstructURI(ServiceInstance instance, URI original);
}
从spring cloud开放的接口可以看到客户端实现的负载均衡中要具备几种功能
- 根据传入的serviceId,从负载均衡器里获取一个对应服务的实例
- 使用serviceId挑选出来的服务实例执行请求
- 根据传入的实例对象构建出一个合适的host:port格式的url
接下来看看为客户端负载均衡器的自动化配置类LoadBalancerAutoConfiguration,首先,看下源码
/**
* Auto configuration for Ribbon (client side load balancing).
*
* @author Spencer Gibb
* @author Dave Syer
* @author Will Tran
* @author Gang Li
*/
@Configuration
@ConditionalOnClass(RestTemplate.class)
@ConditionalOnBean(LoadBalancerClient.class)
@EnableConfigurationProperties(LoadBalancerRetryProperties.class)
public class LoadBalancerAutoConfiguration {
@LoadBalanced
@Autowired(required = false)
private List<RestTemplate> restTemplates = 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);
}
}
});
}
@Autowired(required = false)
private List<LoadBalancerRequestTransformer> transformers = Collections.emptyList();
@Bean
@ConditionalOnMissingBean
public LoadBalancerRequestFactory loadBalancerRequestFactory(
LoadBalancerClient loadBalancerClient) {
return new LoadBalancerRequestFactory(loadBalancerClient, transformers);
}
@Configuration
@ConditionalOnMissingClass("org.springframework.retry.support.RetryTemplate")
static class LoadBalancerInterceptorConfig {
@Bean
public LoadBalancerInterceptor ribbonInterceptor(
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);
};
}
}
@Configuration
@ConditionalOnClass(RetryTemplate.class)
public static class RetryAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public LoadBalancedRetryFactory loadBalancedRetryFactory() {
return new LoadBalancedRetryFactory() {};
}
}
@Configuration
@ConditionalOnClass(RetryTemplate.class)
public static class RetryInterceptorAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public RetryLoadBalancerInterceptor ribbonInterceptor(
LoadBalancerClient loadBalancerClient, LoadBalancerRetryProperties properties,
LoadBalancerRequestFactory requestFactory,
LoadBalancedRetryFactory loadBalancedRetryFactory) {
return new RetryLoadBalancerInterceptor(loadBalancerClient, properties,
requestFactory, loadBalancedRetryFactory);
}
@Bean
@ConditionalOnMissingBean
public RestTemplateCustomizer restTemplateCustomizer(
final RetryLoadBalancerInterceptor loadBalancerInterceptor) {
return restTemplate -> {
List<ClientHttpRequestInterceptor> list = new ArrayList<>(
restTemplate.getInterceptors());
list.add(loadBalancerInterceptor);
restTemplate.setInterceptors(list);
};
}
}
}
从其类头部的注解可以知道,要实现负载均衡需要满足两个条件
- @ConditionalOnClass(RestTemplate.class):RestTemplate必须存在当前工程里
- @ConditionalOnBean(LoadBalancerClient.class):在当前工程中必须要存在LoadBalancerClient实现的bean
从其代码中可以看出,主要做了一下三件事:
- 创建了LoadBalancerInterceptor的bean,用于对客户端发起请求时进行拦截,以实现客户端负载均衡
- 创建了RestTemplateCustomizer的bean,给restTemplate添加LoadBalancerInterceptor
- 维护了一个被@LoadBalanced修饰的RestTemplate列表,并初始化。
接下来,看看LoadBalancerInterceptor时如何将一个普通的请求变成客户端负载均衡的,源码敬上:
/**
* @author Spencer Gibb
* @author Dave Syer
* @author Ryan Baxter
* @author William Tran
*/
public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor {
private LoadBalancerClient loadBalancer;
private LoadBalancerRequestFactory requestFactory;
public LoadBalancerInterceptor(LoadBalancerClient loadBalancer, LoadBalancerRequestFactory requestFactory) {
this.loadBalancer = loadBalancer;
this.requestFactory = requestFactory;
}
public LoadBalancerInterceptor(LoadBalancerClient loadBalancer) {
// for backwards compatibility
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();
Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri);
return this.loadBalancer.execute(serviceName, requestFactory.createRequest(request, body, execution));
}
}
LoadBalancerClient和LoadBalancerRequestFactory实在之前的自动化配置类里注入的,当一个被@LoadBalanced修饰的RestTemplate发起一个请求时,就会被LoadBalancerInterceptor的intercept方法拦截,我们可以看到,LoadBalancerInterceptor时把请求转到具体的LoadBalancerClient进行执行。看到这里,LoadBalancerClient还只是一个抽象的负载均衡接口,所以要去看其具体实现,通过IDE,可以很简单的在org.springframework.cloud.netflix.ribbon包下发现LoadBalancerClient的实现类RibbonLoadBalancerClient。
public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {
ILoadBalancer loadBalancer = getLoadBalancer(serviceId);
Server server = getServer(loadBalancer);
if (server == null) {
throw new IllegalStateException("No instances available for " + serviceId);
}
RibbonServer ribbonServer = new RibbonServer(serviceId, server, isSecure(server,
serviceId), serverIntrospector(serviceId).getMetadata(server));
return execute(serviceId, ribbonServer, request);
}
@Override
public <T> T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest<T> request) throws IOException {
Server server = null;
if(serviceInstance instanceof RibbonServer) {
server = ((RibbonServer)serviceInstance).getServer();
}
if (server == null) {
throw new IllegalStateException("No instances available for " + serviceId);
}
RibbonLoadBalancerContext context = this.clientFactory
.getLoadBalancerContext(serviceId);
RibbonStatsRecorder statsRecorder = new RibbonStatsRecorder(context, server);
try {
T returnVal = request.apply(serviceInstance);
statsRecorder.recordStats(returnVal);
return returnVal;
}
// catch IOException and rethrow so RestTemplate behaves correctly
catch (IOException ex) {
statsRecorder.recordStats(ex);
throw ex;
}
catch (Exception ex) {
statsRecorder.recordStats(ex);
ReflectionUtils.rethrowRuntimeException(ex);
}
return null;
}
可以看到,首先是获取ILoadBalancer,然后根据传入的serviceId去获取具体的服务实例
protected Server getServer(ILoadBalancer loadBalancer) {
if (loadBalancer == null) {
return null;
}
return loadBalancer.chooseServer("default");
}
通过getServer方法可以发现,并没有使用LoadBalancerClient接口中的choose方法,而是使用了ILoadBalancer的chooseServer方法。首先我们来看下ILoadBalancer的定义
public interface ILoadBalancer {
public void addServers(List<Server> newServers);
public Server chooseServer(Object key);
public void markServerDown(Server server);
@Deprecated
public List<Server> getServerList(boolean availableOnly);
public List<Server> getReachableServers();
public List<Server> getAllServers();
}
- 向负载均衡器里维护的服务实例列表增加实例
- 根据某种策略,从实例列表里挑选出一个具体的服务实例
- 通知和标识某个具体实例已经停止工作
- 已废弃
- 获取当前正常运行的所有实例
- 获取所有的已知实例,包括正常和已经停止的
通过IDE所展示的类关系,可以看到ILoadBalancer有很多实现,那么Ribbon默认采用的是哪个实现呢?通过Ribbon的配置类RibbonClientConfiguration可以看到,默认采用的是ZoneAwareLoadBalancer
@Bean
@ConditionalOnMissingBean
public ILoadBalancer ribbonLoadBalancer(IClientConfig config,
ServerList<Server> serverList, ServerListFilter<Server> serverListFilter,
IRule rule, IPing ping, ServerListUpdater serverListUpdater) {
if (this.propertiesFactory.isSet(ILoadBalancer.class, name)) {
return this.propertiesFactory.get(ILoadBalancer.class, config, name);
}
return new ZoneAwareLoadBalancer<>(config, rule, ping, serverList,
serverListFilter, serverListUpdater);
}
好了,下面再回到LoadBalancerClient的execute方法,再通过ZoneAwareLoadBalancer的chooseServer方法获取到对应的实例server后,包装成RibbonServer,然后使用该对象回调LoadBalancerInterceptor中LoadBalancerRequest的apply方法,而apply就会调用execution.execute,即InterceptingClientHttpRequest下的InterceptingRequestExecution的execute方法
@Override
public ClientHttpResponse execute(HttpRequest request, byte[] body) throws IOException {
if (this.iterator.hasNext()) {
ClientHttpRequestInterceptor nextInterceptor = this.iterator.next();
return nextInterceptor.intercept(request, body, this);
}
else {
HttpMethod method = request.getMethod();
Assert.state(method != null, "No standard HTTP method");
ClientHttpRequest delegate = requestFactory.createRequest(request.getURI(), method);
request.getHeaders().forEach((key, value) -> delegate.getHeaders().addAll(key, value));
if (body.length > 0) {
if (delegate instanceof StreamingHttpOutputMessage) {
StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) delegate;
streamingOutputMessage.setBody(outputStream -> StreamUtils.copy(body, outputStream));
}
else {
StreamUtils.copy(body, delegate.getBody());
}
}
return delegate.execute();
}
}
可以看到,再创建请求时,调用的是requestFactory.createRequest(request.getURI(), method);而这的request.getURI()方法会调用apply里的ServiceRequestrWrapper对象里从重写的getURI方法
@Override
public URI getURI() {
URI uri = this.loadBalancer.reconstructURI(
this.instance, getRequest().getURI());
return uri;
}
使用的是RibbonLoadBalancerClient中实现的reconstructURI方法
@Override
public URI reconstructURI(ServiceInstance instance, URI original) {
Assert.notNull(instance, "instance can not be null");
String serviceId = instance.getServiceId();
RibbonLoadBalancerContext context = this.clientFactory
.getLoadBalancerContext(serviceId);
URI uri;
Server server;
if (instance instanceof RibbonServer) {
RibbonServer ribbonServer = (RibbonServer) instance;
server = ribbonServer.getServer();
uri = updateToSecureConnectionIfNeeded(original, ribbonServer);
} else {
server = new Server(instance.getScheme(), instance.getHost(), instance.getPort());
IClientConfig clientConfig = clientFactory.getClientConfig(serviceId);
ServerIntrospector serverIntrospector = serverIntrospector(serviceId);
uri = updateToSecureConnectionIfNeeded(original, clientConfig,
serverIntrospector, server);
}
return context.reconstructURIWithServer(server, uri);
}
从这可以看到,通过ServiceInstance实例对象的saerviceId,从SPringClientFactory类的clientFactory对象中获取对应的serviceId的上下文RibbonLoadBalancerContext,然后,instance之前说过,是RibbonServer的实例,所以直接从RibbonServer里拿到server,再获取对应的uri,updateToSecureConnectionIfNeeded方法就是加上对应的协议,之后呢,就调用RibbonLoadBalancerContext的reconstructURIWithServer
public URI reconstructURIWithServer(Server server, URI original) {
String host = server.getHost();
int port = server.getPort();
String scheme = server.getScheme();
if (host.equals(original.getHost())
&& port == original.getPort()
&& scheme == original.getScheme()) {
return original;
}
if (scheme == null) {
scheme = original.getScheme();
}
if (scheme == null) {
scheme = deriveSchemeAndPortFromPartialUri(original).first();
}
try {
StringBuilder sb = new StringBuilder();
sb.append(scheme).append("://");
if (!Strings.isNullOrEmpty(original.getRawUserInfo())) {
sb.append(original.getRawUserInfo()).append("@");
}
sb.append(host);
if (port >= 0) {
sb.append(":").append(port);
}
sb.append(original.getRawPath());
if (!Strings.isNullOrEmpty(original.getRawQuery())) {
sb.append("?").append(original.getRawQuery());
}
if (!Strings.isNullOrEmpty(original.getRawFragment())) {
sb.append("#").append(original.getRawFragment());
}
URI newURI = new URI(sb.toString());
return newURI;
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
}
从实现中可以看到,它从server里拿出对应的主机名,端口,然后在进行拼接整合,形成最终要访问的具体地址。
看到这,已经大致的沥青了spring cloud ribbon中实现客户端负载均衡的基本东西,知道了是如何通过LoadBalancerInterceptor对RestTemplate的请求进行拦截,并利用Spring cloud的LoadBalancerClient将逻辑服务名转换为具体实例地址的过程。同时通过LoadBalancerClient的实现RibbonLoadBalancerClient,就可以知道再使用Ribbon实现的负载均衡时,实际使用的还是Ribbon中定义的ILoadBalancer接口的实现,且自动化配置类会使用ZoneAwareLoadBalancer作为客户端负载均衡的实现。