Eureka-服务注册与发现组件 Eureka 客户端实现原理解析

原文地址 mp.weixin.qq.com

前面的文章介绍了,如何使用服务注册发现组件:Eureka,并给出使用示例。本文在此基础上,将会讲解 Eureka 客户端实现的内幕,结合源码深入实现的细节,知其所以然。客户端需要重点关注以下几点:

  • 从 Eureka Server 中拉取注册表信息

  • 全量拉取注册表信息

  • 增量式拉取注册表信息

  • 注册表缓存刷新定时器与续租 (心跳) 定时器

  • 服务注册与服务按需注册

  • 服务实例的下线

本文摘录于笔者出版的书籍 《Spring Cloud 微服务架构进阶》

Eureka Client 结构

在 Finchley 版本的 SpringCloud 中,不需要添加任何的额外的注解就可以登记为 Eureka Client,只需要在 pom 文件中添加 spring-cloud-starter-netflix-eureka-client的依赖。

为了跟踪 Eureka 的运行机制,读者可以打开 SpringBoot 的 Debug 模式来查看更多的输出日志:

logging:
  level:
    org.springframework: DEBUG

查看 spring-cloud-netflix-eureka-clientsrc/main/resource.META-INF/spring.factories,查看 Eureka Client 有哪些自动配置类:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.netflix.eureka.config.EurekaClientConfigServerAutoConfiguration,\
org.springframework.cloud.netflix.eureka.config.EurekaDiscoveryClientConfigServiceAutoConfiguration,\
org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration,\
org.springframework.cloud.netflix.ribbon.eureka.RibbonEurekaAutoConfiguration,\
org.springframework.cloud.netflix.eureka.EurekaDiscoveryClientConfiguration
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
org.springframework.cloud.netflix.eureka.config.EurekaDiscoveryClientConfigServiceBootstrapConfiguration

排除掉与配置中心相关的自动配置类,从中可以找到三个与 Eureka Client 密切相关的自动配置类:

  • EurekaClientAutoConfiguration

  • RibbonEurekaAutoConfiguration

  • EurekaDiscoveryClientConfiguration

下面将对这些类进行分析,看看一个正常的 Eureka Client 需要做哪一些初始化配置。

EurekaClientAutoConfiguration

Eureke Client 的自动配置类,负责了 Eureka Client 中关键的 bean 的配置和初始化,以下是其内比较重要的 bean 的介绍与作用。

EurekaClientConfig

提供了 Eureka Client 注册到 Eureka Server 所需要的配置信息,SpringCloud 为其提供了一个默认配置类的 EurekaClientConfigBean,可以在配置文件中通过前缀 eureka.client+ 属性名进行覆盖。

ApplicationInfoManager

该类管理了服务实例的信息类 InstanceInfo,其内包括 Eureka Server 上的注册表所需要的信息,代表了每个 Eureka Client 提交到注册中心的数据,用以供服务发现。同时管理了实例的配置信息 EurekaInstanceConfig,SpringCloud 提供了一个 EurekaInstanceConfigBean的配置类进行默认配置,也可以在配置文件 application.yml中通过 eureka.instance+ 属性名进行自定义配置。

EurekaInstanceConfigBean

继承了 EurekaInstanceConfig接口,是 Eureka Client 注册到服务器上需要提交的关于服务实例自身的相关信息,主要用于服务发现:

通常这些信息在配置文件中的 eureka.instance前缀下进行设置,SpringCloud 通过 EurekaInstanceConfigBean配置类提供了相关的默认配置。以下是一些比较关键的属性,这些信息都将注册到注册中心上。

public class EurekaInstanceConfigBean implements CloudEurekaInstanceConfig, EnvironmentAware {
// 服务实例的应用名
private String appname; 
// 服务实例的Id,通常和appname共同唯一标记一个服务实例
private String instanceId; 
// 自定义添加的元数据,由用户使用以适配扩展业务需求
private Map<String, String> metadataMap;
// 如果服务实例部署在AWS上,该类将持有服务实例部署所在的数据中心的准确信息
private DataCenterInfo dataCenterInfo;
// 服务实例的Ip地址
private String ipAddress;
// 服务实例主页地址
private String homePageUrl;
// 服务实例健康检查地址
private String healthCheckUrlPath;
// 服务实例的状态地址
private String statusPageUrlPath
.....
}

DiscoveryClient

这是 SpringCloud 定义的用来服务发现的顶级接口,在 Netflix Eureka 或者 consul 都有相应的具体实现类,提供的方法如下:

public interface DiscoveryClient {
   // 获取实现类的描述
    String description();
    // 通过服务Id获取服务实例的信息
    List<ServiceInstance> getInstances(String serviceId);
    // 获取所有的服务实例的Id
    List<String> getServices();
}

其在 Eureka 方面的实现的相关的类结构图:

EurekaDiscoveryClient继承了 DiscoveryClient,但是通过查看 EurekaDiscoveryClient中的代码,会发现它是通过组合类 EurekaClient实现接口的功能,如下的 getInstance接口:

@Override
public List<ServiceInstance> getInstances(String serviceId) {
    List<InstanceInfo> infos = this.eurekaClient.getInstancesByVipAddress(serviceId,false);
    List<ServiceInstance> instances = new ArrayList<>();
    for (InstanceInfo info : infos) {
        instances.add(new EurekaServiceInstance(info));
    }
    return instances;
}

EurekaClient来自于 com.netflix.discovery包中,其默认实现为 com.netflix.discovery.DiscoveryClient,这属于 eureka-client 的源代码,它提供了 Eureka Client 注册到 Server 上、续租,下线以及获取 Server 中注册表信息等诸多关键功能。SpringCloud 通过组合方式调用了 Eureka 中的的服务发现方法,关于 EurekaClient的详细代码分析将放在客户端核心代码中介绍。为了适配 spring-cloud,spring 提供了一个 CloudEurekaClient继承了 com.netflix.discovery.DiscoveryClient,同时覆盖了 onCacheRefreshed防止在 spring-boot还没初始化时调用该接口出现 NullPointException

上述的几个配置类之间的关系非常紧密,数据之间存在一定的耦合,所以下面介绍一下它们之间的关系

首先是 EurekaInstanceConfig,代码位于 EurekaClientAutoConfiguration

@Bean
@ConditionalOnMissingBean(value = EurekaInstanceConfig.class, search = SearchStrategy.CURRENT)
public EurekaInstanceConfigBean eurekaInstanceConfigBean(InetUtils inetUtils, ManagementMetadataProvider managementMetadataProvider) {
    // 从配置文件中读取属性
    String hostname = getProperty("eureka.instance.hostname");
    boolean preferIpAddress = Boolean.parseBoolean(getProperty("eureka.instance.prefer-ip-address"));
    String ipAddress = getProperty("eureka.instance.ipAddress");
    boolean isSecurePortEnabled = Boolean.parseBoolean(getProperty("eureka.instance.secure-port-enabled"));
    String serverContextPath = env.getProperty("server.context-path", "/");
    int serverPort = Integer.valueOf(env.getProperty("server.port", env.getProperty("port", "8080")));
    Integer managementPort = env.getProperty("management.server.port", Integer.class);// nullable. should be wrapped into optional
    String managementContextPath = env.getProperty("management.server.context-path");// nullable. should be wrapped into optional
    Integer jmxPort = env.getProperty("com.sun.management.jmxremote.port", Integer.class);//nullable
    EurekaInstanceConfigBean instance = new EurekaInstanceConfigBean(inetUtils);
   // 设置非空属性
    instance.setNonSecurePort(serverPort);
    instance.setInstanceId(getDefaultInstanceId(env));
    instance.setPreferIpAddress(preferIpAddress);
    if (StringUtils.hasText(ipAddress)) {
        instance.setIpAddress(ipAddress);
    }
    if(isSecurePortEnabled) {
        instance.setSecurePort(serverPort);
    }
    if (StringUtils.hasText(hostname)) {
        instance.setHostname(hostname);
    }
    String statusPageUrlPath = getProperty("eureka.instance.status-page-url-path");
    String healthCheckUrlPath = getProperty("eureka.instance.health-check-url-path");
    if (StringUtils.hasText(statusPageUrlPath)) {
        instance.setStatusPageUrlPath(statusPageUrlPath);
    }
    if (StringUtils.hasText(healthCheckUrlPath)) {
        instance.setHealthCheckUrlPath(healthCheckUrlPath);
    }
    ManagementMetadata metadata = managementMetadataProvider.get(instance, serverPort, serverContextPath, managementContextPath, managementPort);
   .....
    return instance;
}

从上面的代码可以发现, EurekaInstanceConfig的属性主要通过 EurekaInstanceConfigBean的实现提供,同时也会尝试从配置文件中读取一部分配置,在例如 eureka.instance.hostnameeureka.instance.status-page-url-patheureka.instance.health-check-url-path等等,它代表了应用实例的应该具备的信息,然后这部分信息会被封装成 InstanceInfo,被注册到 Eureka Server 中。

InstanceInfo是通过 InstanceInfoFactory(org.springframework.cloud.netflix.eureka) 封装 EurekaInstanceConfig中的属性创建的,其中 InstanceInfo的属性基本是 volatile,保证了内存中的该类信息的一致性和原子性。

代码位于 InstanceInfoFactory

public InstanceInfo create(EurekaInstanceConfig config) {
        LeaseInfo.Builder leaseInfoBuilder = LeaseInfo.Builder.newBuilder()
                .setRenewalIntervalInSecs(config.getLeaseRenewalIntervalInSeconds())
                .setDurationInSecs(config.getLeaseExpirationDurationInSeconds());
        // 创建服务实例的信息用来注册到eureka server上
        InstanceInfo.Builder builder = InstanceInfo.Builder.newBuilder();
        String namespace = config.getNamespace();
        if (!namespace.endsWith(".")) {
            namespace = namespace + ".";
        }
        builder.setNamespace(namespace).setAppName(config.getAppname())
                .setInstanceId(config.getInstanceId())
                .setAppGroupName(config.getAppGroupName())
                .setDataCenterInfo(config.getDataCenterInfo())
                .setIPAddr(config.getIpAddress()).setHostName(config.getHostName(false))
                .setPort(config.getNonSecurePort())
                .enablePort(InstanceInfo.PortType.UNSECURE,
                        config.isNonSecurePortEnabled())
                .setSecurePort(config.getSecurePort())
                .enablePort(InstanceInfo.PortType.SECURE, config.getSecurePortEnabled())
                .setVIPAddress(config.getVirtualHostName())
                .setSecureVIPAddress(config.getSecureVirtualHostName())
                .setHomePageUrl(config.getHomePageUrlPath(), config.getHomePageUrl())
                .setStatusPageUrl(config.getStatusPageUrlPath(),
                        config.getStatusPageUrl())
                .setHealthCheckUrls(config.getHealthCheckUrlPath(),
                        config.getHealthCheckUrl(), config.getSecureHealthCheckUrl())
                .setASGName(config.getASGName());
       ....
        InstanceInfo instanceInfo = builder.build();
        instanceInfo.setLeaseInfo(leaseInfoBuilder.build());
        return instanceInfo;
    }

接着是 ApplicationInfoManager,代码位于 EurekaClientAutoConfiguration

@Bean
@ConditionalOnMissingBean(value = ApplicationInfoManager.class, search = SearchStrategy.CURRENT)
public ApplicationInfoManager eurekaApplicationInfoManager(EurekaInstanceConfig config) {
    InstanceInfo instanceInfo = new InstanceInfoFactory().create(config);
    return new ApplicationInfoManager(config, instanceInfo);
        }

通过组合 EurekaInstanceConfigInstanceInfo创建了 ApplicationInfoManager,属于应用信息管理器。

@Bean
@ConditionalOnMissingBean(value = EurekaClientConfig.class, search = SearchStrategy.CURRENT)
public EurekaClientConfigBean eurekaClientConfigBean() {
    EurekaClientConfigBean client = new EurekaClientConfigBean();
    if ("bootstrap".equals(propertyResolver.getProperty("spring.config.name"))) {
        // We don't register during bootstrap by default, but there will be another
        // chance later.
        client.setRegisterWithEureka(false);
    }
    return client;
}

前面有讲到, EurekaClientConfig持有 Eureka Client 与 Eureka Server 进行交互的关键性配置信息,类似 serviceUrl(Server 地址), EurekaClient通过 EurekaClientConfig中配置信息与 Eureka Server 进行服务注册与发现。

最后是 EurekaClient,通过 ApplicationInfoManagerEurekaClientConfig组合创建,即 EurekaClient同时持有了 client 的服务实例信息用于服务发现,与 Eureka Server 注册的配置用于服务注册。

@Bean(destroyMethod = "shutdown")
@ConditionalOnMissingBean(value = EurekaClient.class, search = SearchStrategy.CURRENT)
public EurekaClient eurekaClient(ApplicationInfoManager manager, EurekaClientConfig config){
        return new CloudEurekaClient(manager, config, this.optionalArgs, this.context);
}

整体的类结构如下

EurekaRegistration、EurekaServiceRegistry、EurekaAutoServiceRegistration

这是 SpringCloud 适配 Eureka 所加的将服务注册到服务注册中心的相关类,先来看一下相关的类结构

Registration继承了 ServiceInstance,代表了一个被注册到服务发现系统的一个服务实例,必须具备的信息如 hostname 和 port 等, RegistrationServiceInstance的一个门面类

public interface ServiceInstance {
    //获取服务实例的serviceId
    String getServiceId();
    //获取服务实例的hostname
    String getHost();
   //获取服务实例的端口号
    int getPort();
    boolean isSecure();
    //获取服务实例的uri地址
    URI getUri();
    //获取服务实例的元数据key-value对
    Map<String, String> getMetadata();
}

对应 Eureka, EurekaRegistration实现了 Registration,查看其中的代码,只是照搬了 EurekaInstanceConfigBean中的配置信息,同时注入了 EurekaClient,为 Eureka Client 的服务注册提供实现。

@Bean
@ConditionalOnBean(AutoServiceRegistrationProperties.class)
@ConditionalOnProperty(value = "spring.cloud.service-registry.auto-registration.enabled", matchIfMissing = true)
public EurekaRegistration eurekaRegistration(EurekaClient eurekaClient, CloudEurekaInstanceConfig instanceConfig, ApplicationInfoManager applicationInfoManager, ObjectProvider<HealthCheckHandler> healthCheckHandler) {
        return EurekaRegistration.builder(instanceConfig)
                .with(applicationInfoManager)
                .with(eurekaClient)
                .with(healthCheckHandler)
                .build();
    }

ServiceRegistry里面提供了将服务实例注册到服务注册中心的相关接口:

public interface ServiceRegistry<R extends Registration> {
    //注册服务实例,registration当中通常有关于服务实例的信息,例如hostname和port
    void register(R registration);
   //注销服务实例
    void deregister(R registration);
    //关闭ServiceRegistry,通常在服务关闭的时候被调用
    void close();
    //设置服务实例的状态
    void setStatus(R registration, String status);
    //获取服务实例的状态
    <T> T getStatus(R registration);
}

其中在 EurekaServiceRegistry的注册和下线的实现如下:

@Override
public void register(EurekaRegistration reg) {
   // 初始化EurekaRegistration中的EurekaClient,如果为null
    maybeInitializeClient(reg);
    // 修改服务的状态为UP
    reg.getApplicationInfoManager()
            .setInstanceStatus(reg.getInstanceConfig().getInitialStatus());
    // 设置健康检查
    reg.getHealthCheckHandler().ifAvailable(healthCheckHandler ->
            reg.getEurekaClient().registerHealthCheck(healthCheckHandler));
}
private void maybeInitializeClient(EurekaRegistration reg) {
    reg.getApplicationInfoManager().getInfo();
    reg.getEurekaClient().getApplications();
}
@Override
public void deregister(EurekaRegistration reg) {
    if (reg.getApplicationInfoManager().getInfo() != null) {
        // 设置服务的状态为DOWN
        reg.getApplicationInfoManager().setInstanceStatus(InstanceInfo.InstanceStatus.DOWN);
    }
}

在上面的代码中可以发现,对服务的注册和下线仅仅是修改了服务当前的状态,其实在 EurekaClient的接口实现类中有专门对 InstanceStatus状态修改的监听,当服务实例的信息改变时就会触发不同的事件进行处理。

EurekaAutoServiceRegistration,顾名思义,就是服务实例的自动注册,由前面的类图可知,该类继承了 SmartLifecycle的接口,这是 org.springframework.context包中的相关类,说明 EurekaAutoServiceRegistration类受到了 Spring 的生命周期的管理。

@EventListener(ServletWebServerInitializedEvent.class)
public void onApplicationEvent(ServletWebServerInitializedEvent event) {
    int localPort = event.getWebServer().getPort();
    if (this.port.get() == 0) {
        log.info("Updating port to " + localPort);
        this.port.compareAndSet(0, localPort);
        start();
    }
}
@EventListener(ContextClosedEvent.class)
public void onApplicationEvent(ContextClosedEvent event) {
    if( event.getApplicationContext() == context ) {
        stop();
    }
}

在上述代码中,该类监听了 ServletWebServerInitializedEventContextClosedEvent两个事件,即在应用初始化阶段调用 start()和应用上下文关闭阶段调用 stop(),其实就是在应用启动和关闭时分别进行服务的注册和下线的自动操作。

EurekaAutoServiceRegistration的服务注册和下线是直接调用了 EurekaServiceRegistry 中的方法。

@Override
public void start() {
    // only set the port if the nonSecurePort or securePort is 0 and this.port != 0
    if (this.port.get() != 0) {
        if (this.registration.getNonSecurePort() == 0) {
            this.registration.setNonSecurePort(this.port.get());
        }
        if (this.registration.getSecurePort() == 0 && this.registration.isSecure()) {
            this.registration.setSecurePort(this.port.get());
        }
    }
    if (!this.running.get() && this.registration.getNonSecurePort() > 0) {
       // 注册服务
        this.serviceRegistry.register(this.registration);
        this.context.publishEvent(
                new InstanceRegisteredEvent<>(this, this.registration.getInstanceConfig()));
        this.running.set(true);
    }
}
@Override
public void stop() {
   // 服务下线
    this.serviceRegistry.deregister(this.registration);
    this.running.set(false);
}

EurekaDiscoveryClientConfiguration

EurekaDiscoveryClientConfiguration只做了两件事,监听了 RefreshScopeRefreshedEvent事件以及注入 EurekaHealthCheckHandler接口的实现类。

RefreshScopeRefreshedEvent事件一般在 spring 管理的 bean 被刷新的时候被抛出,此时说明应用环境的配置和参数有可能发生变化,于是需要重新注册服务,防止注册中心的服务实例信息与本地信息不一致。

@EventListener(RefreshScopeRefreshedEvent.class)
public void onApplicationEvent(RefreshScopeRefreshedEvent event) {
    // 保证了一个刷新事件发生后client的重新注册
    if(eurekaClient != null) {
        eurekaClient.getApplications();
    }
    if (autoRegistration != null) {
        // 重新注册防止本地信息与注册表中的信息不一致
        this.autoRegistration.stop();
        this.autoRegistration.start();
    }
}

RibbonEurekaAutoConfiguration

Eureka 中配置负载均衡的配置类,具体关于 Ribbon 的内容将在其他章节进行讲解,这里就略过

客户端核心代码

包结构

主要的代码位于 eureka-client中,项目的 module 为 eureka-client,版本为 v1.8.7,这是 Finchley 版本的 Spring Cloud 所依赖的 eureka 版本

包结构如下

简要的包介绍:

  • com.netflix.appinfo: 主要是关于 eureka-client 的配置信息类,如上面提及的 EurekaInstanceConfigInstanceInfo,其中也包含了类似 AmazonInfoDataCenterInfo等与 AWS 中的架构适配密切相关的接口,在此不做详解的介绍,有兴趣读者可以自行去了解。

  • com.netflix.discovery: 主要实现 Eureka-Client 的服务发现和服务注册功能。

  • com.netflix.discovery.shared.dns: DNS 解析器。

  • com.netflix.discovery.shared.resolver: Euraka Endpoint 解析器, EurekaEndpoint指的是服务端点,一般指的是 Eureka Server 的访问地址,默认实现为 DefaultEndpointClusterResolver将配置的 Eureka Server 地址解析为 EurekaEndpoint,这里面用到了委托者设计模式,类图如下,有很明显的请求委托的处理过程。

  • com.netflix.discovery.shared.transport:Eureka Client 与 Eureka Server 之间进行 HTTP 通信的客户端以及通信的 request 和 response 的封装类。

  • com.netflix.discovery.converters: 主要解决 Eureka 服务之间的数据传输的编码与解码,支持 JSON、XML 等格式。

  • com.netflix.discovery.guice: Googleguice依赖注入配置包,类似 Spring的 configuration。

  • com.netflix.discovery.provider: 提供的 Jersey 中请求与响应的序列化与反序列化实现,默认实现是 DefaultJerseyProvider

  • com.netflix.discovery.providers: 目前只有 DefaultEurekaClientConfigProvider,提供 EurekaClientConfig工厂生成方法。

  • com.netflix.discovery.shared: Eureka Client 与 Eureka Server 共享重用的方法。

DiscoveryClient

DiscoveryClient可以说是 Eureka Client 的核心类,负责了与 Eureka Server 交互的关键逻辑,具备了以下的职能:

  • 注册服务实例到 Eureka Server 中;

  • 更新与 Eureka Server 的契约;

  • 在服务关闭时从 Eureka Server 中取消契约;

  • 查询在 Eureka Server 中注册的服务 / 实例的列表。

DiscoverClient的核心类图如下:

DiscoveryClient的顶层接口为 LookupService,主要的目的是为了发现活跃中的服务实例。

public interface LookupService<T> {
    //根据服务实例注册的appName来获取,获取一个封装有相同appName的服务实例信息的容器
   Application getApplication(String appName);
    //返回当前注册的所有的服务实例信息
   Applications getApplications();
       //根据服务实例的id获取
       List<InstanceInfo> getInstancesById(String id);
   //获取下一个可能的Eureka Server来处理当前对注册表信息的处理,一般是通过循环的方式来获取下一个Server
   InstanceInfo getNextServerFromEureka(String virtualHostname, boolean secure);
}

Application中持有一个特定应用的多个实例的列表,可以理解成同一个服务的集群信息,它们都挂在同一个服务名 appName 下, InstanceInfo代表一个服务实例,部分代码如下:

public class Application {
    private static Random shuffleRandom = new Random();
    //服务名
    private String name;
    @XStreamOmitField
    private volatile boolean isDirty = false;
    @XStreamImplicit
    private final Set<InstanceInfo> instances;
    private final AtomicReference<List<InstanceInfo>> shuffledInstances;
    private final Map<String, InstanceInfo> instancesMap;
    .....
}

为了保证原子性操作以及数据的唯一性,防止脏数据, Application中对 InstanceInfo的操作都是同步操作,感受一下 Application.addInstance方法。

public void addInstance(InstanceInfo i) {
    instancesMap.put(i.getId(), i);
    synchronized (instances) {
    instances.remove(i);
    instances.add(i);
    isDirty = true;
    }
}

通过同步代码块,保证每次只有有一个线程对 instances 进行修改,同时注意 instancesMap 采用的是 ConcurrentHashMap实现,保证了原子性的操作,所以不需要通过同步代码块进行控制。

Applications中代表的是 Eureka Server 中已注册的服务实例的集合信息,主要是对 Application的封装,里面的操作大多也是的同步操作。

EurekaClient继承了 LookupService接口,为 DiscoveryClient提供了一个上层的接口,目的是试图方便从 eureka 1.x 到 eureka 2.x 的过渡,这说明 EurekaClient这个接口属于比较稳定的接口,即使在下一大阶段也会被依旧保留。

EurekaCientLookupService的基础上扩充了更多的接口,提供了更丰富的获取服务实例的功能,主要有:

  • 提供了多种的方式获取 InstanceInfo,例如根据 region,Eureka Server 地址等获取;

  • 提供了本地客户端 (位于的区域,可用区等) 的数据,这部分与 AWS 密切相关;

  • 提供了为客户端注册和获取健康检查处理器;

除去查询相关的接口,关注 EurekaClient中的以下两个接口:

    // 为Eureka Client注册健康检查处理器
    // 一旦注册,客户端将通过调用新注册的健康检查处理器来对注册中instanceInfo
    // 进行一个按需更新,随后按照eurekaclientconfig.getinstanceinforeplicationintervalseconds()
    // 中配置的指定时间调用HealthCheckHandler
    public void registerHealthCheck(HealthCheckHandler healthCheckHandler);
    // 为eureka client注册一个EurekaEventListener(事件监听器)
    // 一旦注册,当eureka client的内部状态发生改变的时候,将会调用EurekaEventListener.onEvent()
    // 触发一定的事件。可以通过这种方式监听client的更新而非通过轮询的方式询问client
    public void registerEventListener(EurekaEventListener eventListener);

Eureka Server 一般通过心跳 (heartbeats) 来识别一个实例的状态。Eureka Client 中存在一个定时任务定时通过 HealthCheckHandler检测当前 client 的状态,如果 client 的状态发生改变,将会触发新的注册事件,同步 Eureka Server 的注册表中该服务实例的相关信息。

public interface HealthCheckHandler {
    InstanceInfo.InstanceStatus getStatus(InstanceInfo.InstanceStatus currentStatus);
}

spring-cloud-netflix-eureka-client中实现了这个的接口, EurekaHealthCheckHandler,主要的组合了 spring-boot-actuator中的 HealthAggregatorHealthIndicator实现了对 spring-boot应用的状态检测。

主要有以下的状态:

public enum InstanceStatus {
    UP, // 可以接受服务请求
    DOWN, // 无法发送流量-健康检查失败
    STARTING, // 正在启动,无法发送流量
    OUT_OF_SERVICE, // 服务关闭,不接受流量
    UNKNOWN; // 未知状态
    }

Eureka 中的事件模式,这是一个很明显的观察者模式,以下为它的类图类图:

客户端的服务注册与发现

DiscoveryClient的代码中,有实现服务注册与发现的功能的具体代码。在 DiscoveryClient构造函数中,Eureka Client 会执行从 Eureka Server 中拉取注册表信息,注册自身等操作。DiscoveryClient的构造函数如下:

DiscoveryClient(ApplicationInfoManager applicationInfoManager, EurekaClientConfig config, 
AbstractDiscoveryClientOptionalArgs args, Provider<BackupRegistry> backupRegistryProvider) 

ApplicationInfoManagerEurekaClientConfig在前面的介绍中已经了解,一个是封装当前服务实例的配置信息的类,另一个是封装了 client 与 server 交互配置信息的类, AbstractDiscoveryClientOptionalArgsBackupRegistry是未介绍过的

BackupRegistry的接口代码如下:

@ImplementedBy(NotImplementedRegistryImpl.class)
public interface BackupRegistry {
    Applications fetchRegistry();
    Applications fetchRegistry(String[] includeRemoteRegions);
}

它充当了备份注册中心的职责,当 Eureka Client 无法从任何一个 Eureka Server 中获取注册表信息时, BackupRegistry将被调用以获取注册表信息,但是默认的实现是 NotImplementedRegistryImpl,即没有实现。

public abstract class AbstractDiscoveryClientOptionalArgs<T> {
    // 生成健康检查回调的工厂类,HealthCheckCallback已废弃
       Provider<HealthCheckCallback> healthCheckCallbackProvider;
   // 生成健康处理器的工厂类
   Provider<HealthCheckHandler> healthCheckHandlerProvider;
   // 向Eureka Server注册之前的预处理器
   PreRegistrationHandler preRegistrationHandler;
   // Jersey过滤器集合,Jersey1和Jersey2均可使用
   Collection<T> additionalFilters;
   // Jersey客户端,主要用于client与server之间的HTTP交互
   EurekaJerseyClient eurekaJerseyClient;
   // 生成Jersey客户端的工厂
   TransportClientFactory transportClientFactory;
   // 生成Jersey客户端的工厂的工厂
   TransportClientFactories transportClientFactories;
   // Eureka事件的监听器
   private Set<EurekaEventListener> eventListeners;
....
}

AbstractDiscoveryClientOptionalArgs是用于注入一些可选参数的,以及一些 jersey1jersey2通用的过滤器, @Inject(optional=true)属性说明了该参数的可选性

在构造方法中,忽略掉大部分的赋值操作,逐步了解配置类中的属性会对 DiscoveryClient的行为造成什么影响

if (config.shouldFetchRegistry()) {
    this.registryStalenessMonitor = new ThresholdLevelsMetric(this, METRIC_REGISTRY_PREFIX + "lastUpdateSec_", new long[]{15L, 30L, 60L, 120L, 240L, 480L});
} else {
    this.registryStalenessMonitor = ThresholdLevelsMetric.NO_OP_METRIC;
}
if (config.shouldRegisterWithEureka()) {
    this.heartbeatStalenessMonitor = new ThresholdLevelsMetric(this, METRIC_REGISTRATION_PREFIX + "lastHeartbeatSec_", new long[]{15L, 30L, 60L, 120L, 240L, 480L});
} else {
    this.heartbeatStalenessMonitor = ThresholdLevelsMetric.NO_OP_METRIC;
}

config.shouldFetchRegistry()(对应配置为 eureka.client.fetch-register),为 true 表示 Eureka Client 将从 Eureka Server 中拉取注册表的信息,config.shouldRegisterWithEureka(对应配置为 eureka.client.register-with-eureka),为 true 表示 Eureka Client 将注册到 Eureka Server 中。

如果上述的两个配置均为 false,那么 Discovery 的初始化就直接结束,表示该客户端既不进行服务注册也不进行服务发现

接着初始化一个基于线程池的定时器线程池 ScheduledExecutorService,线程池大小为 2,一个用于心跳,一个用于缓存刷新,同时初始化了心跳和缓存刷新线程池 (ThreadPoolExecutor)。关于 ScheduledExecutorServiceThreadPoolExecutor之间的关系在此不展开。

scheduler = Executors.newScheduledThreadPool(2,
                    new ThreadFactoryBuilder()
                            .setNameFormat("DiscoveryClient-%d")
                            .setDaemon(true)
                            .build());
            heartbeatExecutor = new ThreadPoolExecutor(
                    1, clientConfig.getHeartbeatExecutorThreadPoolSize(), 0, TimeUnit.SECONDS,
                    new SynchronousQueue<Runnable>(),
                    new ThreadFactoryBuilder()
                            .setNameFormat("DiscoveryClient-HeartbeatExecutor-%d")
                            .setDaemon(true)
                            .build()
            );  // use direct handoff
            cacheRefreshExecutor = new ThreadPoolExecutor(
                    1, clientConfig.getCacheRefreshExecutorThreadPoolSize(), 0, TimeUnit.SECONDS,
                    new SynchronousQueue<Runnable>(),
                    new ThreadFactoryBuilder()
                            .setNameFormat("DiscoveryClient-CacheRefreshExecutor-%d")
                            .setDaemon(true)
                            .build()
            );  // use direct handoff

接着初始化了 Eureka Client 与 Eureka Server 进行 HTTP 交互的 Jersy 客户端,将 AbstractDiscoveryClientOptionalArgs中的属性用来构建 EurekaTransport

eurekaTransport = new EurekaTransport();
scheduleServerEndpointTask(eurekaTransport, args);

EurekaTransportDiscoveryClient中的一个内部类,其内封装了 DiscoveryClient与 Eureka Server 进行 HTTP 调用的 Jersy 客户端:

private static final class EurekaTransport {
   // Server endPoint解析器
    private ClosableResolver bootstrapResolver;
    // Jersy客户端生成工厂
    private TransportClientFactory transportClientFactory;
    // 注册客户端
    private EurekaHttpClient registrationClient;
    // 注册客户端生成工厂
    private EurekaHttpClientFactory registrationClientFactory;
    // 发现服务客户端
    private EurekaHttpClient queryClient;
    // 发现服务客户端生成工厂
     private EurekaHttpClientFactory queryClientFactory;
     ....
}

关于 AWS region中的相关配置略过。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值