美文网首页
RPC(九:优雅关闭)

RPC(九:优雅关闭)

作者: supremecsp | 来源:发表于2022-06-24 19:07 被阅读0次

如果一个客户端下线了,但未从注册中心的注册表移除,就可能被其他服务拉取到。导致请求失败。这样的下线不够优雅,大多数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 &gt; 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 也不能保证线程一定停止运行)
文章来源:研究优雅停机时的一点思考

相关文章

网友评论

      本文标题:RPC(九:优雅关闭)

      本文链接:https://www.haomeiwen.com/subject/nycbvrtx.html