背景
最近我开发了一个基于springboot的服务,服务从网络接收请求,再把请求任务放入队列里交给线程池取异步消费请求任务。后来发现这个服务有个bug修复后需要重新发布到线上,这时需要停止服务更新jar包,那么我们怎么样停止服务才能保证任务队列里的请求都处理完成了呢?不然有可能会丢掉一些请求。这在高并发流量应用里很常见。
停止进程
linux中停止进程的方式主要使用kill命令,该命令后面可以接信号量参数,这个信号量会传递给相应进程并根据信号执行相应的处理
kill [-s 信号声明 | -n 信号编号 | -信号声明] 进程号
常用的停止进程的信号有两种:
| 信号 | 值 | 说明 |
|---|---|---|
| SIGTERM | 15 | (默认值)结束程序(可以被捕获、阻塞或忽略) |
| SIGKILL | 9 | 无条件结束程序(不能被捕获、阻塞或忽略) |
| kill-9 pid 可以理解为操作系统从内核级别强行杀死某个进程 | ||
| kill-15 pid 则可以理解为发送一个通知,告知应用主动关闭 |
Java服务停止方式
我们知道使用kill -15 可以向进程发送一个通知告知现在要关闭服务,请做好善后工作。那么Java中怎么响应这个通知呢?Java提供了shutdown hook的勾子机制,通过调用
Runtime.getRuntime().addShutdownHook(Thread hook);
可以向jvm注册一个线程,当执行kill -15的时候jvm会调用这个线程并等待这个线程执行完毕,最后退出jvm。
springboot优雅停止方式
springboot应用本质上是一个Java应用,所以停止原理就如上面所述。但是实际上springboot服务可能很复杂,依赖的资源比较多。怎么保证按照指定的顺序关闭资源呢?举例如下:我们 springboot 依赖的redis ,并且里面有个ScheduledExecutorService 线程池 定时从队列里面读取任务执行,任务里面使用了RedisTemplate操作redis。我注册了 shutdownHook 来关闭线程池,如下
HookUtils.addShutdownHook(() -> {
log.info("ScheduledExecutorService Name[{}] shutdown",poolName);
gracefulShutdown(scheduledExecutorService,0);
});
但是,当我执行kill -15 关闭进程的时候,却在控制台发现了如下异常
2020-02-14 18:12:47.666 [Thread-7] INFO com.xxx.xxx.xxx.executor.master.component.task.TaskProgressManager - ScheduledExecutorService Name[xxx] shutdown finished
2020-02-14 18:12:47.671 [Thread-7] INFO org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor - Shutting down ExecutorService 'applicationTaskExecutor'
2020-02-14 18:12:47.680 [xxx] WARN com.xxx.xxx.xxx.executor.master.component.mq.RedisMessageHandler - Read error from redis command queue
org.springframework.data.redis.RedisSystemException: Redis exception; nested exception is io.lettuce.core.RedisException: io.lettuce.core.RedisException: Connection closed
at org.springframework.data.redis.connection.lettuce.LettuceExceptionConverter.convert(LettuceExceptionConverter.java:74) ~[spring-data-redis-2.1.5.RELEASE.jar:2.1.5.RELEASE]
at org.springframework.data.redis.connection.lettuce.LettuceExceptionConverter.convert(LettuceExceptionConverter.java:41) ~[spring-data-redis-2.1.5.RELEASE.jar:2.1.5.RELEASE]
at org.springframework.data.redis.PassThroughExceptionTranslationStrategy.translate(PassThroughExceptionTranslationStrategy.java:44) ~[spring-data-redis-2.1.5.RELEASE.jar:2.1.5.RELEASE]
at org.springframework.data.redis.FallbackExceptionTranslationStrategy.translate(FallbackExceptionTranslationStrategy.java:42) ~[spring-data-redis-2.1.5.RELEASE.jar:2.1.5.RELEASE]
at org.springframework.data.redis.connection.lettuce.LettuceConnection.convertLettuceAccessException(LettuceConnection.java:268) ~[spring-data-redis-2.1.5.RELEASE.jar:2.1.5.RELEASE]
at org.springframework.data.redis.connection.lettuce.LettuceListCommands.convertLettuceAccessException(LettuceListCommands.java:490) ~[spring-data-redis-2.1.5.RELEASE.jar:2.1.5.RELEASE]
at org.springframework.data.redis.connection.lettuce.LettuceListCommands.bRPop(LettuceListCommands.java:409) ~[spring-data-redis-2.1.5.RELEASE.jar:2.1.5.RELEASE]
at org.springframework.data.redis.connection.lettuce.LettuceClusterListCommands.bRPop(LettuceClusterListCommands.java:82) ~[spring-data-redis-2.1.5.RELEASE.jar:2.1.5.RELEASE]
at org.springframework.data.redis.connection.DefaultedRedisConnection.bRPop(DefaultedRedisConnection.java:535) ~[spring-data-redis-2.1.5.RELEASE.jar:2.1.5.RELEASE]
at org.springframework.data.redis.core.DefaultListOperations$5.inRedis(DefaultListOperations.java:215) ~[spring-data-redis-2.1.5.RELEASE.jar:2.1.5.RELEASE]
at org.springframework.data.redis.core.AbstractOperations$ValueDeserializingRedisCallback.doInRedis(AbstractOperations.java:59) ~[spring-data-redis-2.1.5.RELEASE.jar:2.1.5.RELEASE]
at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:224) ~[spring-data-redis-2.1.5.RELEASE.jar:2.1.5.RELEASE]
at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:184) ~[spring-data-redis-2.1.5.RELEASE.jar:2.1.5.RELEASE]
这个是在执行线程池里的任务时发行redis连接已经关闭了抛出的异常。所以这里需要先把线程池里的任务执行完毕在关闭redis连接,那么我们怎么控制这个顺序呢?执行kill -15的时候,spring会执行一个shutdown hook 这个hook里会执行关闭动作。这个hook 在 AbstractApplicationContext 里注册
@Override
public void registerShutdownHook() {
if (this.shutdownHook == null) {
// No shutdown hook registered yet.
this.shutdownHook = new Thread() {
@Override
public void run() {
synchronized (startupShutdownMonitor) {
doClose();
}
}
};
Runtime.getRuntime().addShutdownHook(this.shutdownHook);
}
}
最终调用在 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() {
// Check whether an actual close attempt is necessary...
if (this.active.get() && this.closed.compareAndSet(false, true)) {
if (logger.isDebugEnabled()) {
logger.debug("Closing " + this);
}
LiveBeansView.unregisterApplicationContext(this);
try {
// Publish shutdown event.
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.
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);
}
}
doClose 会执行
发布cotext close event 事件通知
Stop all Lifecycle beans
销毁所有的单例bean(会调用bean的destory方法)
关闭beanFactory
我们知道向jvm注册的多个hook,但多个hook是并行执行的并没有先后顺序。所以要保持先后顺序必须要在一个hook里执行关闭和回收资源等操作,这样我们只能在spring hook里做了。从上面看,redis 关闭资源连接等操作应该是在第三步 销毁bean里面做的,所以我们可以在第1步里监听ContextClosedEvent事件,在事件回调方法里面做,那么只要实现ApplicationListener<ContextClosedEvent> 接口即可 ,如下所示:
@Override
public void onApplicationEvent(final ContextClosedEvent event) {
// when spring closed , publish shutdown event before bean destory
log.debug("ApplicationContext Close Event");
//回收资源操作
componentBootstrap.close();
}




