spring cloud eureka 源碼分析

springCloud 是基於 springBoot 實現的一套微服務工具, 它組合了很多其他中間件、工具得以實現, 閱讀 spring 源碼找到閱讀入口, 在Resource/MATE-INF下會有 application.factories文件, 這裏標記了自動配置類, 然後查看整體源碼目錄結構, 已確定適合自己的閱讀方式]

在這裏插入圖片描述

eurekaServer

server node節點同步

EurekaServerBootstrap類, EurekaServer 節點的啓動初始化與結束銷燬功能在這個類中實現

	protected void initEurekaServerContext() throws Exception {
		// For backward compatibility
		JsonXStream.getInstance().registerConverter(new V1AwareInstanceInfoConverter(),
				XStream.PRIORITY_VERY_HIGH);
		XmlXStream.getInstance().registerConverter(new V1AwareInstanceInfoConverter(),
				XStream.PRIORITY_VERY_HIGH);

		if (isAws(this.applicationInfoManager.getInfo())) {
			this.awsBinder = new AwsBinderDelegate(this.eurekaServerConfig,
					this.eurekaClientConfig, this.registry, this.applicationInfoManager);
			this.awsBinder.start();
		}

		EurekaServerContextHolder.initialize(this.serverContext);

		log.info("Initialized server context");

		// Copy registry from neighboring eureka node
    //同步其他EurekaServer 節點的註冊信息
		int registryCount = this.registry.syncUp();
    //開啓當前節點的註冊通信信道默認是 1,
		this.registry.openForTraffic(this.applicationInfoManager, registryCount);

		// Register all monitoring statistics.
		EurekaMonitors.registerAllStats();
	}

初始化 EurekaServer 上下文環境, 兼容兩種數據流 mxl 和 json, 都是最高優先級, (共四種優先級 最高 10000; 一般 20; 低 -10; 最低 -20)

這裏會通信到其他 EurekaServer 同步各個節點的註冊信息

int registryCount = this.registry.syncUp();

this.registryInstanceRegistry的實例, 繼承自EurekaPeerAwareInstanceRegistryImpl(對等感知實例註冊),InstanceRegistry 沒有重寫syncUp()方法, 此方法在父類的實現爲

//同步其它 EurekaServer 節點的實例註冊信息
public int syncUp() {
        int count = 0;
				//循環控制拷貝失敗次數,Eureka 中給定默認次數爲 5 次, springCloud 設置eureka.server.registry-sync-retries屬性配置修改重試次數
        for(int i = 0; i < this.serverConfig.getRegistrySyncRetries() && count == 0; ++i) {
            if (i > 0) {
                try {
                  	//第一次拷貝數據失敗後,進行後續重複拷貝前要等待一段時間, Eureka中給定默認時間 30s,springCloud 設置eureka.server.registry-sync-retry-wait-ms屬性配置修改等待時間
                    Thread.sleep(this.serverConfig.getRegistrySyncRetryWaitMs());
                } catch (InterruptedException var10) {
                    logger.warn("Interrupted during registry transfer..");
                    break;
                }
            }
						//eurekaClient獲取所有應用遍歷每個 eurekaServer, 再遍歷 eurekaServer 中的註冊應用實例,註冊到當前 EurekaServer 中
            Applications apps = this.eurekaClient.getApplications();
            Iterator var4 = apps.getRegisteredApplications().iterator();

            while(var4.hasNext()) {
                Application app = (Application)var4.next();
                Iterator var6 = app.getInstances().iterator();

                while(var6.hasNext()) {
                    InstanceInfo instance = (InstanceInfo)var6.next();

                    try {
                      //判斷應用實例是否可註冊
                        if (this.isRegisterable(instance)) {
                            this.register(instance, instance.getLeaseInfo().getDurationInSecs(), true);
                            ++count;
                        }
                    } catch (Throwable var9) {
                        logger.error("During DS init copy", var9);
                    }
                }
            }
        }

        return count;
    }

應用註冊節點拷貝過程

  • 啓動當前 EurekaServer 初始化時調用 syncUp, 開始同步註冊節點
  • 讀取設定的失敗重複次數,開始同步
  • 使用 EurekaClient 獲取全部註冊中心節點, 再遍歷註冊中心中的註冊節點, 遍歷每個註冊節點實例是否允許註冊到當前註冊中心,若允許註冊進來, 增加註冊的記錄數
  • 如果第一次同步後, 註冊記錄數爲0, 表示沒有將其它註冊中心數據同步到本節點, 重新嘗試同步
  • 第一次同步失敗後的其餘嘗試同步均需要等待一定時間
  • 直到有數據同步成功或全部嘗試次數用盡退出同步

注意:

  • 這裏的 EurekaServer 註冊數據同步是通過 EurekaClient 實現的

  • #將當前節點註冊到 eurekaServer
    eureka.client.register-with-eureka=true
    #獲取註冊中心的註冊信息
    eureka.client.fetch-registry=true
    
  • 上面兩個配置屬性一定是 true, 否則不能同步, 它是通過先將自己(eurekaServer)作爲客戶端註冊到註冊中心中,然後拉取註冊中心的數據到本地, 再將本地 eurekaClient 中獲取的註冊節點同步到當前註冊中心

    this.eurekaClient.getApplications(); 調用的是 AtomicReference::get(), 返回的是 EurekaClient 中定義的EurekaApplications

server Node 節點註冊

先說 eureka 的節點通信使用 http, 那麼對於 註冊中心就應該會提供相應的接口; InstanceRegistry 是 springCloud 實現的實例註冊類, 實現的功能是 實例註冊, 實例續租, 實例刪除 等功能, 對於真正的註冊功能應該在 netflix 的實現中, InstanceRegistry繼承了PeerAwareInstanceRegistryImpl

一路向上找吧!!! 累!

在 eureka-core.jar 中找到了 resource 接口定義, 源碼如下:

@Path("/{version}/apps")
@Produces({"application/xml", "application/json"})
public class ApplicationsResource {
  
      /**
     * Gets information about a particular {@link com.netflix.discovery.shared.Application}.
     *
     * @param version
     *            the version of the request.
     * @param appId
     *            the unique application identifier (which is the name) of the
     *            application.
     * @return information about a particular application.
     */
    @Path("{appId}")
    public ApplicationResource getApplicationResource(
            @PathParam("version") String version,
            @PathParam("appId") String appId) {
        CurrentRequestVersion.set(Version.toEnum(version));
        return new ApplicationResource(appId, serverConfig, registry);
    }
}


@Produces({"application/xml", "application/json"})
public class ApplicationResource {
  @POST
    @Consumes({"application/json", "application/xml"})
    public Response addInstance(InstanceInfo info,
                                @HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication) {
      
        registry.register(info, "true".equals(isReplication));
        return Response.status(204).build();  // 204 to be backwards compatible
    }
}
public class InstanceRegistry extends PeerAwareInstanceRegistryImpl
		implements ApplicationContextAware {
		//註冊
	@Override
	public void register(InstanceInfo info, int leaseDuration, boolean isReplication) {
		handleRegistration(info, leaseDuration, isReplication);
		super.register(info, leaseDuration, isReplication);
	}

	//註冊
	@Override
	public void register(final InstanceInfo info, final boolean isReplication) {
		handleRegistration(info, resolveInstanceLeaseDuration(info), isReplication);
		super.register(info, isReplication);
	}
	
	
	//處理應用註冊
	private void handleRegistration(InstanceInfo info, int leaseDuration,
			boolean isReplication) {
		log("register " + info.getAppName() + ", vip " + info.getVIPAddress()
				+ ", leaseDuration " + leaseDuration + ", isReplication "
				+ isReplication);
		publishEvent(new EurekaInstanceRegisteredEvent(this, info, leaseDuration,
				isReplication));
	}

}
@Singleton
public class PeerAwareInstanceRegistryImpl extends AbstractInstanceRegistry implements PeerAwareInstanceRegistry {    
  @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);
      }
}
public abstract class AbstractInstanceRegistry implements InstanceRegistry {

  /**
       * Registers a new instance with a given duration.
       *
       * @see com.netflix.eureka.lease.LeaseManager#register(java.lang.Object, int, boolean)
       */
      public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {
          try {
              read.lock();
              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());
              // 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);

                  // this is a > instead of a >= because if the timestamps are equal, we still take the remote transmitted
                  // InstanceInfo instead of the server local copy.
                  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.expectedNumberOfClientsSendingRenews > 0) {
                          // Since the client wants to register it, increase the number of clients sending renews
                          this.expectedNumberOfClientsSendingRenews = this.expectedNumberOfClientsSendingRenews + 1;
                          updateRenewsPerMinThreshold();
                      }
                  }
                  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() + ")"));
              }
              // This is where the initial state transfer of overridden status happens
              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);
              }

              // Set the status based on the overridden status rules
              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();
          }
      }
}
  • 上面的接口路徑是/{version}/apps/ post 請求
  • ApplicationsResource轉到ApplicationResource調用方法addInstance(), 大部分都是校驗, 有用的就registry.register(info, "true".equals(isReplication));這一句
  • registry 是注入的 InstanceRegistry
  • InstanceRegistry::register() 調用 super.register()PeerAwareInstanceRegistryImpl
  • PeerAwareInstanceRegistryImpl::register最後調用抽象類AbstractInstanceRegistry::register

找到了方法調用鏈:

ApplicationResource::addInstance —> InstanceRegistry::register —> PeerAwareInstanceRegistryImpl::register —> AbstractInstanceRegistry::register

//todo 未完待續 讀整體整體註冊過程 從 InstanceRegistry 到 AbstractInstanceRegistry(重點)

服務註冊

/**
 * Registers a new instance with a given duration.
 *
 * @see com.netflix.eureka.lease.LeaseManager#register(java.lang.Object, int, boolean)
 */
public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {
    try {
        read.lock();
        //註冊器獲取實例註冊信息
        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());
        // 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);

            // this is a > instead of a >= because if the timestamps are equal, we still take the remote transmitted
            // InstanceInfo instead of the server local copy.
            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.expectedNumberOfClientsSendingRenews > 0) {
                    // Since the client wants to register it, increase the number of clients sending renews
                    this.expectedNumberOfClientsSendingRenews = this.expectedNumberOfClientsSendingRenews + 1;
                    updateRenewsPerMinThreshold();
                }
            }
            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() + ")"));
        }
        // This is where the initial state transfer of overridden status happens
        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);
        }

        // Set the status based on the overridden status rules
        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();
    }
}

註冊信息容器存儲結構

在這裏插入圖片描述

eureka 註冊器(AbstractInstanceRegistry)中 維護了一個ConcurrentHashMap 作爲註冊器容器registry來管理實例註冊信息, 上圖爲註冊容器內部結構:
1.以appName爲key, ConcurrentHashMap類型的容器爲值存儲相同應用不同實例集合
2.內部ConcurrentHashMap集合再以 instanceId(實例id) 爲key, 每個實例應用的註冊信息爲值存儲, 註冊信息包括 應用名, id, 端口, 地址, 註冊時間, 續約時常…等等,

json結構:

{
    "appName1":{
        "instanceId1":{
            "port": 8080,
            "host": 192.168.0.1
        },
        "instanceId2":{

        }
    },
    "appName2":{
        "instanceId3":{
            
        },
        "instanceId14":{

        }
    },
    "appName3":{
        "instanceId5":{
            
        },
        "instanceId6":{

        }
    }
}

服務下架

服務下架是由Eureka的PeerAwareInstanceRegistryImpl註冊器管理的, 源碼調用入口如下:

EurekaServerBootStrap
protected void initEurekaServerContext() throws Exception {
   ...
   //在啓動定時器前同步了個節點註冊數據
   int registryCount = this.registry.syncUp();
   this.registry.openForTraffic(this.applicationInfoManager, registryCount);
   ...
}
PeerAwareInstanceRegistryImpl
@Override
public void openForTraffic(ApplicationInfoManager applicationInfoManager, int count) {
	...
    super.postInit();
}
AbstractInstanceRegistry
protected void postInit() {
    renewsLastMin.start();
    if (evictionTaskRef.get() != null) {
        evictionTaskRef.get().cancel();
    }
    //設置定時任務, 任務內會定時清理過期的註冊節點
    evictionTaskRef.set(new EvictionTask());
    evictionTimer.schedule(evictionTaskRef.get(),
            serverConfig.getEvictionIntervalTimerInMs(),//設置定時時間, springcloud配置默認是60s
            serverConfig.getEvictionIntervalTimerInMs());
}
/**
 * Evicts everything in the instance registry that has expired, if expiry is enabled.
 *
 * @see com.netflix.eureka.lease.LeaseManager#evict()
 */
@Override
public void evict() {
    evict(0l);
}

public void evict(long additionalLeaseMs) {
    logger.debug("Running the evict task");

    if (!isLeaseExpirationEnabled()) {
        logger.debug("DS: lease expiration is currently disabled.");
        return;
    }

    // We collect first all expired items, to evict them in random order. For large eviction sets,
    // if we do not that, we might wipe out whole apps before self preservation kicks in. By randomizing it,
    // the impact should be evenly distributed across all applications.
    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);
                }
            }
        }
    }

    // To compensate for GC pauses or drifting local time, we need to use current registry size as a base for
    // triggering self-preservation. Without that we would wipe out full registry.
    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);
        }
    }
}
AbstractInstanceRegistry.EvictionTask (內部類) 繼承TimerTask
public void evict(long additionalLeaseMs) {
    logger.debug("Running the evict task");

    if (!isLeaseExpirationEnabled()) {
        logger.debug("DS: lease expiration is currently disabled.");
        return;
    }

    // 首先收集所有過期節點,以隨機順序刪除,
    // 如果不這樣, 可能會將整個應用組直接刪除,
    // 隨機處理, 可以將項目的影響均勻的分佈到各個模塊接待你.
    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);
                }
            }
        }
    }

    // To compensate for GC pauses or drifting local time, we need to use current registry size as a base for
    // triggering self-preservation. Without that we would wipe out full registry.
    //爲了補償GC暫停或本地時間漂移,我們需要使用當前註冊表大小作爲,觸發自我保護。如果沒有這些,我們將抹去完整的註冊表。
    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);
        }
    }
}
AbstractInstanceRegistry
/**
 * {@link #cancel(String, String, boolean)} method is overridden by {@link PeerAwareInstanceRegistry}, so each
 * cancel request is replicated to the peers. This is however not desired for expires which would be counted
 * in the remote peers as valid cancellations, so self preservation mode would not kick-in.
 */
 /**
  * 對於取消請求需要同步到其他註冊中心節點, 但對於超時的取消, 不需要, 因爲對於超時, 其他節點做的處理與當前節點是一樣的
  * 
 */
protected boolean internalCancel(String appName, String id, boolean isReplication) {
    try {
        read.lock();
        CANCEL.increment(isReplication);
        Map<String, Lease<InstanceInfo>> gMap = registry.get(appName);
        Lease<InstanceInfo> leaseToCancel = null;
        if (gMap != null) {
            leaseToCancel = gMap.remove(id);
        }
        synchronized (recentCanceledQueue) {
            recentCanceledQueue.add(new Pair<Long, String>(System.currentTimeMillis(), appName + "(" + id + ")"));
        }
        InstanceStatus instanceStatus = overriddenInstanceStatusMap.remove(id);
        if (instanceStatus != null) {
            logger.debug("Removed instance id {} from the overridden map which has value {}", id, instanceStatus.name());
        }
        if (leaseToCancel == null) {
            CANCEL_NOT_FOUND.increment(isReplication);
            logger.warn("DS: Registry: cancel failed because Lease is not registered for: {}/{}", appName, id);
            return false;
        } else {
            leaseToCancel.cancel();
            InstanceInfo instanceInfo = leaseToCancel.getHolder();
            String vip = null;
            String svip = null;
            if (instanceInfo != null) {
                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();
    }
}

自我保護

eureka Server 在一定時間內沒有收到客戶端發來的心跳會認爲客戶端實現, 將註冊信息刪除, 但是在短時間內丟失大量的心跳, eureka Server 會觸發自我保護, 認爲實例都是健康的, 可能會由於網絡或其他原因問題沒有收到心跳, 所以就不會再刪除註冊的實例, 認爲所有實例都是健康的

Renews threshold 與 Renews (last min)

服務器端的續約閥值(Renews threshold)

this.expectedNumberOfRenewsPerMin = count * 2; 
this.numberOfRenewsPerMinThreshold = (int) (this.expectedNumberOfRenewsPerMin * serverConfig.getRenewalPercentThreshold());

其中 , count 爲 服務器的數量 。 2 爲每 30 秒 1 個心跳 , 每分鐘 2 個心跳的固定頻率因。

歸納出的公式爲: 2M * renewalPercentThreshold , M 爲服務器的個數 , renewalPercentThreshold 默認爲 0.85(可以通過 eureka .server . renewal-percent-threshold 配置) , 計算結果只保留整數位 。

其實這就是個固定值 , 對於每個 Eureka Server 來說 , M 只能取 1 。 代碼達到的效果是:

1.expectedNumberOfRenewsPerMin 重置爲固定值 2;

2.numberOfRenewsPerMinThreshold 的值被設置爲 1;

客戶端的續約閥值(Renews threshold)

if (this.expectedNumberOfRenewsPerMin > 0) { 
  this.expectedNumberOfRenewsPerMin = this.expectedNumberOfRenewsPerMin + 2; 		
  this.numberOfRenewsPerMinThreshold = (int) (this.expectedNumberOfRenewsPerMin * serverConfig.getRenewalPercentThreshold()); 
}

注:上面貼出的 PeerAwareInstanceRegistryImpl 繼承自 AbstractInstanceRegistry 。

它們共享 expectedNumberOfRenewsPerMin (預期每分鐘續訂數)和 numberOfRenewsPerMinThreshold 屬性,

具體可自行翻閱源碼。

設有 N 個客戶端:服務器端先啓動 , expectedNumberOfRenewsPerMin 被重置爲固定值 2 。 接着客戶端依次啓動:

N = 1 –> (2 + 2) * renewalPercentThreshold

N = 2 –> (2 + 2 + 2) * renewalPercentThreshold

N = 3 –> (2 + 2 + 2 + 2) * renewalPercentThreshold

歸納出的公式爲: 2(N + 1) * renewalPercentThreshold , 計算結果只保留整數位 。

即 , 如果只有 1 個 Eureka Server 或者有多個 Eureka Server 但它們之間沒有相互註冊:

當 N = 0 時 , 只計算服務器端 。 Renews threshold = 1 。 由於沒有客戶端向服務器發送心跳 , Renews (last min) < Renews threshold , Eureka 自我保護模式被激活;

當 N ≠ 0 時 , 服務器端的計算結果被客戶端覆蓋 , 即只計算客戶端;

當 N = 2 時 , Renews threshold = 2(N + 1) * renewalPercentThreshold = 2 * 3 * 0.85 = 5 。 2 個客戶端以每 30 秒發送 1 個心跳 , 1 分鐘後總共向服務器發送 4 個心跳 , Renews (last min) < Renews threshold , Eureka 自我保護模式被激活;

所以如果 N < 3 , 在 1 分鐘後 , 服務器端收到的客戶端實例續約的總數總是小於期望的閥值 , 因此 Eureka 的自我保護模式自動被激活 。 首頁會出現警告信息:

EMERGENCY! EUREKA MAY BE INCORRECTLY CLAIMING INSTANCES ARE UP WHEN THEY’RE NOT. RENEWALS ARE LESSER THAN THRESHOLD AND HENCE THE INSTANCES ARE NOT BEING EXPIRED JUST TO BE SAFE.

這種情況下 , 由於 Eureka Server 沒有對等的節點 , 同步不到服務註冊信息 , 默認需等待 5 分鐘(可以通過 eureka.server.wait-time-in-ms-when-sync-empty配置) 。 即 5 分鐘之後你應該看到此警告信息 。

爲避免這種情況發生 , 你可以:

關閉自我保護模式( eureka.server.enable-self-preservation 設爲 false)

降低 renewalPercentThreshold 的比例( eureka.server.renewal-percent-threshold 設置爲 0.5 以下 , 比如 0.49)

部署多個 Eureka Server 並開啓其客戶端行爲( eureka.client.register-with-eureka 不要設爲 false , 默認爲 true)

如果是採取部署多個 Eureka Server 並開啓其客戶端行爲使其相互註冊 。 假設有 M 個 Eureka Server , 那麼 , 每個 Eureka Server 每分鐘可以額外收到 2 * (M – 1) 個心跳 。 例如:

當 M = 1 , N = 2 時 , Renews threshold = 2(N + 1) * renewalPercentThreshold = 2 * 3 * 0.85 = 5 , 2 個客戶端以每 30 秒發送 1 個心跳 , 1 分鐘後總共向服務器發送 4 個心跳 , Renews (last min) < Renews threshold ;

當 M = 2 , N = 2 時 , Renews threshold = 2(N + 1) * renewalPercentThreshold = 2 * 3 * 0.85 = 5 , 2 個客戶端以每 30 秒發送 1 個心跳 , 1 分鐘後總共向服務器發送 4 個心跳 , 另外還有 1 個 M 發來的 2 個心跳 , 總共是 6 個心跳 , Renews (last min) > Renews threshold ;

Eureka 的自我保護模式是有意義的 , 該模式被激活後 , 它不會從註冊列表中剔除因長時間沒收到心跳導致租期過期的服務 , 而是等待修復 , 直到心跳恢復正常之後 , 它自動退出自我保護模式 。 這種模式旨在避免因網絡分區故障導致服務不可用的問題 。 例如 , 兩個客戶端實例 C1 和 C2 的連通性是良好的 , 但是由於網絡故障 , C2 未能及時向 Eureka 發送心跳續約 , 這時候 Eureka 不能簡單的將 C2 從註冊表中剔除 。 因爲如果剔除了 , C1 就無法從 Eureka 服務器中獲取 C2 註冊的服務 , 但是這時候 C2 服務是可用的 。 所以 , Eureka 的自我保護模式最好還是開啓它 。

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