Eureka啓動源碼分析
我們從@EnableEurekaServer註解開始分析
1.Eureka Server啓動分析
1.1找到@EnableEurekaServer的實現類 EurekaServerAutoConfiguration
1.2分析EurekaServerInitializerConfiguration(eureka配置的加載類)
//標識爲配置文件
@Configuration
//引入EurekaServerInitializerConfiguration,EurekaServerAutoConfiguration的初始化類
@Import(EurekaServerInitializerConfiguration.class)
//只有當EurekaServerMarkerConfiguration.Marker實例化後才能,實例化當前類
@ConditionalOnBean(EurekaServerMarkerConfiguration.Marker.class)
//加載兩個配置類//EurekaDashboardProperties(面板路徑,是否啓動),InstanceRegistryProperties(每分鐘期望續約的數量,默認打開的通信數量)
@EnableConfigurationProperties({ EurekaDashboardProperties.class,
InstanceRegistryProperties.class })
//加載配置文件位置
@PropertySource("classpath:/eureka/server.properties")
public class EurekaServerAutoConfiguration extends WebMvcConfigurerAdapter {}
1.下面我們對EurekaServerAutoConfiguration 類的屬性和方法進行分析
1.1該方法初始化默認的EurekaServerConfig配置
@Configuration
protected static class EurekaServerConfigBeanConfiguration {
@Bean
@ConditionalOnMissingBean
public EurekaServerConfig eurekaServerConfig(EurekaClientConfig clientConfig) {
EurekaServerConfigBean server = new EurekaServerConfigBean();
//EurekaServer是否互相註冊
if (clientConfig.shouldRegisterWithEureka()) {
//當eureka服務器啓動時嘗試去獲取集羣裏其他服務器上的註冊信息的次數,默認爲5
server.setRegistrySyncRetries(5);
}
return server;
}
}
1.2.初始化EurekaController,提供訪問接口
@Bean
@ConditionalOnProperty(prefix = "eureka.dashboard", name = "enabled", matchIfMissing = true)
public EurekaController eurekaController() {
return new EurekaController(this.applicationInfoManager);
}
1.3.下面的代碼主要是配置解碼器(配置Json和Xml的解碼器),代碼就不展示了
1.4.實例化了eureka多個服務維持節點同步的bean(PeerAwareInstanceRegistry );以及每個eureka服務節點的生命週期管理(PeerEurekaNodes );EurekaServer的上下文(共享部分)維護; EurekaServer的引導程序(提供其他類調用,去實現),通過tomcat管理eureka的生命週期;還有一個靜態類用於更新peer節點信息
//實例化了eureka多個服務維持節點同步的bean;
// 配置服務節點信息,這裏的作用主要是爲了配置Eureka的peer節點,也就是說當有收到有節點註冊上來的時候,需要通知給那些服務節點, (互爲一個集羣)
@Bean
public PeerAwareInstanceRegistry peerAwareInstanceRegistry( ){
//peer實例註冊
}
//每個eureka服務節點的生命週期管理
@Bean
@ConditionalOnMissingBean
public PeerEurekaNodes peerEurekaNodes( ){
//PeerEurekaNodes註冊
}
static class RefreshablePeerEurekaNodes//updates peers when /refresh is invoked(當refresh方法被調用時,更新Node)
// EurekaServer的上下文(共享部分)維護
@Bean
public EurekaServerContext eurekaServerContext(){}
// EurekaServer的引導程序(提供其他類調用,去實現),通過tomcat管理eureka的生命週期;
@Bean
public EurekaServerBootstrap eurekaServerBootstrap(){}
1.5註冊jersey(類似於springmvc),主要通過它去提供對外的restFul接口
//註冊jersey過濾器,通過他來實現eurekaServer對外的restFul接口
@Bean
public FilterRegistrationBean jerseyFilterRegistration(){}
// 生成攔截器實例
@Bean
public jerseyApplication(){}
2. EurekaServerInitializerConfiguration(eureka配置文件的加載邏輯)
2.1啓動一個線程去讀取配置文件,主要初始化服務環境,配置信息;初始化了eureka服務端的上下文,併發布通知eureka註冊成功事件和eureka啓動事件。初始化EurekaRegistryAvailableEvent和EurekaServerStartedEvent
@Override
public void start() {
new Thread(new Runnable() {
@Override
public void run() {
try {
eurekaServerBootstrap.contextInitialized(EurekaServerInitializerConfiguration.this.servletContext);
log.info("Started Eureka Server");
publish(new EurekaRegistryAvailableEvent(getEurekaServerConfig()));
EurekaServerInitializerConfiguration.this.running = true;
publish(new EurekaServerStartedEvent(getEurekaServerConfig()));
}
catch (Exception ex) {
// Help!
log.error("Could not initialize Eureka servlet context", ex);
}
}
}).start();
}
這個類在頂層實現了 org.springframework.context.Lifecycle 接口,通過tomcat管理生命週期;
通過分析上面實例化bean,我們可以看到eureka服務是通過tomcat調用其聲明週期方法來啓動的;
initEurekaEnvironment():初始化環境配置文件
initEurekaServerContext():初始化server上下文,同步了其他節點的信息,啓動了剔除不可用eureka客戶端的定時任務;
public void contextInitialized(ServletContext context) {
try {
initEurekaEnvironment();
//這個方法中有一個syncUp()函數用於註冊peer實例
initEurekaServerContext();
context.setAttribute(EurekaServerContext.class.getName(), this.serverContext);
}
catch (Throwable e) {
log.error("Cannot bootstrap eureka server :", e);
throw new RuntimeException("Cannot bootstrap eureka server :", e);
}
}
3.接收Eureka客戶端請求
3.1.1. 註冊入口
根據idea的啓動日誌我們找到了AbstractInstanceRegistry
找到該處日誌打印,跟蹤到了 com.netflix.eureka.resources.ApplicationResource#addInstance,
3.1該類發送post請求去獲取InstanceInfo (eureka client實例)
作用
- 接收eureka客戶端的註冊請求,完成服務實例向註冊中心的註冊;
- 接收其他註冊中心節點的同步信息.完成節點間服務列表的同步工作;
@POST
@Consumes({"application/json", "application/xml"})
public Response addInstance(InstanceInfo info,@HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication) {
//對instanceInfo的信息進行非空校驗()
//....省略部分代碼....
//同步到註冊中心其他節點
registry.register(info, "true".equals(isReplication));
}
3.1.2 註冊過程
在initEurekaServerContext()調用了,用於獲取並初始化InstanceInfo
public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {
try {
//使用讀寫鎖保證線程的安全性
read.lock();
//本質上是ConcurrentHashMap<key是spring.application.name的值與host和端口信息組合,value是client信息包含服務實例信息及註冊續約>
Map<String, Lease<InstanceInfo>> gMap = registry.get(registrant.getAppName());
REGISTER.increment(isReplication);
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;
}
}
Lease<InstanceInfo> existingLease = gMap.get(registrant.getId());
// 如果已經有租約,則保留最後一個髒的時間戳而不覆蓋它
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 {
// 租約不存在,因此是新登記
synchronized (lock) {
if (this.expectedNumberOfRenewsPerMin > 0) {
// (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);
synchronized (recentRegisteredQueue) {
recentRegisteredQueue.add(new Pair<Long, String>(
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 (!overriddenInstanceStatusMap.containsKey(registrant.getId())) {
logger.info("Not found overridden id {} and hence adding it", registrant.getId());
overriddenInstanceStatusMap.put(registrant.getId(), registrant.getOverriddenStatus());
}
}
InstanceStatus overriddenStatusFromMap = overriddenInstanceStatusMap.get(registrant.getId());
if (overriddenStatusFromMap != null) {
logger.info("Storing overridden status {} from map", overriddenStatusFromMap);
registrant.setOverriddenStatus(overriddenStatusFromMap);
}
// 設置狀態規則
InstanceStatus overriddenInstanceStatus = getOverriddenInstanceStatus(registrant, existingLease, isReplication);
registrant.setStatusWithoutDirty(overriddenInstanceStatus);
// If the lease is registered with UP status, set lease service up timestamp
if (InstanceStatus.UP.equals(registrant.getStatus())) {
lease.serviceUp();
}
registrant.setActionType(ActionType.ADDED);
recentlyChangedQueue.add(new RecentlyChangedItem(lease));
registrant.setLastUpdatedTimestamp();
invalidateCache(registrant.getAppName(), registrant.getVIPAddress(), registrant.getSecureVipAddress());
logger.info("Registered instance {}/{} with status {} (replication={})",
registrant.getAppName(), registrant.getId(), registrant.getStatus(), isReplication);
} finally {
read.unlock();
}
}
3.1.2:該方法用於將client信息複製到其他peer
@Override
public void register(final InstanceInfo info, final boolean isReplication) {
int leaseDuration = Lease.DEFAULT_DURATION_IN_SECS;
if (info.getLeaseInfo() != null && info.getLeaseInfo().getDurationInSecs() > 0) {
leaseDuration = info.getLeaseInfo().getDurationInSecs();
}
super.register(info, leaseDuration, isReplication);
replicateToPeers(Action.Register, info.getAppName(), info.getId(), info, null, isReplication);
}
3.1.3:用於將client信息複製到其他peer中
當註冊中心集羣時,每個節點只會向其配置文件所配置的其他節點同步信息,且是單向的。從ServerCodecs 獲取配置文件信息
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();
}
// 如果已經複製client信息則不再複製
if (peerEurekaNodes == Collections.EMPTY_LIST || isReplication) {
return;
}
for (final PeerEurekaNode node : peerEurekaNodes.getPeerEurekaNodes()) {
// 如果目標節點和本機的hostName一致則不會同步
if (peerEurekaNodes.isThisMyUrl(node.getServiceUrl())) {
continue;
}
//複製到別的Server註冊,指定了要同步到其他節點信息的類型,如實例的註冊,續約等,再根據不同動作執行不同的方法,所有的動作都通過這裏分發同步到其他節點;
replicateInstanceActionsToPeers(action, appName, id, info, newStatus, node);
}
} finally {
tracer.stop();
}
}
3.2 續約
3.2.1:通過跟蹤方法調用找到了續約的接收客戶端請求的入口:com.netflix.eureka.resources.InstanceResource#renewLease
使用registry.renew()方法更新註冊表
@PUT
public Response renewLease(
@HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication,
@QueryParam("overriddenstatus") String overriddenStatus,
@QueryParam("status") String status,
@QueryParam("lastDirtyTimestamp") String lastDirtyTimestamp) {
boolean isFromReplicaNode = "true".equals(isReplication);
//renew()用於更新註冊表邏輯
boolean isSuccess = registry.renew(app.getName(), id, isFromReplicaNode);
// 在註冊表中找不到,請立即請求註冊
if (!isSuccess) {
logger.warn("Not Found (Renew): {} - {}", app.getName(), id);
return Response.status(Status.NOT_FOUND).build();
}
// 檢查是否需要基於髒時間戳進行同步,客戶端實例可能更改了一些值
Response response = null;
if (lastDirtyTimestamp != null && serverConfig.shouldSyncWhenTimestampDiffers()) {
response = this.validateDirtyTimestamp(Long.valueOf(lastDirtyTimestamp), isFromReplicaNode);
// Store the overridden status since the validation found out the node that replicates wins
if (response.getStatus() == Response.Status.NOT_FOUND.getStatusCode()
&& (overriddenStatus != null)
&& !(InstanceStatus.UNKNOWN.name().equals(overriddenStatus))
&& isFromReplicaNode) {
registry.storeOverriddenStatusIfRequired(app.getAppName(), id, InstanceStatus.valueOf(overriddenStatus));
}
} else {
response = Response.ok().build();
}
logger.debug("Found (Renew): {} - {}; reply status={}", app.getName(), id, response.getStatus());
return response;
}
3.2.2:最終調用AbstractInstanceRegistry#renew方法進行更新,最終調用 leaseToRenew.renew()更新到最新的更新時間
public boolean renew(String appName, String id, boolean isReplication) {
RENEW.increment(isReplication);
//從註冊列表中獲取對應實例信息
Map<String, Lease<InstanceInfo>> gMap = registry.get(appName);
Lease<InstanceInfo> leaseToRenew = null;
if (gMap != null) {
leaseToRenew = gMap.get(id);
}
if (leaseToRenew == null) {
RENEW_NOT_FOUND.increment(isReplication);
logger.warn("DS: Registry: lease doesn't exist, registering resource: {} - {}", appName, id);
return false;
} else {
InstanceInfo instanceInfo = leaseToRenew.getHolder();
if (instanceInfo != null) {
// touchASGCache(instanceInfo.getASGName());
InstanceStatus overriddenInstanceStatus = this.getOverriddenInstanceStatus(
instanceInfo, leaseToRenew, isReplication);
if (overriddenInstanceStatus == InstanceStatus.UNKNOWN) {
logger.info("Instance status UNKNOWN possibly due to deleted override for instance {}"
+ "; re-register required", instanceInfo.getId());
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", instanceInfo.getStatus().name(),
instanceInfo.getOverriddenStatus().name(),
instanceInfo.getId());
instanceInfo.setStatusWithoutDirty(overriddenInstanceStatus);
}
}
renewsLastMin.increment();
//最終的方法:更新最近的更新時間
leaseToRenew.renew();
return true;
}
}
3.3 剔除,服務下線(EvictionTask)
首先我們看到這個方法,
if (!isLeaseExpirationEnabled()) {
logger.debug("DS: lease expiration is currently disabled.");
return;
}
看日誌可以看出當前租戶過期不可用,也就是說不會因爲有實例續約過期而被剔除,那麼我們看一下是取決於哪些條件,點進去看一下:
@Override
public boolean isSelfPreservationModeEnabled() {
return serverConfig.shouldEnableSelfPreservation();
}
一是來自於配置文件 eureka.server.enableSelfPreservation自我保護模式是否開啓,自我保護模式,當出現出現網絡分區、eureka在短時間內丟失過多客戶端時,會進入自我保護模式,即一個服務長時間沒有發送心跳,eureka也不會將其刪除,默認爲true.
如果該值配置爲false,則永遠不會進入保護模式,那麼一旦遇到網絡波動,會有大量的服務實例被剔除,但是他們卻都是可用的,這是很危險的;如果是內網則另當別論了;
二是通過閾值控制;
如果Eureka Server最近1分鐘收到renew的次數小於閾值(即預期的最小值),則會觸發自我保護模式,此時Eureka Server此時會認爲這是網絡問題,它不會註銷任何過期的實例。等到最近收到renew的次數大於閾值後,則Eureka Server退出自我保護模式。
自我保護模式閾值計算:
每個instance的預期心跳數目 = 60/每個instance的心跳間隔秒數
閾值 = 所有註冊到服務的instance的數量的預期心跳之和 *自我保護係數
以上的參數都可配置的:
- instance的心跳間隔秒數:eureka.instance.lease-renewal-interval-in-seconds
- 自我保護係數:eureka.server.renewal-percent-threshold
3.3.2 隨機剔除服務實例
遍歷服務列表,並判斷是否過期,若過期將其add到expiredLeases中;
List<Lease<InstanceInfo>> expiredLeases = new ArrayList<>();
for (Entry<String, Map<String, Lease<InstanceInfo>>> groupEntry : registry.entrySet()) {
Map<String, Lease<InstanceInfo>> leaseMap = groupEntry.getValue();
if (leaseMap != null) {
for (Entry<String, Lease<InstanceInfo>> leaseEntry : leaseMap.entrySet()) {
Lease<InstanceInfo> lease = leaseEntry.getValue();
if (lease.isExpired(additionalLeaseMs) && lease.getHolder() != null) {
expiredLeases.add(lease);
}
}
}
}
接下來註冊中心有執行了一個保護的操作:根據本地服務的數量重新計算了續約閾值,然後與註冊的服務數量做差作爲本次剔除服務數量的最大值,再對比放在過期待剔除服務列表中的數量,取最小值作爲本次剔除過期服務的數量.該計算過程爲了避免某些原因使得該註冊中心節點服務實例被全部剔除;
若計算後最終要剔除的服務數量小於待剔除服務列表中的數量,則採取隨機方式剔除;
//重新計算剔除數量
int registrySize = (int) getLocalRegistrySize();
int registrySizeThreshold = (int) (registrySize * serverConfig.getRenewalPercentThreshold());
int evictionLimit = registrySize - registrySizeThreshold;
int toEvict = Math.min(expiredLeases.size(), evictionLimit);
if (toEvict > 0) {
logger.info("Evicting {} items (expired={}, evictionLimit={})", toEvict, expiredLeases.size(), evictionLimit);
//隨機剔除
Random random = new Random(System.currentTimeMillis());
for (int i = 0; i < toEvict; i++) {
// Pick a random item (Knuth shuffle algorithm)
int next = i + random.nextInt(expiredLeases.size() - i);
Collections.swap(expiredLeases, i, next);
Lease<InstanceInfo> lease = expiredLeases.get(i);
String appName = lease.getHolder().getAppName();
String id = lease.getHolder().getId();
EXPIRED.increment();
logger.warn("DS: Registry: expired lease for {}/{}", appName, id);
internalCancel(appName, id, false);
}
}
剔除後通知到其他註冊中心節點;