一、EurekaClient運行流程分析
EurekaClient爲了簡化開發人員的工作量,將很多與EurekaServer交互的工作隱藏起來,自主完成,具體完成的工作如下,代碼見spring-cloud-netflix-eureka-client-2.0.0.RELEASE.jar。
代碼Git地址:https://gitee.com/hankin_chj/springcloud-micro-service.git
1、應用啓動階段:
- 讀取與Eureka Server交互的配置信息,封裝成EurekaClientConfig;
- 讀取自身服務實例配置信息,封裝成EurekalnstanceConfig;
- 從Eureka server拉取註冊表信息並緩存到本地;
- 服務註冊;
- 通過定時任務初始化發送心跳、緩存刷新(拉取註冊表信息更新本地緩存)和按需註冊(監控服務實例信息變化,決定是否重新發起註冊,更新註冊表中的服務實例元數據)定時任務。
2、應用執行階段
- 定時發送心跳到Eureka Server中維持在註冊表的租約;
- 定時從Eureka Server中拉取註冊表信息,更新本地註冊表緩存;
- 監控應用自身信息變化,若發生變化,需要重新發起服務註冊。
3、應用銷燬階段
從Eureka Server註銷自身服務實例。
4、應用啓動與運行階段分析
Eureka Client通過Starter的方式引人依賴,Spring Boot將會爲項目使用以下的自動配置類:
- EurekaClientAutoConfiguration:EurekeClient自動配置類,負責Eureka關鍵Beans的配置和初始化,如AppplicationInfoManager和EurekaClientConfig等。
- RibbonEurekaAutoConfiguration:Ribbon負載均衡相關配置。
- EurekaDiscoveryClientConfiguration:配置自動註冊和應用的健康檢查器。
這些註解對應的配置信息:
spring-cloud-netflix-eureka-client-2.0.0.RELEASE.jar/META-INF/spring.factories
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
4.1、讀取應用自身配置
通過EurekaClientAutoConfiguration配置類,Spring boot幫助Eureka Client完成很多必要Bean的屬性讀取和配置。
1)EurekaClientConfig:封裝Eureka Client與Eureka Server交互所需要的配置信息。Spring Cloud爲其提供了一個默認配置類的EurekaClientConfigBean,可以在配置文件中通過前綴eureka.client屬性名進行屬性覆蓋。源碼見org.springframework.cloud.netflix.eureka.EurekaClientConfigBean:
@ConfigurationProperties(EurekaClientConfigBean.PREFIX)
public class EurekaClientConfigBean implements EurekaClientConfig {
public static final String PREFIX = "eureka.client";
2)ApplicationInfoManager:作爲應用信息管理器,管理服務實例的信息類InstanceInfo和服務實例的配置信息類EurekaInstanceConfig。
它的構造方法如下:
@Inject
public ApplicationInfoManager(EurekaInstanceConfig config, InstanceInfo instanceInfo, OptionalArgs optionalArgs) {
this.config = config;
this.instanceInfo = instanceInfo;
this.listeners = new ConcurrentHashMap<String, StatusChangeListener>();
if (optionalArgs != null) {
this.instanceStatusMapper = optionalArgs.getInstanceStatusMapper();
} else {
this.instanceStatusMapper = NO_OP_MAPPER;
}
instance = this;
}
3)InstanceInfo:封裝將被髮送到EurekaServer進行服務註冊的服務實例元數據。它在EurekServer的註冊表中代表一個服務實例,其他服務實例可以通過Instancelnfo瞭解該服的相關信息從而發起服務請求。
4)EurekaInstanceConfig:封裝EurekaClient自身服務實例的配置信息,主要用於構建InstanceInfo通常這些信息在配置文件中的eureka.instance前綴下進行設置, SpringCloud通過EurekalnstanceConfigBean配置類提供了默認配置。
5)DiscoveryClient:Spring Cloud中定義用來服務發現的客戶端接口,對於DiscoveryClient可以具體查看EurekaDiscoveryClient,EurekaDiscoveryClient又藉助EurekaClient來實現。
另外netflix包裏面還有一個DiscoveryClient,按名字翻譯其實就是服務發現客戶端,他是整個EurekaClient的核心,是與EurekaServer進行交互的核心所在。
@ImplementedBy(DiscoveryClient.class)
public interface EurekaClient extends LookupService {
@Singleton
public class DiscoveryClient implements EurekaClient {
public class EurekaDiscoveryClient implements DiscoveryClient {
public static final String DESCRIPTION = "Spring Cloud Eureka Discovery Client";
private final EurekaInstanceConfig config;
private final EurekaClient eurekaClient;
public EurekaDiscoveryClient(EurekaInstanceConfig config, EurekaClient eurekaClient) {
this.config = config;
this.eurekaClient = eurekaClient;
}
4.2、運行服務
DiscoveryClient核心方法代碼如下所示:
DiscoveryClient(ApplicationInfoManager applicationInfoManager, EurekaClientConfig config,
AbstractDiscoveryClientOptionalArgs args,Provider<BackupRegistry> backupRegistryProvider) {
if (args != null) {
this.healthCheckHandlerProvider = args.healthCheckHandlerProvider;
this.healthCheckCallbackProvider = args.healthCheckCallbackProvider;
this.eventListeners.addAll(args.getEventListeners());
this.preRegistrationHandler = args.preRegistrationHandler;
} else {
this.healthCheckCallbackProvider = null;
this.healthCheckHandlerProvider = null;
this.preRegistrationHandler = null;
}
this.applicationInfoManager = applicationInfoManager;
InstanceInfo myInfo = applicationInfoManager.getInfo();
clientConfig = config;
staticClientConfig = clientConfig;
transportConfig = config.getTransportConfig();
instanceInfo = myInfo;
if (myInfo != null) {
appPathIdentifier = instanceInfo.getAppName() + "/" + instanceInfo.getId();
} else {
logger.warn("Setting instanceInfo to a passed in null value");
}
//傳入BackupRegistry(NotImplementedRegistryImpl)備份註冊中心
this.backupRegistryProvider = backupRegistryProvider;
this.urlRandomizer = new EndpointUtils.InstanceInfoBasedUrlRandomizer(instanceInfo);
localRegionApps.set(new Applications());
fetchRegistryGeneration = new AtomicLong(0);
remoteRegionsToFetch = new AtomicReference<String>(clientConfig.fetchRegistryForRemoteRegions());
remoteRegionsRef = new AtomicReference<>(remoteRegionsToFetch.get() == null ? null : remoteRegionsToFetch.get().split(","));
//從eureka server拉起註冊表信息 eureka.client.fetch-register
if (config.shouldFetchRegistry()) {
this.registryStalenessMonitor = new ThresholdLevelsMetric(this, METRIC_REGISTRY_PREFIX
+ "lastUpdateSec_", new long[]{15L, 30L, 60L, 120L, 240L, 480L});
} else {
this.registryStalenessMonitor = ThresholdLevelsMetric.NO_OP_METRIC;
}
// 當前的客戶端是否應該註冊到erueka中:eureka.client.register-with-eureka
if (config.shouldRegisterWithEureka()) {
this.heartbeatStalenessMonitor = new ThresholdLevelsMetric(this, METRIC_REGISTRATION_PREFIX
+ "lastHeartbeatSec_", new long[]{15L, 30L, 60L, 120L, 240L, 480L});
} else {
this.heartbeatStalenessMonitor = ThresholdLevelsMetric.NO_OP_METRIC;
}
logger.info("Initializing Eureka in region {}", clientConfig.getRegion());
//如果既不需要註冊,也不需要拉去數據,直接返回,初始結束
if (!config.shouldRegisterWithEureka() && !config.shouldFetchRegistry()) {
logger.info("Client configured to neither register nor query for data.");
scheduler = null;
heartbeatExecutor = null;
cacheRefreshExecutor = null;
eurekaTransport = null;
instanceRegionChecker = new InstanceRegionChecker(new PropertyBasedAzToRegionMapper(config), clientConfig.getRegion());
// 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());
return; // no need to setup up an network tasks and we are done
}
try {
//線程池大小爲2,一個用戶發送心跳,另外1個緩存刷新
// default size of 2 - 1 each for heartbeat and cacheRefresh
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
//初始化client與server交互的jersey客戶端
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);
}
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());
}
4.3、總結
DiscoveryClient構造函數做的事情:
- 相關配置賦值
- 備份註冊中心的初始化,實際什麼事都沒做
- 拉取Server註冊表中的信息
- 註冊前的預處理
- 向Server註冊自身
- 初始心跳定時任務,緩存刷新等定時任務
5、Eureka客戶端流程源碼分析
5.1、拉取Server註冊表中的信息代碼分析(重要步驟)
if (clientConfig.shouldFetchRegistry() && !fetchRegistry(false)) {
fetchRegistryFromBackup();
}
判斷是否全量拉去方法源碼fetchRegistry(false):
//是否全量拉去
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)) {
//全量拉取
getAndStoreFullRegistry();
} else {
//增量拉取
getAndUpdateDelta(applications);
}
applications.setAppsHashCode(applications.getReconcileHashCode());
//打印註冊表上所有服務實例信息
logTotalInstances();
} catch (Throwable e) {
logger.error(PREFIX + "{} - was unable to refresh its cache! status = {}", appPathIdentifier, e.getMessage(), e);
return false;
} finally {
if (tracer != null) {
tracer.stop();
}
}
onCacheRefreshed();
// Update remote status based on refreshed data held in the cache
updateInstanceRemoteStatus();
// registry was fetched successfully, so return true
return true;
}
5.2、全量拉取源碼分析
一般只有在第一次拉去註冊表信息的時候,全量拉取調用 getAndStoreFullRegistry()方法:
private void getAndStoreFullRegistry() throws Throwable {
//拉取註冊表的版本信息
long currentUpdateGeneration = fetchRegistryGeneration.get();
logger.info("Getting all instance registry info from the eureka server");
Applications apps = null;
EurekaHttpResponse<Applications> httpResponse = clientConfig.getRegistryRefreshSingleVipAddress() == null
? eurekaTransport.queryClient.getApplications(remoteRegionsRef.get())
: eurekaTransport.queryClient.getVip(clientConfig.getRegistryRefreshSingleVipAddress(), remoteRegionsRef.get());
//拉取成功
if (httpResponse.getStatusCode() == Status.OK.getStatusCode()) {
apps = httpResponse.getEntity();
}
logger.info("The response status is {}", httpResponse.getStatusCode());
}
5.3、增量拉取代碼分析
增量拉取,首先根據拉去信息進行判斷是否爲空,如果爲空則進行全量拉去,反之更新本地緩存,getAndUpdateDelta(applications)代碼如下:
private void getAndUpdateDelta(Applications applications) throws Throwable {
long currentUpdateGeneration = fetchRegistryGeneration.get();
//拉取信息
Applications delta = null;
EurekaHttpResponse<Applications> httpResponse = eurekaTransport.queryClient.getDelta(remoteRegionsRef.get());
if (httpResponse.getStatusCode() == Status.OK.getStatusCode()) {
delta = httpResponse.getEntity();
}
//如果拉取失敗進行全量拉取
if (delta == null) {
logger.warn("The server does not allow the delta revision to be applied because it is not safe. "
+ "Hence got the full registry.");
getAndStoreFullRegistry();
} else if (fetchRegistryGeneration.compareAndSet(currentUpdateGeneration, currentUpdateGeneration + 1)) {
logger.debug("Got delta update with apps hashcode {}", delta.getAppsHashCode());
String reconcileHashCode = "";
if (fetchRegistryUpdateLock.tryLock()) {
try {
//跟新本地緩存
updateDelta(delta);
reconcileHashCode = getReconcileHashCode(applications);
} finally {
fetchRegistryUpdateLock.unlock();
}
} else {
logger.warn("Cannot acquire update lock, aborting getAndUpdateDelta");
}
// There is a diff in number of instances for some reason
if (!reconcileHashCode.equals(delta.getAppsHashCode()) || clientConfig.shouldLogDeltaDiff()) {
reconcileAndLogDifference(delta, reconcileHashCode); // this makes a remoteCall
}
} else {
logger.warn("Not updating application delta as another thread is updating it already");
logger.debug("Ignoring delta update with apps hashcode {}, as another thread is updating it already", delta.getAppsHashCode());
}
}
5.4、服務註冊(重要步驟)
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()方法負責服務的註冊源碼分析:
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() == 204;
}
5.5、定時任務(重要步驟)
initScheduledTasks()是負責定時任務的相關方法,具體源碼實現如下:
private void initScheduledTasks() {
if (clientConfig.shouldFetchRegistry()) {
// 拉取服務默認30秒,eureka.client.register-fetch-interval-seconds
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 心跳服務,默認30秒
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");
}
}
5.6、服務下線
com.netflix.discovery.DiscoveryClient #shutdown
@PreDestroy
@Override
public synchronized void shutdown() {
if (isShutdown.compareAndSet(false, true)) {
logger.info("Shutting down DiscoveryClient ...");
if (statusChangeListener != null && applicationInfoManager != null) {
//註銷狀態監聽器
applicationInfoManager.unregisterStatusChangeListener(statusChangeListener.getId());
}
//取消定時任務
cancelScheduledTasks();
// If APPINFO was registered
if (applicationInfoManager != null && clientConfig.shouldRegisterWithEureka()
&& clientConfig.shouldUnregisterOnShutdown()) {
applicationInfoManager.setInstanceStatus(InstanceStatus.DOWN);
unregister();
}
//關閉與server連接的客戶端
if (eurekaTransport != null) {
eurekaTransport.shutdown();
}
//關閉相關監控
heartbeatStalenessMonitor.shutdown();
registryStalenessMonitor.shutdown();
logger.info("Completed shut down of DiscoveryClient");
}
}
二、EurekaServer運行流程分析
1、流程總覽
EurekaServer是服務的註冊中心,負責Eureka Client的相關信息註冊,主要職責:
- 服務註冊
- 接受心跳服務(client端通過定時任務初始化發送心跳)
- 服務剔除
- 服務下線
- 集羣同步
1.1、服務配置啓動信息:
spring-cloud-netflix-eureka-server-2.0.0.RELEASE.jar!\META-INF\spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.netflix.eureka.server.EurekaServerAutoConfiguration
EurekaServerAutoConfiguration類代碼:
@Configuration
@Import(EurekaServerInitializerConfiguration.class)
@ConditionalOnBean(EurekaServerMarkerConfiguration.Marker.class)
@EnableConfigurationProperties({ EurekaDashboardProperties.class,InstanceRegistryProperties.class })
@PropertySource("classpath:/eureka/server.properties")
public class EurekaServerAutoConfiguration extends WebMvcConfigurerAdapter {
private static final String[] EUREKA_PACKAGES = new String[] { "com.netflix.discovery","com.netflix.eureka" };
@Autowired
private ApplicationInfoManager applicationInfoManager;
@Autowired
private EurekaServerConfig eurekaServerConfig;
@Autowired
private EurekaClientConfig eurekaClientConfig;
@Autowired
private EurekaClient eurekaClient;
@Autowired
private InstanceRegistryProperties instanceRegistryProperties;
EurekaServerAutoConfiguration 是通過配置文件註冊的,註冊方法入口:
@Bean
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());
}
這裏面有個InstanceRegistry()方法就是重點需要關注的了。
1.2、整個接口關係如下如所示:
所有業務的核心方式實現主要集中在PeerAwareInstanceRegistryImpl與AbstractInstanceRegistry這兩個類中,其中服務註冊、接受心跳服務、服務剔除都是在AbstractInstanceRegistry中處理的,服務下線與集羣同步是在PeerAwareInstanceRegistryImpl類中處理的。
1.3、首先看下最上層的接口
public interface LeaseManager<T> {
//註冊
void register(T var1, int var2, boolean var3);
//下線
boolean cancel(String var1, String var2, boolean var3);
//跟新
boolean renew(String var1, String var2, boolean var3);
//服務剔除
void evict();
}
PeerAwareInstanceRegistryImpl是一個子類的實現,在上面的基礎上擴展對集羣的同步操作,使Eureka Server集羣信息保持一致。
2、服務註冊
com.netflix.eureka.registry.AbstractInstanceRegistry#register(); 這方法是負責服務的註冊的:
public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {
try {
//獲取讀鎖
this.read.lock();
// TODO gMap 其實可以發現,這裏註冊中心其實是個ConcurrentHashMap
Map<String, Lease<InstanceInfo>> gMap = (Map)this.registry.get(registrant.getAppName());
EurekaMonitors.REGISTER.increment(isReplication);
if(gMap == null) {
ConcurrentHashMap<String, Lease<InstanceInfo>> gNewMap = new ConcurrentHashMap();
//TODO key爲appName,如果存在,返回存在的值,否則添加,返回null
gMap = (Map)this.registry.putIfAbsent(registrant.getAppName(), gNewMap);
if(gMap == null) {
gMap = gNewMap;
}
}
//TODO 根據instanceId獲取實例的租約
Lease<InstanceInfo> existingLease = (Lease)((Map)gMap).get(registrant.getId());
if(existingLease != null && existingLease.getHolder() != null) {
Long existingLastDirtyTimestamp = ((InstanceInfo)existingLease.getHolder()).getLastDirtyTimestamp();
Long registrationLastDirtyTimestamp = registrant.getLastDirtyTimestamp();
// TODO 如果該實例的租約已經存在,比較最後的更新時間戳大小,取最大值的註冊信息信息
if(existingLastDirtyTimestamp.longValue() > registrationLastDirtyTimestamp.longValue()) {
registrant = (InstanceInfo)existingLease.getHolder();
}
} else {
Object var6 = this.lock;
//TODO 如果租約不存在,註冊一個新的實例
synchronized(this.lock) {
if(this.expectedNumberOfRenewsPerMin > 0) {
this.expectedNumberOfRenewsPerMin += 2;
this.numberOfRenewsPerMinThreshold = (int)((double)this.expectedNumberOfRenewsPerMin
* this.serverConfig.getRenewalPercentThreshold());
}
}
logger.debug("No previous lease information found; it is new registration");
}
//TODO 創建新的租約
Lease<InstanceInfo> lease = new Lease(registrant, leaseDuration);
if(existingLease != null) {
lease.setServiceUpTimestamp(existingLease.getServiceUpTimestamp());
}
//TODO 保存租約到map中
((Map)gMap).put(registrant.getId(), lease);
//TODO 獲得最近註冊隊列
AbstractInstanceRegistry.CircularQueue var20 = this.recentRegisteredQueue;
synchronized(this.recentRegisteredQueue) {
this.recentRegisteredQueue.add(new Pair(Long.valueOf(System.currentTimeMillis()), registrant.getAppName() + "(" + registrant.getId() + ")"));
}
if(!InstanceStatus.UNKNOWN.equals(registrant.getOverriddenStatus())) {
logger.debug("Found overridden status {} for instance {}. Checking to see if needs to be add to the overrides", registrant.getOverriddenStatus(), registrant.getId());
if(!this.overriddenInstanceStatusMap.containsKey(registrant.getId())) {
logger.info("Not found overridden id {} and hence adding it", registrant.getId());
this.overriddenInstanceStatusMap.put(registrant.getId(), registrant.getOverriddenStatus());
}
}
InstanceStatus overriddenStatusFromMap = (InstanceStatus)this.overriddenInstanceStatusMap.get(registrant.getId());
if(overriddenStatusFromMap != null) {
logger.info("Storing overridden status {} from map", overriddenStatusFromMap);
registrant.setOverriddenStatus(overriddenStatusFromMap);
}
InstanceStatus overriddenInstanceStatus = this.getOverriddenInstanceStatus(registrant, existingLease, isReplication);
registrant.setStatusWithoutDirty(overriddenInstanceStatus);
if(InstanceStatus.UP.equals(registrant.getStatus())) {
lease.serviceUp();
}
registrant.setActionType(ActionType.ADDED);
this.recentlyChangedQueue.add(new AbstractInstanceRegistry.RecentlyChangedItem(lease));
registrant.setLastUpdatedTimestamp();
this.invalidateCache(registrant.getAppName(), registrant.getVIPAddress(), registrant.getSecureVipAddress());
} finally { //釋放鎖
this.read.unlock();
}
}
3、接受心跳服務
在Eureka Client完成服務的註冊後,需要定時向Eureka Server發送心跳請求(默認30s),維持自己在EurekaServer的租約有效性com.netflix.eureka.registry.AbstractInstanceRegistry.renew(appName, id,
isReplication);方法源碼如下所示:
public boolean renew(String appName, String id, boolean isReplication) {
EurekaMonitors.RENEW.increment(isReplication);
//TODO 根據appName獲取服務集羣租約集合
Map<String, Lease<InstanceInfo>> gMap = (Map) this.registry.get(appName);
Lease<InstanceInfo> leaseToRenew = null;
if (gMap != null) {
leaseToRenew = (Lease) gMap.get(id);
}
//TODO 如果租約不存在,直接返回false
if (leaseToRenew == null) {
EurekaMonitors.RENEW_NOT_FOUND.increment(isReplication);
logger.warn("DS: Registry: lease doesn't exist, registering resource: {} - {}", appName, id);
return false;
} else {
InstanceInfo instanceInfo = (InstanceInfo) leaseToRenew.getHolder();
if (instanceInfo != null) {
//TODO 得到服務的最終狀態
InstanceStatus overriddenInstanceStatus = this.getOverriddenInstanceStatus(instanceInfo, leaseToRenew, isReplication);
if (overriddenInstanceStatus == InstanceStatus.UNKNOWN) {
//TODO 如果狀態爲UNKNOWN,取消續約
logger.info("Instance status UNKNOWN possibly due to deleted override for instance {}; re-register required", instanceInfo.getId());
EurekaMonitors.RENEW_NOT_FOUND.increment(isReplication);
return false;
}
if (!instanceInfo.getStatus().equals(overriddenInstanceStatus)) {
logger.info("The instance status {} is different from overridden instance status {} for instance {}. Hence setting the status to overridden status", new Object[]{instanceInfo.getStatus().name(), instanceInfo.getOverriddenStatus().name(), instanceInfo.getId()});
instanceInfo.setStatusWithoutDirty(overriddenInstanceStatus);
}
}
this.renewsLastMin.increment();
//TODO 跟新續約有效時間
leaseToRenew.renew();
return true;
}
}
4、服務剔除
如果Eureka Client在註冊後,由於服務的崩潰或網絡異常導致既沒有續約,也沒有下線,那麼服務就處於不可知的狀態,需要剔除這些服務。
代碼見com.netflix.eureka.registry.AbstractInstanceRegistry#evict(long)方法。
這是個定時任務調用的方法:com.netflix.eureka.registry.AbstractInstanceRegistry#postInit()中使用
AbstractInstanceRegistry.EvictionTask()負責調用(默認60s)。
1)AbstractInstanceRegistry#evict(long)剔除方法源碼:
public void evict(long additionalLeaseMs) {
logger.debug("Running the evict task");
//TODO 如果自我保護狀態,不允許剔除服務(前面配置關係自我保護機制代碼判斷)
if(!this.isLeaseExpirationEnabled()) {
logger.debug("DS: lease expiration is currently disabled.");
} else {
List<Lease<InstanceInfo>> expiredLeases = new ArrayList();
//TODO 遍歷註冊表registry,獲取所有過期的租約
Iterator var4 = this.registry.entrySet().iterator();
while(true) {
Map leaseMap;
do {
if(!var4.hasNext()) {
//TODO 獲取註冊表租約總數
int registrySize = (int)this.getLocalRegistrySize();
int registrySizeThreshold = (int)((double)registrySize * this.serverConfig.getRenewalPercentThreshold());
//TODO 計算最多允許剔除的閾值
int evictionLimit = registrySize - registrySizeThreshold;
//TODO 兩者中取小的值,爲本常剔除的數量
int toEvict = Math.min(expiredLeases.size(), evictionLimit);
if(toEvict > 0) {
logger.info("Evicting {} items (expired={}, evictionLimit={})", new Object[]{Integer.valueOf(toEvict), Integer.valueOf(expiredLeases.size()), Integer.valueOf(evictionLimit)});
Random random = new Random(System.currentTimeMillis());
//TODO 逐個剔除
for(int i = 0; i < toEvict; ++i) {
int next = i + random.nextInt(expiredLeases.size() - i);
Collections.swap(expiredLeases, i, next);
Lease<InstanceInfo> lease = (Lease)expiredLeases.get(i);
String appName = ((InstanceInfo)lease.getHolder()).getAppName();
String id = ((InstanceInfo)lease.getHolder()).getId();
EurekaMonitors.EXPIRED.increment();
logger.warn("DS: Registry: expired lease for {}/{}", appName, id);
//TODO 剔除
this.internalCancel(appName, id, false);
}
}
return;
}
Entry<String, Map<String, Lease<InstanceInfo>>> groupEntry = (Entry)var4.next();
leaseMap = (Map)groupEntry.getValue();
} while(leaseMap == null);
Iterator var7 = leaseMap.entrySet().iterator();
while(var7.hasNext()) {
Entry<String, Lease<InstanceInfo>> leaseEntry = (Entry)var7.next();
Lease<InstanceInfo> lease = (Lease)leaseEntry.getValue();
if(lease.isExpired(additionalLeaseMs) && lease.getHolder() != null) {
expiredLeases.add(lease);
}
}
}
}
}
5、服務下線
EurekaClient在應用銷燬時候,會向Eureka Server發送下線請求,對於服務端的服務下線,其主要代碼對應在com.netflix.eureka.registry.AbstractInstanceRegistry#cancel()方法中
PeerAwareInstanceRegistryImpl#cancel()方法調用父類方法AbstractInstanceRegistry#cancel()方法實現:
@Override
public boolean cancel(final String appName, final String id, final boolean isReplication) {
if (super.cancel(appName, id, isReplication)) {
replicateToPeers(Action.Cancel, appName, id, null, null, isReplication);
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());
}
}
return true;
}
return false;
}
com.netflix.eureka.registry.AbstractInstanceRegistry#cancel方法源碼:
public boolean cancel(String appName, String id, boolean isReplication) {
return this.internalCancel(appName, id, isReplication);
}
protected boolean internalCancel(String appName, String id, boolean isReplication) {
boolean var10;
try {
//讀鎖,防止被其他線程進行修改
this.read.lock();
EurekaMonitors.CANCEL.increment(isReplication);
//根據appName獲取服務實例集羣
Map<String, Lease<InstanceInfo>> gMap = (Map)this.registry.get(appName);
Lease<InstanceInfo> leaseToCancel = null;
//TODO 移除服務實例租約
if(gMap != null) {
leaseToCancel = (Lease)gMap.remove(id);
}
AbstractInstanceRegistry.CircularQueue var6 = this.recentCanceledQueue;
synchronized(this.recentCanceledQueue) {
this.recentCanceledQueue.add(new Pair(Long.valueOf(System.currentTimeMillis()), appName + "(" + id + ")"));
}
InstanceStatus instanceStatus = (InstanceStatus)this.overriddenInstanceStatusMap.remove(id);
if(instanceStatus != null) {
logger.debug("Removed instance id {} from the overridden map which has value {}", id, instanceStatus.name());
}
//TODO 租約不存在,返回false
if(leaseToCancel == null) {
EurekaMonitors.CANCEL_NOT_FOUND.increment(isReplication);
logger.warn("DS: Registry: cancel failed because Lease is not registered for: {}/{}", appName, id);
boolean var17 = false;
return var17;
}
//TODO 設置租約的下線時間
leaseToCancel.cancel();
InstanceInfo instanceInfo = (InstanceInfo)leaseToCancel.getHolder();
String vip = null;
String svip = null;
if(instanceInfo != null) {
instanceInfo.setActionType(ActionType.DELETED);
this.recentlyChangedQueue.add(new AbstractInstanceRegistry.RecentlyChangedItem(leaseToCancel));
instanceInfo.setLastUpdatedTimestamp();
vip = instanceInfo.getVIPAddress();
svip = instanceInfo.getSecureVipAddress();
}
//TODO 設置緩存過期
this.invalidateCache(appName, vip, svip);
logger.info("Cancelled instance {}/{} (replication={})", new Object[]{appName, id, Boolean.valueOf(isReplication)});
var10 = true;
} finally {//釋放鎖
this.read.unlock();
}
return var10;
}
6、集羣同步
如果Eureka Server是通過集羣方式進行部署,爲了爲維護整個集羣中註冊表數據一致性所以集羣同步也是非常重要得事情。
集羣同步分爲兩部分:
- EurekaServer在啓動過程中從他的peer節點中拉取註冊表信息,並講這些服務實例註冊到本地註冊表中;
- 另一部分是eureka server每次對本地註冊表進行操作時,同時會講操作同步到他的peer節點中,達到數據一致;
6.1、Eureka Server初始化本地註冊表信息
在eureka server啓動過程中,會從它的peer節點中拉取註冊表來初始化本地註冊表,這部分主要通過
com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl#syncUp()方法實現,他從可能存在的peer節點中,拉取peer節點中的註冊表信息,並將其中的服務實例的信息註冊到本地註冊表中。
public int syncUp() {
// Copy entire entry from neighboring DS node
int count = 0;
for (int i = 0; ((i < serverConfig.getRegistrySyncRetries()) && (count == 0)); i++) {
if (i > 0) {
try {
//TODO 根據配置休停下
Thread.sleep(serverConfig.getRegistrySyncRetryWaitMs());
} catch (InterruptedException e) {
logger.warn("Interrupted during registry transfer..");
break;
}
}
//TODO 獲取所有的服務實例
Applications apps = eurekaClient.getApplications();
for (Application app : apps.getRegisteredApplications()) {
for (InstanceInfo instance : app.getInstances()) {
try {
//TODO 判斷是否可以註冊
if (isRegisterable(instance)) {
//TODO 註冊到自身的註冊表中
register(instance, instance.getLeaseInfo().getDurationInSecs(), true);
count++;
}
} catch (Throwable t) {
logger.error("During DS init copy", t);
}
}
}
}
return count;
}
通過這一步保證了eureka啓動時的數據一致性。
6.2、Eureka Server之間註冊表信息同步複製
爲了保證Eureka Server集羣運行時候註冊表的信息一致性,每個eureka server在對本地註冊表進行管理操作時,會將相應的信息同步到peer節點中。其中以下幾個方法:
com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl#cancel();
com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl#register();
com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl#renew();
等方法中,都回調用com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl#replicateToPeers()方法:
private void replicateToPeers(Action action, String appName, String id,InstanceInfo info /* optional */,
InstanceStatus newStatus /* optional */, boolean isReplication) {
Stopwatch tracer = action.getTimer().start();
try {
if (isReplication) {
numberOfReplicationsLastMin.increment();
}
// If it is a replication already, do not replicate again as this will create a poison replication
if (peerEurekaNodes == Collections.EMPTY_LIST || isReplication) {
return;
}
//TODO 向peer集羣中的每一個peer進行同步
for (final PeerEurekaNode node : peerEurekaNodes.getPeerEurekaNodes()) {
// If the url represents this host, do not replicate to yourself.
if (peerEurekaNodes.isThisMyUrl(node.getServiceUrl())) {
continue;
}
//TODO 根據action調用不同的同步請求
replicateInstanceActionsToPeers(action, appName, id, info, newStatus, node);
}
} finally {
tracer.stop();
}
}
根據action調用不同的同步請求:
private void replicateInstanceActionsToPeers(Action action, String appName, String id,
InstanceInfo info, InstanceStatus newStatus,PeerEurekaNode node) {
try {
InstanceInfo infoFromRegistry = null;
CurrentRequestVersion.set(Version.V2);
switch (action) {
case Cancel:
node.cancel(appName, id);
break;
case Heartbeat:
InstanceStatus overriddenStatus = overriddenInstanceStatusMap.get(id);
infoFromRegistry = getInstanceByAppAndId(appName, id, false);
node.heartbeat(appName, id, infoFromRegistry, overriddenStatus, false);
break;
case Register:
node.register(info);
break;
case StatusUpdate:
infoFromRegistry = getInstanceByAppAndId(appName, id, false);
node.statusUpdate(appName, id, newStatus, infoFromRegistry);
break;
case DeleteStatusOverride:
infoFromRegistry = getInstanceByAppAndId(appName, id, false);
node.deleteStatusOverride(appName, id, infoFromRegistry);
break;
}
} catch (Throwable t) {
logger.error("Cannot replicate information to {} for action {}", node.getServiceUrl(), action.name(), t);
}
}
public void heartbeat(final String appName, final String id,final InstanceInfo info,
final InstanceStatus overriddenStatus, boolean primeConnection) throws Throwable {
if (primeConnection) {
// We do not care about the result for priming request.
replicationClient.sendHeartBeat(appName, id, info, overriddenStatus);
return;
}
ReplicationTask replicationTask = new InstanceReplicationTask(targetHost, Action.Heartbeat, info, overriddenStatus, false) {
@Override
public EurekaHttpResponse<InstanceInfo> execute() throws Throwable {
return replicationClient.sendHeartBeat(appName, id, info, overriddenStatus);
}
@Override
public void handleFailure(int statusCode, Object responseEntity) throws Throwable {
super.handleFailure(statusCode, responseEntity);
if (statusCode == 404) {
logger.warn("{}: missing entry.", getTaskName());
if (info != null) {
logger.warn("{}: cannot find instance id {} and hence replicating the instance with status {}",
getTaskName(), info.getId(), info.getStatus());
register(info);
}
} else if (config.shouldSyncWhenTimestampDiffers()) {
InstanceInfo peerInstanceInfo = (InstanceInfo) responseEntity;
if (peerInstanceInfo != null) {
syncInstancesIfTimestampDiffers(appName, id, info, peerInstanceInfo);
}
}
}
};
long expiryTime = System.currentTimeMillis() + getLeaseRenewalOf(info);
batchingDispatcher.process(taskId("heartbeat", info), replicationTask, expiryTime);
}