一、前言
先了解下eureka发现/注册流程,跳转链接
client端:1、服务启动后registry到注册中心(可配置不注册上去)
2、每30秒,往注册中心丢心跳包,进行续约
3、每60秒,从注册中心拉取服务信息, 拉取到的信息,缓存到本地
二、背景
噱头:在编程开发的节奏上,敏捷开发已经成为了一种主流;
问题1:那么问题来了,服务需要发布上线,线上的流量又还在请求进来?
目前springcloud服务发现及下线都是有缓存的,每个服务都会保存一份其他服务请求地址在本地缓存中。
那么线上的流量又在请求服务且服务又在发布,所以客户会操作不了功能。而且重启服务或者发布服务会带来并发症状。
问题2:比如:其他服务流量暴增、服务可用性降低。
springcloud主打的就是cap中的ap;
三、服务下线过程
直接杀进程 kill -9
0s,kill掉服务,未通知eureka-service通知下线。
29s,检查服务在是否续约,是否超过90%续约失败,未超过,则不会剔除。注意:超过这里也不会被剔除 (剔除的前提,是60s内,超过90%未续约,才会被剔除节点)。发出ping指令。
60s,继续检查是否续约,不会剔除
90s,检查是否已经续约,超过了90s且超过90%未续约,剔除该节点。
其他服务拉去eureka-service的注册信息是60s更新一次。
而剔除下线需要90s;所以其他感知到下线需要 60s+90 s?在这个期间系统功能可能就是不可用了。当然服务也可能是做了集群,所以有时好,有时就可能不行了。
四、针对问题,解决问题
使用kill -15 进程id
JVM进程在接收到kill -15信号通知的时候,会做一些清理动作的。同时,也提供了hook机制,来让开发者自定义清理动作,对应的方法为:Java.Runtime.getRunTime().addShutdownHook(Thread hook)。
OK..
来看下springboot启动,是怎么提供了一个入口?
上代码。
// 第一步方法
public static void main(String[] args) {
SpringApplication.run(XXApplication.class, args);
}
// 第二部方法
/**
* Static helper that can be used to run a {@link SpringApplication} from the
* specified sources using default settings and user supplied arguments.
* @param primarySources the primary sources to load
* @param args the application arguments (usually passed from a Java main method)
* @return the running {@link ApplicationContext}
*/
public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
return new SpringApplication(primarySources).run(args);
}
// 第三步方法
/**
* Run the Spring application, creating and refreshing a new
* {@link ApplicationContext}.
* @param args the application arguments (usually passed from a Java main method)
* @return a running {@link ApplicationContext}
*/
public ConfigurableApplicationContext run(String... args) {
....
try {
...
prepareContext(context, environment, listeners, applicationArguments, printedBanner);
// 在这里
refreshContext(context);
afterRefresh(context, applicationArguments);
stopWatch.stop();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}
listeners.started(context);
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, listeners);
throw new IllegalStateException(ex);
}
....
return context;
}
// 第四个方法
// 这里是扩展自定义xx
private void refreshContext(ConfigurableApplicationContext context) {
refresh(context);
// 默认为true
if (this.registerShutdownHook) {
try {
// 在这里注册了一个SpringApplicationShutdownHook,进去看看
context.registerShutdownHook();
}
catch (AccessControlException ex) {
// Not allowed in some environments.
}
}
}
// 第五步
/**
* Register a shutdown hook {@linkplain Thread#getName() named}
* {@code SpringContextShutdownHook} 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 ConfigurableApplicationContext#SHUTDOWN_HOOK_THREAD_NAME
* @see #close()
* @see #doClose()
*/
// 看,注册了一个关闭线程的钩子。好,我们看看doClose都干了什么。
@Override
public void registerShutdownHook() {
if (this.shutdownHook == null) {
// No shutdown hook registered yet.
this.shutdownHook = new Thread(SHUTDOWN_HOOK_THREAD_NAME) {
@Override
public void run() {
synchronized (startupShutdownMonitor) {
doClose();
}
}
};
Runtime.getRuntime().addShutdownHook(this.shutdownHook);
}
}
/**
* 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() {
// Check whether an actual close attempt is necessary...
if (this.active.get() && this.closed.compareAndSet(false, true)) {
...
try {
// 看这里,有个close的事件,这个事件可以自行定义的
publishEvent(new ContextClosedEvent(this));
}
catch (Throwable ex) {
logger.warn("Exception thrown from ApplicationListener handling ContextClosedEvent", ex);
}
// 以下只是清空spring容器里面的东西。
// Stop all Lifecycle beans, to avoid delays during individual destruction.
if (this.lifecycleProcessor != null) {
try {
this.lifecycleProcessor.onClose();
}
catch (Throwable ex) {
logger.warn("Exception thrown from LifecycleProcessor on context close", ex);
}
}
// Destroy all cached singletons in the context's BeanFactory.
destroyBeans();
// Close the state of this context itself.
closeBeanFactory();
// Let subclasses do some final clean-up if they wish...
onClose();
// Reset local application listeners to pre-refresh state.
if (this.earlyApplicationListeners != null) {
this.applicationListeners.clear();
this.applicationListeners.addAll(this.earlyApplicationListeners);
}
// Switch to inactive.
this.active.set(false);
}
}
如上是spring的入口;
那么来自定义我们自行实现这个事件监听
五、自定义ContextClosedEvent监听器
/**
* @Description : kill -15; SpringApplicationShutdownHook钩子的监听器
* @author:
* @date : 2023/6/8
*/
@Slf4j
@Configuration
public class GracefulShutDownHookListener implements ApplicationListener<ContextClosedEvent>, TomcatConnectorCustomizer {
/**
* 连接器
*/
private volatile Connector connector;
/**
* 线程休眠时间
*/
private final static Integer THREAD_SLEEP_TIME = 150000;
/**
* 注意,这里是可以拿到上下文ApplicationContext的
* @param contextClosedEvent 上下文关闭事件监听
* @return: void
*/
@Override
public void onApplicationEvent(ContextClosedEvent contextClosedEvent) {
// 官方虽然不建议使用,但是内部的ribbon,也还在使用,内部管理eureka-client还是在使用这个
DiscoveryManager discoveryManager = DiscoveryManager.getInstance();
// 获取eureka-client
EurekaClient eurekaClient = discoveryManager.getEurekaClient();
if(null == eurekaClient) {
threadShutdown();
return;
}
try {
long start = System.currentTimeMillis();
log.info("服务下线开始:{}", start);
eurekaClient.shutdown();
log.info("服务下线结束:{}", System.currentTimeMillis() - start);
} catch (Exception e) {
log.error("服务下线失败:{},异常:{}", contextClosedEvent, e);
}
try {
Thread.sleep(THREAD_SLEEP_TIME);
} catch (Exception e) {
log.error("取消eureka-client下下线休眠失败:{}", e);
}
threadShutdown();
}
/**
* 因为我们服务是通过kill -15 ,所以我们内部可能还是有线程在处理
* 如果线程处理时间过慢了,我们这里需要手动进行阻断线程的.
*/
private void threadShutdown() {
// 阻断线程进来了
this.connector.pause();
// 获取连接器里面的线程
Executor executor = this.connector.getProtocolHandler().getExecutor();
if(executor instanceof ThreadPoolExecutor) {
ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) executor;
threadPoolExecutor.shutdown();
try {
// 线程是否30秒终止了
if(!threadPoolExecutor.awaitTermination(30, TimeUnit.SECONDS)) {
// 目前还没有说明需要做变更更
}
} catch (Exception e) {
// 线程阻塞下
Thread.currentThread().interrupt();
log.error("");
}
}
}
@Bean
public ConfigurableServletWebServerFactory servletWebServerFactory(final GracefulShutDownHookListener gracefulShutDownHookListener) {
TomcatServletWebServerFactory tomcatServletWebServerFactory = new TomcatServletWebServerFactory();
tomcatServletWebServerFactory.addConnectorCustomizers(gracefulShutDownHookListener);
return tomcatServletWebServerFactory;
}
@Override
public void customize(Connector connector) {
this.connector = connector;
}
}