答上期問題:
接着上篇文章 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 id, boolean 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/
參考:SpringCloud微服務實戰