springcloud实践与源码分析(2)-Eureka服务注册源码分析

602 阅读11分钟

1.Eureka详情

     上一节我们搭建了一个springcloud的微服务,构建了微服务中三个核心要素,一个服务注册中心、一个服务提供者和一个服务消费者,相信大家对微服务有了基本的了解,但是在实践中项目中的系统结构要比我们的示例要复杂的多。需要我们进一步的去调整或扩展。

    本节我们主要来讲下springcloud的服务治理的核心Eureka,在上一节中我们知道Eureka为我们构建了服务注册中心即Eureka的服务端和服务的注册和消费端即Eureka的客户端,

其结构如下图:



这个一个双注册节点的的微服务架构。服务提供者主要完成:

服务的注册

      使用rest请求的方式将自己的元数据信息注册到eureka-server上

服务的续约

      在注册完成之后,服务提供者也就是eureka-client 会维持一个心跳来告诉eureka-server 自己还存活着,防止eureka-server将自己剔除

服务的下线

     服务关闭时会通过rest请求告诉eureka-server,将该服务状态置为DOWN

服务的获取

    服务消费者发送rest请求到服务注册中心eureka-server获取可用的服务列表,同时缓存到本地,默认会30秒更新一次,可以自己设定

服务的调用

     服务消费拿到服务中心提供的服务列表时,根据服务名可以获取具体的服务实例和服务的元数据信息,根据一定的负载均衡策略来调用服务,默认采用轮询的方式获取服务

    我们大致了解了Eureka服务治理的基本情况,接下来我们通过源码来具体看下,Eureka 是如何实现这些治理细节的

2.Eureka源码分析

大家是否还记得,我们将一个spring boot 项目作为服务的提供者或者服务的消费者时,其实就做了两个动作:

 1:在启动类中配置了@EnableDiscoveryClient注解

 2:在application.properties同配置服务注册中心的位置:eureka.client.service-url.defaultZone

这个就是我们分析Eureka的切入点@EnableDiscoveryClient


我进入这个注解中



发现这个注解与DiscoveryClient 接口绑定了,实现了DiscoveryClient

我们看到包路径下有个DiscoveryClient


进入这个接口:


发现它定义了发现服务的4个抽象方法,我们进一步的看下他的实现类,这个类是被一个叫

EurekaDiscoveryClient给实现了,


    我们发现这个类中 引入了 EurekaInstanceConfig 和EurekaClient 对象,EurekaInstanceConfig是 服务注册的时的元素就配置信息,包括像服务的实例id、服务名称、心跳间隔时间、IP 地址等待,一般都是在启动时从application.properties中加载来的,

还有一个是EurekaClient接口,


他同时集成了LookupServic接口 ,定义了针对Eureka的发现服务的抽象方法,

我们进一步的去看,它真正的实现类是DiscoveryClient。   和DiscoveryClient接口同名



我们可以整理下通过 @EnableDiscoveryClient注解  eureka客户端连接注册中心时的对象关系



     首先右边接口DiscoveryClient的实现类EurekaDiscoveryClient 中依赖EurekaClient接口,EurekaClient接口集成了LookupService,并且左边的DiscoveryClient的类实现 EurekaClient接口

    接下来我们我们看看 客户端到底怎么和我们注册中心打交道的

    我们知道启动类启动的时候会加载 DiscoveryClient接口,它的实现接口EurekaDiscoveryClient中依赖两个重要的对象EurekaClient和EurekaInstanceConfig

我们看看EurekaDiscoveryClient被谁调用了,我们可以使用DEGUG,看到他被 EurekaClientAutoConfiguration 给自动加载了


     作为服务发现的主要接口EurekaClient它是怎么进行服务发现的,我们看下他的实现类DiscoveryClient

    在初始化的时候, 客户端的配置类EurekaClientConfiguration加载了DiscoveryClient的构造函数:


构造函数里引入了几个重要参数:

ApplicationInfoManager:这个类是用来管理应用实例信息的。在DiscoveryClient初始化的时候,会向EurekaServer注册服务,

EurekaClientConfig:客户端的服务的配置信息

BackupRegistry:客户端启动的时候,访问不到指定的eureka server的话,会回滚到到指定的备份注册应用表,然后将备份注册应用表返回的应用信息填充到本地。

在进行一些必要的配置后,这个构造函数中调用了一个非常重要的定时任务函数initScheduledTasks()

 /**
     * Initializes all scheduled tasks.
     */
    private void initScheduledTasks() {
        if (clientConfig.shouldFetchRegistry()) {
            // registry cache refresh timer
            int registryFetchIntervalSeconds = clientConfig.getRegistryFetchIntervalSeconds();
            int expBackOffBound = clientConfig.getCacheRefreshExecutorExponentialBackOffBound();
            scheduler.schedule(
                    new TimedSupervisorTask(
                            "cacheRefresh",
                            scheduler,
                            cacheRefreshExecutor,
                            registryFetchIntervalSeconds,
                            TimeUnit.SECONDS,
                            expBackOffBound,
                            new CacheRefreshThread()
                    ),
                    registryFetchIntervalSeconds, TimeUnit.SECONDS);
        }

        if (clientConfig.shouldRegisterWithEureka()) {
            int renewalIntervalInSecs = instanceInfo.getLeaseInfo().getRenewalIntervalInSecs();
            int expBackOffBound = clientConfig.getHeartbeatExecutorExponentialBackOffBound();
            logger.info("Starting heartbeat executor: " + "renew interval is: " + renewalIntervalInSecs);

            // Heartbeat timer
            scheduler.schedule(
                    new TimedSupervisorTask(
                            "heartbeat",
                            scheduler,
                            heartbeatExecutor,
                            renewalIntervalInSecs,
                            TimeUnit.SECONDS,
                            expBackOffBound,
                            new HeartbeatThread()
                    ),
                    renewalIntervalInSecs, TimeUnit.SECONDS);

            // InstanceInfo replicator
            instanceInfoReplicator = new InstanceInfoReplicator(
                    this,
                    instanceInfo,
                    clientConfig.getInstanceInfoReplicationIntervalSeconds(),
                    2); // burstSize

            statusChangeListener = new ApplicationInfoManager.StatusChangeListener() {
                @Override
                public String getId() {
                    return "statusChangeListener";
                }

                @Override
                public void notify(StatusChangeEvent statusChangeEvent) {
                    if (InstanceStatus.DOWN == statusChangeEvent.getStatus() ||
                            InstanceStatus.DOWN == statusChangeEvent.getPreviousStatus()) {
                        // log at warn level if DOWN was involved
                        logger.warn("Saw local status change event {}", statusChangeEvent);
                    } else {
                        logger.info("Saw local status change event {}", statusChangeEvent);
                    }
                    instanceInfoReplicator.onDemandUpdate();
                }
            };

            if (clientConfig.shouldOnDemandUpdateStatusChange()) {
                applicationInfoManager.registerStatusChangeListener(statusChangeListener);
            }

            instanceInfoReplicator.start(clientConfig.getInitialInstanceInfoReplicationIntervalSeconds());
        } else {
            logger.info("Not registering with Eureka server per configuration");
        }
    }

    我们发现这个定时任务,根据clientConfig.shouldFetchRegistry()和clientConfig.shouldRegisterWithEureka() 开启了服务发现和服务注册两个任务计划

   我们首先看下服务注册的,在服务注册中启动一个心跳定时计划和一个服务注册的服务元数据复制器instanceInfoReplicator,它是个线程类。

   我们首先看下 服务的初始化注册,然后在看下客户端的心跳计划


服务的初始化注册

      instanceInfoReplicator用于更新本地InstanceInfo并将其复制到远程服务器的任务。它是个任务类,负责将自身的信息周期性的上报到Eureka server,InstanceInfo就是我们客户端和服务端维持信息交互的载体,也就是服务的元数据信息,我们可以看下对象里都记录了啥


    可以看出主要记录该客户端服务基本属性,包括像服务实例ID、服务名、Ip地址、端口号等等。

instanceInfoReplicator.start(clientConfig.getInitialInstanceInfoReplicationIntervalSeconds());

getInitialInstanceInfoReplicationIntervalSeconds() :多久实例信息复制到Eureka服务器的时间,默认40s


我们进入instanceInfoReplicator:


class InstanceInfoReplicator implements Runnable {
  
   
    public void start(int initialDelayMs) {
        if (started.compareAndSet(false, true)) {
            instanceInfo.setIsDirty();  // for initial register
            Future next = scheduler.schedule(this, initialDelayMs, TimeUnit.SECONDS);
            scheduledPeriodicRef.set(next);
        }
    }

    public void stop() {
        scheduler.shutdownNow();
        started.set(false);
    }

    public boolean onDemandUpdate() {
        if (rateLimiter.acquire(burstSize, allowedRatePerMinute)) {
            if (!scheduler.isShutdown()) {
                scheduler.submit(new Runnable() {
                    @Override
                    public void run() {
                        logger.debug("Executing on-demand update of local InstanceInfo");
    
                        Future latestPeriodic = scheduledPeriodicRef.get();
                        if (latestPeriodic != null && !latestPeriodic.isDone()) {
                            logger.debug("Canceling the latest scheduled update, it will be rescheduled at the end of on demand update");
                            latestPeriodic.cancel(false);
                        }
    
                        InstanceInfoReplicator.this.run();
                    }
                });
                return true;
            } else {
                logger.warn("Ignoring onDemand update due to stopped scheduler");
                return false;
            }
        } else {
            logger.warn("Ignoring onDemand update due to rate limiter");
            return false;
        }
    }

    public void run() {
        try {
            discoveryClient.refreshInstanceInfo();

            Long dirtyTimestamp = instanceInfo.isDirtyWithTime();
            if (dirtyTimestamp != null) {
                discoveryClient.register();
                instanceInfo.unsetIsDirty(dirtyTimestamp);
            }
        } catch (Throwable t) {
            logger.warn("There was a problem with the instance info replicator", t);
        } finally {
            Future next = scheduler.schedule(this, replicationIntervalSeconds, TimeUnit.SECONDS);
            scheduledPeriodicRef.set(next);
        }
    }

}

      start()将 对InstanceInfo设置一个标志,表示该服务发送变化或者没有被注册,需要去注册。并且开启延时线程任务 Future next = scheduler.schedule(this, initialDelayMs, TimeUnit.SECONDS);, 停顿initialDelayMs秒执行该任务。 

onDemandUpdate这个方法主要是在InstanceInfo变化监听器里面被调用

instanceInfoReplicator是一个负责服务注册的线程任务, 有两个地方可以执行这个任务

1.定时线程,每40秒执行一次。

2.当instance的状态发生变更(除去DOWN这个状态)的时候,会有statusChangeListener 这个监听器监听到去执行服务注册。

instance的状态发生变更有兴趣可以自己去看下,我们主要看定时线程任务如何去注册服务的,

我们看下run 函数这个就是定时线程任务执行的逻辑:

      首先 discoveryClient.refreshInstanceInfo()去刷新下本地实例,在观察到有更改后,instanceinfo上的isdirty标志设置为true。以便让他再次去注册。我们具体深入看下refreshInstanceInfo()里的逻辑:


1. refreshDataCenterInfoIfRequired()判断数据中心的数据是否发生改变,

2.refreshLeaseInfoIfRequired()判断配置是否发生改变

 数据中心里主要查看ip地址是否发生变化,具体可以看下:


在获取新的ip地址时 他会判断数据中心是否是AWS,如果是AWS的话,会进行一些特殊处理

一般都走 else 分支语句,获取我们本地的IP地址,最后如果地址发生变化,一定要 instanceInfo.setIsDirty(),表示该服务发生变化,需要重新注册。

返回上一层我看看refreshLeaseInfoIfRequired(),,它判断配置是否发生变化,主要包括 续约时间、续约过期时间等

      如果我们进行了更改,那么客户端就需要更新本地instance的信息,同时调用setIsDirty()方法表示信息已经被改变,需要重新注册

   好了! discoveryClient.refreshInstanceInfo() 刷新本地实例已经看完了,我们接着往下走

来到下面这里:


   检查实例是否发生变化  Long dirtyTimestamp = instanceInfo.isDirtyWithTime();

如果没有变化就不去注册了,如果有变化 他就会返回最近一次修改的时间,从而去调用 discoveryClient.register()注册服务。注册完之后调用了instanceInfo.unsetIsDirty(dirtyTimestamp) ,表面这服务已经注册过了,不需要去注册了。

     最后finally 里设置  replicationIntervalSeconds(40秒)再次执行这个线程任务,从而形成一个不断执行的定时任务去去注册服务。

    我们最关心的是服务具体的是怎去注册的也就是discoveryClient.register()里面的内容。

上面讲的这个其实是在我们项目启动后,每隔40秒,我们的客户端会扫描下自己是否实例发生变化然后注册到服务端,

      其实我们客户端启动时,会直接注册一次服务。也就是说当应用启动的时候,如果应用开启了自动注册(默认开启), 那么在自动配置类加载的时候,会通过EurekaAutoServiceRegistration实例化的时候,去改变instance的status, 最终被监听器监听到,执行服务注册的代码,这一块有兴趣的同学可以自己去看下

    但最终我们注册服务会调用discoveryClient.register()来注册我们的服务:

我么看下discoveryClient.register()是如何注册服务的

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

eurekaTransport是DiscoveryClient的静态内部类,使用EurekaHttpClient去注册服务。


EurekaHttpClient是一个接口,他有个实现抽象类EurekaHttpClientDecorator


可以看出其实这里利用装配者设计模式,EurekaHttpClientDecorator实现了EurekaHttpClient,并依赖了EurekaHttpClient的实现类AbstractJerseyEurekaHttpClient

最后调用了AbstractJerseyEurekaHttpClient的register方法


      最后利用Jersey RESTful  向地址serviceUrl发送restful请求,将服务元数据对象InstanceInfo发送给服务注册中心。这serviceUrl其实就是我们配置的服务注册中心的地址,后续我们介绍,客户端是怎么获取这个serviceUrl的。总之现在我们的客户端已经向服务注册中心注册了自己的服务了。

心跳计划

initScheduledTasks 中我们服务注册时还有个心跳定时任务,心跳就是我们客户端需要时刻要告诉我们的服务注册中心我还活着,防止注册中心将我们从服务列表中剔除掉

int renewalIntervalInSecs = instanceInfo.getLeaseInfo().getRenewalIntervalInSecs();
int expBackOffBound = clientConfig.getHeartbeatExecutorExponentialBackOffBound();
  // Heartbeat timer
            scheduler.schedule(
                    new TimedSupervisorTask(
                            "heartbeat",
                            scheduler,
                            heartbeatExecutor,
                            renewalIntervalInSecs,
                            TimeUnit.SECONDS,
                            expBackOffBound,
                            new HeartbeatThread()
                    ),
                    renewalIntervalInSecs, TimeUnit.SECONDS);

renewalIntervalInSecs 就是我们定义的间隔多久向服务注册中心发送心跳,这个可以自己设定

我们来看看,心跳的实现,利用了HeartbeatThread线程

 private class HeartbeatThread implements Runnable {

        public void run() {
            if (renew()) {
                lastSuccessfulHeartbeatTimestamp = System.currentTimeMillis();
            }
        }
    }

线程调用renew()续约方法,如果续约成功,lastSuccessfulHeartbeatTimestamp最新续约成功时间改为当前时间。

我们可以看出这个线程任务执行了一个”时间监控任务“TimedSupervisorTask,并且传入了:

(1)延迟线程池scheduler, 延时 客户端指定的续订间隔renewalIntervalInSecs 执行TimedSupervisorTask

(2)一个线程池ThreadPoolExecutor  heartbeatExecutor,执行心跳线程HeartbeatThread

(3)renewalIntervalInSecs  客户端指定的续订间隔

(4)expBackOffBound:重试延迟

(5)HeartbeatThread() 心跳线程

我们进入TimedSupervisorTask这个任务中去:


heartbeatExecutor执行 HeartbeatThread心跳线程,最后延时任务再次执行这个TimedSupervisorTask,形成心跳续约的定时计划

我们进一步看下HeartbeatThread中调用的renew()续约方法


      他依然通过装配者EurekaHttpClientDecorator,利用EurekaHttpClient的实现者AbstractJerseyEurekaHttpClient 来发送心跳请求


   与注册不同的是,是发送的GET请求 发送服务的状态staus,'UP'或者'DOWN'来表示服务的存活或死亡

这时客户端已经续约完成了,如果发送心跳失败 返回404 程序,renew()中还会进行一次服务注册,保证服务的可靠

 if (httpResponse.getStatusCode() == 404) {
                REREGISTER_COUNTER.increment();
                logger.info("{} - Re-registering apps/{}", PREFIX + appPathIdentifier, instanceInfo.getAppName());
                return register();
            }

    至此我们客户端进行服务的注册和服务的续约,我们分析完了,其实核心就是:

     客户端的心跳,客户端会启动一个延时任务不断的去调用心跳线程,从而完成一个不断续约的定时任务。

      服务注册则是启动一个延时任务去执行服务元数据复制器线程InstanceInfoReplicator 去进行注册,注册完之后又会再次启动这个延时任务 ,从而完成一个只要服务元数据发送变化就去注册的这样一个定时任务。

    而服务的元数据是否发生变化由其中的 instanceInfo.setIsDirty()来设置标志。

下一节我们继续分析下服务的发现和服务端的功能。