SpringCloud微服務(wù)如何優(yōu)雅停機(jī)及源碼分析

版本:

SpringBoot 1.5.4.RELEASE

SpringCloud Dalston.RELEASE

本文主要討論的是微服務(wù)注冊(cè)到Eureka注冊(cè)中心,并使用Zuul網(wǎng)關(guān)負(fù)載訪問的情況,如何停機(jī)可以使用戶無感知。


方式一:kill -9 java進(jìn)程id【不建議】

kill -9 屬于強(qiáng)殺進(jìn)程,首先微服務(wù)正在執(zhí)行的任務(wù)被強(qiáng)制中斷了;其次,沒有通過Eureka注冊(cè)中心服務(wù)下線,Zuul網(wǎng)關(guān)作為Eureka Client仍保存這個(gè)服務(wù)的路由信息,會(huì)繼續(xù)調(diào)用服務(wù),Http請(qǐng)求返回500,后臺(tái)異常是Connection refuse連接拒絕

這種情況默認(rèn)最長需要等待:

90s(微服務(wù)在Eureka Server上租約到期)

+

30s(Eureka Server服務(wù)列表刷新到只讀緩存ReadOnlyMap的時(shí)間,Eureka Client默認(rèn)讀此緩存)

+

30s(Zuul作為Eureka Client默認(rèn)每30秒拉取一次服務(wù)列表)

+

30s(Ribbon默認(rèn)動(dòng)態(tài)刷新其ServerList的時(shí)間間隔)

= 180s,即 3分鐘

總結(jié):

此種方式既會(huì)導(dǎo)致正在執(zhí)行中的任務(wù)無法執(zhí)行完,又會(huì)導(dǎo)致服務(wù)沒有從Eureka Server摘除,并給Eureka Client時(shí)間刷新到服務(wù)列表,導(dǎo)致了通過Zuul仍然調(diào)用已停掉服務(wù)報(bào)500錯(cuò)誤的情況,不推薦。


方式二:kill -15 java進(jìn)程id 或 直接使用/shutdown 端點(diǎn)【不建議】

kill 與/shutdown 的含義

首先,kill等于kill -15,根據(jù)man kill的描述信息

The command kill sends the specified signal to the specified process or process group. If no signal is specified, the TERM signal is sent.

即kill沒有執(zhí)行信號(hào)等同于TERM(終止,termination)

kill -l查看信號(hào)編號(hào)與信號(hào)之間的關(guān)系,kill -15就是 SIGTERM,TERM信號(hào)

kill -l.jpg

給JVM進(jìn)程發(fā)送TERM終止信號(hào)時(shí),會(huì)調(diào)用其注冊(cè)的 Shutdown Hook,當(dāng)SpringBoot微服務(wù)啟動(dòng)時(shí)也注冊(cè)了 Shutdown Hook

而直接調(diào)用/shutdown端點(diǎn)本質(zhì)和使用 Shutdown Hook是一樣的,所以無論是使用killkill -15,還是直接使用/shutdown端點(diǎn),都會(huì)調(diào)用到JVM注冊(cè)的Shutdown Hook

注意:

啟用 /shutdown端點(diǎn),需要如下配置

endpoints.shutdown.enabled = true
endpoints.shutdown.sensitive = false

所有問題都導(dǎo)向了 Shutdown Hook會(huì)執(zhí)行什么??


Spring注冊(cè)的Shutdown Hook

通過查詢項(xiàng)目組使用Runtime.getRuntime().addShutdownHook(Thread shutdownHook)的地方,發(fā)現(xiàn)ribbon注冊(cè)了一些Shutdown Hook,但這不是我們這次關(guān)注的,我們關(guān)注的是Spring的應(yīng)用上下文抽象類AbstractApplicationContext注冊(cè)了針對(duì)整個(gè)Spring容器的Shutdown Hook,在執(zhí)行Shutdown Hook時(shí)的邏輯在 AbstractApplicationContext#doClose()

//## org.springframework.context.support.AbstractApplicationContext#registerShutdownHook 
/**
 * Register a shutdown hook with the JVM runtime, closing this context
 * on JVM shutdown unless it has already been closed at that time.
 * <p>Delegates to {@code doClose()} for the actual closing procedure.
 * @see Runtime#addShutdownHook
 * @see #close()
 * @see #doClose()
 */
@Override
public void registerShutdownHook() {
    if (this.shutdownHook == null) {
        // No shutdown hook registered yet.
        // 注冊(cè)shutdownHook,線程真正調(diào)用的是 doClose()
        this.shutdownHook = new Thread() {
            @Override
            public void run() {
                synchronized (startupShutdownMonitor) {
                    doClose();
                }
            }
        };
        Runtime.getRuntime().addShutdownHook(this.shutdownHook);
    }
}


//## org.springframework.context.support.AbstractApplicationContext#doClose 
/**
 * Actually performs context closing: publishes a ContextClosedEvent and
 * destroys the singletons in the bean factory of this application context.
 * <p>Called by both {@code close()} and a JVM shutdown hook, if any.
 * @see org.springframework.context.event.ContextClosedEvent
 * @see #destroyBeans()
 * @see #close()
 * @see #registerShutdownHook()
 */
protected void doClose() {
    if (this.active.get() && this.closed.compareAndSet(false, true)) {
        if (logger.isInfoEnabled()) {
            logger.info("Closing " + this);
        }

        // 注銷注冊(cè)的MBean
        LiveBeansView.unregisterApplicationContext(this);

        try {
            // Publish shutdown event.
            // 發(fā)送ContextClosedEvent事件,會(huì)有對(duì)應(yīng)此事件的Listener處理相應(yīng)的邏輯
            publishEvent(new ContextClosedEvent(this));
        }
        catch (Throwable ex) {
            logger.warn("Exception thrown from ApplicationListener handling ContextClosedEvent", ex);
        }

        // Stop all Lifecycle beans, to avoid delays during individual destruction.
        // 調(diào)用所有 Lifecycle bean 的 stop() 方法
        try {
            getLifecycleProcessor().onClose();
        }
        catch (Throwable ex) {
            logger.warn("Exception thrown from LifecycleProcessor on context close", ex);
        }

        // Destroy all cached singletons in the context's BeanFactory.
        // 銷毀所有單實(shí)例bean
        destroyBeans();

        // Close the state of this context itself.
        closeBeanFactory();

        // Let subclasses do some final clean-up if they wish...
        // 調(diào)用子類的 onClose() 方法,比如 EmbeddedWebApplicationContext#onClose()
        onClose();

        this.active.set(false);
    }
}

AbstractApplicationContext#doClose() 的關(guān)鍵點(diǎn)在于

  • publishEvent(new ContextClosedEvent(this)): 發(fā)送ContextClosedEvent事件,會(huì)有對(duì)應(yīng)此事件的Listener處理相應(yīng)的邏輯
  • getLifecycleProcessor().onClose(): 調(diào)用所有 Lifecycle bean 的 stop() 方法

而ContextClosedEvent事件的Listener有很多,實(shí)現(xiàn)了Lifecycle生命周期接口的bean也很多,但其中我們只關(guān)心一個(gè),即 EurekaAutoServiceRegistration ,它即監(jiān)聽了ContextClosedEvent事件,也實(shí)現(xiàn)了Lifecycle接口

EurekaAutoServiceRegistration的stop()事件

//## org.springframework.cloud.netflix.eureka.serviceregistry.EurekaAutoServiceRegistration
public class EurekaAutoServiceRegistration implements AutoServiceRegistration, SmartLifecycle, Ordered {

    // lifecycle接口的 stop()
    @Override
    public void stop() {
        this.serviceRegistry.deregister(this.registration);
        this.running.set(false);  // 設(shè)置liffecycle的running標(biāo)示為false
    }
    
    // ContextClosedEvent事件監(jiān)聽器
    @EventListener(ContextClosedEvent.class)
    public void onApplicationEvent(ContextClosedEvent event) {
        // register in case meta data changed
        stop();
    }
    
}

如上可以看到,EurekaAutoServiceRegistration中對(duì) ContextClosedEvent事件 和 Lifecycle接口 的實(shí)現(xiàn)都調(diào)用了stop()方法,雖然都調(diào)用了stop()方法,但由于各種對(duì)于狀態(tài)的判斷導(dǎo)致不會(huì)重復(fù)執(zhí)行,如

  • Lifecycle的running標(biāo)示置為false,就不會(huì)調(diào)用到此Lifecycle#stop()
  • EurekaServiceRegistry#deregister()方法包含將實(shí)例狀態(tài)置為DOWN 和 EurekaClient#shutdown() 兩個(gè)操作,其中狀態(tài)置為DOWN一次后,下一次只要狀態(tài)不變就不會(huì)觸發(fā)狀態(tài)復(fù)制請(qǐng)求;EurekaClient#shutdown() 之前也會(huì)判斷AtomicBoolean isShutdown標(biāo)志位

下面具體看看EurekaServiceRegistry#deregister()方法

EurekaServiceRegistry#deregister() 注銷

//## org.springframework.cloud.netflix.eureka.serviceregistry.EurekaServiceRegistry#deregister
@Override
public void deregister(EurekaRegistration reg) {
    if (reg.getApplicationInfoManager().getInfo() != null) {

        if (log.isInfoEnabled()) {
            log.info("Unregistering application " + reg.getInstanceConfig().getAppname()
                    + " with eureka with status DOWN");
        }

        // 更改實(shí)例狀態(tài),會(huì)立即觸發(fā)狀態(tài)復(fù)制請(qǐng)求
        reg.getApplicationInfoManager().setInstanceStatus(InstanceInfo.InstanceStatus.DOWN);

        //TODO: on deregister or on context shutdown
        // 關(guān)閉EurekaClient
        reg.getEurekaClient().shutdown();
    }
}

主要涉及兩步:

  • 更新Instance狀態(tài)為 DOWN: 更新狀態(tài)會(huì)觸發(fā)StatusChangeListener監(jiān)聽器,狀態(tài)復(fù)制器InstanceInfoReplicator會(huì)向Eureka Server發(fā)送狀態(tài)更新請(qǐng)求。實(shí)際上狀態(tài)更新和Eureka Client第一次注冊(cè)時(shí)都是調(diào)用的DiscoveryClient.register(),都是發(fā)送POST /eureka/apps/appID請(qǐng)求到Eureka Server,只不過請(qǐng)求Body中的Instance實(shí)例狀態(tài)不同。執(zhí)行完此步驟后,Eureka Server頁面上變成
instance_change_status_to_down.jpg
  • EurekaClient.shutdown(): 整個(gè)Eureka Client的關(guān)閉操作包含以下幾步

    • @PreDestroy
      @Override
      public synchronized void shutdown() {
          if (isShutdown.compareAndSet(false, true)) {
              logger.info("Shutting down DiscoveryClient ...");
      
              // 1、注銷所有 StatusChangeListener
              if ( statusChangeListener != null && applicationInfoManager != null) {
                  applicationInfoManager.unregisterStatusChangeListener(statusChangeListener.getId());
              }
      
              // 2、停掉所有定時(shí)線程(實(shí)例狀態(tài)復(fù)制、心跳、client緩存刷新、監(jiān)督線程)
              cancelScheduledTasks();
      
              // If APPINFO was registered
              // 3、向Eureka Server注銷實(shí)例
              if (applicationInfoManager != null && clientConfig.shouldRegisterWithEureka()) {
                  applicationInfoManager.setInstanceStatus(InstanceStatus.DOWN);
                  unregister();
              }
      
              // 4、各種shutdown關(guān)閉
              if (eurekaTransport != null) {
                  eurekaTransport.shutdown();
              }
      
              heartbeatStalenessMonitor.shutdown();
              registryStalenessMonitor.shutdown();
      
              logger.info("Completed shut down of DiscoveryClient");
          }
      }
      
    • 其中應(yīng)關(guān)注unregister()注銷,其調(diào)用AbstractJerseyEurekaHttpClient#cancel()方法,向Eureka Server發(fā)送DELETE /eureka/v2/apps/appID/instanceID請(qǐng)求,DELETE請(qǐng)求成功后,Eureka Server頁面上服務(wù)列表就沒有當(dāng)前實(shí)例信息了。注意: 由于在注銷上一步已經(jīng)停掉了定時(shí)心跳線程,否則注銷后的下次心跳又會(huì)導(dǎo)致服務(wù)上線


總結(jié)

使用killkill -15/shutdown端點(diǎn)都會(huì)調(diào)用Shutdown Hook,觸發(fā)Eureka Instance實(shí)例的注銷操作,這一步是沒有問題的,優(yōu)雅下線的第一步就是從Eureka注冊(cè)中心注銷實(shí)例,但關(guān)鍵問題是shutdown操作除了注銷Eureka實(shí)例,還會(huì)馬上停止服務(wù),而此時(shí)無論Eureka Server端,Zuul作為Eureka Client端都存在陳舊的緩存還未刷新,服務(wù)列表中仍然有注銷下線的服務(wù),通過zuul再次調(diào)用報(bào)500錯(cuò)誤,后臺(tái)是connection refuse連接拒絕異常,故不建議使用

另外,由于unregister注銷操作涉及狀態(tài)更新DOWN 和 注銷下線 兩步操作,且是分兩個(gè)線程執(zhí)行的,實(shí)際注銷時(shí),根據(jù)兩個(gè)線程執(zhí)行完成的先后順序,最終在Eureka Server上體現(xiàn)的結(jié)果不同,但最終效果是相同的,經(jīng)過一段時(shí)間的緩存刷新后,此服務(wù)實(shí)例不會(huì)再被調(diào)用

  • 狀態(tài)更新DOWN先結(jié)束,注銷實(shí)例后結(jié)束: Eureka Server頁面清除此服務(wù)實(shí)例信息
  • 注銷實(shí)例先結(jié)束,狀態(tài)更新DOWN后結(jié)束: Eureka Server頁面顯示此服務(wù)實(shí)例狀態(tài)為DOWN


方式三:/pause 端點(diǎn)【可用,但有缺陷】

/pause 端點(diǎn)

首先,啟用/pause端點(diǎn)需要如下配置

endpoints.pause.enabled = true
endpoints.pause.sensitive = false

PauseEndpointRestartEndPoint的內(nèi)部類

//## Restart端點(diǎn)
@ConfigurationProperties("endpoints.restart")
@ManagedResource
public class RestartEndpoint extends AbstractEndpoint<Boolean>
        implements ApplicationListener<ApplicationPreparedEvent> {
        
    // Pause端點(diǎn)
    @ConfigurationProperties("endpoints")
    public class PauseEndpoint extends AbstractEndpoint<Boolean> {

        public PauseEndpoint() {
            super("pause", true, true);
        }

        @Override
        public Boolean invoke() {
            if (isRunning()) {
                pause();
                return true;
            }
            return false;
        }
    }
    
    // 暫停操作
    @ManagedOperation
    public synchronized void pause() {
        if (this.context != null) {
            this.context.stop();
        }
    }
}

如上可見,/pause端點(diǎn)最終會(huì)調(diào)用Spring應(yīng)用上下文的stop()方法

AbstractApplicationContext#stop()

//## org.springframework.context.support.AbstractApplicationContext#stop
@Override
public void stop() {
    // 1、所有實(shí)現(xiàn)Lifecycle生命周期接口 stop()
    getLifecycleProcessor().stop();
    
    // 2、觸發(fā)ContextStoppedEvent事件
    publishEvent(new ContextStoppedEvent(this));
}

查看源碼,并沒有發(fā)現(xiàn)有用的ContextStoppedEvent事件監(jiān)聽器,故stop的邏輯都在Lifecycle生命周期接口實(shí)現(xiàn)類的stop()

getLifecycleProcessor().stop() 與 方式二中shutdown調(diào)用的 getLifecycleProcessor().doClose() 內(nèi)部邏輯都是一樣的,都是調(diào)用了DefaultLifecycleProcessor#stopBeans(),進(jìn)而調(diào)用Lifecycle接口實(shí)現(xiàn)類的stop(),如下

//## DefaultLifecycleProcessor
@Override
public void stop() {
    stopBeans();
    this.running = false;
}

@Override
public void onClose() {
    stopBeans();
    this.running = false;
}

所以,執(zhí)行/pause端點(diǎn) 和 shutdown時(shí)的其中一部分邏輯是一樣的,依賴于EurekaServiceRegistry#deregister() 注銷,會(huì)依次執(zhí)行:

  • 觸發(fā)狀態(tài)復(fù)制為DOWN,和Eureka Client注冊(cè)上線register調(diào)用方法一樣DiscoveryClient#register(),發(fā)送POST /eureka/apps/appID請(qǐng)求到Eureka Server,只不過請(qǐng)求Body中的Instance實(shí)例狀態(tài)不同。執(zhí)行完此步驟后,Eureka Server頁面上實(shí)例狀態(tài)變成DOWN
  • 觸發(fā) EurekaClient.shutdown
    • 1、注銷所有 StatusChangeListener
    • 2、停掉所有定時(shí)線程(實(shí)例狀態(tài)復(fù)制、心跳、client緩存刷新、監(jiān)督線程)
    • 3、向Eureka Server注銷實(shí)例
      • 調(diào)用AbstractJerseyEurekaHttpClient#cancel()方法,向Eureka Server發(fā)送DELETE /eureka/v2/apps/appID/instanceID請(qǐng)求,DELETE請(qǐng)求成功后,Eureka Server頁面上服務(wù)列表就沒有當(dāng)前實(shí)例信息了。注意: 由于在注銷上一步已經(jīng)停掉了定時(shí)心跳線程,否則注銷后的下次心跳又會(huì)導(dǎo)致服務(wù)上線
    • 4、各種shutdown關(guān)閉
  • stop()執(zhí)行完畢后,Eureka Server端當(dāng)前實(shí)例狀態(tài)是DOWN,還是下線,取決于 狀態(tài)DOWN的復(fù)制線程 和 注銷請(qǐng)求 哪個(gè)執(zhí)行快

總結(jié)

/pause端點(diǎn)可以用于讓服務(wù)從Eureka Server下線,且與shutdown不一樣的是,其不會(huì)停止整個(gè)服務(wù),導(dǎo)致整個(gè)服務(wù)不可用,只會(huì)做從Eureka Server注銷的操作,最終在Eureka Server上體現(xiàn)的是 服務(wù)下線服務(wù)狀態(tài)為DOWN,且eureka client相關(guān)的定時(shí)線程也都停止了,不會(huì)再被定時(shí)線程注冊(cè)上線,所以可以在sleep一段時(shí)間,待服務(wù)實(shí)例下線被像Zuul這種Eureka Client刷新到,再停止微服務(wù),就可以做到優(yōu)雅下線(停止微服務(wù)的時(shí)候可以使用/shutdown端點(diǎn) 或 直接暴利kill -9

注意:

我實(shí)驗(yàn)的當(dāng)前版本下,使用/pause端點(diǎn)下線服務(wù)后,無法使用/resume端點(diǎn)再次上線,即如果發(fā)版過程中想重新注冊(cè)服務(wù),只有重啟微服務(wù)。且為了從Eureka Server下線服務(wù),將整個(gè)Spring容器stop(),也有點(diǎn)“興師動(dòng)眾”

/resume端點(diǎn)無法讓服務(wù)再次上線的原因是,雖然此端點(diǎn)會(huì)調(diào)用AbstractApplicationContext#start() --> EurekaAutoServiceRegistration#start() --> EurekaServiceRegistry#register(),但由于之前已經(jīng)停止了Eureka Client的所有定時(shí)任務(wù)線程,比如狀態(tài)復(fù)制 和 心跳線程,重新注冊(cè)時(shí)雖然有maybeInitializeClient(eurekaRegistration)嘗試重新啟動(dòng)EurekaClient,但并沒有成功(估計(jì)是此版本的Bug),導(dǎo)致UP狀態(tài)并沒有發(fā)送給Eureka Server

可下線,無法重新上線


方式四:/service-registry 端點(diǎn)【可用,但有坑】

/service-registry 端點(diǎn)

首先,在我使用的版本 /service-registry 端點(diǎn)默認(rèn)是啟用的,但是是sensitive 的,也就是需要認(rèn)證才能訪問

我試圖找一個(gè)可以單獨(dú)將/service-registrysensitive置為false的方式,但在當(dāng)前我用的版本沒有找到,/service-registry端點(diǎn)是通過 ServiceRegistryAutoConfiguration自動(dòng)配置的 ServiceRegistryEndpoint,而 ServiceRegistryEndpoint這個(gè)MvcEndpoint的isSensitive()方法寫死了返回true,并沒有給可配置的地方或者自定義什么實(shí)現(xiàn),然后在ManagementWebSecurityAutoConfiguration這個(gè)安全管理自動(dòng)配置類中,將所有這些sensitive==true的通過Spring Security的 httpSecurity.authorizeRequests().xxx.authenticated()設(shè)置為必須認(rèn)證后才能訪問,目前我找到只能通過 management.security.enabled=false 這種將所有端點(diǎn)都關(guān)閉認(rèn)證的方式才可以無認(rèn)證訪問

# 無認(rèn)證訪問 /service-registry 端點(diǎn)
management.security.enabled=false

更新遠(yuǎn)端實(shí)例狀態(tài)

/service-registry端點(diǎn)的實(shí)現(xiàn)類是ServiceRegistryEndpoint,其暴露了兩個(gè)RequestMapping,分別是GET 和 POST請(qǐng)求的/service-registry,GET請(qǐng)求的用于獲取實(shí)例本地的status、overriddenStatus,POST請(qǐng)求的用于調(diào)用Eureka Server修改當(dāng)前實(shí)例狀態(tài)

//## org.springframework.cloud.client.serviceregistry.endpoint.ServiceRegistryEndpoint
@ManagedResource(description = "Can be used to display and set the service instance status using the service registry")
@SuppressWarnings("unchecked")
public class ServiceRegistryEndpoint implements MvcEndpoint {
    private final ServiceRegistry serviceRegistry;

    private Registration registration;

    public ServiceRegistryEndpoint(ServiceRegistry<?> serviceRegistry) {
        this.serviceRegistry = serviceRegistry;
    }

    public void setRegistration(Registration registration) {
        this.registration = registration;
    }

    @RequestMapping(path = "instance-status", method = RequestMethod.POST)
    @ResponseBody
    @ManagedOperation
    public ResponseEntity<?> setStatus(@RequestBody String status) {
        Assert.notNull(status, "status may not by null");

        if (this.registration == null) {
            return ResponseEntity.status(HttpStatus.NOT_FOUND).body("no registration found");
        }

        this.serviceRegistry.setStatus(this.registration, status);
        return ResponseEntity.ok().build();
    }

    @RequestMapping(path = "instance-status", method = RequestMethod.GET)
    @ResponseBody
    @ManagedAttribute
    public ResponseEntity getStatus() {
        if (this.registration == null) {
            return ResponseEntity.status(HttpStatus.NOT_FOUND).body("no registration found");
        }

        return ResponseEntity.ok().body(this.serviceRegistry.getStatus(this.registration));
    }

    @Override
    public String getPath() {
        return "/service-registry";
    }

    @Override
    public boolean isSensitive() {
        return true;
    }

    @Override
    public Class<? extends Endpoint<?>> getEndpointType() {
        return null;
    }
}

我們關(guān)注的肯定是POST請(qǐng)求的/service-registry,如上可以看到,其調(diào)用了 EurekaServiceRegistry.setStatus() 方法更新實(shí)例狀態(tài)

//## org.springframework.cloud.netflix.eureka.serviceregistry.EurekaServiceRegistry
public class EurekaServiceRegistry implements ServiceRegistry<EurekaRegistration> {
    
    // 更新狀態(tài)
    @Override
    public void setStatus(EurekaRegistration registration, String status) {
        InstanceInfo info = registration.getApplicationInfoManager().getInfo();

        // 如果更新的status狀態(tài)為CANCEL_OVERRIDE,調(diào)用EurekaClient.cancelOverrideStatus()
        //TODO: howto deal with delete properly?
        if ("CANCEL_OVERRIDE".equalsIgnoreCase(status)) {
            registration.getEurekaClient().cancelOverrideStatus(info);
            return;
        }

        // 調(diào)用EurekaClient.setStatus()
        //TODO: howto deal with status types across discovery systems?
        InstanceInfo.InstanceStatus newStatus = InstanceInfo.InstanceStatus.toEnum(status);
        registration.getEurekaClient().setStatus(newStatus, info);
    }
    
}

EurekaServiceRegistry.setStatus() 方法支持像Eureka Server發(fā)送兩種請(qǐng)求,分別是通過 EurekaClient.setStatus()EurekaClient.cancelOverrideStatus() 來支持的,下面分別分析:

  • EurekaClient.setStatus()
  • 實(shí)際是發(fā)送 PUT /eureka/apps/appID/instanceID/status?value=xxx 到Eureka Server,這是注冊(cè)中心對(duì)于 Take instance out of service 實(shí)例下線 而開放的Rest API,可以做到更新Eureka Server端的實(shí)例狀態(tài)(status 和 overriddenstatus),一般會(huì)在發(fā)版部署時(shí)使用,讓服務(wù)下線,更新為 OUT_OF_SERVICE
  • 由于overriddenstatus更新為了OUT_OF_SERVICE,故即使有 心跳UP狀態(tài)復(fù)制,也不會(huì)改變其OUT_OF_SERVICE的狀態(tài),overriddenstatus覆蓋狀態(tài)就是為了避免服務(wù)下線后又被定時(shí)線程上線或更新狀態(tài)而設(shè)計(jì)的,有很多所謂的 “覆蓋策略”
  • 也正是由于overriddenstatus覆蓋狀態(tài)無法被 心跳 和 UP狀態(tài)復(fù)制(其實(shí)就是EurekaClient.register())而影響,故在發(fā)版部署完新版本后,最好先調(diào)用Rest API清除overriddenstatus,再啟動(dòng)服務(wù),如果直接啟動(dòng)服務(wù),可能導(dǎo)致Server端仍是OUT_OF_SERVICE狀態(tài)的問題
  • 實(shí)驗(yàn): 更新狀態(tài)為OUT_OF_SERVICE后,直接停服務(wù),只有等到Server端服務(wù)租約到期下線后,再啟動(dòng)客戶端上線才能成功注冊(cè)并狀態(tài)為UP;如果沒等Server端下線服務(wù)不存在后就啟動(dòng)服務(wù),注冊(cè)上線后無法改變overriddenstatus==OUT_OF_SERVICE
  • EurekaClient.cancelOverrideStatus()
    • 實(shí)際是發(fā)送 DELETE /eureka/v2/apps/appID/instanceID/status 到Eureka Server,用于清除覆蓋狀態(tài),其實(shí)官方給出的是 DELETE /eureka/v2/apps/appID/instanceID/status?value=UP,其中 value=UP可選,是刪除overriddenstatus為UNKNOWN之后,建議status回滾為什么狀態(tài),但我當(dāng)前使用版本里沒有這個(gè) value=UP可選參數(shù),就導(dǎo)致發(fā)送后,Eureka Server端 status=UNKNOWN 且 overriddenstatus=UNKNOWN,但UNKNOWN覆蓋狀態(tài)不同的事,雖然心跳線程仍對(duì)其無作用,但注冊(cè)(等同于UP狀態(tài)更新)是可以讓服務(wù)上線的

總結(jié)

  • /service-registry端點(diǎn)可以更新服務(wù)實(shí)例狀態(tài)為 OUT_OF_SERVICE,再經(jīng)過一段Server端、Client端緩存的刷新,使得服務(wù)不會(huì)再被調(diào)用,此時(shí)再通過/shutdown端點(diǎn) 或 暴利的kill -9 停止服務(wù)進(jìn)程,可以達(dá)到優(yōu)雅下線的效果

  • 如希望回滾,可以通過幾種方式

    • 還是/service-registry端點(diǎn),只不過狀態(tài)為 CANCEL_OVERRIDE,具體邏輯在 EurekaServiceRegistry.setStatus() 中,其等同于直接調(diào)用Eureka Server API : DELETE /eureka/v2/apps/appID/instanceID/status,可以讓Server端 status=UNKNOWN 且 overriddenstatus=UNKNOWN
    • 也可以用 /service-registry端點(diǎn),狀態(tài)為UP,可使得Server端 status=UP且 overriddenstatus=UP,雖然可以臨時(shí)起到上線目的,但 overriddenstatus=UP 仍需要上一步的DELETE請(qǐng)求才能清楚,很麻煩,不建議使用
    • 不通過Eureka Client的端點(diǎn),直接調(diào)用Eureka Server端點(diǎn): DELETE /eureka/apps/appID/instanceID/status?value=UP
  • 實(shí)際使用過程中建議如下順序

    • 1、調(diào)用/service-registry端點(diǎn)將狀態(tài)置為 OUT_OF_SERVICE

    • 2、sleep 緩存刷新時(shí)間 + 單個(gè)請(qǐng)求處理時(shí)間

      • 緩存刷新時(shí)間 指的是Eureka Server刷新只讀緩存、Eureka Client刷新本地服務(wù)列表、Ribbon刷新ServerList的時(shí)間,默認(rèn)都是30s,可以適當(dāng)縮短緩存刷新時(shí)間

        # Eureka Server端配置
        eureka.server.responseCacheUpdateIntervalMs=5000
        eureka.server.eviction-interval-timer-in-ms=5000
        
        # Eureka Client端配置
        eureka.client.registryFetchIntervalSeconds=5
        ribbon.ServerListRefreshInterval=5000
        
- **單個(gè)請(qǐng)求處理時(shí)間** 是為了怕服務(wù)還有請(qǐng)求沒處理完
  • 3、調(diào)用 /service-registry端點(diǎn)將狀態(tài)置為 CANCEL_OVERRIDE,其實(shí)就是向Server端發(fā)送DELETE overriddenstatus的請(qǐng)求,這會(huì)讓Server端 status=UNKNOWN 且 overriddenstatus=UNKNOWN

  • 4、使用 /shutdown端點(diǎn) 或 暴利kill -9終止服務(wù)

  • 5、發(fā)版部署后,啟動(dòng)服務(wù)注冊(cè)到Eureka Server,服務(wù)狀態(tài)變?yōu)閁P


方式五: 直接調(diào)用Eureka Server Rest API【可用,但URL比較復(fù)雜】

上面說了這么多,其實(shí)這些都是針對(duì)Eureka Server Rest API在Eureka客戶端上的封裝,即通過Eureka Client服務(wù)由于引入了actuator,增加了一系列端點(diǎn),其實(shí)一些端點(diǎn)通過調(diào)用Eureka Server暴露的Rest API的方式實(shí)現(xiàn)Eureka實(shí)例服務(wù)下線功能

Eureka Rest API包括:

Operation HTTP action Description
Register new application instance POST /eureka/apps/appID Input: JSON/XMLpayload HTTPCode: 204 on success
De-register application instance DELETE /eureka/apps/appID/instanceID HTTP Code: 200 on success
Send application instance heartbeat PUT /eureka/apps/appID/instanceID HTTP Code: * 200 on success * 404 if instanceID doesn’t exist
Query for all instances GET /eureka/apps HTTP Code: 200 on success Output: JSON/XML
Query for all appID instances GET /eureka/apps/appID HTTP Code: 200 on success Output: JSON/XML
Query for a specific appID/instanceID GET /eureka/apps/appID/instanceID HTTP Code: 200 on success Output: JSON/XML
Query for a specific instanceID GET /eureka/instances/instanceID HTTP Code: 200 on success Output: JSON/XML
Take instance out of service PUT /eureka/apps/appID/instanceID/status?value=OUT_OF_SERVICE HTTP Code: * 200 on success * 500 on failure
Move instance back into service (remove override) DELETE /eureka/apps/appID/instanceID/status?value=UP (The value=UP is optional, it is used as a suggestion for the fallback status due to removal of the override) HTTP Code: * 200 on success * 500 on failure
Update metadata PUT /eureka/apps/appID/instanceID/metadata?key=value HTTP Code: * 200 on success * 500 on failure
Query for all instances under a particular vip address GET /eureka/vips/vipAddress * HTTP Code: 200 on success Output: JSON/XML * 404 if the vipAddressdoes not exist.
Query for all instances under a particular secure vip address GET /eureka/svips/svipAddress * HTTP Code: 200 on success Output: JSON/XML * 404 if the svipAddressdoes not exist.

其中大多數(shù)非查詢類的操作在之前分析Eureka Client的端點(diǎn)時(shí)都分析過了,其實(shí)調(diào)用Eureka Server的Rest API是最直接的,但由于目前多采用一些類似Jenkins的發(fā)版部署工具,其中操作均在腳本中執(zhí)行,Eureka Server API雖好,但URL中都涉及appID 、instanceID,對(duì)于制作通用的腳本來說拼接出調(diào)用端點(diǎn)的URL有一定難度,且不像調(diào)用本地服務(wù)端點(diǎn)IP使用localhost 或 127.0.0.1即可,需要指定Eureka Server地址,所以整理略顯復(fù)雜。不過在比較規(guī)范化的公司中,也是不錯(cuò)的選擇


參考:

實(shí)用技巧:Spring Cloud中,如何優(yōu)雅下線微服務(wù)

Eureka REST operations

eureka 源碼分析三--InstanceInfo 中OverriddenStatus的作用

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容