Eureka Server之注冊服務實例租約信息管理LeaseManager

Eureka 注冊中心服務端對注冊進來的服務信息提供了一個基于時間的可用性管理(租約Lease)。通過此來管理注冊服務的有效性

提供租約管理的包路徑

com.netflix.eureka.lease

描述注冊服務基于時間可用性

/**
 * 實例租約包裝類,描述實例基于時間的可用性
 */
public class Lease<T> {

    /**
     * 實例針對租約變化的動作類型
     */
    enum Action {
        //實例注冊
        Register,
        //實例取消
        Cancel,
        //實例刷新
        Renew
    };

    /**
     * 默認的租約有效時長 90s
     */
    public static final int DEFAULT_DURATION_IN_SECS = 90;

    /**
     * 實例信息,此處默認為 InstanceInfo
     */
    private T holder;
    /**
     * 取消注冊時間
     */
    private long evictionTimestamp;
    /**
     * 注冊時間
     */
    private long registrationTimestamp;
    /**
     *
     */
    private long serviceUpTimestamp;
    /**
     * 最后更新時間
     */
    // Make it volatile so that the expiration task would see this quicker
    private volatile long lastUpdateTimestamp;

    /**
     * 租約的有效時長
     */
    private long duration;

    /**
     * 構造函數
     * @param r
     * @param durationInSecs
     */
    public Lease(T r, int durationInSecs) {
        holder = r;
        registrationTimestamp = System.currentTimeMillis();
        lastUpdateTimestamp = registrationTimestamp;
        duration = (durationInSecs * 1000);

    }

在Lease類中可以看到是使用了裝飾模式對注冊實例進行了包裝,提供了基于時間可用性的新功能。

  • enum Action 定義了租約時間變更的動作類型,此處包含注冊,下線,刷新(心跳發送刷新租約信息)等三個動作
  • evictionTimestamp:服務下線時間
  • registrationTimestamp: 服務注冊時間
  • lastUpdateTimestamp: 最后更新時間
  • duration : 租約的有效時長默認90s

Lease類對實例InstanceInfo進行功能擴展的方法:

  1. renew
    /**
     * 刷新租約的時間信息,租約續訂更新最后更新時間為當前時間+租約有效時長
     * Renew the lease, use renewal duration if it was specified by the
     * associated {@link T} during registration, otherwise default duration is
     * {@link #DEFAULT_DURATION_IN_SECS}.
     */
    public void renew() {
        lastUpdateTimestamp = System.currentTimeMillis() + duration;

    }
  1. cancel
    /**
     * 注冊列表剔除當前服務,更新租約的時間信息
     * Cancels the lease by updating the eviction time.
     */
    public void cancel() {
        if (evictionTimestamp <= 0) {
            evictionTimestamp = System.currentTimeMillis();
        }
    }
  1. serviceUp
    /**
     * 服務上線時間
     * Mark the service as up. This will only take affect the first time called,
     * subsequent calls will be ignored.
     */
    public void serviceUp() {
        if (serviceUpTimestamp == 0) {
            serviceUpTimestamp = System.currentTimeMillis();
        }
    }
  1. isExpired
    /**
     * 檢測租約是否過期
     */
    public boolean isExpired(long additionalLeaseMs) {
        //剔除服務時間>0或當前時間>(最后更新時間+租約有效時長+時間偏差)時 表示租約已失效
        return (evictionTimestamp > 0 || System.currentTimeMillis() > (lastUpdateTimestamp + duration + additionalLeaseMs));
    }
  1. renew:針對動作Renew
  2. cancel:針對動作cancel
  3. serviceUp:針對動作Register
  4. isExpired: 用于校驗當前實例的租約是否有效,對外提供服務可用性的檢測

注冊服務基于時間可用性管理

/**
 * 實例租約管理接口,根據租約更新的動作類型操作實例的租約信息
 */
public interface LeaseManager<T> {

    /**
     * 注冊實例,更新實例租約信息
     * Assign a new {@link Lease} to the passed in {@link T}.
     */
    void register(T r, int leaseDuration, boolean isReplication);

    /**
     * 剔除實例時,更新實例租約信息
     * Cancel the {@link Lease} associated w/ the passed in <code>appName</code>
     * and <code>id</code>.
     */
    boolean cancel(String appName, String id, boolean isReplication);

    /**
     * 刷新實例時更新實例租約信息
     * Renew the {@link Lease} associated w/ the passed in <code>appName</code>
     * and <code>id</code>.
     */
    boolean renew(String appName, String id, boolean isReplication);

    /**
     * 剔除已過期的租約實例信息
     */
    void evict();
}

LeaseManager接口用于提供服務實例基于時間可控性的管理(租約管理),提供以下功能:

  1. register : 服務注冊進來設置租約的serviceUp
  2. cancel : 服務下線,設置實例租約的cancel
  3. renew : 服務報活(心跳檢測)設置租約的renew
  4. evict : 遍歷注冊表通過實例租約isExpired方法校驗是否過期,過期強制服務下線操作

其中 1-3 是Eureka 客戶端發起操作,4,為服務端定時任務輪詢服務注冊表,主動剔除過期實例。

LeaseManager默認實現類 AbstractInstanceRegistry

  1. register
/**
     * 注冊實例,更新實例租約信息
     * Registers a new instance with a given duration.
     */
    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;
                }
            }
            //在集群列表中根據服務ID查詢實例信息
            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)) {
                //服務實例信息已存在與集群中
                //根據傳入實例與已存在實例的LastDirtyTimestamp判斷使用具體實例
                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) {
                    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);
            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());
                    //將當前服務實例的重載狀態添加到重載狀態MAP集合中
                    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);

            // 判斷服務注冊狀態,當狀態為UP 更新租約信息
            // If the lease is registered with UP status, set lease service up timestamp
            if (InstanceStatus.UP.equals(registrant.getStatus())) {
                lease.serviceUp();
            }
            //設置服務實例的動作類型為ADD
            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();
        }
    }
  1. cancel
 /**
     * 根據集群ID ,服務實例ID剔除集群中服務信息
     */
    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) {
                //在就集群列表中根據服務ID移除服務
                leaseToCancel = gMap.remove(id);
            }
            synchronized (recentCanceledQueue) {
                //添加最近下線隊列,將當前的下線信息
                recentCanceledQueue.add(new Pair<Long, String>(System.currentTimeMillis(), appName + "(" + id + ")"));
            }
            //重載實例狀態map集合中移除當前的實例的重載狀態
            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) {
                    //設置實例操作類型為DELETED
                    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();
        }
    }
  1. 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) {
                    RENEW_NOT_FOUND.increment(isReplication);
                    return false;
                }
                if (!instanceInfo.getStatus().equals(overriddenInstanceStatus)) {
                    Object[] args = {
                            instanceInfo.getStatus().name(),
                            instanceInfo.getOverriddenStatus().name(),
                            instanceInfo.getId()
                    };
                    //設置實例狀態
                    instanceInfo.setStatusWithoutDirty(overriddenInstanceStatus);
                }
            }
            renewsLastMin.increment();
            //租約刷新
            leaseToRenew.renew();
            return true;
        }
    }
  1. evict
 /**
     * 剔除已過期的租約實例信息
     * @param additionalLeaseMs
     */
    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();
                    //lease.isExpired() 判斷租約是否過期
                    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();
        //注冊表閾值,由registrySize * 配置百分比參數RenewalPercentThreshold
        int registrySizeThreshold = (int) (registrySize * serverConfig.getRenewalPercentThreshold());
        //剔除租約過期的數量 由于RenewalPercentThreshold取值在0-1之間,當為1時,evictionLimit為0
        // 即toEvict為0,每次時不會進行對過期的租約服務下線操作,同時 evictionLimit < expiredLeases.size() 時,
        // 每次操作不會將全部的過期租約實例下線
        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類中保存這服務注冊表信息,1-3方法為客戶端主動發起請求到server端的resources模塊,resource接受到客戶端的請求,根據請求類型調用
AbstractInstanceRegistry中的1-3方法。

4 方法為服務端定時清除注冊表中的租約過期實例;在AbstractInstanceRegistry類中如下實現:

  protected void postInit() {
        renewsLastMin.start();
        if (evictionTaskRef.get() != null) {
            evictionTaskRef.get().cancel();
        }
        //創建清除過期租約任務
        evictionTaskRef.set(new EvictionTask());
        //啟動定時清除注冊表中過期租約信息定時任務
        evictionTimer.schedule(evictionTaskRef.get(),
                serverConfig.getEvictionIntervalTimerInMs(),
                serverConfig.getEvictionIntervalTimerInMs());
    }

postInit 初始化定時任務信息,執行定時清除租約過期任務

class EvictionTask extends TimerTask {

        private final AtomicLong lastExecutionNanosRef = new AtomicLong(0l);

        @Override
        public void run() {
            try {
                long compensationTimeMs = getCompensationTimeMs();
             
                // 執行租約管理方法evict
                evict(compensationTimeMs);
            } catch (Throwable e) {
                logger.error("Could not run the evict task", e);
            }
        }

EvictionTask任務run方法中,執行租約管理的evict方法檢測注冊表中的過期租約實例,并對其進行下線操作。

至此,Eureka賦予了服務注冊實例InstanceInfo的基于時間的可用性,并對實例提供了基于時間的可用性的管理。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容