服务注册与发现:Eureka

服务注册与发现:Eureka

整体简介

Eureka 整体上可分为两个主体: Eureka ServerEureka Client.

服务端注册

  1. 服务注册: 服务提供者启动时, 会通过 Eureka Client 向 Eureka Server 注册信息, Eureka Server 会存储该服务的信息, Eureka Server 内部有二层缓存机制来维护整个注册表.
  2. 提供注册表: 服务消费者在调用服务时, 如果 Eureka Client 没有缓存注册表的话, 会从 Eureka Server 获取最新的注册表.
  3. 同步状态: Eureka Client 通过注册、心跳机制和 Eureka Server 同步当前客户端的状态.

客户端

Eureka Client 是一个 Java 客户端, 用于简化与 Eureka Server 的交互. Eureka Client 会拉取、更新和缓存 Eureka Server 中的信息. 因此当所有的 Eureka Server 节点都宕掉, 服务消费者依然可以使用缓存中的信息找到服务提供者, 但是当服务有更改的时候会出现信息不一致.

Register: 服务注册

服务的提供者,将自身注册到注册中心,服务提供者也是一个 Eureka Client。当 Eureka Client 向 Eureka Server 注册时,它提供自身的元数据,比如 IP 地址、端口,运行状况指示符 URL,主页等。

Renew:服务续约

Eureka Client 会每隔 30 秒发送一次心跳来续约。 通过续约来告知 Eureka Server 该 Eureka Client 运行正常,没有出现问题。 默认情况下,如果 Eureka Server 在 90 秒内没有收到 Eureka Client 的续约,Server 端会将实例从其注册表中删除,此时间可配置,一般情况不建议更改。

# 服务续约任务的调用间隔时间,默认为30秒
eureka.instance.lease-renewal-interval-in-seconds=30
# 服务失效的时间,默认为90秒。
eureka.instance.lease-expiration-duration-in-seconds=90

Eviction: 服务剔除

当 Eureka Client 和 Eureka Server 不再有心跳时, Eureka Server 会将该服务实例从服务注册列表中删除, 即服务剔除.

Cancel: 服务下线

Eureka Client 在程序关闭时向 Eureka Server 发送取消请求. 发送请求后, 该客户端实例信息将从 Eureka Server 的实例注册表中删除. 该下线请求不会自动完成, 它需要调用以下内容:
DiscoveryManager.getInstance().shutdownComponent();

GetRegisty: 获取注册列表信息

Eureka Client 从服务器获取注册表信息, 并将其缓存在本地. 客户端会使用该信息查找其他服务, 从而进行远程调用. 该注册列表信息定期 (每 30 秒钟) 更新一次. 每次返回注册列表信息可能与 Eureka Client 的缓存信息不同, Eureka Client 自动处理.

如果由于某种原因导致注册列表信息不能及时匹配,Eureka Client 则会重新获取整个注册表信息. Eureka Server 缓存注册列表信息, 整个注册表以及每个应用程序的信息进行了压缩,压缩内容和没有压缩的内容完全相同. Eureka Client 和 Eureka Server 可以使用 JSON/XML 格式进行通讯. 在默认情况下 Eureka Client 使用压缩 JSON 格式来获取注册列表的信息.
获取服务是服务消费者的基础,所以必有两个重要参数需要注意:

# 启用服务消费者从注册中心拉取服务列表的功能
eureka.client.fetch-registry=true

# 设置服务消费者从注册中心拉取服务列表的间隔
eureka.client.registry-fetch-interval-seconds=30

Remote Call: 远程调用

当 Eureka Client 从注册中心获取到服务提供者信息后, 就可以通过 Http 请求调用对应的服务; 服务提供者有多个时, Eureka Client 客户端会通过 Ribbon 自动进行负载均衡.

自我保护机制

默认情况下, 若 Eureka Server 在一定的 90s 内没有接收到某个微服务实例的心跳, 会注销该实例.
但是在微服务架构下服务之间通常都是跨进程调用, 网络通信往往会面临着各种问题, 比如微服务状态正常, 网络分区故障, 导致此实例被注销.

固定时间内大量实例被注销, 可能会严重威胁整个微服务架构的可用性. 为了解决此问题, Eureka 开发了自我保护机制,
那么什么是自我保护机制呢?

Eureka Server 在运行期间会去统计心跳失败比例在 15 分钟之内是否低于 85%, 如果低于 85%, Eureka Server 即会进入自我保护机制.

Eureka Server 进入自我保护机制, 会出现以下几种情况:
	1. Eureka 不再从注册列表中移除因为长时间没收到心跳而应该过期的服务.
	2. Eureka 仍然能够接受新服务的注册和查询请求, 但是不会被同步到其它节点上 (即保证当前节点依然可用).
	3. 当网络稳定时, 当前实例新的注册信息会被同步到其它节点中.

Eureka 自我保护机制是为了防止误杀服务而提供的一个机制. 当个别客户端出现心跳失联时, 则认为是客户端的问题, 剔除掉客户端; 当 Eureka 捕获到大量的心跳失败时, 则认为可能是网络问题, 进入自我保护机制; 当客户端心跳恢复时, Eureka 会自动退出自我保护机制.

通过在 Eureka Server 配置如下参数, 启或者关闭保护机制, 生产环境建议打开:

eureka.server.enable-self-preservation=true

Eureka 集群原理

Eureka Server 集群相互之间通过 Replicate 来同步数据. 架构中, 节点具有平等性, 彼此可依赖型.

Eureka Server 集群之间的状态是采用异步方式同步, 所以不保证节点间的状态一定是一致的, 不过基本能保证最终状态是一致的.
Eureka 分区: 提供了 Region 和 Zone 两个概念来进行分区, 其中 Region 可以划分为多个 Zone.
Eurka 保证 AP: 由于 Eureka Server 各个节点具有平等性, 几个节点挂掉不会影响其他节点的正常工作,

  1. 其依然可以提供注册和查询服务. 则只要有一台 Eureka Server 还在, 就能保证注册服务可用 (保证可用性),
  2. 不过查到的信息可能不是最新的 (不保证强一致性).

Eurka 工作流程

  1. Eureka Server 启动成功, 等待服务端注册. 在启动过程中如果配置了集群, 集群之间定时通过 Replicate 同步注册表, 每个 Eureka Server 都存在独立完整的服务注册表信息.
  2. Eureka Client 启动时根据配置的 Eureka Server 地址去注册中心注册服务.
  3. Eureka Client 会每 30s 向 Eureka Server 发送一次心跳请求, 证明客户端服务正常.
  4. 当 Eureka Server 90s 内没有收到 Eureka Client 的心跳, 注册中心则认为该节点失效, 会注销该实例.
  5. 单位时间内 Eureka Server 统计到有大量的 Eureka Client 没有上送心跳, 则认为可能为网络异常, 进入自我保护机制, 不再剔除没有上送心跳的客户端.
  6. 当 Eureka Client 心跳请求恢复正常之后, Eureka Server 自动退出自我保护模式.
  7. Eureka Client 定时全量或者增量从注册中心获取服务注册表, 并且将获取到的信息缓存到本地.
  8. 服务调用时,Eureka Client 会先从本地缓存找寻调取的服务. 如果获取不到, 先从注册中心刷新注册表, 再同步到本地缓存.
  9. Eureka Client 获取到目标服务器信息, 发起服务调用.
  10. Eureka Client 程序关闭时向 Eureka Server 发送取消请求, Eureka Server 将实例从注册表中删除.

Eureka 的简单使用

启动报错

解决Eureka启动时候报错:sun.reflect.annotation.TypeNotPresentExceptionProxy

<dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web-services</artifactId>          
 </dependency>

服务发现原理

Eureka架构图如下:
![image.png](https://img-blog.csdnimg.cn/img_convert/cb132df0e5926e63ae4151a6b471215e.png#align=left&display=inline&height=179&margin=[object Object]&name=image.png&originHeight=716&originWidth=1544&size=308611&status=done&style=none&width=386)

Region与Availability Zone

Eureka最初设计的目的是AWS(亚马逊网络服务系统)中用于部署分布式系统,所以首先对AWS上的区域(Regin)和可用区(Availability Zone)进行简单的介绍。

  • 区域:AWS根据地理位置把某个地区的基础设施服务集合称为一个区域,区域之间相对独立。在架构图上,us-east-1c、us-east-1d、us-east-1e表示AWS中的三个设施服务区域,这些区域中分别部署了一个Eureka集群。
  • 可用区:AWS的每个区域都是由多个可用区组成的,而一个可用区一般都是由多个数据中心(简单理解成一个原子服务设施)组成的。可用区与可用区之间是相互独立的,有独立的网络和供电等,保证了应用程序的高可用性。在上述的架构图中,一个可用区中可能部署了多个Eureka,一个区域中有多个可用区,这些Eureka共同组成了一个Eureka集群。

组件与行为

  • Application Service:是一个Eureka Client,扮演服务提供者的角色,提供业务服务,向Eureka Server注册和更新自己的信息,同时能从Eureka Server注册表中获取到其他服务的信息。
  • Eureka Server:扮演服务注册中心的角色,提供服务注册和发现的功能。每个Eureka Cient向Eureka Server注册自己的信息,也可以通过Eureka Server获取到其他服务的信息达到发现和调用其他服务的目的。
  • Application Client:是一个Eureka Client,扮演了服务消费者的角色,通过Eureka Server获取注册到其上其他服务的信息,从而根据信息找到所需的服务发起远程调用。
  • Replicate:Eureka Server之间注册表信息的同步复制,使Eureka Server集群中不同注册表中服务实例信息保持一致。
  • Make Remote Call:服务之间的远程调用。
  • Register:注册服务实例,Client端向Server端注册自身的元数据以供服务发现。
  • Renew:续约,通过发送心跳到Server以维持和更新注册表中服务实例元数据的有效性。当在一定时长内,Server没有收到Client的心跳信息,将默认服务下线,会把服务实例的信息从注册表中删除。
  • Cancel:服务下线,Client在关闭时主动向Server注销服务实例元数据,这时Client的服务实例数据将从Server的注册表中删除。
  • Get Registry:获取注册表,Client向Server请求注册表信息,用于服务发现,从而发起服务间远程调用。

Eureka Client源码解析

Eureka Client为了简化开发人员的开发工作,将很多与Eureka Server交互的工作隐藏起来,自主完成。在应用的不同运行阶段在后台完成工作如图4-4所示
![image.png](https://img-blog.csdnimg.cn/img_convert/06ad2f9139f0e28e9cca6f4ac4e37124.png#align=left&display=inline&height=145&margin=[object Object]&name=image.png&originHeight=289&originWidth=707&size=88125&status=done&style=none&width=353.5)
SpringBoot汇项目启动如下自动装配类:
Eukeka Client通过Starter的方式引入依赖,Spring Boot将会为项目使用以下的自动配置类:

  • EurekaClientAutoConfiguration:Eureke Client自动配置类,负责EurekaClient中关键Beans的配置和初始化,如ApplicationInfoManager和EurekaClientConfig等。
  • RibbonEurekaAutoConfiguration:Ribbon负载均衡相关配置。
  • EurekaDiscoveryClientConfiguration:配置自动注册和应用的健康检查器。

读取应用自身配置

通过EurekaDiscoveryClientConfiguration配置类,Spring Boot帮助EurekaClient完成很多必要Bean的属性读取和配置,表4-1列出了EurekaDiscoveryClientConfiguration中的属性读取和配置类。
![image.png](https://img-blog.csdnimg.cn/img_convert/4b4ec0f31194cc7bc6e2c2bb31bea922.png#align=left&display=inline&height=179&margin=[object Object]&name=image.png&originHeight=357&originWidth=756&size=144915&status=done&style=none&width=378)

服务发现

DiscoveryClient是Spring Cloud中用来进行服务发现的顶级接口,在NetflixEureka或者Consul中都有相应的具体实现类
![image.png](https://img-blog.csdnimg.cn/img_convert/3fde573f44679a57e4baa4c7a9edc1b2.png#align=left&display=inline&height=288&margin=[object Object]&name=image.png&originHeight=1150&originWidth=1646&size=732641&status=done&style=none&width=412)

服务发现客户端

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

  • 注册服务实例到Eureka Server中;
  • 发送心跳更新与Eureka Server的租约;
  • 在服务关闭时从Eureka Server中取消租约,服务下线;
  • 查询在Eureka Server中注册的服务实例列表。

类结构

![image.png](https://img-blog.csdnimg.cn/img_convert/10b771a34e14672918acabd8f05f2128.png#align=left&display=inline&height=359&margin=[object Object]&name=image.png&originHeight=1436&originWidth=1750&size=1104641&status=done&style=none&width=438)

DiscoveryClient类结构

EurekaDiscoveryClient包含一个EurekaClient实例,EurekaClient集成了LookupService的作用是发现活跃的服务实例,并持有Application服务实例信息列表。
![image.png](https://img-blog.csdnimg.cn/img_convert/026ad4c1dcd1ce1460b50e8c4a193bc2.png#align=left&display=inline&height=274&margin=[object Object]&name=image.png&originHeight=548&originWidth=830&size=242501&status=done&style=none&width=415)
![image.png](https://img-blog.csdnimg.cn/img_convert/f768af81bfa98cf512b58000f02217c9.png#align=left&display=inline&height=169&margin=[object Object]&name=image.png&originHeight=676&originWidth=1318&size=406984&status=done&style=none&width=330)
EurekaCient在LookupService的基础上扩充了更多的接口,提供了更丰富的获取服务实例的方式,主要有:

  • 提供了多种方式获取InstanceInfo,例如根据区域、EurekaServer地址等获取。
  • 提供了本地客户端(所处的区域、可用区等)的数据,这部分与AWS密切相关。
  • 提供了为客户端注册和获取健康检查处理器的能力。

Eureka中的事件模式

Eureka中的事件模式属于观察者模式,事件监听器将监听Client的服务实例信息变化,触发对应的处理事件,图为Eureka事件的类图
![image.png](https://img-blog.csdnimg.cn/img_convert/10be0cf0c69bc0d1e482d8149fc36884.png#align=left&display=inline&height=183&margin=[object Object]&name=image.png&originHeight=732&originWidth=1376&size=416034&status=done&style=none&width=344)

DiscoveryClient构造函数

在DiscoveryClient构造函数中,Eureka Client会执行从EurekaServer中拉取注册表信息、服务注册、初始化发送心跳、缓存刷新(重新拉取注册表信息)和按需注册定时任务等操作,可以说DiscoveryClient的构造函数贯穿了Eureka Client启动阶段的各项工作

    public DiscoveryClient(ApplicationInfoManager applicationInfoManager, EurekaClientConfig config) {
        this(applicationInfoManager, config, null);
    }

核心的构造方法如下,比较长

  @Inject
    DiscoveryClient(ApplicationInfoManager applicationInfoManager, EurekaClientConfig config, AbstractDiscoveryClientOptionalArgs args,
                    Provider<BackupRegistry> backupRegistryProvider, EndpointRandomizer endpointRandomizer) {
        if (args != null) {
            this.healthCheckHandlerProvider = args.healthCheckHandlerProvider;
            this.healthCheckCallbackProvider = args.healthCheckCallbackProvider;
            this.eventListeners.addAll(args.getEventListeners());
            this.preRegistrationHandler = args.preRegistrationHandler;
        } else {
            this.healthCheckCallbackProvider = null;
            this.healthCheckHandlerProvider = null;
            this.preRegistrationHandler = null;
        }
        
        this.applicationInfoManager = applicationInfoManager;
        InstanceInfo myInfo = applicationInfoManager.getInfo();

        clientConfig = config;
        staticClientConfig = clientConfig;
        transportConfig = config.getTransportConfig();
        instanceInfo = myInfo;
        if (myInfo != null) {
            appPathIdentifier = instanceInfo.getAppName() + "/" + instanceInfo.getId();
        } else {
            logger.warn("Setting instanceInfo to a passed in null value");
        }

        this.backupRegistryProvider = backupRegistryProvider;
        this.endpointRandomizer = endpointRandomizer;
        this.urlRandomizer = new EndpointUtils.InstanceInfoBasedUrlRandomizer(instanceInfo);
        localRegionApps.set(new Applications());

        fetchRegistryGeneration = new AtomicLong(0);

        remoteRegionsToFetch = new AtomicReference<String>(clientConfig.fetchRegistryForRemoteRegions());
        remoteRegionsRef = new AtomicReference<>(remoteRegionsToFetch.get() == null ? null : remoteRegionsToFetch.get().split(","));
				//config#shouldFetchRegistry(对应配置为eureka.client.fetch-register)为true表示Eureka Client将从Eureka Server中拉取注册表信息
        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;
        }
				//config#shouldRegisterWithEureka(对应配置为eureka.client.register-with-eureka)为true表示EurekaClient将注册到Eureka Server中
        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;
        }

        logger.info("Initializing Eureka in region {}", clientConfig.getRegion());
				//如果上述的两个配置均为false,那么Discovery的初始化将直接结束,表示该客户端既不进行服务注册也不进行服务发现。
        if (!config.shouldRegisterWithEureka() && !config.shouldFetchRegistry()) {
            logger.info("Client configured to neither register nor query for data.");
            scheduler = null;
            heartbeatExecutor = null;
            cacheRefreshExecutor = null;
            eurekaTransport = null;
            instanceRegionChecker = new InstanceRegionChecker(new PropertyBasedAzToRegionMapper(config), clientConfig.getRegion());

            // This is a bit of hack to allow for existing code using DiscoveryManager.getInstance()
            // to work with DI'd DiscoveryClient
            DiscoveryManager.getInstance().setDiscoveryClient(this);
            DiscoveryManager.getInstance().setEurekaClientConfig(config);

            initTimestampMs = System.currentTimeMillis();
            initRegistrySize = this.getApplications().size();
            registrySize = initRegistrySize;
            logger.info("Discovery Client initialized at timestamp {} with initial instances count: {}",
                    initTimestampMs, initRegistrySize);

            return;  // no need to setup up an network tasks and we are done
        }

        try {
            // 定义一个基于线程池的定时器线程池ScheduledExecutorService,线程池大小为2,一个线程用于发送心跳,另一个线程用于缓存刷新,同时定义了发送心跳和缓存刷新线程池
            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交互的Jersey客户端,将AbstractDiscoveryClientOptionalArgs中的属性用来构建EurekaTransport
            eurekaTransport = new EurekaTransport();
            scheduleServerEndpointTask(eurekaTransport, args);

            AzToRegionMapper azToRegionMapper;
            if (clientConfig.shouldUseDnsForFetchingServiceUrls()) {
                azToRegionMapper = new DNSBasedAzToRegionMapper(clientConfig);
            } else {
                azToRegionMapper = new PropertyBasedAzToRegionMapper(clientConfig);
            }
            if (null != remoteRegionsToFetch.get()) {
                azToRegionMapper.setRegionsToFetch(remoteRegionsToFetch.get().split(","));
            }
            instanceRegionChecker = new InstanceRegionChecker(azToRegionMapper, clientConfig.getRegion());
        } catch (Throwable e) {
            throw new RuntimeException("Failed to initialize DiscoveryClient!", e);
        }
				//从Eureka Server中拉取注册表信息
        //在Eureka Client向EurekaServer注册前,需要先从Eureka Server拉取注册表中的信息,这是服务发现的前提。
        // 通过将Eureka Server中的注册表信息缓存到本地,就可以就近获取其他服务的相关信息,减少与Eureka Server的网络通信。
        if (clientConfig.shouldFetchRegistry()) {
            try {
                boolean primaryFetchRegistryResult = fetchRegistry(false);
                if (!primaryFetchRegistryResult) {
                    logger.info("Initial registry fetch from primary servers failed");
                }
                boolean backupFetchRegistryResult = true;
                if (!primaryFetchRegistryResult && !fetchRegistryFromBackup()) {
                    backupFetchRegistryResult = false;
                    logger.info("Initial registry fetch from backup servers failed");
                }
                if (!primaryFetchRegistryResult && !backupFetchRegistryResult && clientConfig.shouldEnforceFetchRegistryAtInit()) {
                    throw new IllegalStateException("Fetch registry error at startup. Initial fetch failed.");
                }
            } catch (Throwable th) {
                logger.error("Fetch registry error at startup: {}", th.getMessage());
                throw new IllegalStateException(th);
            }
        }
				//拉取完Eureka Server中的注册表信息后,将对服务实例进行注册
        // call and execute the pre registration handler before all background tasks (inc registration) is started
        if (this.preRegistrationHandler != null) {
            this.preRegistrationHandler.beforeRegistration();
        }

        if (clientConfig.shouldRegisterWithEureka() && clientConfig.shouldEnforceRegistrationAtInit()) {
            try {
            		// 发起注册
                if (!register() ) {
                		//注册失败
                    throw new IllegalStateException("Registration error at startup. Invalid server response.");
                }
            } catch (Throwable th) {
                logger.error("Registration error at startup: {}", th.getMessage());
                throw new IllegalStateException(th);
            }
        }

        // finally, init the schedule tasks (e.g. cluster resolvers, heartbeat, instanceInfo replicator, fetch
        //构造函数的最后将初始化并启动发送心跳、缓存刷新和按需注册等定时任务。
        initScheduledTasks();

        try {
            Monitors.registerObject(this);
        } catch (Throwable e) {
            logger.warn("Cannot register timers", e);
        }

        // This is a bit of hack to allow for existing code using DiscoveryManager.getInstance()
        // to work with DI'd DiscoveryClient
        DiscoveryManager.getInstance().setDiscoveryClient(this);
        DiscoveryManager.getInstance().setEurekaClientConfig(config);

        initTimestampMs = System.currentTimeMillis();
        initRegistrySize = this.getApplications().size();
        registrySize = initRegistrySize;
        logger.info("Discovery Client initialized at timestamp {} with initial instances count: {}",
                initTimestampMs, initRegistrySize);
    }

最后总结一下,在DiscoveryClient的构造函数中,主要依次做了以下的事情:
1)相关配置的赋值,类似ApplicationInfoManager、EurekaClientConfig等。
2)备份注册中心的初始化,默认没有实现。
3)拉取Eureka Server注册表中的信息。
4)注册前的预处理。
5)向Eureka Server注册自身。
6)初始化心跳定时任务、缓存刷新和按需注册等定时任务。

DiscoveryClient拉取注册表信息

在DiscoveryClient的构造函数中,调用了DiscoveryClient#fetchRegistry方法从Eureka Server中拉取注册表信息。
一般来讲,在Eureka客户端,除了第一次拉取注册表信息,之后的信息拉取都会尝试只进行增量拉取(第一次拉取注册表信息为全量拉取),下面将分别介绍拉取注册表信息的两种实现:

  • 全量拉取注册表信息DiscoveryClient#getAndStoreFullRegistry
  • 增量式拉取注册表信息DiscoveryClient#getAndUpdateDelta

![image.png](https://img-blog.csdnimg.cn/img_convert/64706c1bb2bc5a70dc9dc066b11b880a.png#align=left&display=inline&height=104&margin=[object Object]&name=image.png&originHeight=208&originWidth=1190&size=87474&status=done&style=none&width=595)
getAndStoreFullRegistry方法可能被多个线程同时调用,导致新拉取的注册表被旧的注册表覆盖,对此,Eureka通过类型为AtomicLong的currentUpdateGeneration对apps的更新版本进行跟踪。如果更新版本不一致,说明本次拉取注册表信息已过时,不需要缓存到本地。

服务注册

在拉取完Eureka Server中的注册表信息并将其缓存在本地后,Eureka Client将向Eureka Server注册自身服务实例元数据,主要逻辑位于Discovery#register方法中

    boolean register() throws Throwable {
        logger.info(PREFIX + "{}: registering service...", appPathIdentifier);
        EurekaHttpResponse<Void> httpResponse;
        try {
            httpResponse = eurekaTransport.registrationClient.register(instanceInfo);
        } catch (Exception e) {
            logger.warn(PREFIX + "{} - registration failed {}", appPathIdentifier, e.getMessage(), e);
            throw e;
        }
        if (logger.isInfoEnabled()) {
            logger.info(PREFIX + "{} - registration status: {}", appPathIdentifier, httpResponse.getStatusCode());
        }
        return httpResponse.getStatusCode() == Status.NO_CONTENT.getStatusCode();
    }

初始化定时任务

在DiscoveryClient#initScheduledTasks方法中初始化了三个定时器任务:

  • 一个用于向Eureka Server拉取注册表信息刷新本地缓存;
  • 一个用于向Eureka Server发送心跳;
  • 一个用于进行按需注册的操作

服务下线

一般情况下,应用服务在关闭的时候,Eureka Client会主动向Eureka Server注销自身在注册表中的信息。DiscoveryClient中对象销毁前执行的清理方法如下所示:

  @PreDestroy
    @Override
    public synchronized void shutdown() {
    		//同步方法
        if (isShutdown.compareAndSet(false, true)) {
            logger.info("Shutting down DiscoveryClient ...");

            if (statusChangeListener != null && applicationInfoManager != null) {
            		//注销监听器
                applicationInfoManager.unregisterStatusChangeListener(statusChangeListener.getId());
            }

            cancelScheduledTasks();

            // If APPINFO was registered
            if (applicationInfoManager != null
                    && clientConfig.shouldRegisterWithEureka()
                    && clientConfig.shouldUnregisterOnShutdown()) {
                    //服务下线
                applicationInfoManager.setInstanceStatus(InstanceStatus.DOWN);
                unregister();
            }
						//关闭,Jersy客户端
            if (eurekaTransport != null) {
                eurekaTransport.shutdown();
            }
				
            heartbeatStalenessMonitor.shutdown();
            registryStalenessMonitor.shutdown();

            Monitors.unregisterObject(this);

            logger.info("Completed shut down of DiscoveryClient");
        }
    }

Eureka Server源码解析

Eureka Server作为一个开箱即用的服务注册中心,提供了以下的功能,用以满足与Eureka Client交互的需求:

  • 服务注册
  • 接受服务心跳
  • 服务剔除
  • 服务下线
  • 集群同步
  • 获取注册表中服务实例信息

服务实例注册表

InstanceRegistry是Eureka Server中注册表管理的核心接口。图中出现了两个InstanceRegistry,最下面的InstanceRegistry对EurekaServer的注册表实现类PeerAwareInstanceRegistryImpl进行了继承和扩展,使其适配Spring Cloud的使用环境,主要实现由PeerAwareInstanceRegistryImpl提供。
![image.png](https://img-blog.csdnimg.cn/img_convert/a0e80ee36243b4ee62b594bab4c47aff.png#align=left&display=inline&height=483&margin=[object Object]&name=image.png&originHeight=966&originWidth=1674&size=593969&status=done&style=none&width=837)
其中,LeaseManager接口作用是对注册到Eureka Server中的服务实例租约进行管理,分别有服务注册、服务下线、服务租约更新以及服务剔除等操作
LeaseManager中管理的对象是Lease, Lease代表一个Eureka Client服务实例信息的租约,它提供了对其内持有的类的时间有效性操作。Lease持有的类是代表服务实例信息的InstanceInfo。Lease中定义了租约的操作类型,分别是注册、下线、更新,同时提供了对租约中时间属性的各项操作。租约默认有效时长(duration)为90秒。

public class Lease<T> {

    enum Action {
        Register, Cancel, Renew
    };

    public static final int DEFAULT_DURATION_IN_SECS = 90;

服务注册

Eureka Client在发起服务注册时会将自身的服务实例元数据封装在InstanceInfo中,然后将InstanceInfo发送到Eureka Server。Eureka Server在接收到Eureka Client发送的InstanceInfo后将会尝试将其放到本地注册表中以供其他Eureka Client进行服务发现。
服务注册的主要实现位于AbstractInstanceRegistry#registry方法中,代码如下所示:

public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {
				//读锁
        read.lock();
        try {
        		//获取根据 appName分组后的registry
            Map<String, Lease<InstanceInfo>> gMap = registry.get(registrant.getAppName());
            REGISTER.increment(isReplication);
            if (gMap == null) {
                final ConcurrentHashMap<String, Lease<InstanceInfo>> gNewMap = new ConcurrentHashMap<String, Lease<InstanceInfo>>();
                //如果存在阿禁止,则返回已经存在的数据,否则返回null
                gMap = registry.putIfAbsent(registrant.getAppName(), gNewMap);
                if (gMap == null) {
                    gMap = gNewMap;
                }
            }
            //根据id获取已经存在的实例的租约
            Lease<InstanceInfo> existingLease = gMap.get(registrant.getId());
            // Retain the last dirty timestamp without overwriting it, if there is already a lease
            //租约信息已经存在
            if (existingLease != null && (existingLease.getHolder() != null)) {
                Long existingLastDirtyTimestamp = existingLease.getHolder().getLastDirtyTimestamp();
                Long registrationLastDirtyTimestamp = registrant.getLastDirtyTimestamp();
                logger.debug("Existing lease found (existing={}, provided={}", existingLastDirtyTimestamp, registrationLastDirtyTimestamp);

                // this is a > instead of a >= because if the timestamps are equal, we still take the remote transmitted
                // InstanceInfo instead of the server local copy.
                //如果该实例租约已经存在,则比较更新时间,最大值的注册信息有效
                if (existingLastDirtyTimestamp > registrationLastDirtyTimestamp) {
                    logger.warn("There is an existing lease and the existing lease's dirty timestamp {} is greater" +
                            " than the one that is being registered {}", existingLastDirtyTimestamp, registrationLastDirtyTimestamp);
                    logger.warn("Using the existing instanceInfo instead of the new instanceInfo as the registrant");
                    registrant = existingLease.getHolder();
                }
            } else {//不存在租约信息
                // The lease does not exist and hence it is a new registration
                synchronized (lock) {
                    if (this.expectedNumberOfClientsSendingRenews > 0) {
                        // Since the client wants to register it, increase the number of clients sending renews
                        this.expectedNumberOfClientsSendingRenews = this.expectedNumberOfClientsSendingRenews + 1;
                        updateRenewsPerMinThreshold();
                    }
                }
                logger.debug("No previous lease information found; it is new registration");
            }
            //创建新的租约信息
            Lease<InstanceInfo> lease = new Lease<InstanceInfo>(registrant, leaseDuration);
            if (existingLease != null) {
                lease.setServiceUpTimestamp(existingLease.getServiceUpTimestamp());
            }
            gMap.put(registrant.getId(), lease);
            recentRegisteredQueue.add(new Pair<Long, String>(
                    System.currentTimeMillis(),
                    registrant.getAppName() + "(" + registrant.getId() + ")"));
            // This is where the initial state transfer of overridden status happens
            if (!InstanceStatus.UNKNOWN.equals(registrant.getOverriddenStatus())) {
                logger.debug("Found overridden status {} for instance {}. Checking to see if needs to be add to the "
                                + "overrides", registrant.getOverriddenStatus(), registrant.getId());
                if (!overriddenInstanceStatusMap.containsKey(registrant.getId())) {
                    logger.info("Not found overridden id {} and hence adding it", registrant.getId());
                    overriddenInstanceStatusMap.put(registrant.getId(), registrant.getOverriddenStatus());
                }
            }
            InstanceStatus overriddenStatusFromMap = overriddenInstanceStatusMap.get(registrant.getId());
            if (overriddenStatusFromMap != null) {
                logger.info("Storing overridden status {} from map", overriddenStatusFromMap);
                registrant.setOverriddenStatus(overriddenStatusFromMap);
            }

            // Set the status based on the overridden status rules
            InstanceStatus overriddenInstanceStatus = getOverriddenInstanceStatus(registrant, existingLease, isReplication);
            registrant.setStatusWithoutDirty(overriddenInstanceStatus);

            // If the lease is registered with UP status, set lease service up timestamp
            if (InstanceStatus.UP.equals(registrant.getStatus())) {
                lease.serviceUp();
            }
            registrant.setActionType(ActionType.ADDED);
            recentlyChangedQueue.add(new RecentlyChangedItem(lease));
            registrant.setLastUpdatedTimestamp();
            invalidateCache(registrant.getAppName(), registrant.getVIPAddress(), registrant.getSecureVipAddress());
            logger.info("Registered instance {}/{} with status {} (replication={})",
                    registrant.getAppName(), registrant.getId(), registrant.getStatus(), isReplication);
        } finally {
        //释放锁
            read.unlock();
        }
    }

接受服务心跳

在Eureka Client完成服务注册之后,它需要定时向Eureka Server发送心跳请求(默认30秒一次),维持自己在Eureka Server中租约的有效性。
Eureka Server处理心跳请求的核心逻辑位于AbstractInstanceRegistry#renew方法中

public boolean renew(String appName, String id, boolean isReplication) {
        RENEW.increment(isReplication);
        //获取集群租约信息
        Map<String, Lease<InstanceInfo>> gMap = registry.get(appName);
        Lease<InstanceInfo> leaseToRenew = null;
        if (gMap != null) {
            leaseToRenew = gMap.get(id);
        }
        //租约不存在则返回false
        if (leaseToRenew == null) {
            RENEW_NOT_FOUND.increment(isReplication);
            logger.warn("DS: Registry: lease doesn't exist, registering resource: {} - {}", appName, id);
            return false;
        } else {
            InstanceInfo instanceInfo = leaseToRenew.getHolder();
            if (instanceInfo != null) {
                // touchASGCache(instanceInfo.getASGName());
                InstanceStatus overriddenInstanceStatus = this.getOverriddenInstanceStatus(
                        instanceInfo, leaseToRenew, isReplication);
                if (overriddenInstanceStatus == InstanceStatus.UNKNOWN) {
                    logger.info("Instance status UNKNOWN possibly due to deleted override for instance {}"
                            + "; re-register required", instanceInfo.getId());
                    RENEW_NOT_FOUND.increment(isReplication);
                    return false;
                }
                if (!instanceInfo.getStatus().equals(overriddenInstanceStatus)) {
                    logger.info(
                            "The instance status {} is different from overridden instance status {} for instance {}. "
                                    + "Hence setting the status to overridden status", instanceInfo.getStatus().name(),
                                    overriddenInstanceStatus.name(),
                                    instanceInfo.getId());
                    instanceInfo.setStatusWithoutDirty(overriddenInstanceStatus);

                }
            }
            renewsLastMin.increment();
            //更新租约中的有效时间
            leaseToRenew.renew();
            return true;
        }
    }

服务剔除

如果Eureka Client在注册后,既没有续约,也没有下线(服务崩溃或者网络异常等原因),那么服务的状态就处于不可知的状态,不能保证能够从该服务实例中获取到回馈,所以需要服务剔除AbstractInstanceRegistry#evict方法定时清理这些不稳定的服务,该方法会批量将注册表中所有过期租约剔除
服务剔除#evict方法中有很多限制,都是为了保证Eureka Server的可用性:

  • 自我保护时期不能进行服务剔除操作。
  • 过期操作是分批进行。
  • 服务剔除是随机逐个剔除,剔除均匀分布在所有应用中,防止在同一时间内同一服务集群中的服务全部过期被剔除,以致大量剔除发生时,在未进行自我保护前促使了程序的崩溃。

总结

Eureka为Spring Cloud提供了高可用的服务发现与注册组件,利用Eureka, SpringCloud开发者能够更快地融入到微服务的开发中。Eureka Server作为服务注册中心,为Eureka Client提供服务注册和服务发现的能力,它既可单机部署,也可以通过集群的方式进行部署,通过自我保护机制和集群同步复制机制保证Eureka的高可用性和网络分区容忍性,保证Eureka Server集群的注册表数据的最终一致性;Eureka Client方便了与Eureka Server的交互,它与Eureka Server的一切交互,包括服务注册、发送心跳续租、服务下线和服务发现,都是在后台自主完成的,简化了开发者的开发工作。
当然Eureka也存在缺陷。由于集群间的同步复制是通过HTTP的方式进行,基于网络的不可靠性,集群中的Eureka Server间的注册表信息难免存在不同步的时间节点,不满足CAP中的C(数据一致性)。

参考

https://blog.csdn.net/YKenan/article/details/106033838

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值