OpenFeign学习(十):Spring Cloud OpenFeign 集成 Ribbon,Eureka 实现请求负载均衡流程解析

    科技2022-08-25  106

    说明

    通过之前的博文,我简单介绍了 Spring Cloud OpenFeign 的使用方式及加载配置原理,以及在脱离 Eureka 的情况下使用 Ribbon 以及 Spring Cloud Ribbon 的加载配置原理。通过这些内容我们简单了解到了 OpenFeign 和 Ribbon 的使用方式以及 Spring Cloud 是如何对这些组件进行集成加载配置的。

    本篇博文,我将继续通过源码来简单介绍 Spring Cloud OpenFeign 在集成 Eureka,Ribbon 后如何实现自动请求负载。

    正文

    NamedContextFactory

    先回忆下Spring Cloud OpenFeign 相关内容,通过 @FeignClient 和 @EnableFeignClients 注解配置 FeignClient,再通过 FeignContext 获取对应 clientName 的应用上下文。详看《OpenFeign学习(七):Spring Cloud OpenFeign的使用》和《OpenFeign学习(九):Spring Cloud OpenFeign的加载配置原理 II》。

    在使用 Ribbon时,通过 @RabbionClient 注解配置 RibbonClient,再通过 SpringClientFactory 获取对应 clientName 的应用上下文。详看《Ribbon 学习(一):脱离 Eureka 使用 Ribbon》和《Ribbon 学习(二):Spring Cloud Ribbon 加载配置原理》。

    通过源码我们可以发现,SpringClientFactory 和 FeignContext 都继承自 NamedContextFactory 抽象类。

    FeignRibbonClientAutoConfiguration

    针对该类,我在之前的博文《OpenFeign学习(九):Spring Cloud OpenFeign的加载配置原理 II》中,提到该类是 RibbonClient 的支持类,通过引入不同的配置类,根据配置创建不同的 feignClient。默认创建的为 LoadBalancerFeignClient 类型。在之前我们忽略了其作为配置类,在其内部定义的 Bean。

    @Bean @Primary @ConditionalOnMissingBean @ConditionalOnMissingClass({"org.springframework.retry.support.RetryTemplate"}) public CachingSpringLoadBalancerFactory cachingLBClientFactory(SpringClientFactory factory) { return new CachingSpringLoadBalancerFactory(factory); }

    可以看到,在内部定义配置了 CachingSpringLoadBalancerFactory ,同时在创建生成时注入了 Ribbon 的 SpringClientFactory。从名称可以看出,该类用来创建缓存 feignClient 对应的负载均衡器。

    对于 SpringClientFactory 类,我在《Ribbon 学习(二):Spring Cloud Ribbon 加载配置原理》一文中提到,该类是创建 Ribbon 客户端,负载均衡器,客户端配置实例的工厂,将 RibbonClientConfiguration 类作为所有 RibbonClient 的默认配置类,并且将所有 @RibbonClient 定义生成的 RibbonClientSpecification 集合作为参数传递到 NamedContextFactory,作为创建 Ribbon Client 上下文的依据。

    RibbonEurekaAutoConfiguration

    @Configuration @EnableConfigurationProperties @ConditionalOnRibbonAndEurekaEnabled @AutoConfigureAfter({RibbonAutoConfiguration.class}) @RibbonClients( defaultConfiguration = {EurekaRibbonClientConfiguration.class} ) public class RibbonEurekaAutoConfiguration { public RibbonEurekaAutoConfiguration() { } }

    该类是 Ribbon 对 Eureka 的自动配置类,通过源码可以看到,该类在 RibbonAutoConfiguration 之后配置,并且该类使用了 @RibbonClients 注解设置了 RibbonClient 的默认配置类 EurekaRibbonClientConfiguration。

    EurekaRibbonClientConfiguration

    @Configuration public class EurekaRibbonClientConfiguration { ... @Bean @ConditionalOnMissingBean public IPing ribbonPing(IClientConfig config) { if (this.propertiesFactory.isSet(IPing.class, this.serviceId)) { return (IPing)this.propertiesFactory.get(IPing.class, config, this.serviceId); } else { NIWSDiscoveryPing ping = new NIWSDiscoveryPing(); ping.initWithNiwsConfig(config); return ping; } } @Bean @ConditionalOnMissingBean public ServerList<?> ribbonServerList(IClientConfig config, Provider<EurekaClient> eurekaClientProvider) { if (this.propertiesFactory.isSet(ServerList.class, this.serviceId)) { return (ServerList)this.propertiesFactory.get(ServerList.class, config, this.serviceId); } else { DiscoveryEnabledNIWSServerList discoveryServerList = new DiscoveryEnabledNIWSServerList(config, eurekaClientProvider); DomainExtractingServerList serverList = new DomainExtractingServerList(discoveryServerList, config, this.approximateZoneFromHostname); return serverList; } } ... }

    在该类中配置了Ribbon Client 的 IPing 和 ServerList,在之前博文中我介绍了 Spring 为 Ribbon Client 提供了默认配置类,IPing 对应为 DummyPing,ServerList 对应为 ConfigurationBasedServerList。在这里可以看到,通过 PropertiesFactory 判断是否为该 Client 配置 IPing,若没有则生成 NIWSDiscoveryPing 作为默认配置。ServerList 同理,使用 DiscoveryEnabledNIWSServerList 作为默认配置。

    LoadBalancerFeignClient.execute

    在配置完成后,进入 Feign 的请求流程,通过 SynchronousMethodHandler 代理到 LoadBalancerFeignClient 的 execute 方法。

    public Response execute(Request request, Options options) throws IOException { try { URI asUri = URI.create(request.url()); String clientName = asUri.getHost(); URI uriWithoutHost = cleanUrl(request.url(), clientName); RibbonRequest ribbonRequest = new RibbonRequest(this.delegate, request, uriWithoutHost); IClientConfig requestConfig = this.getClientConfig(options, clientName); return ((RibbonResponse)this.lbClient(clientName).executeWithLoadBalancer(ribbonRequest, requestConfig)).toResponse(); } catch (ClientException var8) { IOException io = this.findIOException(var8); if (io != null) { throw io; } else { throw new RuntimeException(var8); } } }

    可以看到在该方法中,通过请求 url 获取对应的服务名,构建 RibbonRequest,IClientConfig 对象,最后通过 lbClient 方法创建对应 FeignLoadBalancer。

    private FeignLoadBalancer lbClient(String clientName) { return this.lbClientFactory.create(clientName); }

    通过 CachingSpringLoadBalancerFactory 的 create 方法创建 FeignLoadBalancer。

    public FeignLoadBalancer create(String clientName) { FeignLoadBalancer client = (FeignLoadBalancer)this.cache.get(clientName); if (client != null) { return client; } else { IClientConfig config = this.factory.getClientConfig(clientName); ILoadBalancer lb = this.factory.getLoadBalancer(clientName); ServerIntrospector serverIntrospector = (ServerIntrospector)this.factory.getInstance(clientName, ServerIntrospector.class); FeignLoadBalancer client = this.loadBalancedRetryFactory != null ? new RetryableFeignLoadBalancer(lb, config, serverIntrospector, this.loadBalancedRetryFactory) : new FeignLoadBalancer(lb, config, serverIntrospector); this.cache.put(clientName, client); return (FeignLoadBalancer)client; } }

    在初次请求时会创建 FeignLoadBalancer 实例,第一次创建时通过 SpringClientFactory 来创建对应 的 AnnotationConfigApplicationContext,在这里会初始化之前通过 EurekaRibbonClientConfiguration 和 RibbonClientSpecification 配置类定义的 bean。创建初始化 IClientConfig, IPing,ServerList,ILoadBalancer 等对象。

    在初始化 ZoneAwareLoadBalancer 对象时,会通过 DomainExtractingServerList 的 getUpdatedListOfServers 方法获取服务地址列表,DomainExtractingServerList 由 EurekaRibbonClientConfiguration 定义配置,代理了 DiscoveryEnabledNIWSServerList 类,最终调用 DiscoveryEnabledNIWSServerList 的 obtainServersViaDiscovery 方法从 Eureka Server 获取服务地址。

    FeignLoadBalancer

    在创建完成 FeignLoadBalancer 对象后,调用其父类 AbstractLoadBalancerAwareClient 的 executeWithLoadBalancer 方法。在该方法中调用 FeignLoadBalancer 的 execute 方法执行请求。

    executeWithLoadBalancer

    public T executeWithLoadBalancer(final S request, final IClientConfig requestConfig) throws ClientException { // 创建 Ribbon 的 Command 命令用来控制请求的执行流程 LoadBalancerCommand<T> command = buildLoadBalancerCommand(request, requestConfig); try { // 用 lambda 表达式声明实现 ServerOperation 接口 return command.submit( new ServerOperation<T>() { @Override public Observable<T> call(Server server) { // 获取到 server 后重构 URI URI finalUri = reconstructURIWithServer(server, request.getUri()); S requestForServer = (S) request.replaceUri(finalUri); try { // 调用 FeignLoadBalancer 的 execute 方法执行请求 return Observable.just(AbstractLoadBalancerAwareClient.this.execute(requestForServer, requestConfig)); } catch (Exception e) { return Observable.error(e); } } }) .toBlocking() .single(); } catch (Exception e) { Throwable t = e.getCause(); if (t instanceof ClientException) { throw (ClientException) t; } else { throw new ClientException(e); } } }

    通过源码可以看到,Ribbon 创建了 LoadBalancerCommand 对象,采用与 Hystrix 类似的命令模式来控制请求流程。 在 submit 方法中用 lambda 表达式声明了在获取到 server 后的处理流程,在该方法中调用了 FeignLoadBalancer 的 execute 方法。

    selectServer

    在 LoadBalancerCommand 中通过 selectServer 方法获取服务地址,本质是通过 ZoneAwareLoadBalancer 的 chooseServer 方法获取,根据 IRule 进行选择。

    private Observable<Server> selectServer() { return Observable.create(new OnSubscribe<Server>() { @Override public void call(Subscriber<? super Server> next) { try { Server server = loadBalancerContext.getServerFromLoadBalancer(loadBalancerURI, loadBalancerKey); next.onNext(server); next.onCompleted(); } catch (Exception e) { next.onError(e); } } }); }

    execute

    public FeignLoadBalancer.RibbonResponse execute(FeignLoadBalancer.RibbonRequest request, IClientConfig configOverride) throws IOException { Options options; if (configOverride != null) { RibbonProperties override = RibbonProperties.from(configOverride); options = new Options(override.connectTimeout(this.connectTimeout), override.readTimeout(this.readTimeout)); } else { options = new Options(this.connectTimeout, this.readTimeout); } Response response = request.client().execute(request.toRequest(), options); return new FeignLoadBalancer.RibbonResponse(request.getUri(), response); }

    最后,通过 RibbonRequest 中封装的 client 进行请求。在这里使用的是 Client 的默认实现类 Default,底层使用的是 HttpURLConnection。

    总结

    在 Spring Cloud OpenFeign 集成 Ribbon,Eureka 后,会自动进行请求负载。在加载配置时,FeignRibbonClientAutoConfiguration 配置类定义配置了 feign.Client 接口的实现。默认为 LoadBalancerFeignClient。

    在 RibbonAutoConfiguration 加载后,RibbonEurekaAutoConfiguration 配置类被加载,该类使用了 @RibbonClients 注解,并设置了默认配置类 EurekaRibbonClientConfiguration,定义配置了 IPing,ServerList 等 Bean。

    配置完成后,通过 FeignClient 进行服务器请求,代理到 LoadBalancerFeignClient 进行负载,这里会创建 RibbonRequest 对象,并生成实际的负载客户端 FeignLoadBalancer。

    通过 Ribbon 的 SpringClientFactory 来创建对应 clientName 的应用上下文 AnnotationConfigApplicationContext,由于会先加载通过 @RibbonClient 或 @RibbonClients 设置的配置类,所以 EurekaRibbonClientConfiguration 配置的 IPing,ServerList 会优先被加载配置,Ribbon 对应 Eureka 加载类分别为 NIWSDiscoveryPing 和 DiscoveryEnabledNIWSServerList。

    之后调用 FeignLoadBalancer 的父类 AbstractLoadBalancerAwareClient 的 executeWithLoadBalancer 方法继续执行请求。在该方法中创建 LoadBalancerCommand 对象进行请求流程控制,在方法执行过程中,通过 LoadBalancerCommand 的 selectServer 方法使用 Ribbon 的 ZoneAwareLoadBalancer 进行服务实例的选择。

    最后,获取到服务实例后,重新构建 URI,再通过 LoadBalancerFeignClient 的代理类 Default 进行请求,实际是通过 HttpURLConnection 进行请求。

    Processed: 0.009, SQL: 9