如果一个客户端下线了,但未从注册中心的注册表移除,就可能被其他服务拉取到。导致请求失败。这样的下线不够优雅,大多数rpc框架客户端下线后都会通知服务器删除注册表数据,eureka是怎么处理这个问题的?
client端有一个shutdown方法,服务实例下线的时候,会主动调用这个方法。方法中会将client端启动的定时任务都停止,包括:心跳续约,定时拉取增量注册表等等。
随后如果client注册到了server的话,通知server端进行实例下线。com.netflix.discovery.DiscoveryClient#unregister方法里就是对server发起了一个远程调用。我们直接去看server的处理逻辑
这个方法有三步:
调用父类AbstractInstanceRegistry#cancel方法,处理服务下线的逻辑
通知server集群内的其他节点
父类的cancel方法中,直接调用了com.netflix.eureka.registry.AbstractInstanceRegistry#internalCancel方法,这个方法在《服务踢除》中也被提到过,说明这是server处理服务下线的统一入口。
方法逻辑不复杂,有四步:
1,从原始注册表数据结构中,删除下线的实例
2,对实例执行下线的逻辑,其实没什么逻辑,就是保存了一下下线的时间戳evictionTimestamp
3,放入最近变更队列中,以便client拉取增量注册表的时候可以感知到有服务实例下线了
4,清空读写缓存,避免client拉取缓存时拉取到已经下线的服务实例
服务下线不会主动通知其他客户端,而是等其他客户端来拉取增量注册表的时候,才会感知到
文章来源:Eureka源码系列 —— 8.服务下线
服务提供方已经开始进入关闭流程,那么很多对象就可能已经被销毁了,关闭后再收到的请求按照正常业务请求来处理,肯定是没法保证能处理的。所以我们可以在关闭的时候,设置一个请求“挡板”,挡板的作用就是告诉调用方,我已经开始进入关闭流程了,我不能再处理你这个请求了。
Tomcat 关闭的时候也是先从外层到里层逐层进行关闭,先保证不接收新请求,然后再处理关闭前收到的请求。
Tomcat 关闭过程(shutdown 脚本):
shutdown 主要做了两件事:
1,初始化Server组件,和Tomcat启动时类似,这一步主要是解析server.xml文件,然后根据server.xml中的属性初始化Tomcat组件的成员变量,这里主要关注Server组件的几个成员变量:port、address、shutdown,默认值分别为8005、127.0.0.1、SHUTDOWN等,需要和启动时读取的server.xml保持一致。
2,往address port所监听的Socket端口发生“SHUTDOWN”字符串。对应启动的第三种阻塞情况,"SHUTDOWN"字符串让main主线程结束了等待状态,并在接下来通过调用各组件的stop()和destroy()方法进行资源的释放。
我们的线程是在loader中被尝试停止的,而loader的stop方法在listenerStop方法之后,也就是说,即使loader成功终止了用户自己启动的线程,依然有可能在线程终止之前使用Sping框架,而此时Spring框架已经在Listener中关闭了!况且在loader的清理线程过程中只有配置了clearReferencesStopThreads参数,用户自己启动的线程才会被强制终止(使用Thread.stop()),而在大多数情况下,为了保证数据的完整性,这个参数不会被配置。也就是说,在WebApp中,用户自己启动的线程(包括Executors),都不会因为容器的退出而终止。
SpringBoot 的优雅关闭:(SpringBoot 2.3.0 之前)
SpringBoot Web 项目, 如果使用的是外置 tomcat, 可以直接使用上面 tomcat 命令完成优雅停机. 但通常使用的是内置 tomcat 服务器, 这时就需要编写代码来支持优雅停止。
增加 GracefulShutdown Connector 监听类
public class GracefulShutdown implements TomcatConnectorCustomizer, ApplicationListener<ContextClosedEvent> {
private static final Logger log = LoggerFactory.getLogger(GracefulShutdown.class);
private volatile Connector connector;
@Override
public void customize(Connector connector) {
this.connector = connector;
}
@Override
public void onApplicationEvent(ContextClosedEvent event) {
this.connector.pause();
Executor executor = this.connector.getProtocolHandler().getExecutor();
if (executor instanceof ThreadPoolExecutor) {
try {
ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) executor;
threadPoolExecutor.shutdown();
if (!threadPoolExecutor.awaitTermination(30, TimeUnit.SECONDS)) {
log.warn("Tomcat thread pool did not shut down gracefully within "
+ "30 seconds. Proceeding with forceful shutdown");
}
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
}
}
注册自定义的 Connector 监听器
@SpringBootApplication
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
@Bean
public GracefulShutdown gracefulShutdown() {
return new GracefulShutdown();
}
@Bean
public ConfigurableServletWebServerFactory webServerFactory(final GracefulShutdown gracefulShutdown) {
TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();
factory.addConnectorCustomizers(gracefulShutdown);
return factory;
}
}
SpringBoot 2.3.0 之后
在最新的 SpringBoot 2.3.0 版本中,正式内置了优雅停机功能,不需要再自行扩展线程池来处理。
当启动server.shutdown=graceful,在 web 容器关闭时,web服务器将不再接受新请求(Tomcat、Reactor Netty : 等待超时,Undertow:直接返回503),并等待活动请求完成的缓冲时间。
# 开启优雅停机,默认值:immediate 为立即关闭
server.shutdown=graceful
# 设置缓冲期,最大等待时间,默认:30秒
spring.lifecycle.timeout-per-shutdown-phase=60s
文章来源:Tomcat,SpringBoot,Docker 的优雅关闭
如何销毁作为成员变量的线程池?
尽管 JVM 关闭时会帮我们回收一定的资源,但一些服务如果大量使用异步回调,定时任务,处理不当很有可能会导致业务出现问题,在这其中,线程池如何关闭是一个比较典型的问题。
使用 DisposableBean 接口
@Service
public class SomeService implements DisposableBean{
ExecutorService executorService = Executors.newFixedThreadPool(10);
public void concurrentExecute() {
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println("executed...");
}
});
}
@Override
public void destroy() throws Exception {
executorService.shutdownNow();
//executorService.shutdown();
}
紧接着问题又来了,是 shutdown 还是 shutdownNow 呢?这两个方法还是经常被误用的,简单对比这两个方法。
ThreadPoolExecutor 在 shutdown 之后会变成 SHUTDOWN 状态,无法接受新的任务,随后等待正在执行的任务执行完成。意味着,shutdown 只是发出一个命令,至于有没有关闭还是得看线程自己。
ThreadPoolExecutor 对于 shutdownNow 的处理则不太一样,方法执行之后变成 STOP 状态,并对执行中的线程调用 Thread.interrupt() 方法(但如果线程未处理中断,则不会有任何事发生),所以并不代表“立刻关闭”。
我们需要额外执行 awaitTermination 方法,仅仅执行 shutdown/shutdownNow 是不够的。
最终方案:参考 spring 中线程池的回收策略,我们得到了最终的解决方案。
public abstract class ExecutorConfigurationSupport extends CustomizableThreadFactory
implements DisposableBean{
@Override
public void destroy() {
shutdown();
}
/**
* Perform a shutdown on the underlying ExecutorService.
* @see java.util.concurrent.ExecutorService#shutdown()
* @see java.util.concurrent.ExecutorService#shutdownNow()
* @see #awaitTerminationIfNecessary()
*/
public void shutdown() {
if (this.waitForTasksToCompleteOnShutdown) {
this.executor.shutdown();
}
else {
this.executor.shutdownNow();
}
awaitTerminationIfNecessary();
}
/**
* Wait for the executor to terminate, according to the value of the
* {@link #setAwaitTerminationSeconds "awaitTerminationSeconds"} property.
*/
private void awaitTerminationIfNecessary() {
if (this.awaitTerminationSeconds > 0) {
try {
this.executor.awaitTermination(this.awaitTerminationSeconds, TimeUnit.SECONDS));
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
}
保留了注释,去除了一些日志代码,一个优雅关闭线程池的方案呈现在我们的眼前。
1 通过 waitForTasksToCompleteOnShutdown 标志来控制是想立刻终止所有任务,还是等待任务执行完成后退出。
2 executor.awaitTermination(this.awaitTerminationSeconds, TimeUnit.SECONDS)); 控制等待的时间,防止任务无限期的运行(前面已经强调过了,即使是 shutdownNow 也不能保证线程一定停止运行)
文章来源:研究优雅停机时的一点思考










网友评论