• Feign源码解析6:如何集成discoveryClient获取服务列表


    背景#

    我们上一篇介绍了feign调用的整体流程,在@FeignClient没有写死url的情况下,就会生成一个支持客户端负载均衡的LoadBalancerClient。这个LoadBalancerClient可以根据服务名,去获取服务对应的实例列表,然后再用一些客户端负载均衡算法,从这堆实例列表中选择一个实例,再进行http调用即可。

    image-20240114113200793

    上图中,最核心的也就是2处。我们本次就从这里入手,去研究下,服务实例列表是如何获取到的,以及如何配置静态的服务实例地址。

    服务实例列表相关bean初始化#

    在上图的2处开始执行前,有这么一行:

    image-20240120154109681

    这里就会去查找bean,类型是LoadBalancerLifecycle.class。去哪里查找呢,spring容器,但是是各个loadbalancer自己的spring容器。

    image-20240120154327878

    刚开始嘛,容器还没有,此时就会触发spring容器的创建和初始化。这个容器里有哪些bean呢?

    主要的bean来源于LoadBalancerClientConfiguration这个配置类。里面包含了两个重要的bean,一个是loadbalancer,支持随机获取某个实例,但这个bean,可以从下面的代码看到,它的第一个构造参数,是去获取一个ServiceInstanceListSupplier类型的bean的provider,要靠这个provider提供服务实例列表。

    所以,这个bean其实是依赖于ServiceInstanceListSupplier这种bean的。

    image-20240120154830749

    下面这个则是ServiceInstanceListSupplier类型,也就是实例列表提供者。

    image-20240120154912336

    ServiceInstanceListSupplierBuilder#

    Copy
    ServiceInstanceListSupplier.builder(): static ServiceInstanceListSupplierBuilder builder() { return new ServiceInstanceListSupplierBuilder(); }

    这个就是普通的建造者,没有什么特别。接下来,则是给builder设置DiscoveryClient,这个就是服务发现相关的client,比如eureka、nacos这些的客户端:

    Copy
    .withBlockingDiscoveryClient()

    image-20240120155817648

    这里我们发现一个一个箭头函数,这个箭头函数有一个入参,名字是context,然后return了一个DiscoveryClientServiceInstanceListSupplier类型的对象。

    函数最终赋值给了:

    Copy
    private Creator baseCreator;

    它的类型:

    Copy
    Allows creating a {@link ServiceInstanceListSupplier} instance based on provided {@link ConfigurableApplicationContext}. public interface Creator extends Function<ConfigurableApplicationContext, ServiceInstanceListSupplier> { } @FunctionalInterface public interface Function<T, R> { R apply(T t); }

    这个把参数带入,就是:

    Copy
    ServiceInstanceListSupplier apply(ConfigurableApplicationContext t);

    也就是接受一个spring上下文参数,返回一个ServiceInstanceListSupplier类型的对象。

    所以再看下图,也就是从spring中获取DiscoveryClient类型的bean,然后new一个DiscoveryClientServiceInstanceListSupplier类型的对象返回。

    image-20240120160309894

    接下来,builder又设置了缓存:

    Copy
    ServiceInstanceListSupplier.builder().withBlockingDiscoveryClient().withCaching()

    image-20240120160558992

    Copy
    private DelegateCreator cachingCreator; public interface DelegateCreator extends BiFunction<ConfigurableApplicationContext, ServiceInstanceListSupplier, ServiceInstanceListSupplier> { } @FunctionalInterface public interface BiFunction<T, U, R> { R apply(T t, U u); } 翻译后就是: ServiceInstanceListSupplier apply(<ConfigurableApplicationContext t, ServiceInstanceListSupplier u);

    这里其实不用说太细,无非是装饰器模式,又套了一层缓存。

    接下来,进入最终的build环节:

    image-20240120162700048

    可以看到,首先是执行了baseCreator,传入了spring上下文,此时就会触发之前看到的:

    image-20240120162805147

    CompositeDiscoveryClient#

    image-20240120162920373

    我们上图中,获取到的DiscoveryClient类型的bean为CompositeDiscoveryClient。它就像它的名字一样,里面聚合了多个DiscoveryClient。

    image-20240120163223173

    这个bean的定义在哪里呢?这是靠自动装配引入的:

    image-20240120163446228

    image-20240120163401160

    这个聚合类依赖的discoveryClient哪里来的呢?

    首先是nacosDiscoveryClient:

    image-20240120163733879

    image-20240120163747810

    再一个是SimpleDiscoveryClient类型:

    image-20240120163911180

    image-20240120163934666

    多个DiscoveryClient的顺序#

    在CompositeDiscoveryClient中,是用list维护各个DiscoveryClient。

    Copy
    private final List<DiscoveryClient> discoveryClients;

    谁先谁后,重要吗?看看下面的方法,是用来获取服务实例的:

    image-20240120164341770

    这里是先获取到则直接返回,说明还是很重要的。

    各个DiscoveryClient的order值怎么获取呢?

    Copy
    public interface DiscoveryClient extends Ordered { /** * Default order of the discovery client. */ int DEFAULT_ORDER = 0; ... default int getOrder() { return DEFAULT_ORDER; } }
    Copy
    nacos中,实现了这个类,但是没有覆写getOrder,所以对于NacosDiscoveryClient,值就是0. public class NacosDiscoveryClient implements DiscoveryClient

    对于SimpleDiscoveryClient来说,我们先不管它是啥,我们看其类定义:

    其支持从配置文件中获取order:

    Copy
    @Override public int getOrder() { return this.simpleDiscoveryProperties.getOrder(); }

    image-20240120165034092

    你没有显示设置这个order属性的话,默认也是0.

    所以,不显式设置SimpleDiscoveryProperties的order的话,SimpleDiscoveryClient和NacosDiscoveryClient的order值相同,那谁先谁后就难讲了,这块待细挖才知道。

    SimpleDiscoveryClient#

    这个discoveryClient是干嘛的呢,没啥存在感?

    其实它是用来从配置文件中获取服务实例的。

    Copy
    A DiscoveryClient that will use the properties file as a source of service instances.

    它依赖的配置类如下:

    Copy
    public class SimpleDiscoveryClient implements DiscoveryClient { private SimpleDiscoveryProperties simpleDiscoveryProperties; public SimpleDiscoveryClient(SimpleDiscoveryProperties simpleDiscoveryProperties) { this.simpleDiscoveryProperties = simpleDiscoveryProperties; }

    image-20240120165617210

    它可以配置各个Feign服务的服务实例,以及我们前面提到的order(通过把这里的order改小,可以排到nacosDiscoveryClient前面,达成屏蔽nacos中的服务实例的效果)

    我们可以像下面这样来配置:

    Copy
    spring: application: discovery: client: simple: instances: echo-service-provider: - uri: http://1.1.1.1:8082 metadata: my: instance1 - uri: http://2.2.2.2:8082 metadata: my: instance2

    正常像上面这样就可以了,但是,nacos会排在它前面,导致无法生效:

    image-20240120171004638

    所以,还得配上order:

    Copy
    spring: application: discovery: client: simple: order: -1 instances: echo-service-provider: - uri: http://1.1.1.1:8082 metadata: my: instance1 - uri: http://2.2.2.2:8082 metadata: my: instance2

    image-20240120171446534

    DiscoveryClientServiceInstanceListSupplier#

    构造好了前面的CompositeDiscoveryClient,我们就会开始创建服务实例supplier。

    image-20240120171806395

    上图可以看到,这里有delegate.getInstances(serviceId),但后面又进行了封装,最终的类型是:

    Copy
    private final Flux<List<ServiceInstance>> serviceInstances;

    这个Flux是反应式编程相关的api,不是很懂,但内部主要就是封装了一个数据源,等到需要获取服务实例的时候,就会真正调用到:

    Copy
    delegate.getInstances(serviceId)

    届时,就会调用到:

    image-20240120172923861

    缓存包装#

    这个DiscoveryClientServiceInstanceListSupplier,后续又经过cache相关包装,最终的类型是:

    Copy
    CachingServiceInstanceListSupplier

    image-20240120174647535

    这个bean咱们就讲到这里。

    reactorServiceInstanceLoadBalancer#

    接下来,开始看:

    image-20240120173048151

    第一个参数:

    Copy
    loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class) public <T> ObjectProvider<T> getLazyProvider(String name, Class<T> type) { return new ClientFactoryObjectProvider<>(this, name, type); } ClientFactoryObjectProvider(NamedContextFactory<?> clientFactory, String name, Class<T> type) { this.clientFactory = clientFactory; this.name = name; this.type = type; }

    接下来构造这个随机的loadbalancer:

    Copy
    public RoundRobinLoadBalancer(ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider, String serviceId, int seedPosition) { this.serviceId = serviceId; this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider; this.position = new AtomicInteger(seedPosition); }

    到此,就构造完成了。

    此时,我们也基本完成了loadbalancer对应的整个spring容器的初始化。

    loadBalancerClient.choose#

    完成了spring容器初始化后,接下来开始真正执行下图2处:

    image-20240114113200793

    首先就是获取loadbalancer,就是从容器内获取ReactorServiceInstanceLoadBalancer类型的bean:

    Copy
    @Override public ReactiveLoadBalancer<ServiceInstance> getInstance(String serviceId) { return getInstance(serviceId, ReactorServiceInstanceLoadBalancer.class); } public <T> T getInstance(String name, Class<T> type) { AnnotationConfigApplicationContext context = getContext(name); try { return context.getBean(type); } catch (NoSuchBeanDefinitionException e) { // ignore } return null; }

    容器中,这种类型的bean,就只有前面讲的RoundRobinLoadBalancer.

    image-20240120183510301

    然后调用loadBalancer.choose(request):image-20240120183623851

    Copy
    org.springframework.cloud.loadbalancer.core.RoundRobinLoadBalancer#choose public Mono<Response<ServiceInstance>> choose(Request request) { // 1 ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider .getIfAvailable(NoopServiceInstanceListSupplier::new); // 2 return supplier.get(request).next() .map(serviceInstances -> processInstanceResponse(supplier, serviceInstances)); }

    1处就从容器中获取到前面提到的CachingServiceInstanceListSupplier。

    2处的supplier.get(request):

    image-20240120175141133

    image-20240120175343055

    然后对这个Flux>类型的对象,执行next,把当前对象变成了MonoNext类型的对象,MonoNext的注释是:Emits a single item at most from the source.

    image-20240120175522455

    接下来是map操作,转成了一个MonoMap类型的对象:

    image-20240120180301113

    这里还不会实际触发上面的客户端负载均衡逻辑,此时只是封装成了MonoMap:

    image-20240120180206884

    把MonoMap丢给了如下的from函数,里面把MonoMap强转为了Mono类型:

    image-20240120180407958

    image-20240120180522105

    接下来执行block操作,转为同步阻塞:

    Copy
    Response<ServiceInstance> loadBalancerResponse = Mono.from(loadBalancer.choose(request)).block();

    image-20240120180641337

    这里我感觉就是,创建了一个实际的订阅者,且这个订阅者订阅了当前这个MonoMap,所以这个MonoMap就得真正开始干活了(之前只是把一堆操作给封装进去了,但没有实际做)。

    此时,也会真正触发如下地方:

    image-20240120181034847

    这里完成后呢,就真正拿到了服务实例列表,此时,就会触发之前那个map函数:

    image-20240120181221534

    根据当前loadbalancer的算法(随机算法),进行多个服务实例中选一个的操作:

    image-20240120181247633

    image-20240120181343593

    接着,我们终于拿到了一个实例了,可以进行后续调用了:

    image-20240120181647071

    总结#

    反应式编程,这个真是太难看懂了,实在是劝退。

    今天是大寒,马上要更冷了,不过再坚持一阵,就能春暖花开了,兄弟们

    参考#

    https://docs.spring.io/spring-cloud-commons/docs/3.1.8/reference/html/#zone-based-load-balancing

    image-20240120182508336

    https://mp.weixin.qq.com/s/aRpwCtgENCwubMF3idQQzQ




  • 相关阅读:
    基于SSM的生活缴费系统的设计与实现
    难以置信!四面斩获字节offer,全靠这份“算法最优解”宝典
    uniapp uni-combox 数据源使用对象,选择后获取对应项的ID,可指定自定义的balbel,value
    Python学习笔记()——文件操作
    qmake eval(string) 函数
    Linux下路由表的转发流程
    ConfigMap概述
    前端常见vue面试题合集
    Chrome开发者模式去除时间轴
    C专家编程 第10章 再论指针 10.7 使用指针创建和使用动态数组
  • 原文地址:https://www.cnblogs.com/grey-wolf/p/17976978