微服务开发(8)--Eureka源码深入分析(客户端)

Eureka客户端解析

下图是我们Eureka客户端的主要流程分析:
在这里插入图片描述

根据上图分析源码

1. 查看我们的pom文件,发现我们导入了maven依赖

        <!--Eureka的客户端依赖-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>

2. 根据我们导入的maven依赖查看导入的源码,查看其下面的spring.factories文件,利用SpringBoot的原理查看自动导入的类
在这里插入图片描述

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

3. 我们观察上面自动导入的类,有点多,这时候就靠猜了,看看和哪一个相关,这里我们查看的是EurekaClientAutoConfiguration
4. 查看EurekaClientAutoConfiguration类
在这里插入图片描述
(4.1)这里我们关注下@AutoConfigureAfter注解。该注解的意思是当前类EurekaClientAutoConfiguration会在该注解中的类加载完成之后再加载。
(4.2)我们的@AutoConfigureAfter中导入了org.springframework.cloud.netflix.eureka.EurekaDiscoveryClientConfiguration
该类的作用就是创建一个Marker对象
在这里插入图片描述
(4.3)再回头看EurekaClientAutoConfiguration类的@ConditionalOnBean(EurekaDiscoveryClientConfiguration.Marker.class)注解,发现该注解的意思就是当我们类中有Marker对象的时候进行加载,这样我们就已将其联系起来。
在这里插入图片描述
5. EurekaClientAutoConfiguration类的Bean加载太多,我们只看关键的。这里我们看下DiscoveryClient类。
在这里插入图片描述
(5.1)这里我们自动注入了EurekaClient,该类也是在EurekaClientAutoConfiguration加载到Spring中的。

6. 观察EurekaClient类
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
(6.1)我们一步一步点进去,最终是来到了com.netflix.discovery.DiscoveryClient 中的构造函数

7. 观察该构造函数

    @Inject
    DiscoveryClient(ApplicationInfoManager applicationInfoManager, EurekaClientConfig config, AbstractDiscoveryClientOptionalArgs args,
                    Provider<BackupRegistry> backupRegistryProvider) {
   
   
                    
		// .... 省略关键代码   
        
        logger.info("Initializing Eureka in region {}", clientConfig.getRegion());

		// .... 省略关键代码
		
        try {
   
   
            // 定义一个线程池,设置其为守护线程,该线程池是一个延迟或者定期执行的线程池
            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

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

        if (clientConfig.shouldFetchRegistry() && !fetchRegistry(false)) {
   
   
            fetchRegistryFromBackup();
        }

        // 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();
        logger.info("Discovery Client initialized at timestamp {} with initial instances count: {}",
                initTimestampMs, this.getApplications().size());
    }

(7.1)@Inject注解的作用是其参数在运行时由配置好的IoC容器提供

8. 初始化时启动核心功能的定时任务:initScheduledTasks()

    /**
     * Initializes all scheduled tasks.
     */
    private void initScheduledTasks() {
   
   
    	// 服务是否开启拉取,是否开启了fetch-register
        if (clientConfig.shouldFetchRegistry()) {
   
   
            // 服务注册列表的更新的周期时间
            int registryFetchIntervalSeconds = clientConfig.getRegistryFetchIntervalSeconds();
            int expBackOffBound = clientConfig.getCacheRefreshExecutorExponentialBackOffBound();
            // 定时更新服务的注册列表
            scheduler.schedule(
                    new TimedSupervisorTask(
                            "cacheRefresh",
                            scheduler,
                            cacheRefreshExecutor,
                            registryFetchIntervalSeconds,
                            TimeUnit.SECONDS,
                            expBackOffBound,
                            // 具体的更新服务的方法的具体逻辑
                            new CacheRefreshThread()
                    ),
                    registryFetchIntervalSeconds, TimeUnit.SECONDS);
        }
		
		// 如果我们允许当前服务向Eureka注册
        if (clientConfig.shouldRegisterWithEureka()) {
   
   
        	// 服务续约的周期时间
            int renewalIntervalInSecs = instanceInfo.getLeaseInfo().getRenewalIntervalInSecs();
            int expBackOffBound = clientConfig.getHeartbeatExecutorExponentialBackOffBound();
            // 从日志看,如果我们没有配置续约的时间,默认是30s
            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);
            }
			// 调用start方法,该方法中会启动线程,该线程的作用是执行服务注册的逻辑
            instanceInfoReplicator.start(clientConfig.getInitialInstanceInfoReplicationIntervalSeconds());
        } else {
   
   
            logger.info("Not registering with Eureka server per configuration");
        }
    }

9. initScheduledTasks方法中的TimedSupervisorTask任务

  • 我们在启动服务续约和定时更新服务注册表的线程都是通过new TimedSupervisorTask来实现的。
    在这里插入图片描述
  • 这里我们详细看下这个类的作用,TimedSupervisorTask类是一个Runnable接口,主要这里看其run方法
    @Override
    public void run() {
   
   
        Future<?> future = null;
        try {
   
   
        	// 该task是我们新建该类的对象的时候传进来的一个对象
        	// 如果我们是用来进行服务续约则该task就是new HeartbeatThread()
        	// 如果我们是用来更新服务注册表的话,该task就是new CacheRefreshThread()
        	// future模式执行线程
            future = executor.submit(task);
            threadPoolLevelGauge.set((long) executor.getActiveCount());
            // 指定等待子线程的最长的时间
            future.get(timeoutMillis, TimeUnit.MILLISECONDS);  // block until done or timeout
            // 这一步很关键,这里记得我们每次都会重新赋值这个值时间,delay变量在这里每次都被重置
            delay.set(timeoutMillis);
            threadPoolLevelGauge.set((long) executor.getActiveCount());
        } catch (TimeoutException e) {
   
   
        	// 进入到这里表示的是执行超时,在规定的时间内线程还没有执行完成
            logger.warn("task supervisor timed out", e);
            timeoutCounter.increment();
			// 得到当前delay的值
            long currentDelay = delay.get();
            // 任务线程超时的时候,就把delay变量翻倍,但是不会超过外部调用的时候设定的最大的延误时间
            long newDelay = Math.min(maxDelay, currentDelay * 2);
            // 将delay设置为最新的值,注意,这里因为有并发的情况。使用了CAS的机制,进行赋值
            // CAS就是将内存中的值和期望值做对比,若一致,将其改为更新值
            // 这里就是用cureentDelay的值和内存的值做对比,若相等,将其值改为newDelay
            delay.compareAndSet(currentDelay, newDelay);

        } catch (RejectedExecutionException e) {
   
   
        	// 一旦线程池的阻塞队列中放满了待处理的任务,出发了拒绝策略,就会停止掉调度器
            if (executor.isShutdown() || scheduler.isShutdown()) {
   
   
                logger.warn("task supervisor shutting down, reject the task", e);
            } else {
   
   
                logger.warn("task supervisor rejected the task", e);
            }

            rejectedCounter.increment();
        } catch (Throwable e) {
   
   
            if (executor.isShutdown() || scheduler.isShutdown()) {
   
   
                logger.warn("task supervisor shutting down, can't accept the task");
            } else {
   
   
                logger.warn("task supervisor threw an exception", e);
            }

            throwableCounter.increment();
        } finally {
   
   
            if (future != null) {
   
   
                future.cancel(true);
            }

            if (!scheduler.isShutdown()) {
   
   
            	// 亮点啊 这里是只要调度器没有停止,就等待一段时间后,再执行一次任务
            	// 不管我们是服务续约还是服务注册表的更新,都不应该只执行一遍,应该不停的执行
            	// 这里我们重新调用其定时任务将其执行
            	// 这里的delay的时间已经变为原来设置值的2倍
            	// 假设外部调用的时候传入的超时时间是30s,构造方法中的timeout,最大的时间间隔为50s,构造方法中的expBackOffBound
            	// 如果最近一次的任务没有超时,那么再30s后重新开始新的任务
            	// 如果最近一次的任务超时了,那么就在50s后开始新的任务(这里的50s因为我们最大的间隔是50s,30*2大于了50,所以这里取的是50s)
                scheduler.schedule(this, delay.get(), TimeUnit.MILLISECONDS);
            }
        }
    }
  • scheduler.schedule(this, delay.get(), TimeUnit.MILLISECONDS);从代码的注释上可以看出这个方法是一次性调用的方法,但是实际上这个方法执行的任务是会反复执行的(服务续约,服务更新注册表),这里的关键就是类TimedSupervisorTask的run方法里,run方法任务执行完成之后,会再次调用schedule方法,在指定的时间之后执行一次相同的任务,这个时间间隔和最近一次任务是否超时有关,如果超时了则下一次执行任务的间隔时间就会变大。
  • 代码精髓

从整体上来看,TimedSupervisorTask是固定间隔的周期性任务,一旦遇到了超时就会将下一个周期的间隔时间调大,如果连续超时的话,那么每次间隔时间都会增大一倍,一直到达外部参数设定的上限为止,因为只要超时就会进catch方法中,2倍扩充间隔时间,一旦新任务不再超时,间隔时间又会恢复初始值,不进入catch中,将delay重新赋值为初始。另外还有CAS来控制多线程的同步

  1. 定时更新服务注册列表线程CacheRefreshThread
    在这里插入图片描述
  • 代码解析
    /**
     * The task that fetches the registry information at specified intervals.
     *
     */
    class CacheRefreshThread implements Runnable {
   
   
        public void run() {
   
   
        	// 多线程开始执行发方法
            refreshRegistry();
        }
    }

	// 多线程执行的方法
	// 用来定时刷新服务注册表
    @VisibleForTesting
    void refreshRegistry() {
   
   
        try {
   
   
            boolean isFetchingRemoteRegionRegistries = isFetchingRemoteRegionRegistries();

            boolean remoteRegionsModified = false;
            // This makes sure that a dynamic change to remote regions to fetch is honored.
            String latestRemoteRegions = clientConfig.fetchRegistryForRemoteRegions();
            // 这些东西根本不用看,我们没有设置亚马逊的云,不会执行if逻辑
            if (null != latestRemoteRegions) {
   
   
                String currentRemoteRegions = remoteRegionsToFetch.get();
                if (!latestRemoteRegions.equals(currentRemoteRegions)) {
   
   
                    // Both remoteRegionsToFetch and AzToRegionMapper.regionsToFetch need to be in sync
                    synchronized (instanceRegionChecker.getAzToRegionMapper()) {
   
   
                        if (remoteRegionsToFetch.compareAndSet(currentRemoteRegions, latestRemoteRegions)) {
   
   
                            String[] remoteRegions = latestRemoteRegions.split(",");
                            remoteRegionsRef.set(remoteRegions);
                            instanceRegionChecker.getAzToRegionMapper().setRegionsToFetch(remoteRegions);
                            remoteRegionsModified = true;
                        } else {
   
   
                            logger.info("Remote regions to fetch modified concurrently," +
                                    " ignoring change from {} to {}", currentRemoteRegions, latestRemoteRegions);
                        }
                    }
                } else 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值