目錄java
版本:git
SpringBoot 1.5.4.RELEASEgithub
SpringCloud Dalston.RELEASEspring
本文主要討論的是微服務註冊到Eureka註冊中心,並使用Zuul網關負載訪問的狀況,如何停機可使用戶無感知。api
kill -9
屬於強殺進程,首先微服務正在執行的任務被強制中斷了;其次,沒有經過Eureka註冊中心服務下線,Zuul網關做爲Eureka Client仍保存這個服務的路由信息,會繼續調用服務,Http請求返回500,後臺異常是Connection refuse鏈接拒絕緩存
這種狀況默認最長鬚要等待:安全
90s(微服務在Eureka Server上租約到期)app
+less
30s(Eureka Server服務列表刷新到只讀緩存ReadOnlyMap的時間,Eureka Client默認讀此緩存)ide
+
30s(Zuul做爲Eureka Client默認每30秒拉取一次服務列表)
+
30s(Ribbon默認動態刷新其ServerList的時間間隔)
= 180s,即 3分鐘
總結:
此種方式既會致使正在執行中的任務沒法執行完,又會致使服務沒有從Eureka Server摘除,並給Eureka Client時間刷新到服務列表,致使了經過Zuul仍然調用已停掉服務報500錯誤的狀況,不推薦。
首先,kill
等於kill -15
,根據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沒有執行信號等同於TERM(終止,termination)
而kill -l
查看信號編號與信號之間的關係,kill -15
就是 SIGTERM,TERM信號
給JVM進程發送TERM終止信號時,會調用其註冊的 Shutdown Hook,當SpringBoot微服務啓動時也註冊了 Shutdown Hook
而直接調用/shutdown
端點本質和使用 Shutdown Hook是同樣的,因此不管是使用kill
或 kill -15
,仍是直接使用/shutdown
端點,都會調用到JVM註冊的Shutdown Hook
注意:
啓用 /shutdown端點,須要以下配置
endpoints.shutdown.enabled = true
endpoints.shutdown.sensitive = false
全部問題都導向了 Shutdown Hook會執行什麼??
經過查詢項目組使用Runtime.getRuntime().addShutdownHook(Thread shutdownHook)
的地方,發現ribbon註冊了一些Shutdown Hook,但這不是咱們此次關注的,咱們關注的是Spring的應用上下文抽象類AbstractApplicationContext
註冊了針對整個Spring容器的Shutdown Hook,在執行Shutdown Hook時的邏輯在 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. // 註冊shutdownHook,線程真正調用的是 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); } // 註銷註冊的MBean LiveBeansView.unregisterApplicationContext(this); try { // Publish shutdown event. // 發送ContextClosedEvent事件,會有對應此事件的Listener處理相應的邏輯 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. // 調用全部 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. // 銷燬全部單實例bean destroyBeans(); // Close the state of this context itself. closeBeanFactory(); // Let subclasses do some final clean-up if they wish... // 調用子類的 onClose() 方法,好比 EmbeddedWebApplicationContext#onClose() onClose(); this.active.set(false); } }
AbstractApplicationContext#doClose()
的關鍵點在於
而ContextClosedEvent事件的Listener有不少,實現了Lifecycle生命週期接口的bean也不少,但其中咱們只關心一個,即 EurekaAutoServiceRegistration
,它即監聽了ContextClosedEvent事件,也實現了Lifecycle接口
//## 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); // 設置liffecycle的running標示爲false } // ContextClosedEvent事件監聽器 @EventListener(ContextClosedEvent.class) public void onApplicationEvent(ContextClosedEvent event) { // register in case meta data changed stop(); } }
如上能夠看到,EurekaAutoServiceRegistration
中對 ContextClosedEvent事件 和 Lifecycle接口 的實現都調用了stop()
方法,雖然都調用了stop()
方法,但因爲各類對於狀態的判斷致使不會重複執行,如
EurekaServiceRegistry#deregister()
方法包含將實例狀態置爲DOWN 和 EurekaClient#shutdown() 兩個操做,其中狀態置爲DOWN一次後,下一次只要狀態不變就不會觸發狀態複製請求;EurekaClient#shutdown() 以前也會判斷AtomicBoolean isShutdown
標誌位下面具體看看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"); } // 更改實例狀態,會當即觸發狀態複製請求 reg.getApplicationInfoManager().setInstanceStatus(InstanceInfo.InstanceStatus.DOWN); //TODO: on deregister or on context shutdown // 關閉EurekaClient reg.getEurekaClient().shutdown(); } }
主要涉及兩步:
StatusChangeListener
監聽器,狀態複製器InstanceInfoReplicator
會向Eureka Server發送狀態更新請求。實際上狀態更新和Eureka Client第一次註冊時都是調用的DiscoveryClient.register()
,都是發送POST /eureka/apps/appID
請求到Eureka Server,只不過請求Body中的Instance實例狀態不一樣。執行完此步驟後,Eureka Server頁面上變成EurekaClient.shutdown(): 整個Eureka Client的關閉操做包含如下幾步
@PreDestroy @Override public synchronized void shutdown() { if (isShutdown.compareAndSet(false, true)) { logger.info("Shutting down DiscoveryClient ..."); // 一、註銷全部 StatusChangeListener if ( statusChangeListener != null && applicationInfoManager != null) { applicationInfoManager.unregisterStatusChangeListener(statusChangeListener.getId()); } // 二、停掉全部定時線程(實例狀態複製、心跳、client緩存刷新、監督線程) cancelScheduledTasks(); // If APPINFO was registered // 三、向Eureka Server註銷實例 if (applicationInfoManager != null && clientConfig.shouldRegisterWithEureka()) { applicationInfoManager.setInstanceStatus(InstanceStatus.DOWN); unregister(); } // 四、各類shutdown關閉 if (eurekaTransport != null) { eurekaTransport.shutdown(); } heartbeatStalenessMonitor.shutdown(); registryStalenessMonitor.shutdown(); logger.info("Completed shut down of DiscoveryClient"); } }
unregister()
註銷,其調用AbstractJerseyEurekaHttpClient#cancel()
方法,向Eureka Server發送DELETE /eureka/v2/apps/appID/instanceID
請求,DELETE請求成功後,Eureka Server頁面上服務列表就沒有當前實例信息了。注意: 因爲在註銷上一步已經停掉了定時心跳線程,不然註銷後的下次心跳又會致使服務上線使用kill
、kill -15
或 /shutdown
端點都會調用Shutdown Hook,觸發Eureka Instance實例的註銷操做,這一步是沒有問題的,優雅下線的第一步就是從Eureka註冊中心註銷實例,但關鍵問題是shutdown操做除了註銷Eureka實例,還會立刻中止服務,而此時不管Eureka Server端,Zuul做爲Eureka Client端都存在陳舊的緩存還未刷新,服務列表中仍然有註銷下線的服務,經過zuul再次調用報500錯誤,後臺是connection refuse鏈接拒絕異常,故不建議使用
另外,因爲unregister
註銷操做涉及狀態更新DOWN 和 註銷下線 兩步操做,且是分兩個線程執行的,實際註銷時,根據兩個線程執行完成的前後順序,最終在Eureka Server上體現的結果不一樣,但最終效果是相同的,通過一段時間的緩存刷新後,此服務實例不會再被調用
首先,啓用/pause
端點須要以下配置
endpoints.pause.enabled = true endpoints.pause.sensitive = false
PauseEndpoint
是RestartEndPoint
的內部類
//## Restart端點 @ConfigurationProperties("endpoints.restart") @ManagedResource public class RestartEndpoint extends AbstractEndpoint<Boolean> implements ApplicationListener<ApplicationPreparedEvent> { // Pause端點 @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
端點最終會調用Spring應用上下文的stop()
方法
//## org.springframework.context.support.AbstractApplicationContext#stop @Override public void stop() { // 一、全部實現Lifecycle生命週期接口 stop() getLifecycleProcessor().stop(); // 二、觸發ContextStoppedEvent事件 publishEvent(new ContextStoppedEvent(this)); }
查看源碼,並無發現有用的ContextStoppedEvent事件監聽器,故stop的邏輯都在Lifecycle生命週期接口實現類的stop()
而getLifecycleProcessor().stop()
與 方式二中shutdown調用的 getLifecycleProcessor().doClose()
內部邏輯都是同樣的,都是調用了DefaultLifecycleProcessor#stopBeans()
,進而調用Lifecycle接口實現類的stop(),以下
//## DefaultLifecycleProcessor @Override public void stop() { stopBeans(); this.running = false; } @Override public void onClose() { stopBeans(); this.running = false; }
因此,執行/pause
端點 和 shutdown時的其中一部分邏輯是同樣的,依賴於EurekaServiceRegistry#deregister() 註銷
,會依次執行:
DiscoveryClient#register()
,發送POST /eureka/apps/appID
請求到Eureka Server,只不過請求Body中的Instance實例狀態不一樣。執行完此步驟後,Eureka Server頁面上實例狀態變成DOWNEurekaClient.shutdown
AbstractJerseyEurekaHttpClient#cancel()
方法,向Eureka Server發送DELETE /eureka/v2/apps/appID/instanceID
請求,DELETE請求成功後,Eureka Server頁面上服務列表就沒有當前實例信息了。注意: 因爲在註銷上一步已經停掉了定時心跳線程,不然註銷後的下次心跳又會致使服務上線/pause
端點能夠用於讓服務從Eureka Server下線,且與shutdown不同的是,其不會中止整個服務,致使整個服務不可用,只會作從Eureka Server註銷的操做,最終在Eureka Server上體現的是 服務下線 或 服務狀態爲DOWN,且eureka client相關的定時線程也都中止了,不會再被定時線程註冊上線,因此能夠在sleep一段時間,待服務實例下線被像Zuul這種Eureka Client刷新到,再中止微服務,就能夠作到優雅下線(中止微服務的時候可使用/shutdown端點
或 直接暴利kill -9
)
注意:
我實驗的當前版本下,使用/pause
端點下線服務後,沒法使用/resume
端點再次上線,即若是發版過程當中想從新註冊服務,只有重啓微服務。且爲了從Eureka Server下線服務,將整個Spring容器stop(),也有點「興師動衆」
/resume
端點沒法讓服務再次上線的緣由是,雖然此端點會調用AbstractApplicationContext#start()
--> EurekaAutoServiceRegistration#start()
--> EurekaServiceRegistry#register()
,但因爲以前已經中止了Eureka Client的全部定時任務線程,好比狀態複製 和 心跳線程,從新註冊時雖然有maybeInitializeClient(eurekaRegistration)
嘗試從新啓動EurekaClient,但並無成功(估計是此版本的Bug),致使UP狀態並無發送給Eureka Server
可下線,沒法從新上線
首先,在我使用的版本 /service-registry
端點默認是啓用的,可是是sensitive
的,也就是須要認證才能訪問
我試圖找一個能夠單獨將/service-registry
的sensitive
置爲false的方式,但在當前我用的版本沒有找到,/service-registry
端點是經過 ServiceRegistryAutoConfiguration
自動配置的 ServiceRegistryEndpoint
,而 ServiceRegistryEndpoint
這個MvcEndpoint的isSensitive()
方法寫死了返回true,並無給可配置的地方或者自定義什麼實現,而後在ManagementWebSecurityAutoConfiguration
這個安全管理自動配置類中,將全部這些sensitive==true
的經過Spring Security的 httpSecurity.authorizeRequests().xxx.authenticated()
設置爲必須認證後才能訪問,目前我找到只能經過 management.security.enabled=false
這種將全部端點都關閉認證的方式才能夠無認證訪問
# 無認證訪問 /service-registry 端點 management.security.enabled=false
/service-registry端點的實現類是ServiceRegistryEndpoint
,其暴露了兩個RequestMapping,分別是GET 和 POST請求的/service-registry,GET請求的用於獲取實例本地的status、overriddenStatus,POST請求的用於調用Eureka Server修改當前實例狀態
//## 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; } }
咱們關注的確定是POST請求的/service-registry,如上能夠看到,其調用了 EurekaServiceRegistry.setStatus()
方法更新實例狀態
//## org.springframework.cloud.netflix.eureka.serviceregistry.EurekaServiceRegistry public class EurekaServiceRegistry implements ServiceRegistry<EurekaRegistration> { // 更新狀態 @Override public void setStatus(EurekaRegistration registration, String status) { InstanceInfo info = registration.getApplicationInfoManager().getInfo(); // 若是更新的status狀態爲CANCEL_OVERRIDE,調用EurekaClient.cancelOverrideStatus() //TODO: howto deal with delete properly? if ("CANCEL_OVERRIDE".equalsIgnoreCase(status)) { registration.getEurekaClient().cancelOverrideStatus(info); return; } // 調用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發送兩種請求,分別是經過 EurekaClient.setStatus()
和 EurekaClient.cancelOverrideStatus()
來支持的,下面分別分析:
EurekaClient.setStatus()
:PUT /eureka/apps/appID/instanceID/status?value=xxx
到Eureka Server,這是註冊中心對於 Take instance out of service 實例下線
而開放的Rest API,能夠作到更新Eureka Server端的實例狀態(status 和 overriddenstatus),通常會在發版部署時使用,讓服務下線,更新爲 OUT_OF_SERVICEEurekaClient.cancelOverrideStatus()
:
DELETE /eureka/v2/apps/appID/instanceID/status
到Eureka Server,用於清除覆蓋狀態,其實官方給出的是 DELETE /eureka/v2/apps/appID/instanceID/status?value=UP
,其中 value=UP
可選,是刪除overriddenstatus爲UNKNOWN以後,建議status回滾爲何狀態,但我當前使用版本里沒有這個 value=UP
可選參數,就致使發送後,Eureka Server端 status=UNKNOWN 且 overriddenstatus=UNKNOWN,但UNKNOWN覆蓋狀態不一樣的事,雖然心跳線程仍對其無做用,但註冊(等同於UP狀態更新)是可讓服務上線的/service-registry
端點能夠更新服務實例狀態爲 OUT_OF_SERVICE,再通過一段Server端、Client端緩存的刷新,使得服務不會再被調用,此時再經過/shutdown
端點 或 暴利的kill -9
中止服務進程,能夠達到優雅下線的效果/service-registry
端點,只不過狀態爲 CANCEL_OVERRIDE,具體邏輯在 EurekaServiceRegistry.setStatus()
中,其等同於直接調用Eureka Server API : DELETE /eureka/v2/apps/appID/instanceID/status
,可讓Server端 status=UNKNOWN 且 overriddenstatus=UNKNOWN/service-registry
端點,狀態爲UP,可以使得Server端 status=UP且 overriddenstatus=UP,雖然能夠臨時起到上線目的,但 overriddenstatus=UP 仍須要上一步的DELETE請求才能清楚,很麻煩,不建議使用DELETE /eureka/apps/appID/instanceID/status?value=UP
實際使用過程當中建議以下順序
一、調用/service-registry
端點將狀態置爲 OUT_OF_SERVICE
二、sleep 緩存刷新時間 + 單個請求處理時間
緩存刷新時間 指的是Eureka Server刷新只讀緩存、Eureka Client刷新本地服務列表、Ribbon刷新ServerList的時間,默認都是30s,能夠適當縮短緩存刷新時間
# Eureka Server端配置 eureka.server.responseCacheUpdateIntervalMs=5000 eureka.server.eviction-interval-timer-in-ms=5000 # Eureka Client端配置 eureka.client.registryFetchIntervalSeconds=5 ribbon.ServerListRefreshInterval=5000
單個請求處理時間 是爲了怕服務還有請求沒處理完
三、調用 /service-registry
端點將狀態置爲 CANCEL_OVERRIDE,其實就是向Server端發送DELETE overriddenstatus的請求,這會讓Server端 status=UNKNOWN 且 overriddenstatus=UNKNOWN
四、使用 /shutdown
端點 或 暴利kill -9
終止服務
五、發版部署後,啓動服務註冊到Eureka Server,服務狀態變爲UP
上面說了這麼多,其實這些都是針對Eureka Server Rest API在Eureka客戶端上的封裝,即經過Eureka Client服務因爲引入了actuator,增長了一系列端點,其實一些端點經過調用Eureka Server暴露的Rest API的方式實現Eureka實例服務下線功能
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. |
其中大多數非查詢類的操做在以前分析Eureka Client的端點時都分析過了,其實調用Eureka Server的Rest API是最直接的,但因爲目前多采用一些相似Jenkins的發版部署工具,其中操做均在腳本中執行,Eureka Server API雖好,但URL中都涉及appID 、instanceID,對於製做通用的腳原本說拼接出調用端點的URL有必定難度,且不像調用本地服務端點IP使用localhost 或 127.0.0.1便可,須要指定Eureka Server地址,因此整理略顯複雜。不過在比較規範化的公司中,也是不錯的選擇
參考: