最近接手一个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是如何获取服务列表,然后负载均衡去调用其他服务的。