【源码】Spring Cloud —— Eureka Client 2 DiscoveryClient

前言

上一章节对 Spring Cloud Netflix Eureka Client 提供的 核心组件类 做了大致的了解,本章节结合部分源码解读核心类 DiscoveryClient

该系列章节需要相关的内容做铺垫,传送门:

关于延时、周期任务调度 —— ScheduledExecutorService ScheduledThreadPoolExecutor

版本

Spring Cloud Netflix 版本:2.2.3.RELEASE
对应 Netflix-Eureka 版本:1.9.21

DiscoveryClient

DiscoveryClient 的核心业务逻辑发生在 构造方法 中,也就是说,在启动 Spring 应用注册对应的 Bean 时同时注册对应的 服务实例

	if (!config.shouldRegisterWithEureka() && !config.shouldFetchRegistry()) {
            
        // 略

        return;
    }

属性 shouldRegisterWithEurekashouldFetchRegistryfalse 时,不进行 服务注册、发现,即对应我们的配置项 eureka.client.register-with-eurekaeureka.client.fetch-register

     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()
     );

     cacheRefreshExecutor = new ThreadPoolExecutor(
             1, clientConfig.getCacheRefreshExecutorThreadPoolSize(), 0, TimeUnit.SECONDS,
             new SynchronousQueue<Runnable>(),
             new ThreadFactoryBuilder()
                     .setNameFormat("DiscoveryClient-CacheRefreshExecutor-%d")
                     .setDaemon(true)
                     .build()
     );

如果允许 服务注册、发现,则实例化用于 发送心跳、刷新缓存 相关的 线程池

	eurekaTransport = new EurekaTransport();
	// 构建
    scheduleServerEndpointTask(eurekaTransport, args);

EurekaTransport 封装了 DiscoveryClientEureka Server 进行 HTTP 调用的 Jersey 客户端

注册表信息拉取

	// 允许拉取注册表信息,则进行拉取
	// 如果拉取失败,则调用 fetchRegistryFromBackup
	if (clientConfig.shouldFetchRegistry() && !fetchRegistry(false)) {
        fetchRegistryFromBackup();
    }

如果允许注册表信息拉取,则调用 fetchRegistry 方法拉取,拉取失败则调用交由成员属性 backupRegistryProvider 获取,其默认实现为 NotImplementedRegisteryImpl,即没有实现

	private boolean fetchRegistry(boolean forceFullRegistryFetch) {
        Stopwatch tracer = FETCH_REGISTRY_TIMER.start();

        try {
            Applications applications = getApplications();

            if (clientConfig.shouldDisableDelta()
                    || (!Strings.isNullOrEmpty(clientConfig.getRegistryRefreshSingleVipAddress()))
                    || forceFullRegistryFetch
                    || (applications == null)
                    || (applications.getRegisteredApplications().size() == 0)
                    || (applications.getVersion() == -1)) //Client application does not have latest library supporting delta
            {
                // 全量拉取
                getAndStoreFullRegistry();
            } else {
            	// 增量拉取
                getAndUpdateDelta(applications);
            }
    		
    		// hash 码计算
            applications.setAppsHashCode(applications.getReconcileHashCode());
            // 打印实例数
            logTotalInstances();
        } catch (Throwable e) {
            // ...
        }

        // ...
        
        return true;
    }
  • 全量拉取:第一次拉取时,必然是 全量拉取 并缓存到本地,请求 urleureka/apps,拉取的实例经过过滤、调整后缓存在 localRegionApps 属性
  • 增量拉取:之后的拉取则只针对一段时间内发生的变更信息,并根据变更的状态进行 增量式更新,请求 url/eureka/app/delta

服务注册

	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);
        }
    }

注册方法为 register,PS:这种在判断条件中执行逻辑的写法,在 JUC 源码中十分常用

	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();
    }

请求 url/eureka/${APP_NAME},传递参数为 InstanceInfo,如果服务器返回 204 状态码,则表明注册成功

定时任务

	// 初始化定时任务
	initScheduledTasks();

定时任务包括

  • 定时拉取注册列表信息,因为第一次全量拉取注册列表之后缓存在本地,因此需要定时获取 Eureka Server 的注册列表信息,进行更新
  • 心跳续约,需要定时向 Eureka Server 发送心跳以保证 服务实例 的健康
  • 按需注册,需要定时或者在 实例状态 发生改变时,重新注册 对应的实例,以保证服务的 可用性

定时拉取注册列表信息

	if (clientConfig.shouldFetchRegistry()) {
        // 拉取时间间隔,可由属性 
        // eureka.client.registry-fetch-interval-seconds 设置
        int registryFetchIntervalSeconds = clientConfig.getRegistryFetchIntervalSeconds();
        int expBackOffBound = clientConfig.getCacheRefreshExecutorExponentialBackOffBound();
        cacheRefreshTask = new TimedSupervisorTask(
                "cacheRefresh",
                scheduler,
                cacheRefreshExecutor,
                registryFetchIntervalSeconds,
                TimeUnit.SECONDS,
                expBackOffBound,
                new CacheRefreshThread()
        );
        scheduler.schedule(
                cacheRefreshTask,
                registryFetchIntervalSeconds, TimeUnit.SECONDS);
    }

定时拉取注册列表信息,初始化对应的 cacheRefreshTask 后交由 scheduler 延时执行,对于 周期 执行并未由 scheduler 实现,而是委托给了 TimedSupervisorTask#run

	@Override
    public void run() {
        Future<?> future = null;
        try {
        	// 任务执行
            future = executor.submit(task);
            threadPoolLevelGauge.set((long) executor.getActiveCount());
			
			// 结果获取
            future.get(timeoutMillis, TimeUnit.MILLISECONDS);  // block until done or timeout
            delay.set(timeoutMillis);
            threadPoolLevelGauge.set((long) executor.getActiveCount());

			// 成功计数
            successCounter.increment();
        } catch (...) {
            
            // 略
        
        } finally {
            if (future != null) {
                future.cancel(true);
            }

			// 此处再次将任务交给 scheduler,实现周期执行
            if (!scheduler.isShutdown()) {
                scheduler.schedule(this, delay.get(), TimeUnit.MILLISECONDS);
            }
        }
    }

拉取注册表信息的业务逻辑由 CacheRefreshThread 定义

	class CacheRefreshThread implements Runnable {
        public void run() {
            refreshRegistry();
        }
    }

	refreshRegistry 略

心跳续约

	// 续约周期,默认 30s
	int renewalIntervalInSecs = instanceInfo.getLeaseInfo().getRenewalIntervalInSecs();
    int expBackOffBound = clientConfig.getHeartbeatExecutorExponentialBackOffBound();
    logger.info("Starting heartbeat executor: " + "renew interval is: {}", renewalIntervalInSecs);

    heartbeatTask = new TimedSupervisorTask(
            "heartbeat",
            scheduler,
            heartbeatExecutor,
            renewalIntervalInSecs,
            TimeUnit.SECONDS,
            expBackOffBound,
            new HeartbeatThread()
    );
    scheduler.schedule(
            heartbeatTask,
            renewalIntervalInSecs, TimeUnit.SECONDS);

续约逻辑由 HeartbeatThread 提供

	private class HeartbeatThread implements Runnable {

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

	---------------------- renew ----------------------

	boolean renew() {
        EurekaHttpResponse<InstanceInfo> httpResponse;
        try {
            httpResponse = eurekaTransport.registrationClient.sendHeartBeat(instanceInfo.getAppName(), instanceInfo.getId(), instanceInfo, null);
            logger.debug(PREFIX + "{} - Heartbeat status: {}", appPathIdentifier, httpResponse.getStatusCode());
            if (httpResponse.getStatusCode() == Status.NOT_FOUND.getStatusCode()) {
                REREGISTER_COUNTER.increment();
                logger.info(PREFIX + "{} - Re-registering apps/{}", appPathIdentifier, instanceInfo.getAppName());
                long timestamp = instanceInfo.setIsDirtyWithTime();

				// 对于 404 的实例,重新进行注册
                boolean success = register();
                if (success) {
                    instanceInfo.unsetIsDirty(timestamp);
                }
                return success;
            }
            return httpResponse.getStatusCode() == Status.OK.getStatusCode();
        } catch (Throwable e) {
            logger.error(PREFIX + "{} - was unable to send heartbeat!", appPathIdentifier, e);
            return false;
        }
    }

请求的 urlapps/${APP_NAME}/${INSTANCE_INFO_ID}HTTP 方法为 put,续约成功返回 200 状态码

按需注册

	// 负责定时刷新实例,检测状态按需注册
    instanceInfoReplicator = new InstanceInfoReplicator(
            this,
            instanceInfo,
            clientConfig.getInstanceInfoReplicationIntervalSeconds(),
            2);

	// 监听器,负责监听实例的状态,按需注册
    statusChangeListener = new ApplicationInfoManager.StatusChangeListener() {
        @Override
        public String getId() {
            return "statusChangeListener";
        }

        @Override
        public void notify(StatusChangeEvent statusChangeEvent) {

            // ...

			// 按需注册
            instanceInfoReplicator.onDemandUpdate();
        }
    };

	// 注册监听器 statusChangeListener
    if (clientConfig.shouldOnDemandUpdateStatusChange()) {
        applicationInfoManager.registerStatusChangeListener(statusChangeListener);
    }

	// 定时任务启动    
    instanceInfoReplicator.start(clientConfig.getInitialInstanceInfoReplicationIntervalSeconds());

按需注册 主要分两部分

  • 启动一个定时任务 instanceInfoReplicator,定时刷新 服务实例 的信息和检查 应用状态 的变化,在 服务实例 信息发生变化的情况下向 Eureka Server 重新发起注册
  • 注册一个监听器 statusChangeListener,在 应用状态 发生变化向 Eureka Server 重新发起注册

定时任务的执行逻辑由 InstanceInfoReplicator#run 提供

	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);
        }
    }

监听器 statusChangeListener 监听到事件 StatusChangeEvent,调用 instanceInfoReplicator#onDemandUpdate

	public boolean onDemandUpdate() {
        if (rateLimiter.acquire(burstSize, allowedRatePerMinute)) {
            if (!scheduler.isShutdown()) {
                scheduler.submit(new Runnable() {
                    @Override
                    public void run() {
                        
                        // ...
    
                        InstanceInfoReplicator.this.run();
                    }
                });
                return true;
            } else {
                // ...
            }
        } else {
            // ...
        }
    }

最终也是委托给了 InstanceInfoReplicator#run

服务下线

服务下线 交由 Spring 生命周期管理,DiscoveryClient 对象销毁时会调用 unregister 方法进行服务下线,调用 urlapps/${APP_NAME}/${INSTANCE_INFO_ID},传递参数为服务名和服务实例 id,HTTP 方法为 delete

总结

本章节结合部分源码重点解读了 DiscoveryClient 类,该类主要的业务逻辑:注册表信息拉取服务注册心跳续约按需注册 等都在 构造方法 中实现,因此当我们引入对应的依赖并指定对应的 Eureka Server 时,就会注册对应的服务到 注册中心

上一篇:【源码】Spring Cloud —— Eureka Client 1 核心组件

参考

《Spring Cloud 微服务架构进阶》 —— 朱荣鑫 张天 黄迪璇

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值