spring-cloud-eureka (三) 註冊中心源碼分析

Eureka是一個開源的服務治理框架,它提供了完成的Service Registry和Service Discovery實現,並且和Spring Cloud無縫集成,使用Spring Boot + Spring Cloud可以輕鬆的將註冊中心搭建起來。

Eureka架構

基礎架構

這裏寫圖片描述
上圖簡單的描述了Eureka的基本結構,由3個角色組成:

  • Eureka Server: 維護服務信息,包括實例信息,提供服務治理基礎功能的功能,如服務註冊和服務發現。
  • Service Provider: 服務提供者,將自身提供的服務註冊到Eureka Server,使服務消費者能夠從Eureka Server中獲取到。
  • Service Consumer: 服務消費者,從Eureka Server中獲取註冊服務列表,從而調用相關的服務。

上述三個角色都是抽象的邏輯角色,在實際運行中,這幾個角色可以是同一個實例。



高可用架構

這裏寫圖片描述

上圖更進一步的展示了3個角色之間的交互。

  • 服務提供者向Eureka Server發送服務註冊、服務續約、服務下線等操作。
  • Eureka Server 之間會進行註冊服務的同步, 從而保證服務狀態一致。
  • 服務消費者向Eureka Server獲取註冊服務列表,並調用服務。

源碼分析

服務註冊

Eureka Server會維護一個服務清單列表,這個列表是一個雙層結構的Map對象,其中第一層的key是服務名,第二層的key是服務實例名。

我們從Eureka Server 的配置類org.springframework.cloud.netflix.eureka.server.EurekaServerAutoConfiguration中可以找到, 發佈的服務實例註冊表是org.springframework.cloud.netflix.eureka.server.InstanceRegistry。

這裏寫圖片描述

InstanceRegistry類繼承了PeerAwareInstanceRegistryImpl類,所以它支持集羣。
還有一個InstanceRegistry接口,它的完成路徑是com.netflix.eureka.registry.InstanceRegistry。org.springframework.cloud.netflix.eureka.server.InstanceRegistry類是它的實現類。該接口從字面意思可以理解爲實例註冊表,它繼承了LeaseManager接口和LookupService接口。

  • LookupService接口主要是查找正常運行的服務實例。
  • LeaseManager接口主要是維護可用服務清單的,它將服務的可能期限抽象爲租約期限,該接口負責爲一個實例的租約的創建、續約、和下線。



發佈事件

org.springframework.cloud.netflix.eureka.server.InstanceRegistry類會幫服務註冊、服務續約、服務下線操作發佈一個相應的事件,然後調用父類的方法。

...
public class InstanceRegistry extends PeerAwareInstanceRegistryImpl
        implements ApplicationContextAware {
    ...
    //服務註冊
    @Override
    public void register(final InstanceInfo info, final boolean isReplication) {
        handleRegistration(info, resolveInstanceLeaseDuration(info), isReplication);
        super.register(info, isReplication);
    }

    //服務下線
    @Override
    public boolean cancel(String appName, String serverId, boolean isReplication) {
        handleCancelation(appName, serverId, isReplication);
        return super.cancel(appName, serverId, isReplication);
    }

    //服務續約
    @Override
    public boolean renew(final String appName, final String serverId,
            boolean isReplication) {
        log("renew " + appName + " serverId " + serverId + ", isReplication {}"
                + isReplication);
        List<Application> applications = getSortedApplications();
        for (Application input : applications) {
            if (input.getName().equals(appName)) {
                InstanceInfo instance = null;
                for (InstanceInfo info : input.getInstances()) {
                    if (info.getId().equals(serverId)) {
                        instance = info;
                        break;
                    }
                }
                publishEvent(new EurekaInstanceRenewedEvent(this, appName, serverId,
                        instance, isReplication));
                break;
            }
        }
        return super.renew(appName, serverId, isReplication);
    }
    ...
}
操作 事件
服務註冊 EurekaInstanceRegisteredEvent
服務續約 EurekaInstanceRenewedEvent
服務下線 EurekaInstanceCanceledEvent



Eureka Server 集羣同步

InstanceRegistry類繼承了 PeerAwareInstanceRegistryImpl類,所以服務註冊、續約、下線等操作完成後,會去調用PeerAwareInstanceRegistryImpl的相關邏輯。而PeerAwareInstanceRegistryImpl中主要是添加了一個廣播的功能,擁有了將服務實例的註冊、續約、下線等操作同步到其它Eureka Server的能力。

com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl

package com.netflix.eureka.registry;
...
public class PeerAwareInstanceRegistryImpl extends AbstractInstanceRegistry implements PeerAwareInstanceRegistry {
    ...
    @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);
            ...
            return true;
        }
        return false;
    }
    ...
    @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 boolean renew(final String appName, final String id, final boolean isReplication) {
        //調用父類方法
        if (super.renew(appName, id, isReplication)) {
            //發送廣播
            replicateToPeers(Action.Heartbeat, appName, id, null, null, isReplication);
            return true;
        }
        return false;
    }
    ...
    //發送同步消息的邏輯,如果是其它Eureka Server同步過來的,就返回不再發送同步消息。
    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;
            }

            for (final PeerEurekaNode node : peerEurekaNodes.getPeerEurekaNodes()) {
                // If the url represents this host, do not replicate to yourself.
                if (peerEurekaNodes.isThisMyUrl(node.getServiceUrl())) {
                    continue;
                }
                replicateInstanceActionsToPeers(action, appName, id, info, newStatus, node);
            }
        } finally {
            tracer.stop();
        }
    }



    //發送同步消息的操作, PeerEurekaNode.replicationClient最終調用的是JerseyReplicationClient
    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);
        }
    }
    ...
}

在同步過程中,會通過PeerEurekaNode對象中的replicationClient字段發送消息,該字段是com.netflix.eureka.cluster.HttpReplicationClient接口的實現類,默認是com.netflix.eureka.transport.JerseyReplicationClient。

這裏寫圖片描述

從上圖中可以看到,該類是AbstractJerseyEurekaHttpClient類的子類,在上一章中我們分析過AbstractJerseyEurekaHttpClient類,這裏就只分析JerseyReplicationClient類了。

JerseyReplicationClient類除了對服務續約、服務狀態變更進行了擴展外,就是在http請求頭中設置x-netflix-discovery-replication=true。

com.netflix.eureka.transport.JerseyReplicationClient

public class JerseyReplicationClient extends AbstractJerseyEurekaHttpClient implements HttpReplicationClient {
    ...
    @Override
    protected void addExtraHeaders(Builder webResource) {
        webResource.header(PeerEurekaNode.HEADER_REPLICATION, "true");
    }
    ...
}

com.netflix.eureka.cluster.PeerEurekaNode

public class PeerEurekaNode {
    ...
    public static final String HEADER_REPLICATION = "x-netflix-discovery-replication";
    ...
}

Eureka Server之間的同步是在具體操作之後。



服務註冊

比如服務註冊,是在服務註冊完成後,再執行服務同步。所以PeerAwareInstanceRegistryImpl會在同步之前調用父類AbstractInstanceRegistry的相關邏輯,比如服務註冊,而我們在InstanceRegistry類和PeerAwareInstanceRegistryImpl類中都沒有看到服務註冊的邏輯,那麼主要邏輯就肯定在它們的父類com.netflix.eureka.registry.AbstractInstanceRegistry類中了。

這裏寫圖片描述

package com.netflix.eureka.registry;
...
public abstract class AbstractInstanceRegistry implements InstanceRegistry {
    ...
    //服務清單
    private final ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>> registry
            = new ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>>();
    ...
    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;
                }
            }
            //獲取實例的信息,如果以前沒有註冊過,就返回null
            Lease<InstanceInfo> existingLease = gMap.get(registrant.getId());
            // Retain the last dirty timestamp without overwriting it, if there is already a lease
            if (existingLease != null && (existingLease.getHolder() != null)) {
                Long existingLastDirtyTimestamp = existingLease.getHolder().getLastDirtyTimestamp();
                Long registrationLastDirtyTimestamp = registrant.getLastDirtyTimestamp();
                logger.debug("Existing lease found (existing={}, provided={}", existingLastDirtyTimestamp, registrationLastDirtyTimestamp);
                if (existingLastDirtyTimestamp > registrationLastDirtyTimestamp) {
                    logger.warn("There is an existing lease and the existing lease's dirty timestamp {} is greater" +
                            " than the one that is being registered {}", existingLastDirtyTimestamp, registrationLastDirtyTimestamp);
                    logger.warn("Using the existing instanceInfo instead of the new instanceInfo as the registrant");
                    registrant = existingLease.getHolder();
                }
            } else {
                // The lease does not exist and hence it is a new registration
                synchronized (lock) {
                    if (this.expectedNumberOfRenewsPerMin > 0) {
                        // Since the client wants to cancel it, reduce the threshold
                        // (1
                        // for 30 seconds, 2 for a minute)
                        this.expectedNumberOfRenewsPerMin = this.expectedNumberOfRenewsPerMin + 2;
                        this.numberOfRenewsPerMinThreshold =
                                (int) (this.expectedNumberOfRenewsPerMin * serverConfig.getRenewalPercentThreshold());
                    }
                }
                logger.debug("No previous lease information found; it is new registration");
            }
            //創建一個新的租約實例
            Lease<InstanceInfo> lease = new Lease<InstanceInfo>(registrant, leaseDuration);
            if (existingLease != null) {
            //如果以前註冊過,就設置爲以前的註冊(啓用)時間。(沒有明白這個操作的目的)
                lease.setServiceUpTimestamp(existingLease.getServiceUpTimestamp());
            }
            //保存進到服務實例集合中
            gMap.put(registrant.getId(), lease);
            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 Server服務註冊的主要邏輯。



服務下線

在瞭解了服務註冊的邏輯後, 服務下線的邏輯就簡單很多了。主要是將服務實例從服務實例集合中刪除,並留下刪除時間和記錄。

這裏寫圖片描述

package com.netflix.eureka.registry;
...
public abstract class AbstractInstanceRegistry implements InstanceRegistry {
    ...
    @Override
    public boolean cancel(String appName, String id, boolean isReplication) {
        return internalCancel(appName, id, isReplication);
    }

    /**
     * {@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報告, 我還正常, 這樣纔不會被服務剔除機制給刪除掉。在代碼邏輯裏面,就是更新一個租約的最後修改時間,而這個最後修改時間一般會往後加一個租憑期限,這個期限默認是90秒;而在判斷租約過期時,也會再加一次租憑期限,所以,默認情況下,一個服務實例如果180秒還沒有續約的話,就會判定這個服務實例已不能正常提供服務。

這裏寫圖片描述

package com.netflix.eureka.registry;
...
public abstract class AbstractInstanceRegistry implements InstanceRegistry {
    ...
    public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {
    ...
    //將租憑期限做爲租約的實例屬性,這個值是從子類PeerAwareInstanceRegistryImpl傳遞過來的
    Lease<InstanceInfo> lease = new Lease<InstanceInfo>(registrant, leaseDuration);
    ...
    }
    ...

    //服務續約的邏輯
    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)) {
                    Object[] args = {
                            instanceInfo.getStatus().name(),
                            instanceInfo.getOverriddenStatus().name(),
                            instanceInfo.getId()
                    };
                    logger.info(
                            "The instance status {} is different from overridden instance status {} for instance {}. "
                                    + "Hence setting the status to overridden status", args);
                    instanceInfo.setStatus(overriddenInstanceStatus);
                }
            }
            renewsLastMin.increment();
            //更新續約時間/最後修改時間
            leaseToRenew.renew();
            return true;
        }
    }
    ...
}
public class Lease<T> {
    ...
    //默認的租憑期限
    public static final int DEFAULT_DURATION_IN_SECS = 90;
    ...
    private long duration;

    public Lease(T r, int durationInSecs) {
        holder = r;
        registrationTimestamp = System.currentTimeMillis();
        lastUpdateTimestamp = registrationTimestamp;
        duration = (durationInSecs * 1000);

    }

    //續約更新最後修改時間
    public void renew() {
        lastUpdateTimestamp = System.currentTimeMillis() + duration;

    }
    ...
}
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);
    }
    ...
}



服務剔除

上面分析中說到如果一個服務實例180秒還沒有更新最後修改時間的話, 就將會被服務剔除機制刪除。

在該機制中,還有一個保護機制,就是如果在一定時間段內,判斷服務續約成功的實例數低於續約閾值(最大心跳總數的85%,相當於服務實例數*2,因爲心跳消息默認30s發送一次),Eureka Server會將當前的實例信息保護起來,讓這些實例不會過期。當然前提是要開啓自我保護機制,默認是開啓的,也可以配置eureka.server.enable-self-preservation=false來關閉保護機制。

這裏寫圖片描述

public abstract class AbstractInstanceRegistry implements InstanceRegistry {

    ...
    @Override
    public void evict() {
        evict(0l);
    }

    //服務剔除邏輯
    public void evict(long additionalLeaseMs) {
        logger.debug("Running the evict task");

        //保護機制,實現邏輯在PeerAwareInstanceRegistryImpl中。
        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);

            //傳入當前時間爲種子生成隨機,避免 Java 的僞隨機情況
            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);
                //隨機調換後面的元素到當前位置( 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);
            }
        }
    }
    ...
}
public class Lease<T> {
    ...
    //默認的租憑期限
    public static final int DEFAULT_DURATION_IN_SECS = 90;
    ...
    private long duration;

    public Lease(T r, int durationInSecs) {
        holder = r;
        registrationTimestamp = System.currentTimeMillis();
        lastUpdateTimestamp = registrationTimestamp;
        duration = (durationInSecs * 1000);

    }

    //續約更新最後修改時間
    public void renew() {
        lastUpdateTimestamp = System.currentTimeMillis() + duration;

    }

    //判斷是否過期,當前時間>最後修改時間+租憑期限
    public boolean isExpired(long additionalLeaseMs) {
        return (evictionTimestamp > 0 || System.currentTimeMillis() > (lastUpdateTimestamp + duration + additionalLeaseMs));
    }
    ...
}

下面我們來看看自保護機制的邏輯

public class PeerAwareInstanceRegistryImpl extends AbstractInstanceRegistry implements PeerAwareInstanceRegistry {
    ...
    //定時更新續約閾值,默認15分鐘
    private void scheduleRenewalThresholdUpdateTask() {
        timer.schedule(new TimerTask() {
                           @Override
                           public void run() {
                               updateRenewalThreshold();
                           }
                       }, serverConfig.getRenewalThresholdUpdateIntervalMs(),
                serverConfig.getRenewalThresholdUpdateIntervalMs());
    }
    ...
    //保護機制的判斷邏輯
    @Override
    public boolean isLeaseExpirationEnabled() {
        if (!isSelfPreservationModeEnabled()) {
            // The self preservation mode is disabled, hence allowing the instances to expire.
            return true;
        }
        //判斷服務續約成功的實例數低於註冊數的85%
        return numberOfRenewsPerMinThreshold > 0 && getNumOfRenewsInLastMin() > numberOfRenewsPerMinThreshold;
    }

    //判斷是否打開保護機制
     @Override
    public boolean isSelfPreservationModeEnabled() {
        return serverConfig.shouldEnableSelfPreservation();
    }
    ...

    //更新續約閾值
    private void updateRenewalThreshold() {
        try {
            Applications apps = eurekaClient.getApplications();
            int count = 0;
            for (Application app : apps.getRegisteredApplications()) {
                for (InstanceInfo instance : app.getInstances()) {
                    if (this.isRegisterable(instance)) {
                        ++count;
                    }
                }
            }
            synchronized (lock) {
                // Update threshold only if the threshold is greater than the
                // current expected threshold of if the self preservation is disabled.
                // 更新續約閾值的判斷(這裏存在一定問題,會使保護機制一直存在),應用實例每分鐘最大心跳數( count * 2 ) 小於期望最小每分鐘續租次數( serverConfig.getRenewalPercentThreshold() * numberOfRenewsPerMinThreshold ),不重新計算
                if ((count * 2) > (serverConfig.getRenewalPercentThreshold() * numberOfRenewsPerMinThreshold)
                        || (!this.isSelfPreservationModeEnabled())) {
                    this.expectedNumberOfRenewsPerMin = count * 2;
                    this.numberOfRenewsPerMinThreshold = (int) ((count * 2) * serverConfig.getRenewalPercentThreshold());
                }
            }
            logger.info("Current renewal threshold is : {}", numberOfRenewsPerMinThreshold);
        } catch (Throwable e) {
            logger.error("Cannot update renewal threshold", e);
        }
    }
    ...

}

上面的代碼中有兩個邏輯

  • 自我保護的判斷邏輯
    isLeaseExpirationEnabled()方法是其入口,首先判斷是否打開保護機制,默認開啓。然後再判斷服務續約成功的實例數低於續約閾值,默認是註冊數*2的85%,因爲心跳是30秒發送一次,而剔除機制是1分鐘執行一次。

  • 更新自我保護
    scheduleRenewalThresholdUpdateTask()方法是該邏輯的入口, 在這個方法中,創建了一個定時任務,默認是每15分鐘更新一次續約閾值。
    updateRenewalThreshold()方法封裝了更新閾值的邏輯。但是如果應用實例每分鐘最大心跳數( count * 2 ) 小於期望最小每分鐘續租次數( serverConfig.getRenewalPercentThreshold() * numberOfRenewsPerMinThreshold ),不重新計算時,並且服務實例確實不可用了,那麼自我保護狀態會一直存在。

參考資料
《spring cloud 微服務實戰》
Eureka 源碼解析 —— 應用實例註冊發現(四)之自我保護機制
深度剖析服務發現組件Netflix Eureka

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