04.Spring Cloud 之 Eureka Client 源碼解析

1. 環境搭建

代碼已經上傳至 https://github.com/masteryourself-tutorial/tutorial-spring,工程是 tutorial-spring-cloud/tutorial-spring-cloud-eureka/tutorial-spring-cloud-eureka-single-7001

2. 源碼解析

詳細的源碼註釋可參考 https://github.com/masteryourself/eureka

2.1 服務 註冊/續約/下線 處理

2.1.1 流程分析

服務註冊續約下線處理

2.1.2 核心源碼剖析
1. com.netflix.eureka.registry.AbstractInstanceRegistry#register
public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {
    try {
        read.lock();
        // 註冊中心,實際上就是一個 ConcurrentHashMap
        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;
            }
        }
        // 正常來說,第一次註冊時候 existingLease 是空數據,走 else 流程
        // 但是也存在這種情況:第一次註冊時候,由於網絡原因還沒有處理,心跳的請求已經先處理結束,然後這裏的 existingLease 是有值的
        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.
            // 存在這種情況:心跳的請求先處理了,然後再處理註冊,這時候就有可能 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.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() + ")"));
        }
        
        ...
        
    } finally {
        read.unlock();
    }
}
2. com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl#replicateInstanceActionsToPeers
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);
    }
}
3. com.netflix.eureka.cluster.PeerEurekaNode#register
public void register(final InstanceInfo info) throws Exception {
    // 獲取過期時間
    long expiryTime = System.currentTimeMillis() + getLeaseRenewalOf(info);
    batchingDispatcher.process(
            taskId("register", info),
            new InstanceReplicationTask(targetHost, Action.Register, info, null, true) {
                public EurekaHttpResponse<Void> execute() {
                    // 在過期時間之前需要完成 register() 任務
                    return replicationClient.register(info);
                }
            },
            expiryTime
    );
}
4. com.netflix.eureka.registry.AbstractInstanceRegistry#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) {
        // 獲取其對應的 Lease 信息
        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) {
        
            ...
            
            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);

            }
        }
        // 計數器 +1
        renewsLastMin.increment();
        // 更新最後修改時間
        leaseToRenew.renew();
        return true;
    }
}
5. com.netflix.eureka.cluster.PeerEurekaNode#heartbeat
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);
        }

        ...
       
    };
    long expiryTime = System.currentTimeMillis() + getLeaseRenewalOf(info);
    // 在過期時間之前需要完成 sendHeartBeat() 任務
    batchingDispatcher.process(taskId("heartbeat", info), replicationTask, expiryTime);
}
6. com.netflix.eureka.registry.AbstractInstanceRegistry#internalCancel
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 + ")"));
        }
        // 從狀態 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();
            
            ...
            
            return true;
        }
    } finally {
        read.unlock();
    }
}
7. com.netflix.eureka.cluster.PeerEurekaNode#cancel
public void cancel(final String appName, final String id) throws Exception {
    long expiryTime = System.currentTimeMillis() + maxProcessingDelayMs;
    batchingDispatcher.process(
            taskId("cancel", appName, id),
            new InstanceReplicationTask(targetHost, Action.Cancel, appName, id) {
                @Override
                public EurekaHttpResponse<Void> execute() {
                    // 在過期時間之前需要完成 cancel() 任務
                    return replicationClient.cancel(appName, id);
                }

                ...
                
            },
            expiryTime
    );
}

2.2 EurekaServerAutoConfiguration 裝配流程

EurekaServerAutoConfiguration 裝配流程

spring-cloud-starter-netflix-eureka-server-xxx.jar -> spring-cloud-netflix-eureka-server-xxx.jar -> 定義 spring.factories 文件 -> 向容器中導入 EurekaServerAutoConfiguration 組件,其中配置文件內容如下:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  org.springframework.cloud.netflix.eureka.server.EurekaServerAutoConfiguration

EurekaServerAutoConfiguration -> EurekaServerInitializerConfiguration + EurekaServerConfigBean

EurekaServerConfigBean 其實就是配置類,自動關聯 eureka.server 前綴的配置文件

EurekaServerInitializerConfiguration 實現了 SmartLifecycle 接口,Spring 在 finishRefresh() -> getLifecycleProcessor().onRefresh() 方法中會去調用 start() 方法

2.3 定時清理 client 信息

2.3.1 流程分析

定時清理 client 信息

2.3.2 核心源碼剖析
1. com.netflix.eureka.registry.AbstractInstanceRegistry#postInit
protected void postInit() {
    // 啓動 renewsLastMin 定時器,即更新 Renews(last min)
    renewsLastMin.start();
    if (evictionTaskRef.get() != null) {
        evictionTaskRef.get().cancel();
    }
    evictionTaskRef.set(new EvictionTask());
    // 啓動 evictionTimer 定時器,清理註冊中心信息
    evictionTimer.schedule(evictionTaskRef.get(),
            serverConfig.getEvictionIntervalTimerInMs(),
            serverConfig.getEvictionIntervalTimerInMs());
}
2. com.netflix.eureka.registry.AbstractInstanceRegistry.EvictionTask#getCompensationTimeMs
long getCompensationTimeMs() {
    // 當前時間
    long currNanos = getCurrentTimeNano();
    // 上一次清理後的時間
    long lastNanos = lastExecutionNanosRef.getAndSet(currNanos);
    if (lastNanos == 0l) {
        return 0l;
    }
    // 算出上一次的實際清理時間
    long elapsedMs = TimeUnit.NANOSECONDS.toMillis(currNanos - lastNanos);
    // 如果上次實際清理時間 > 指定的清理時間,計算出補償時間
    long compensationTime = elapsedMs - serverConfig.getEvictionIntervalTimerInMs();
    return compensationTime <= 0l ? 0l : compensationTime;
}
3. com.netflix.eureka.registry.AbstractInstanceRegistry#evict(long)
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();
                // 判斷依據:是否過期,當前 holder 是否爲空
                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);
        // 洗牌清除客戶端數量,避免每次清除的都是前面的 client 信息
        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);
            // 清理工作,即之前分析的 cancel() 服務下線操作
            internalCancel(appName, id, false);
        }
    }
}
4. com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl#isLeaseExpirationEnabled
public boolean isLeaseExpirationEnabled() {
    // 如果禁用了自我保護機制,直接返回
    if (!isSelfPreservationModeEnabled()) {
        // The self preservation mode is disabled, hence allowing the instances to expire.
        return true;
    }
    // 當 renewsLastMin (實際在最後一分鐘收到的 client 端發送的續約的數量) < numberOfRenewsPerMinThreshold(期待每分鐘收到 client 端發送的續約總數) 時,觸發自動保護機制
    return numberOfRenewsPerMinThreshold > 0 && getNumOfRenewsInLastMin() > numberOfRenewsPerMinThreshold;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章