1. 程式人生 > >【一起學原始碼-微服務】Nexflix Eureka 原始碼十:服務下線及例項摘除,一個client下線到底多久才會被其他例項感知?

【一起學原始碼-微服務】Nexflix Eureka 原始碼十:服務下線及例項摘除,一個client下線到底多久才會被其他例項感知?

前言

前情回顧

上一講我們講了 client端向server端傳送心跳檢查,也是預設每30鍾傳送一次,server端接收後會更新登錄檔的一個時間戳屬性,然後一次心跳(續約)也就完成了。

本講目錄

這一篇有兩個知識點及一個疑問,這個疑問是在工作中真真實實遇到過的。

例如我有服務A、服務B,A、B都註冊在同一個註冊中心,當B下線後,A多久能感知到B已經下線了呢?

不知道大家有沒有這個困惑,這篇文章最後會對此問題答疑,如果能夠看到文章的結尾,或許你就知道答案了,當然答案也會在結尾揭曉。

目錄如下:

  1. Client端服務例項下線通知Server端
  2. Server端定時任務 服務摘除

技術亮點:定時任務錯誤觸發時間補償機制

在Server端定時任務進行服務故障自動感知摘除的時候有一個設計很巧妙的點,時間補償機制。

我們知道,在做定時任務的時候,基於某個固定點觸發的操作都可能由於一些其他原因導致固定的點沒有執行對應的操作,這時再次執行定時操作後,計算的每次任務相隔時間就會出現問題。而Eureka 這裡採用了一種補償機制,再計算時間差值的時候完美解決此問題。

說明

原創不易,如若轉載 請標明來源:一枝花算不算浪漫

原始碼分析

Client端服務例項下線通知Server端

Client下線 我們還是依照之前的原則,從DiscoveryClient 看起,可以看到有一個shutdown() 方法,然後接著跟一下這個方法:

@PUT
public Response renewLease(
        @HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication,
        @QueryParam("overriddenstatus") String overriddenStatus,
        @QueryParam("status") String status,
        @QueryParam("lastDirtyTimestamp") String lastDirtyTimestamp) {
    boolean isFromReplicaNode = "true".equals(isReplication);
    boolean isSuccess = registry.renew(app.getName(), id, isFromReplicaNode);

    // 省略部分程式碼

    logger.debug("Found (Renew): {} - {}; reply status={}" + app.getName(), id, response.getStatus());
    return response;
}


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.setStatusWithoutDirty(overriddenInstanceStatus);
            }
        }
        renewsLastMin.increment();
        leaseToRenew.renew();
        return true;
    }
}

程式碼也很簡單,做一些資源釋放,取消排程任等操作,這裡主要還是關注的是通知Server端的邏輯,及Server端是如何做例項下線的。這裡請求Server端請求主要看下unregister方法,這裡是呼叫jersey中的cancel 方法,呼叫Server端ApplicationsResource中的@DELETE 請求。(看到這裡,前面看到各種client端呼叫server端,都是通過請求方式來做restful風格呼叫的,這裡不僅要感嘆 妙啊)

我們到Server端看下接收請求的入口程式碼:

InstanceResource.cancelLease()

@DELETE
public Response cancelLease(
        @HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication) {
    boolean isSuccess = registry.cancel(app.getName(), id,
            "true".equals(isReplication));

    if (isSuccess) {
        logger.debug("Found (Cancel): " + app.getName() + " - " + id);
        return Response.ok().build();
    } else {
        logger.info("Not Found (Cancel): " + app.getName() + " - " + id);
        return Response.status(Status.NOT_FOUND).build();
    }
}

然後接著往下跟,AbstractInstanceRegistry.internalCancel 方法:

protected boolean internalCancel(String appName, String id, boolean isReplication) {
    try {
        read.lock();
        CANCEL.increment(isReplication);
        // 通過appName獲取登錄檔資訊
        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 + ")"));
        }
        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 {
            // 執行下線操作的cancel方法
            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();
    }
}

接著看 Lease.cancel :

public void cancel() {
    // 這裡只是更新服務例項中下線的時間戳
    if (evictionTimestamp <= 0) {
        evictionTimestamp = System.currentTimeMillis();
    }
}

這裡已經加了註釋,再總結下:

1、加上讀鎖,支援多服務例項下線
2、通過appName獲取登錄檔資訊map
3、通過appId移除對應登錄檔資訊
4、recentCanceledQueue新增該服務例項
5、更新Lease中的服務例項下線時間
6、recentlyChangedQueue新增該服務例項
7、invalidateCache() 使登錄檔的讀寫快取失效

這裡針對於6、7再解釋一下,我們在第八講:【一起學原始碼-微服務】Nexflix Eureka 原始碼八:EurekaClient服務發現之登錄檔抓取 精妙設計分析! 中講過,當client端第一次進行增量登錄檔抓取的時候,是會從recentlyChangedQueue中獲取資料的,然後放入到讀寫快取,然後再同步到只讀快取,下次再獲取的時候直接從只讀快取獲取即可。

這裡會存在一個問題,如果一個服務下線了,讀寫快取更新了,但是隻讀快取並未更新,30s後由定時任務重新整理 讀寫快取的資料到了只讀快取,這時其他客戶端才會感知到該下線的服務例項。

配合文字說明這裡加一個EurekaClient下線流程圖,紅色線是下線邏輯,黑色線是抓取登錄檔 感知服務下線邏輯:

記住一點,這裡是正常的服務下線,走shutdown邏輯,如果一個服務突然自己宕機了,那麼註冊中心怎麼去自動感知這個服務下線呢?緊接著往下看吧。

Server端定時任務 服務摘除

舉例一個場景,上面也說過,一個Client服務端自己掛掉了,並沒有正常的去執行shutdown方法,那麼註冊中心該如何感知這個服務例項下線了並從登錄檔摘除這個例項呢?

我們知道,eureka靠心跳機制來感知服務例項是否還存活著,如果某個服務掛掉了是不會再發送心跳過來了,如果在一段時間內沒有接收到某個服務的心跳,那麼就將這個服務例項給摘除掉,認為這個服務例項以及宕機了。

這裡自動檢測服務例項是否宕機的入口在:EurekaBootStrap,eureka server在啟動初始化的時候,有個方法registry.openForTraffic(applicationInfoManager, registryCount) 裡面會有一個服務例項檢測的排程任務(這個入口真的很隱蔽,網上查了別人的分析才找到),接著直接看程式碼吧。

EurekaBootStrap.initEurekaServerContext()

protected void initEurekaServerContext() throws Exception {
    // 省略部分程式碼...
    
    int registryCount = registry.syncUp();
    registry.openForTraffic(applicationInfoManager, registryCount);
}

這裡的程式碼前面看過很多次,syncUp是獲取其他EurekaServer中登錄檔資料,然後拿到登錄檔中服務例項registryCount,然後和自己本地登錄檔服務例項數量進行對比等等。

接著是openForTraffic方法,這裡會計算預期的1分鐘所有服務例項心跳次數expectedNumberOfRenewsPerMin
(插個眼,後面eureka server自我保護機制會用到這個屬性)後面會詳細講解,而且這裡設定還是有bug的。

在方法的最後會有一個:super.postInit(); 到了這裡才是真正的服務例項自動感知的排程任務邏輯。兜兜轉轉 在這個不起眼的地方 隱藏了這麼重要的邏輯。

PeerAwareInstanceRegistryImpl.java

public int syncUp() {
    // Copy entire entry from neighboring DS node
    int count = 0;

    for (int i = 0; ((i < serverConfig.getRegistrySyncRetries()) && (count == 0)); i++) {
        if (i > 0) {
            try {
                Thread.sleep(serverConfig.getRegistrySyncRetryWaitMs());
            } catch (InterruptedException e) {
                logger.warn("Interrupted during registry transfer..");
                break;
            }
        }
        Applications apps = eurekaClient.getApplications();
        for (Application app : apps.getRegisteredApplications()) {
            for (InstanceInfo instance : app.getInstances()) {
                try {
                    // isRegisterable:是否可以在當前服務例項所在的註冊中心註冊。這個方法一定返回true,那麼count就是相鄰註冊中心所有服務例項數量
                    if (isRegisterable(instance)) {
                        register(instance, instance.getLeaseInfo().getDurationInSecs(), true);
                        count++;
                    }
                } catch (Throwable t) {
                    logger.error("During DS init copy", t);
                }
            }
        }
    }
    return count;
}

@Override
public void openForTraffic(ApplicationInfoManager applicationInfoManager, int count) {
    // Renewals happen every 30 seconds and for a minute it should be a factor of 2.
    // 如果有20個服務例項,乘以2 代表需要40次心跳
    // 這裡有bug,count * 2 是硬編碼,作者是不是按照心跳時間30秒計算的?所以計算一分鐘得心跳就是 * 2,但是心跳時間是可以自己配置修改的
    // 看了master原始碼,這一塊已經改為:
    /**
     * this.expectedNumberOfClientsSendingRenews = this.expectedNumberOfClientsSendingRenews + 1;
     * updateRenewsPerMinThreshold();
     *
     * 主要是看 updateRenewsPerMinThreshold 方法:
     * this.numberOfRenewsPerMinThreshold = (int) (this.expectedNumberOfClientsSendingRenews * (60.0 / serverConfig.getExpectedClientRenewalIntervalSeconds() * serverConfig.getRenewalPercentThreshold());
     * 這裡完全是讀取使用者自己配置的心跳檢查時間,然後用60s / 配置時間
     */
    this.expectedNumberOfRenewsPerMin = count * 2;
    // numberOfRenewsPerMinThreshold = count * 2 * 0.85 = 34 期望一分鐘 20個服務例項,得有34個心跳
    this.numberOfRenewsPerMinThreshold =
            (int) (this.expectedNumberOfRenewsPerMin * serverConfig.getRenewalPercentThreshold());
    logger.info("Got " + count + " instances from neighboring DS node");
    logger.info("Renew threshold is: " + numberOfRenewsPerMinThreshold);
    this.startupTime = System.currentTimeMillis();
    if (count > 0) {
        this.peerInstancesTransferEmptyOnStartup = false;
    }
    DataCenterInfo.Name selfName = applicationInfoManager.getInfo().getDataCenterInfo().getName();
    boolean isAws = Name.Amazon == selfName;
    if (isAws && serverConfig.shouldPrimeAwsReplicaConnections()) {
        logger.info("Priming AWS connections for all replicas..");
        primeAwsReplicas(applicationInfoManager);
    }
    logger.info("Changing status to UP");
    applicationInfoManager.setInstanceStatus(InstanceStatus.UP);
    // 此方法會做服務例項的自動摘除任務
    super.postInit();
}
  1. 關於syncUp 方法,這裡知道它是獲取其他服務登錄檔資訊,然後獲取註冊例項數量就行了,後面還會有更詳細的講解。

  2. 接著openForTraffic 方法,第一行程式碼:this.expectedNumberOfRenewsPerMin = count * 2; 這個count是相鄰登錄檔中所有服務例項數量,至於乘以2 是什麼意思呢? 首先是這個欄位的含義是:期待的一分鐘所有服務例項心跳次數,因為服務續約renew 預設是30s執行一次,所以這裡就想當然一分鐘就乘以2了。

  3. 大家看出來了吧?這是個很明顯的bug。因為續約時間是可配置的,如果手動配置成10s,那麼這裡乘以6才對。看了下公司程式碼 spring-cloud版本是Finchley.RELEASE, 其中以來的netflix eureka 是1.9.2 仍然存在這個問題。

  4. 我也翻看了master分支的程式碼,此bug已經修復了,修改如下:

    其實這一塊還有很多bug,包括服務註冊、下線 用的都是+2 -2操作,後面一篇文章會有更多講解。

繼續看服務例項自動感知的排程任務:

AbstractInstanceRegistry.java :

protected void postInit() {
    renewsLastMin.start();
    if (evictionTaskRef.get() != null) {
        evictionTaskRef.get().cancel();
    }
    evictionTaskRef.set(new EvictionTask());
    evictionTimer.schedule(evictionTaskRef.get(),
            serverConfig.getEvictionIntervalTimerInMs(),
            serverConfig.getEvictionIntervalTimerInMs());
}

class EvictionTask extends TimerTask {
    private final AtomicLong lastExecutionNanosRef = new AtomicLong(0l);

    @Override
    public void run() {
        try {
            // 獲取補償時間 可能大於0
            long compensationTimeMs = getCompensationTimeMs();
            logger.info("Running the evict task with compensationTime {}ms", compensationTimeMs);
            evict(compensationTimeMs);
        } catch (Throwable e) {
            logger.error("Could not run the evict task", e);
        }
    }

    /**
     * compute a compensation time defined as the actual time this task was executed since the prev iteration,
     * vs the configured amount of time for execution. This is useful for cases where changes in time (due to
     * clock skew or gc for example) causes the actual eviction task to execute later than the desired time
     * according to the configured cycle.
     */
    long getCompensationTimeMs() {
        // 第一次進來先獲取當前時間 currNanos=20:00:00
        // 第二次過來,此時currNanos=20:01:00
        // 第三次過來,currNanos=20:03:00才過來,本該60s排程一次的,由於fullGC或者其他原因,到了這個時間點沒執行
        long currNanos = getCurrentTimeNano();

        // 獲取上一次這個EvictionTask執行的時間 getAndSet :以原子方式設定為給定值,並返回以前的值
        // 第一次 將20:00:00 設定到lastNanos,然後return 0
        // 第二次過來後,拿到的lastNanos為20:00:00
        // 第三次過來,拿到的lastNanos為20:01:00
        long lastNanos = lastExecutionNanosRef.getAndSet(currNanos);
        if (lastNanos == 0l) {
            return 0l;
        }

        // 第二次進來,計算elapsedMs = 60s
        // 第三次進來,計算elapsedMs = 120s
        long elapsedMs = TimeUnit.NANOSECONDS.toMillis(currNanos - lastNanos);
        // 第二次進來,配置的服務驅逐間隔預設時間為60s,計算的補償時間compensationTime=0
        // 第三次進來,配置的服務驅逐間隔預設時間為60s,計算的補償時間compensationTime=60s
        long compensationTime = elapsedMs - serverConfig.getEvictionIntervalTimerInMs();
        return compensationTime <= 0l ? 0l : compensationTime;
    }

    long getCurrentTimeNano() {  // for testing
        return System.nanoTime();
    }

}
  1. 這裡執行postInit 方法,然後執行EvictionTask 任務,執行時間是serverConfig.getEvictionIntervalTimerInMs() 預設是60s執行一次。

  2. 接著呼叫EvictionTask ,這裡也加了一些註釋,我們再來分析一下。
    2.1 首先是獲取補償時間,compenstationTimeMs,這個時間很關鍵
    2.2 呼叫evict 方法,摘除過期沒有傳送心跳的例項

檢視getCompensationTimeMs 方法,這裡我添加了很詳細的註釋,這個方法主要是 為了防止 定時任務觸發點,服務因為某些原因沒有執行該排程任務,此時elapsedMs 會超過60s的,最後返回的compensationTime 就是實際延誤且需要補償的時間。

接著再看下evict 邏輯:

public void evict(long additionalLeaseMs) {

    // 是否允許主動刪除宕機節點資料,這裡判斷是否進入自我保護機制,如果是自我保護了則不允許摘除服務
    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);
                }
            }
        }
    }

    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);
        }
    }
}
public boolean isLeaseExpirationEnabled() {
    if (!isSelfPreservationModeEnabled()) {
        // The self preservation mode is disabled, hence allowing the instances to expire.
        return true;
    }

    // 這行程式碼觸發自我保護機制,期望的一分鐘要有多少次心跳傳送過來,所有服務例項一分鐘得傳送多少次心跳
    // getNumOfRenewsInLastMin 上一分鐘所有服務例項一共傳送過來多少心跳,10次
    // 如果上一分鐘 的心跳次數太少了(20次)< 我期望的100次,此時會返回false
    return numberOfRenewsPerMinThreshold > 0 && getNumOfRenewsInLastMin() > numberOfRenewsPerMinThreshold;
}
  1. 首先看isLeaseExpirationEnabled 方法,這個方法是判斷是否需要自我保護的,裡面邏輯其實也很簡單,獲取山一分鐘所有例項心跳的次數和numberOfRenewsPerMinThreshold (期望的每分鐘所有例項心跳次數x85%) 進行對比,如果大於numberOfRenewsPerMinThreshold 才允許摘除例項,否則進入自我保護模式。下一節會詳細講解這個方法。
  2. 如果服務例項可以被移除,接著往下看,這裡是遍歷所有的服務註冊資訊,然後一個個遍歷服務例項心跳時間是否超過了對應的時間,主要看 lease.isExpired(additionalLeaseMs) 方法:

Lease.isExpired()

/**
 * Checks if the lease of a given {@link com.netflix.appinfo.InstanceInfo} has expired or not.
 *
 * Note that due to renew() doing the 'wrong" thing and setting lastUpdateTimestamp to +duration more than
 * what it should be, the expiry will actually be 2 * duration. This is a minor bug and should only affect
 * instances that ungracefully shutdown. Due to possible wide ranging impact to existing usage, this will
 * not be fixed.
 *
 * @param additionalLeaseMs any additional lease time to add to the lease evaluation in ms.
 */
public boolean isExpired(long additionalLeaseMs) {
    // lastUpdateTimestamp renew成功後就會重新整理這個時間,可以理解為最近一次活躍時間
    // 檢視 Lease.renew方法:lastUpdateTimestamp = System.currentTimeMillis() + duration;
    // duration可以檢視為:LeaseInfo中的DEFAULT_LEASE_RENEWAL_INTERVAL=90s 預設為90s
    // 這段邏輯為 當前時間 > 上一次心跳時間 + 90s + 補償時間
    /**
     * 這裡先不看補償時間,假設補償時間為0,這段的含義是 如果當前時間大於上次續約的時間+90s,那麼就認為該例項過期了
     * 因為lastUpdateTimestamp=System.currentTimeMillis()+duration,所以這裡可以理解為 超過180是還沒有續約,那麼就認為該服務例項過期了
     *
     * additionalLeaseMs 時間是一個容錯的機制,也是服務保持最終一致性的一種手段,針對於定時任務 因為一些不可控原因在某些時間點沒有定時執行,那麼這個就是很好的容錯機制
     * 這段程式碼 意思現在理解為:服務如果宕機了,那麼最少180s 才會被註冊中心摘除掉
     */
    return (evictionTimestamp > 0 || System.currentTimeMillis() > (lastUpdateTimestamp + duration + additionalLeaseMs));
}

這裡註釋已經寫得很清楚了,System.currentTimeMillis() > lastUpdateTimestamp + duration + additionalLeaseMs 如果將補償時間記為0,那麼這段程式碼的含義是 如果服務如果宕機了,那麼最少180s 才會被註冊中心摘除掉

上面這段程式碼翻譯完了,接著看一個彩蛋
看這段程式碼註釋,我先谷歌翻譯給大家看下:

翻譯的不是很好,我再來說下,這裡說的是在renew() 方法中,我們寫了一個bug,那裡不應該多加一個duration(預設90s)時間的,加上了會導致這裡duration * 2了,所以也就是至少180s才會被摘除。但是又由於修改會產生其他的問題,所以我們不予修改。

順便看下renew() 做了什麼錯事:

這裡確實多給加了一個duration,哈哈 通過這個註釋 可以感受到作者就像一個嬌羞的小媳婦一樣,我做錯了事 我就不改 哼!~

言歸正傳,這裡接著看evict()後面的操作:

  1. 將所有需要摘除的服務例項放到expiredLeases 集合中去
  2. 計算服務摘除的閾值,registrySizeThreshold 為註冊例項總數量 * 85%
  3. 計算最多可摘除的服務例項個數:總數量 - 總數量 * 85%
    這裡實則也是一種保護機制,即使我很多服務宕機了,但是最多隻能摘除15%的服務例項。
  4. 隨機摘取指定的服務例項數量,然後遍歷呼叫internalCancel 方法來remove宕機的服務例項, 這裡就是上面講解的服務下線呼叫的方法

總結

分析完了上面所有的程式碼 是不是有一種大跌眼鏡的感覺?我們現在檢視的版本確實還存在bug的,有一些bug在master中已經被修復,但仍有些存在。後面一講會重點跟進這些問題。

接下來就回答開頭丟擲來的一個問題了:

例如我有服務A、服務B,A、B都註冊在同一個註冊中心,當B下線後,A多久能感知到B已經下線了呢?

答案是:最快180s才會被感知。如果有補償時間,或者服務摘除的時候 計算隨機摘除服務的時候 沒有摘除此服務,那麼又會等待180s 來摘除。所以這個只能說一個最塊180被感知到。

這一講還是寫了很多,其實這裡麵包含了很多下一講的內容,下一講會對本講做一個補充。敬請期待。

申明

本文章首發自本人部落格:https://www.cnblogs.com/wang-meng 和公眾號:壹枝花算不算浪漫,如若轉載請標明來源!

感興趣的小夥伴可關注個人公眾號:壹枝花算不算浪漫

相關推薦

一起原始碼-服務Nexflix Eureka 原始碼服務下線例項摘除一個client下線到底多久其他例項感知

前言 前情回顧 上一講我們講了 client端向server端傳送心跳檢查,也是預設每30鍾傳送一次,server端接收後會更新登錄檔的一個時間戳屬性,然後一次心跳(續約)也就完成了。 本講目錄 這一篇有兩個知識點及一個疑問,這個疑問是在工作中真真實實遇到過的。 例如我有服務A、服務B,A、B都註冊在同一個註

一起原始碼-服務Nexflix Eureka 原始碼EurekaServer啟動之配置檔案載入以及面向介面的配置項讀取

前言 上篇文章已經介紹了 為何要讀netflix eureka原始碼了,這裡就不再概述,下面開始正式原始碼解讀的內容。 如若轉載 請標明來源:一枝花算不算浪漫 程式碼總覽 還記得上文中,我們通過web.xml找到了eureka server入口的類EurekaBootStrap,這裡我們就先來簡單地看下: /

一起原始碼-服務Nexflix Eureka 原始碼EurekaServer啟動之EurekaServer上下文EurekaClient建立

前言 上篇文章已經介紹了 Eureka Server 環境和上下文初始化的一些程式碼,其中重點講解了environment初始化使用的單例模式,以及EurekaServerConfigure基於介面對外暴露配置方法的設計方式。這一講就是講解Eureka Server上下文初始化剩下的內容:Eureka Cli

一起原始碼-服務Nexflix Eureka 原始碼在眼花繚亂的程式碼中EurekaClient是如何註冊的?

前言 上一講已經講解了EurekaClient的啟動流程,到了這裡已經有6篇Eureka原始碼分析的文章了,看了下之前的文章,感覺程式碼成分太多,會影響閱讀,後面會只擷取主要的程式碼,加上註釋講解。 這一講看的是EurekaClient註冊的流程,當然也是一塊核心,標題為什麼會寫上眼花繚亂呢?關於Eureka

一起原始碼-服務Nexflix Eureka 原始碼通過單元測試來Debug Eureka註冊過程

前言 上一講eureka client是如何註冊的,一直跟到原始碼傳送http請求為止,當時看eureka client註冊時如此費盡,光是找一個regiter的地方就找了半天,那麼client端傳送了http請求給server端,server端是如何處理的呢? 帶著這麼一個疑問 就開始今天原始碼的解讀了。

一起原始碼-服務Nexflix Eureka 原始碼EurekaClient登錄檔抓取 精妙設計分析!

前言 前情回顧 上一講 我們通過單元測試 來梳理了EurekaClient是如何註冊到server端,以及server端接收到請求是如何處理的,這裡最重要的關注點是登錄檔的一個數據結構:ConcurrentHashMap<String, Map<String, Lease<InstanceI

一起原始碼-服務Nexflix Eureka 原始碼服務續約原始碼分析

前言 前情回顧 上一講 我們講解了服務發現的相關邏輯,所謂服務發現 其實就是登錄檔抓取,服務例項預設每隔30s去註冊中心抓取一下注冊表增量資料,然後合併本地登錄檔資料,最後有個hash對比的操作。 本講目錄 今天主要是看下服務續約的邏輯,服務續約就是client端給server端傳送心跳檢測,告訴對方我還活著

一起原始碼-服務Nexflix Eureka 原始碼EurekaServer自我保護機制竟然有這麼多Bug?

前言 前情回顧 上一講主要講了服務下線,已經註冊中心自動感知宕機的服務。 其實上一講已經包含了很多EurekaServer自我保護的程式碼,其中還發現了1.7.x(1.9.x)包含的一些bug,但這些問題在master分支都已修復了。 服務下線會將服務例項從登錄檔中刪除,然後放入到recentQueue中,下

一起原始碼-服務Nexflix Eureka 原始碼EurekaServer叢集模式原始碼分析

前言 前情回顧 上一講看了Eureka 註冊中心的自我保護機制,以及裡面提到的bug問題。 哈哈 轉眼間都2020年了,這個系列的文章從12.17 一直寫到現在,也是不容易哈,每天持續不斷學習,輸出部落格,這一段時間確實收穫很多。 今天在公司給組內成員分享了Eureka原始碼剖析,反響效果還可以,也算是感覺收

一起原始碼-服務Nexflix Eureka 原始碼十三Eureka原始碼解讀完結撒花篇~!

前言 想說的話 【一起學原始碼-微服務-Netflix Eureka】專欄到這裡就已經全部結束了。 實話實說,從最開始Eureka Server和Eureka Client初始化的流程還是一臉悶逼,到現在Eureka各種操作都瞭然於心了。 本專欄從12.17開始寫,一直到今天12.30(文章在平臺是延後釋出的

一起原始碼-服務Ribbon 原始碼Ribbon概念理解Demo除錯

前言 前情回顧 前面文章已經梳理清楚了Eureka相關的概念及原始碼,接下來開始研究下Ribbon的實現原理。 我們都知道Ribbon在spring cloud中擔當負載均衡的角色, 當兩個Eureka Client互相呼叫的時候,Ribbon能夠做到呼叫時的負載,保證多節點的客戶端均勻接收請求。(這個有點類

一起原始碼-服務Ribbon 原始碼通過Debug找出Ribbon初始化流程ILoadBalancer原理分析

前言 前情回顧 上一講講了Ribbon的基礎知識,通過一個簡單的demo看了下Ribbon的負載均衡,我們在RestTemplate上加了@LoadBalanced註解後,就能夠自動的負載均衡了。 本講目錄 這一講主要是繼續深入RibbonLoadBalancerClient和Ribbon+Eureka整合的

一起原始碼-服務Ribbon 原始碼Ribbon與Eureka整合原理分析

前言 前情回顧 上一篇講了Ribbon的初始化過程,從LoadBalancerAutoConfiguration 到RibbonAutoConfiguration 再到RibbonClientConfiguration,我們找到了ILoadBalancer預設初始化的物件等。 本講目錄 這一講我們會進一步往下

一起原始碼-服務Ribbon 原始碼進一步探究Ribbon的IRule和IPing

前言 前情回顧 上一講深入的講解了Ribbon的初始化過程及Ribbon與Eureka的整合程式碼,與Eureka整合的類就是DiscoveryEnableNIWSServerList,同時在DynamicServerListLoadBalancer中會呼叫PollingServerListUpdater 進

一起原始碼-服務Ribbon原始碼Ribbon原始碼解讀彙總篇~

前言 想說的話 【一起學原始碼-微服務-Ribbon】專欄到這裡就已經全部結束了,共更新四篇文章。 Ribbon比較小巧,這裡是直接 讀的spring cloud 內嵌封裝的版本,裡面的各種configuration確實有點繞,不過看看第三講Ribbon初始化的過程總結圖就會清晰很多。 緊接著會繼續整理學習F

一起原始碼-服務Feign 原始碼原始碼初探通過Demo Debug Feign原始碼

前言 前情回顧 上一講深入的講解了Ribbon的初始化過程及Ribbon與Eureka的整合程式碼,與Eureka整合的類就是DiscoveryEnableNIWSServerList,同時在DynamicServerListLoadBalancer中會呼叫PollingServerListUpdater 進

一起原始碼-服務Feign 原始碼Feign動態代理構造過程

前言 前情回顧 上一講主要看了@EnableFeignClients中的registerBeanDefinitions()方法,這裡面主要是 將EnableFeignClients註解對應的配置屬性注入,將FeignClient註解對應的屬性注入。 最後是生成FeignClient對應的bean,注入到Spr

一起原始碼-服務Feign 原始碼Feign結合Ribbon實現負載均衡的原理分析

前言 前情回顧 上一講我們已經知道了Feign的工作原理其實是在專案啟動的時候,通過JDK動態代理為每個FeignClinent生成一個動態代理。 動態代理的資料結構是:ReflectiveFeign.FeignInvocationHandler。其中包含target(裡面是serviceName等資訊)和d

一起原始碼-服務Hystrix 原始碼Hystrix基礎原理與Demo搭建

說明 原創不易,如若轉載 請標明來源! 歡迎關注本人微信公眾號:壹枝花算不算浪漫 更多內容也可檢視本人部落格:一枝花算不算浪漫 前言 前情回顧 上一個系列文章講解了Feign的原始碼,主要是Feign動態代理實現的原理,及配合Ribbon實現負載均衡的機制。 這裡我們講解一個新的元件Hystrix,也是和Fe

一起原始碼-服務Hystrix 原始碼Hystrix核心流程Hystix非降級邏輯流程梳理

說明 原創不易,如若轉載 請標明來源! 歡迎關注本人微信公眾號:壹枝花算不算浪漫 更多內容也可檢視本人部落格:一枝花算不算浪漫 前言 前情回顧 上一講我們講了配置了feign.hystrix.enabled=true之後,預設的Targeter就會構建成HystrixTargter, 然後通過對應的Hystr