最近接手一個springcloud項目,需要優雅地註銷服務後再重啓,這樣就不會因爲ribbon負載均衡到重啓的服務上從而丟失請求,那麼我們先來研究下怎麼手動註銷服務。
以下所有代碼都是基於springboot2.0.9 RELEASE和springcloud Finchley.RELEASE
如何優雅的手動註銷服務
1.eurekaserver:發送delete請求
比如說現在想註銷下圖中的PRODUCER服務
那麼拼接url http://39.96.0.32:8000/eureka/apps/producer/DESKTOP-UJN399N:producer:8081 發送delete請求
發現eureka註冊中心不再顯示producer服務了,說明我們已成功從eureka註冊表中移除了producer服務。
好戲來了,過一段時間後,發現producer自動回來了。
仔細想想也應該是這樣,因爲eureka-server作爲註冊中心,自然要不停地獲取各client發送來的心跳,既然Producer程序沒關,自然會給eureka-server發送心跳,這樣就重新註冊了。
下圖是producer服務重新註冊自身服務的日誌。
顯然這種方式註銷服務談不上優雅,因爲很可能你剛註銷服務,剛好下1s服務就發送了次心跳給server,又重新給註冊上了。
2.eureka server:發送PUT請求
PUT請求 http://39.96.0.32:8000/eureka/apps/producer/DESKTOP-UJN399N:producer:8080/status?value=UP
服務上線:UP 服務下線 OUT_OF_SERVICE or DOWN
服務重新上線 UP
PUT 請求eureka server設置status,可以改變服務的狀態(如OUT_OF_SERVICE,DOWN,UP等),這樣更優雅!
3.client端:寫web接口,執行shutDownComponent命令
用戶調用Producer服務中自定義的 /offline 端口,即可實現從eureka-server註冊表移除該服務。
/** 用戶自定義 TestController中 **/
@RequestMapping(value = "/offline", method = RequestMethod.GET)
public void offLine(){
DiscoveryManager.getInstance().shutdownComponent();
}
/** com.netflix.discovery.DisconveryManager **/
public void shutdownComponent() {
if (discoveryClient != null) {
try {
discoveryClient.shutdown();
discoveryClient = null;
} catch (Throwable th) {
logger.error("Error in shutting down client", th);
}
}
}
shutDown()直接關閉了發送心跳的定時器,如果想重新上線,可以向 http://39.96.0.32:8001/eureka/apps/CONSUMER 發送Post請求,同時帶有instanceInfo的json字符串作爲BODY,但是就算是上線了,由於heartbeat的定時器一直處於關閉狀態,過段時間server還是會將該服務下線。目前我還沒找到重新啓動定時器的方法。
4.client端:調用actuator/service-registry接口
pom中引用spring-boot-starter-actuator,在yml裏放開actuator的所有端口,啓動程序後會發現多了很多actuator端口。
management:
endpoints:
web:
exposure:
# 默認只放出health,info等接口,現全部開放
include: "*"
通過POST請求 /actuator/service-registry 接口,可以使當前服務變爲DOWN和UP狀態。
- POST status=“DOWN”
- eureka server 顯示consumer服務狀態爲DOWN
- 使服務重新上線,post status=“UP”
- eureka server顯示consumer服務處於up狀態
這種方式非常優雅,set status=“DOWN”等流量都沒有進來後(這可能需要一段時間),可以選擇重啓該服務,這樣不會丟失請求。也可以down掉幾個其他服務,將請求負載均衡到本地服務,方便DEBUG,然後再將其他服務UP起來
後面會講到,POST請求 /actuator/service-registry 接口實質上還是PUT調用 http://39.96.0.32:8001/eureka/apps/CONSUMER/DESKTOP-UJN399N:consumer:7777/status?value=UP
註冊服務底層代碼
啓動consumer client服務,eureka server就會很快顯示consumer服務已經上線,這是爲什麼呢?
向eureka server發送了DELETE consumer的請求,過段時間consumer又會重新上線,這又是爲什麼呢?
1.準備工作
- pom
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.9.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.huoli</groupId>
<artifactId>consumer</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>consumer</name>
<description>Demo project for Spring Boot</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
- application.yml
eureka:
client:
# 客戶端從eureka獲取applications到本地緩存的時間間隔,默認30s
# DiscoveryClient 中啓動 registry cache refresh timer 定時任務,每3s從server獲取一次註冊服務列表,但獲取的信息並不是實時的狀態
registry-fetch-interval-seconds: 3
service-url:
defaultZone: http://39.96.0.32:8000/eureka/,http://39.96.0.32:8001/eureka/,http://39.96.0.32:8002/eureka/
instance:
# 表示eureka server至上一次收到client的心跳之後,等待下一次心跳的超時時間,在這個時間內若沒收到下一次心跳,則將移除該instance,默認90s
lease-expiration-duration-in-seconds: 90
# 表示eureka client發送心跳給server端的頻率。如果在leaseExpirationDurationInSeconds後,server端沒有收到client的心跳,則將摘除該instance,默認30s
# DiscoveryClient 中啓動 Heartbeat timer 定時任務,每5s發送次請求給server,告訴自己處於上線狀態
lease-renewal-interval-in-seconds: 5
server:
port: 7777
spring:
jackson:
date-format: yyyy-MM-dd HH:mm:ss
application:
name: consumer
logging:
level:
root: info
# 一定要寫這句話,否則hystrix無法檢測到請求
feign:
hystrix:
enabled: true
client:
config:
default:
connectTimeout: 7000
readTimeout: 7000
#hystrix 配置
hystrix:
command:
default:
execution:
timeout:
#如果enabled設置爲false,則請求超時交給ribbon控制
enabled: true
isolation:
thread:
timeoutInMilliseconds: 180000
management:
endpoints:
web:
exposure:
# 默認只放出health,info等接口,查看localhost:7777/acurator/loggers 可看到各包對應的日誌等級
# 在bootstrap.properties中配置包的日誌等級,方便定位問題
include: 'loggers'
- bootstrap.properties
爲了方便理解代碼,在bootstrap.properties中將DiscoveryClient和loadbalancer的日誌等級設爲DEBUG。
# 設置指定包的日誌級別
logging.level.com.netflix.discovery.DiscoveryClient=DEBUG
logging.level.com.netflix.loadbalancer=DEBUG
- java
/**
* APPLICATION
*/
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
public class ConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class, args);
}
}
/**
* FeignClient
*/
@FeignClient(name = "producer")
public interface ProducerRemote {
@RequestMapping(value = "/hello",method = RequestMethod.GET)
public String hello(@RequestParam("name") String name);
@RequestMapping("/test")
public List<Object> test(@RequestBody Map<String,String> map);
}
/**
* TestController
*/
@RestController
public class TestController {
@Autowired
ProducerRemote producerRemote;
@Autowired
private ObjectFactory<HttpMessageConverters> messageConverters;
@RequestMapping("/hello")
public String hello(){
return producerRemote.hello("abc");
}
}
2.啓動服務
啓動服務後,console打印如下日誌,先大致瀏覽一遍,記住打印日誌的類是DiscoveryClient
瀏覽器輸入 http://localhost:7777/actuator/loggers,看見DiscoveryClient的日誌級別是DEBUG,說明bootstrap.properties中的配置生效。
註冊服務步驟
- EurekaClientAutoConfiguration中static class RefreshableEurekaClientConfiguration,@Bean生成EurekaClient的實例
- DiscoveryClient作爲EurekaClient的繼承類開始實例化,調用方法initScheduledTasks(),該方法生成了2個定時器,分別是
- 間隔registryFetchIntervalSeconds時間執行**CacheRefreshThread()**方法,從eureka server獲取註冊信息,存儲在本地緩存
- 間隔renewalIntervalInSecs時間執行**HeartbeatThread()**方法,向eureka server發送heart beat,保持自身服務在線狀態
- 顯然第2個定時器纔是註冊服務的,最終調用的方法是DiscoveryClient.renew()中的sendHeartBeat()和register(),定位到AbstractJerseyEurekaHttpClient.class。
sendHeartBeat 發送PUT請求 http://39.96.0.32:8001/eureka/apps/CONSUMER/DESKTOP-UJN399N:consumer:7777?status=UP&lastDirtyTimestamp=1562750824897 不停告訴server,本服務很健壯!
sendHeartBeat返回404,表示這個服務根本還未上線,需要先register,即Post http://39.96.0.32:8001/eureka/apps/CONSUMER 並傳遞instanceInfo的json對象。
至此,註冊服務流程講解完畢,該服務會間隔renewalIntervalInSecs時間,反覆執行renew()的代碼,第一次先register註冊服務,再然後不停地跟sendHeartBeat說自己處於UP狀態。
其中renewalIntervalInSecs可在yml中配置,下圖中配置的是5s,當然這個數值大小還是根據實際情況去設置。
DiscoveryClient.shutDown()
unregister()最後調用的核心代碼是 EurekaHttpClient.cancle(),即向eureka server發送DELETE請求 。
總結
至此介紹完了如何優雅的關閉和啓動eureka client服務,以及底層的代碼實現。
需要記住的名詞有
- DiscoveryClient (父類爲EurekaClient) 操作和eureka server連接的工具類
- initScheduledTasks 啓動2個定時器,一個發送heartbeat,一個從server拉取註冊信息(fetch registry)
- renewalIntervalInSecs 發送heartbeat的時間間隔
eureka server的rest接口可參閱 https://github.com/Netflix/eureka/wiki/Eureka-REST-operations
這裏我們自己總結下規律,注意sendHeartBeat和statusUpdate之間的不同
EurekaHttpClient中的方法 | 請求方式 | 請求地址 | 其他 |
---|---|---|---|
register | post | http://39.96.0.32:8001/eureka/apps/CONSUMER | InstanceInfo的json對象 |
cancel | delete | http://39.96.0.32:8001/eureka/apps/CONSUMER/DESKTOP-UJN399N:consumer:7777 | |
sendHeartBeat | put | http://39.96.0.32:8001/eureka/apps/CONSUMER/DESKTOP-UJN399N:consumer:7777?status=UP&lastDirtyTimestamp=1562759993141 | |
statusUpdate | put | http://39.96.0.32:8001/eureka/apps/CONSUMER/DESKTOP-UJN399N:consumer:7777/status?value=UP&lastDirtyTimestamp=1562759993141 | status=UP,OUT_OF_SERVICE,DOWN等等 |
下一章將介紹EurekaClient是如何獲取服務列表,然後負載均衡去調用其他服務的。