基于 spring-cloud-starter-netflix-ribbon-2.2.5.RELEASE 。
1. 测试用例
@Bean
@LoadBalanced
RestTemplate restTemplate() {
return new RestTemplate();
}
@Autowired
RestTemplate restTemplate;
public String ribbonTest() {
return restTemplate.getForEntity("http://micro-provider-srv/hi?id=" + UUID.randomUUID().toString(), String.class).getBody();
}
2. 源码解读
2.1 AutoConfig之 LoadBalancerAutoConfiguration
本配置类的主要功能:
- 从容器中收集注解了
@LoadBalanced 的RestTemplate 实例。 - 借助
RestTemplateCustomizer 扩展将 LoadBalancerInterceptor 实例应用到上一步收集到的每个RestTemplate 实例中。
@Configuration(proxyBeanMethods = false)
@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);
}
}
});
}
@Configuration(proxyBeanMethods = false)
@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);
};
}
}
2.2 AutoConfig之 RibbonAutoConfiguration
本配置类的主要功能:
- 收集向容器中注册的
RibbonClientSpecification 实例(NamedContextFactory.Specification 实现类)。作为讲其作为参数向容器中注册为SpringClientFactory 实例。注:这一点和 FeignAutoConfiguration 中的操作相同,区别只在于后者收集的配置Bean是FeignClientSpecification ,注入到容器中的FeignContext 实例。 - 向容器中注入其它必要的Ribbon辅助实例。
@Configuration
@Conditional(RibbonAutoConfiguration.RibbonClassesConditions.class)
@RibbonClients
@AutoConfigureAfter(
name = "org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration")
@AutoConfigureBefore({ LoadBalancerAutoConfiguration.class,
AsyncLoadBalancerAutoConfiguration.class })
@EnableConfigurationProperties({ RibbonEagerLoadProperties.class,
ServerIntrospectorProperties.class })
public class RibbonAutoConfiguration {
@Autowired(required = false)
private List<RibbonClientSpecification> configurations = new ArrayList<>();
@Autowired
private RibbonEagerLoadProperties ribbonEagerLoadProperties;
@Bean
@ConditionalOnMissingBean
public SpringClientFactory springClientFactory() {
SpringClientFactory factory = new SpringClientFactory();
factory.setConfigurations(this.configurations);
return factory;
}
@Bean
@ConditionalOnMissingBean(LoadBalancerClient.class)
public LoadBalancerClient loadBalancerClient() {
return new RibbonLoadBalancerClient(springClientFactory());
}
@Bean
@ConditionalOnMissingBean
public PropertiesFactory propertiesFactory() {
return new PropertiesFactory();
}
@Bean
@ConditionalOnProperty("ribbon.eager-load.enabled")
public RibbonApplicationContextInitializer ribbonApplicationContextInitializer() {
return new RibbonApplicationContextInitializer(springClientFactory(),
ribbonEagerLoadProperties.getClients());
}
......
}
关于注解@RibbonClients :
- 正如上面讲解过的,除非特殊配置需要,否则该注解无需在项目中进行显式声明。
- 该注解会导致
RibbonClientConfigurationRegistrar 的启用。 - 而
RibbonClientConfigurationRegistrar 参与Spring生命周期,根据@RibbonClients 配置项向容器中注入 RibbonClientSpecification (继承自 NamedContextFactory.Specification ) 并最终被收集到 SpringClientFactory 中(注意看SpringClientFactory 的基类)。 RibbonClientConfiguration 。 被 SpringClientFactory 直接在构造函数中写死方式引入- 而对于
SpringClientFactory ,其会在 OkHttpRibbonCommandFactory ,HttpClientRibbonCommandFactory 等中使用,目的是获取各个client对应的自定义配置项。
2.3 执行阶段 - LoadBalancerInterceptor (实现负载均衡的入口)
LoadBalancerInterceptor 的内部实现其实很简单:拦截每个请求,将控制权转移给LoadBalancerClient 实现者(本例中是RibbonLoadBalancerClient ),进而实现客户端的负载均衡。
public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor {
private LoadBalancerClient 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,
this.requestFactory.createRequest(request, body, execution));
}
}
2.4 执行阶段 - RibbonLoadBalancerClient (负载均衡的实际实现)
作为LoadBalancerClient 实现类,我们重点关注其实现的choose , execute ,reconstructURI 方法。
1. 实现choose(String serviceId) 方法。
- 该方法实际是RibbonLoadBalancerClient对于其所继承接口
ServiceInstanceChooser 的实现。 - 本方法负责实现客户端的负载均衡,由传入的条件,按照预先制定规则选举出对应服务下的某个节点。
public ServiceInstance choose(String serviceId, Object hint) {
Server server = getServer(getLoadBalancer(serviceId), hint);
if (server == null) {
return null;
}
return new RibbonServer(serviceId, server, isSecure(server, serviceId),
serverIntrospector(serviceId).getMetadata(server));
}
2. 实现reconstructURI(ServiceInstance instance, URI original) 方法。
- 本方法是由
RibbonLoadBalancerClient所继承的直接接口 LoadBalancerClient`定义的。 - 该方法负责以选举出的服务节点信息为基础,拼接出发起最终访问请求时的http地址。
@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);
}
3. 实现execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest<T> request) 方法。
- 负责发起http请求调用,获取响应。
- 注意其中使用
RibbonStatsRecorder 来做调用情况统计。外界可以通过向容器中注入扩展的RibbonLoadBalancerContext 实现类来参与这部分统计过程,加入自定义逻辑。
3. Feign集成
-
在feign实例构建阶段,决定是否启用 loadbalancer是在 FeignClientFactoryBean.getTarget() 中。(feign源码解析 - 初始化)
<T> T getTarget() {
FeignContext context = applicationContext.getBean(FeignContext.class);
Feign.Builder builder = feign(context);
if (!StringUtils.hasText(url)) {
if (!name.startsWith("http")) {
url = "http://" + name;
}
else {
url = name;
}
url += cleanPath();
return (T) loadBalance(builder, context,
new HardCodedTarget<>(type, name, url));
}
}
-
通过实现feign.Client 接口(典型如LoadBalancerFeignClient ),保持外界的Feign调用习惯。同时在LoadBalancerFeignClient 实现中将控制权调度给 ribbon中的 com.netflix.client.IClient 接口实现类(典型如FeignLoadBalancer )。所以在 LoadBalancerFeignClient 类中你同时可以看到 feign.Client 和 com.netflix.client.IClient 实现类(前者代表:LoadBalancerFeignClient ,后者代表:FeignLoadBalancer )。
@Override
public Response execute(Request request, Request.Options options) throws IOException {
try {
URI asUri = URI.create(request.url());
String clientName = asUri.getHost();
URI uriWithoutHost = cleanUrl(request.url(), clientName);
FeignLoadBalancer.RibbonRequest ribbonRequest = new FeignLoadBalancer.RibbonRequest(
this.delegate, request, uriWithoutHost);
IClientConfig requestConfig = getClientConfig(options, clientName);
return lbClient(clientName)
.executeWithLoadBalancer(ribbonRequest, requestConfig).toResponse();
}
catch (ClientException e) {
IOException io = findIOException(e);
if (io != null) {
throw io;
}
throw new RuntimeException(e);
}
}
@Override
public RibbonResponse execute(RibbonRequest request, IClientConfig configOverride)
throws IOException {
Request.Options options;
if (configOverride != null) {
RibbonProperties override = RibbonProperties.from(configOverride);
options = new Request.Options(override.connectTimeout(this.connectTimeout),
override.readTimeout(this.readTimeout));
}
else {
options = new Request.Options(this.connectTimeout, this.readTimeout);
}
Response response = request.client().execute(request.toRequest(), options);
return new RibbonResponse(request.getUri(), response);
}
-
最终整体逻辑调用链条如下: HystrixInvocationHandler.this.dispatch.get(method).invoke(args)
SynchronousMethodHandler.invoke(Object[] argv)
SynchronousMethodHandler.executeAndDecode(RequestTemplate template, Options options)
LoadBalancerFeignClient.execute(Request request, Request.Options options)
FeignLoadBalancer.executeWithLoadBalancer(final S request, final IClientConfig requestConfig)
FeignLoadBalancer.execute(RibbonRequest request, IClientConfig configOverride)
feign.okhttp.OkHttpClient.execute(RibbonRequest request, IClientConfig configOverride)
-
对于LoadBalancerFeignClient 实例,SpringCloud是借助 spring-cloud-openfeign-core-2.2.5.RELEASE.jar中定义的 FeignRibbonClientAutoConfiguration 来实现的。 4.1 该类上采用 @Import方式引入 HttpClientFeignLoadBalancedConfiguration (feign.httpclient.enabled=true,默认就是它)和 OkHttpFeignLoadBalancedConfiguration (feign.okhttp.enabled=true) 和 DefaultFeignLoadBalancedConfiguration ,最终三选一。 4.2 以上@Import方式引入的feign.Client,统一都是 LoadBalancerFeignClient 。 其内部的 delegate 字段则是各自的 OkHttpClient / ApacheHttpClient 等,来完成实际的请求调用。
4. Zuul集成
Zuul与Ribbon的集成,入口是在类型 RibbonRoutingFilter 中。
protected ClientHttpResponse forward(RibbonCommandContext context) throws Exception {
Map<String, Object> info = this.helper.debug(context.getMethod(),
context.getUri(), context.getHeaders(), context.getParams(),
context.getRequestEntity());
RibbonCommand command = this.ribbonCommandFactory.create(context);
try {
ClientHttpResponse response = command.execute();
this.helper.appendDebug(info, response.getRawStatusCode(),
response.getHeaders());
return response;
}
catch (HystrixRuntimeException ex) {
return handleException(info, ex);
}
}
protected ClientHttpResponse run() throws Exception {
final RequestContext context = RequestContext.getCurrentContext();
RQ request = createRequest();
RS response;
boolean retryableClient = this.client instanceof AbstractLoadBalancingClient
&& ((AbstractLoadBalancingClient) this.client)
.isClientRetryable((ContextAwareRequest) request);
if (retryableClient) {
response = this.client.execute(request, config);
}
else {
response = this.client.executeWithLoadBalancer(request, config);
}
context.set("ribbonResponse", response);
if (this.isResponseTimedOut()) {
if (response != null) {
response.close();
}
}
return new RibbonHttpResponse(response);
}
- 最终整体逻辑调用链条如下:
RibbonRoutingFilter.run()
RibbonCommand.execute()
AbstractLoadBalancerAwareClient.executeWithLoadBalancer()
4. 补充
4.1 RibbonLoadBalancerClient VS RibbonLoadBalancingHttpClient
其实仔细观察这两个的区别还是挺明显的,但笔者在阅读源码期间碰壁好几次,因此这里特意总结下。
-
前者是基于 spring-cloud-commons-2.2.5.RELEASE.jar 定义的 LoadBalancerClient 接口 -
后者基于 ribbon-core-2.3.0.jar中定义的 com.netflix.client.IClient 接口。 spring-cloud-netflix-ribbon-2.2.5.RELEASE.jar -
双方都有实现接口 ServiceInstanceChooser (spring-cloud-commons-2.2.5.RELEASE.jar 定义)。 3.1 AbstractLoadBalancingClient 对其实现是基于ribbon筛选出ribbon对象Server,然后再做一次封装为RibbonLoadBalancerClient.RibbonServer 这一ServiceInstance 实现类。(实现了从ribbon到spring-cloud-commons-2.2.5.RELEASE.jar 的适配。) -
最后直观感受下层级关系:
5. 未完待续
综上,其实只介绍了Ribbon中的执行主线逻辑,在实现负载均衡中的诸多考量与对应解决方案,本文不做更多的涉猎,感兴趣的读者可以自行查找资料,或者看看底部参考链接。
6. 参考
- 深入理解Ribbon原理
- SpringBoot 之 脱离Eureka使用Ribbon
- feign源码解析 - 初始化
- feign源码解析 - 运行时
|