Eureka源碼深度解析(下)

答上期問題:

    接着上篇文章 Eureka源碼深度解析(上) 來看,上篇文章,我們留下了一個問題,就是這些服務註冊、續約的方法是什麼時候被調用的呢?

    我們還是來看下com.netflix.discovery.DiscoveryClient這個類,這個類在應用啓動的時候被加載到容器中,肯定會調用其構造方法,構造方法如下:

    @Inject
    DiscoveryClient(ApplicationInfoManager applicationInfoManager, EurekaClientConfig config, AbstractDiscoveryClientOptionalArgs args,
                    Provider<BackupRegistry> backupRegistryProvider) {
        // 一系列的參數配置
        ...

        try {
            // 下面是幾個線程池的創建
            scheduler = Executors.newScheduledThreadPool(3,
                    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

            ...
            
        // 重要方法在這裏
        initScheduledTasks();
        try {
            Monitors.registerObject(this);
        } catch (Throwable e) {
            logger.warn("Cannot register timers", e);
        }
        ...
    }
        
//initScheduledTasks()
    private void initScheduledTasks() {
        if (clientConfig.shouldFetchRegistry()) {
            // registry cache refresh timer
            int registryFetchIntervalSeconds = clientConfig.getRegistryFetchIntervalSeconds();
            int expBackOffBound = clientConfig.getCacheRefreshExecutorExponentialBackOffBound();
            // 1.執行刷新任務的定時器
            scheduler.schedule(
                    new TimedSupervisorTask(
                            "cacheRefresh",
                            scheduler,
                            cacheRefreshExecutor,
                            registryFetchIntervalSeconds,
                            TimeUnit.SECONDS,
                            expBackOffBound,
                            //線程任務在這裏,主要就是爲了執行這個Thread的run()
                            new CacheRefreshThread()//在1)中繼續詳細分析
                    ),
                    registryFetchIntervalSeconds, TimeUnit.SECONDS);
        }

        // 2.客戶端默認註冊到eureka,shouldRegisterWithEureka()默認爲true
        if (clientConfig.shouldRegisterWithEureka()) {
            int renewalIntervalInSecs = instanceInfo.getLeaseInfo().getRenewalIntervalInSecs();
            int expBackOffBound = clientConfig.getHeartbeatExecutorExponentialBackOffBound();
            logger.info("Starting heartbeat executor: " + "renew interval is: " + renewalIntervalInSecs);

            // 3.執行心跳任務的定時器
            scheduler.schedule(
                    new TimedSupervisorTask(
                            "heartbeat",
                            scheduler,
                            heartbeatExecutor,
                            renewalIntervalInSecs,
                            TimeUnit.SECONDS,
                            expBackOffBound,
                            // 主要就是執行該Thread的run()
                            new HeartbeatThread()// 在2)中詳細分析
                    ),
                    renewalIntervalInSecs, TimeUnit.SECONDS);

            // 獲取當前註冊服務的基本信息
            instanceInfoReplicator = new InstanceInfoReplicator(
                    this,
                    instanceInfo,
                    clientConfig.getInstanceInfoReplicationIntervalSeconds(),
                    2); // burstSize

            ...

            // 4.將當前服務註冊到註冊中心 
            // 在3)中詳細分析
            instanceInfoReplicator.start(clientConfig.getInitialInstanceInfoReplicationIntervalSeconds());
        } else {
            logger.info("Not registering with Eureka server per configuration");
        }
    }        

    1)CacheRefreshThread執行刷新任務的線程

    class CacheRefreshThread implements Runnable {
        public void run() {
            refreshRegistry();//主要定時執行該方法
        }
    }

    @VisibleForTesting
    void refreshRegistry() {
        try {
            ...
            // 獲取註冊信息,客戶端定時去刷新服務信息
            boolean success = fetchRegistry(remoteRegionsModified);
            if (success) {
                registrySize = localRegionApps.get().size();
                lastSuccessfulRegistryFetchTimestamp = System.currentTimeMillis();
            }

            ...
        } catch (Throwable e) {
            logger.error("Cannot fetch registry from server", e);
        }        
    }
//fetchRegistry()
    private boolean fetchRegistry(boolean forceFullRegistryFetch) {
        Stopwatch tracer = FETCH_REGISTRY_TIMER.start();

        try {
            // If the delta is disabled or if it is the first time, get all
            // applications
            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
            {
                ...
                // 主要執行該方法(發送HTTP請求到註冊中心來獲取註冊信息,並緩存到本地)
                getAndStoreFullRegistry();
            } else {
                getAndUpdateDelta(applications);
            }
            applications.setAppsHashCode(applications.getReconcileHashCode());
            logTotalInstances();
        } catch (Throwable e) {
            logger.error(PREFIX + appPathIdentifier + " - was unable to refresh its cache! status = " + e.getMessage(), e);
            return false;
        } finally {
            if (tracer != null) {
                tracer.stop();
            }
        }
        ...
    }        

    2)HeartbeatThread執行心跳續約任務的線程

    private class HeartbeatThread implements Runnable {

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

//續約任務
    boolean renew() {
        EurekaHttpResponse<InstanceInfo> httpResponse;
        try {
            // 1.發送HTTP請求到註冊中心
            httpResponse = eurekaTransport.registrationClient.sendHeartBeat(instanceInfo.getAppName(), instanceInfo.getId(), instanceInfo, null);
            logger.debug("{} - Heartbeat status: {}", PREFIX + appPathIdentifier, httpResponse.getStatusCode());
            // 2.如果請求響應失敗,則重新調用註冊方法
            if (httpResponse.getStatusCode() == 404) {
                REREGISTER_COUNTER.increment();
                logger.info("{} - Re-registering apps/{}", PREFIX + appPathIdentifier, instanceInfo.getAppName());
                return register();
            }
            return httpResponse.getStatusCode() == 200;
        } catch (Throwable e) {
            logger.error("{} - was unable to send heartbeat!", PREFIX + appPathIdentifier, e);
            return false;
        }
    }

    3)InstanceInfoReplicator.instanceInfoReplicator.start(clientConfig.getInitialInstanceInfoReplicationIntervalSeconds())發送註冊請求到註冊中心

    public void start(int initialDelayMs) {
        if (started.compareAndSet(false, true)) {
            instanceInfo.setIsDirty();  // for initial register
            // 啓動一個定時任務,任務指向this,
            // class InstanceInfoReplicator implements Runnable ,由該類可知,真正執行的是其run()方法
            Future next = scheduler.schedule(this, initialDelayMs, TimeUnit.SECONDS);
            scheduledPeriodicRef.set(next);
        }
    }

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

            Long dirtyTimestamp = instanceInfo.isDirtyWithTime();
            if (dirtyTimestamp != null) {
                // 真正的註冊實現在這裏,參考上篇問章可知,這裏也是發送一個HTTP請求到註冊中心
                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);
        }
    }

    總結:由以上代碼分析可知,在DiscoveryClient被創建的時候,在其構造方法中,啓動了三個線程池,

    然後分別啓動了三個定時器任務:註冊當前服務到註冊中心;持續發送心跳進行續約任務;定時刷新註冊中心註冊細信息到本地

    所以可以說,在項目啓動的時候這些任務就被執行了

 

 

前言:

    下面我們接着來看本期的內容。上期看了作爲Eureka客戶端的一系列行爲,那麼做爲服務端的註冊中心,又做了哪些事情呢?是如何接收客戶端的一系列請求呢,註冊信息又是如何存儲的呢?

    想了解Eureka的具體使用的可參考筆者的另一篇文章:https://blog.csdn.net/qq_26323323/article/details/78652849  

    可知,Eureka服務端,maven引入了spring-cloud-starter-eureka-server,在Application類上引入了@EnableEurekaServer註解,那麼一切都是從註解開始的,我們就先從該註解開始分析

 

1.@EnableEurekaServer

@EnableDiscoveryClient
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(EurekaServerMarkerConfiguration.class)// 主要就是註冊該類到Spring中
public @interface EnableEurekaServer {

}

// EurekaServerMarkerConfiguration
@Configuration
public class EurekaServerMarkerConfiguration {

	@Bean
	public Marker eurekaServerMarkerBean() {
		return new Marker();
	}

	class Marker {
	}
}

    看下該類還是蠻失望的,基本就是個空架子,那麼@EnableEurekaServer究竟是如何提供服務的呢?

    通過之前的文章,我們實際可以發現SpringBoot相關項目的一些套路了,很多的類並不是被顯示的加載到容器中,而是通過配置的方式,最經典的方式就是放到META-INF/spring.factories文件中去加載,那麼我們也來看下spring-cloud-netflix-eureka-server-1.3.1.RELEASE-sources.jar這個包下的這個文件,具體內容如下:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  org.springframework.cloud.netflix.eureka.server.EurekaServerAutoConfiguration

    通過筆者的另一篇文章https://blog.csdn.net/qq_26323323/article/details/81204284 可知, EnableAutoConfiguration對應的value值列表中的類會在SpringBoot項目啓動的時候註冊到Spring容器中,那麼EurekaServerAutoConfiguration會被默認加載到Spring中,真正的動作應該都在這個類裏。

 

 

2.EurekaServerAutoConfiguration

@Configuration
@Import(EurekaServerInitializerConfiguration.class)
@ConditionalOnBean(EurekaServerMarkerConfiguration.Marker.class)
@EnableConfigurationProperties({ EurekaDashboardProperties.class,
		InstanceRegistryProperties.class })
@PropertySource("classpath:/eureka/server.properties")
public class EurekaServerAutoConfiguration extends WebMvcConfigurerAdapter {
	...

	@Configuration
	protected static class EurekaServerConfigBeanConfiguration {
		@Bean
		@ConditionalOnMissingBean
        // 1.創建並加載EurekaServerConfig的實現類,主要是Eureka-server的配置信息
		public EurekaServerConfig eurekaServerConfig(EurekaClientConfig clientConfig) {
			EurekaServerConfigBean server = new EurekaServerConfigBean();
			if (clientConfig.shouldRegisterWithEureka()) {
				// Set a sensible default if we are supposed to replicate
				server.setRegistrySyncRetries(5);
			}
			return server;
		}
	}

	@Bean
	@ConditionalOnProperty(prefix = "eureka.dashboard", name = "enabled", matchIfMissing = true)
    // 2.Eureka-server的可視化界面就是通過EurekaController提供的
	public EurekaController eurekaController() {
		return new EurekaController(this.applicationInfoManager);
	}
    ...

	@Bean
    // 3.接收客戶端的註冊等請求就是通過InstanceRegistry來處理的
    // 是真正處理業務的類,接下來會詳細分析
	public PeerAwareInstanceRegistry peerAwareInstanceRegistry(
			ServerCodecs serverCodecs) {
		this.eurekaClient.getApplications(); // force initialization
		return new InstanceRegistry(this.eurekaServerConfig, this.eurekaClientConfig,
				serverCodecs, this.eurekaClient,
				this.instanceRegistryProperties.getExpectedNumberOfRenewsPerMin(),
				this.instanceRegistryProperties.getDefaultOpenForTrafficCount());
	}

	@Bean
	@ConditionalOnMissingBean
	public PeerEurekaNodes peerEurekaNodes(PeerAwareInstanceRegistry registry,
			ServerCodecs serverCodecs) {
		return new PeerEurekaNodes(registry, this.eurekaServerConfig,
				this.eurekaClientConfig, serverCodecs, this.applicationInfoManager);
	}

	@Bean
	public EurekaServerContext eurekaServerContext(ServerCodecs serverCodecs,
			PeerAwareInstanceRegistry registry, PeerEurekaNodes peerEurekaNodes) {
		return new DefaultEurekaServerContext(this.eurekaServerConfig, serverCodecs,
				registry, peerEurekaNodes, this.applicationInfoManager);
	}

	@Bean
    // 4.初始化Eureka-server,會同步其他註冊中心的數據到當前註冊中心
	public EurekaServerBootstrap eurekaServerBootstrap(PeerAwareInstanceRegistry registry,
			EurekaServerContext serverContext) {
		return new EurekaServerBootstrap(this.applicationInfoManager,
				this.eurekaClientConfig, this.eurekaServerConfig, registry,
				serverContext);
	}
	...
}

    總結:通過以上分析可知,EurekaServer在啓動的時候,會加載很多bean到Spring容器中,每個bean都實現了各自的功能,鑑於篇幅,筆者只分析最重要的一個bean,也就是真正處理客戶端請求的類InstanceRegistry.java,其他類請讀者自行分析

 

 

3.org.springframework.cloud.netflix.eureka.server.InstanceRegistry

	// 1.接收客戶端註冊請求
    public void register(final InstanceInfo info, final boolean isReplication) {
		handleRegistration(info, resolveInstanceLeaseDuration(info), isReplication);
		super.register(info, isReplication);// 在1)中詳細分析
	}

    // 2.接收客戶端下線請求
	@Override
	public boolean cancel(String appName, String serverId, boolean isReplication) {
		handleCancelation(appName, serverId, isReplication);
		return super.cancel(appName, serverId, isReplication);// 在2)中詳細分析
	}

    // 3.接收客戶端續約請求
	@Override
	public boolean renew(final String appName, final String serverId,
			boolean isReplication) {
		log("renew " + appName + " serverId " + serverId + ", isReplication {}"
				+ isReplication);
		List<Application> applications = getSortedApplications();
		for (Application input : applications) {
			if (input.getName().equals(appName)) {
				InstanceInfo instance = null;
				for (InstanceInfo info : input.getInstances()) {
					if (info.getId().equals(serverId)) {
						instance = info;
						break;
					}
				}
				publishEvent(new EurekaInstanceRenewedEvent(this, appName, serverId,
						instance, isReplication));
				break;
			}
		}
		return super.renew(appName, serverId, isReplication);// 在3)中詳細分析
	}

    疑問:上面的方式是在處理客戶端的不同請求,但是,客戶端發送的是HTTP請求,這只是一個接口,服務端應該也有一個接收HTTP請求的類,然後將接收到的請求封裝後委託給InstanceRegistry來處理具體業務。

    我們通過查詢這些方法被調用的情況可以看到,確實有一個類接收客戶端請求,並將具體業務處理委託給InstanceRegistry,這個類就是com.netflix.eureka.resources包下的ApplicationResource、InstanceResource類

    注意:這種接收請求的方式是採用jax-rs的方式,有關於jax-rs的技術細節筆者不再贅述,讀者可自行查看相關技術實現

 

    注意:以上方法中均出現了handleRegistration()方法,實際其主要操作就是publishEvent(),發送不同的事件,SpringCloud中沒有實現相應的監聽器,應該是設置給用戶自定義實現的

 

    1)AbstractInstanceRegistry.register(InstanceInfo registrant, int leaseDuration, boolean isReplication)註冊

    public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {
        try {
            read.lock();
            // 1.所有的服務信息都添加到registry這個map中,
            // registry 格式爲:ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>>()
            Map<String, Lease<InstanceInfo>> gMap = registry.get(registrant.getAppName());
            REGISTER.increment(isReplication);
            // 1.如果沒有該服務的信息,則新建,並添加到registry中
            if (gMap == null) {
                final ConcurrentHashMap<String, Lease<InstanceInfo>> gNewMap = new ConcurrentHashMap<String, Lease<InstanceInfo>>();
                gMap = registry.putIfAbsent(registrant.getAppName(), gNewMap);
                if (gMap == null) {
                    gMap = gNewMap;
                }
            }
            // 2.existingLease信息即服務的一些註冊時間等信息,主要是爲了校驗該服務是否過期,如果已過期,則剔除
            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);
                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.expectedNumberOfRenewsPerMin > 0) {
                        // Since the client wants to cancel it, reduce the threshold
                        // (1
                        // for 30 seconds, 2 for a minute)
                        this.expectedNumberOfRenewsPerMin = this.expectedNumberOfRenewsPerMin + 2;
                        this.numberOfRenewsPerMinThreshold =
                                (int) (this.expectedNumberOfRenewsPerMin * serverConfig.getRenewalPercentThreshold());
                    }
                }
                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);
            ...
        } finally {
            read.unlock();
        }
    }

    總結1):通過上述分析可知,服務註冊信息最終存放到

// 外層map的key即爲應用的服務名,內層map的key爲我們設置的eureka.instance.instance-id
// 設置成這種格式,當多個應用提供相同服務時,那麼外層map的key都相同,內層map的key不同   
private final ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>> registry
            = new ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>>();    

  所有的操作都是針對這個map進行操作

 

    2)AbstractInstanceRegistry.renew(String appName, String id, boolean isReplication)續約

    public boolean renew(String appName, String id, boolean isReplication) {
        RENEW.increment(isReplication);
        // 1.獲取對應map
        Map<String, Lease<InstanceInfo>> gMap = registry.get(appName);
        Lease<InstanceInfo> leaseToRenew = null;
        if (gMap != null) {
            // 2.主要是爲了獲取當前服務的一些過期信息
            leaseToRenew = gMap.get(id);
        }
        ...
        
            renewsLastMin.increment();
            // 主要操作在這裏,將最新更新時間重置,剔除任務檢查的也就是這個最新更新時間
            // lastUpdateTimestamp = System.currentTimeMillis() + duration;
            leaseToRenew.renew();
            return true;
        }
    }

    3)AbstractInstanceRegistry.cancel(String appName, String idboolean isReplication)下線

    public boolean cancel(String appName, String id, boolean isReplication) {
        return internalCancel(appName, id, isReplication);
    }

// internalCancel()
    protected boolean internalCancel(String appName, String id, boolean isReplication) {
        try {
            read.lock();
            CANCEL.increment(isReplication);
            // 1.獲取gmap
            Map<String, Lease<InstanceInfo>> gMap = registry.get(appName);
            Lease<InstanceInfo> leaseToCancel = null;
            if (gMap != null) {
                // 2.刪除gmap中該服務id
                leaseToCancel = gMap.remove(id);
            }
            ...
            } else {
                // 3.將當前服務的剔除時間置爲當前時間
                //evictionTimestamp = System.currentTimeMillis();
                leaseToCancel.cancel();
                // 4.獲取服務信息
                InstanceInfo instanceInfo = leaseToCancel.getHolder();
                String vip = null;
                String svip = null;
                if (instanceInfo != null) {
                    // 5.將服務信息置爲已刪除
                    instanceInfo.setActionType(ActionType.DELETED);
                    recentlyChangedQueue.add(new RecentlyChangedItem(leaseToCancel));
                    instanceInfo.setLastUpdatedTimestamp();
                    vip = instanceInfo.getVIPAddress();
                    svip = instanceInfo.getSecureVipAddress();
                }
                invalidateCache(appName, vip, svip);
                logger.info("Cancelled instance {}/{} (replication={})", appName, id, isReplication);
                return true;
            }
        } finally {
            read.unlock();
        }
    }

    總結:

    * 服務的註冊實際上是將服務信息添加到一個map中,map的key是服務名稱,value也是一個map,是提供該服務的所有客戶端信息;

    * 服務的續約實際上是獲取map中該服務的客戶端信息,然後修改其最新更新時間

    * 服務的下線實際上是刪除該map中該服務信息,然後修改服務狀態

 

    以上就是Eureka的client和server行爲的分析。筆者只分析了最重要的部分,實際Eureka還做了很多事情。

    更多行爲分析用戶可參考:

        http://nobodyiam.com/2016/06/25/dive-into-eureka/ 

https://github.com/xusuy/SpringCloudTutorial/blob/9d6807e5653328b72bf7a44cb50453cb836aa94d/doc/flow-analysis/Eureka_01.md  

 

 

參考:SpringCloud微服務實戰

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章