系统设计-当服务重启时,大部分人没考虑到的一点

点击上方名片关注我,为你带来更多踩坑案例

ed83b436fd11ba5fa91ca7af665d94fc.png

- 引言 -

    如果你是一个摸爬滚打几年的开发者,那么这个阶段,对系统设计的合理性绝对是衡量一个人水平的重要标准。

    一个好的设计不光能让你工作中避免很多麻烦,还能为你面试的时候增加很多谈资

    而且,不同设计之间理念都是有借鉴性参考性的,你见过的设计多了,思考的多了,再次面临一个问题的时候,就会有很多点子不由自主的冒出来。

    希望这个系列的文章,能够和大家互相借鉴参考,共同进步。

- 直接停机会有什么问题? -

    系统的更新重启发布部署,这是我们每个开发者都会遇到的问题,它方式多样,比如粗暴点手动kill掉再启动,或者写个脚本更简单,再或者有专门的流水线来跑这个流程,自动打包发布部署,这个过程可以再加上灰度,实现热更新。

    但是仅仅这样就够了么?

    其实对很多系统来说已经够了。

    但是还会存在一些场景,即便有灰度发布,还是会需要我们考虑得更细致。

    比如我最近做了一个任务系统,系统设计-领导让我设计一个任务系统用到了内存队列,不清楚的同学可以看一下之前的文章,如果内存队列中有在执行的任务,停机的时候会出现什么问题呢?

    没错,执行状态的任务会一直在执行态,甚至任务执行进度无法追溯。

- spring自带优雅停机 -

    springboot2.3.0之后,就集成了优雅停机的模块

server:
    shutdown: graceful #开启优雅停机,web容器关闭时,断流+等待,默认是立即停机IMMEDIATE
spring:
    lifecycle:
        timeout-per-shutdown-phase: 10s #缓冲器即最大等待时间,不能大于QAE容器的杀死等待时间,不然没意义了,可以根据具体业务调整二者的值

    没错,只要确认自己的spring-boot是2.3.0且容器用的是它自带的,比如tomcat即可。

    然后就可以开启优雅停机,参数不用过多解释。他开启以后有两个效果

  1. 断流:系统发出kill-15命令后,之后的请求再打进来,会被拒绝

  2. 缓冲:系统发出kill-15命令后,会有一个缓冲时间让正在跑的线程走完。如果提前走完,则系统会提前结束;如果到时没走完,则会强行停止即kill-9。

    需要注意的是

    1. 它只针对spring自带的线程池,一般而言也就是http请求,如果你的请求是来自消息队列或者其他线程池,则不在上述优雅停机管辖范围内。

    2. 如果缓冲时间内线程没跑完,在停机的最后一刻,我们想做一些操作,上述也是不能满足的。

    当然,如果你的请求都来自于http请求,使用的是spring自带线程池,而且任务能够保证在一个指定时间内跑完,那么上述配置完全可以满足了。

    还有一个小技巧,就是你如果在本地idea测试,记得别开debug模式,直接使用run模式,然后点击它关闭任务,而不是右上角的stop。

7a7657aac56f1eb77d6f4d49d04c834d.png

- 自行实现优雅停机 -

    网上还有一些优雅停机的办法,我这里选用了一种最适合我也是最灵活的一种,推荐给大家。

@Slf4j
@Component
public class ShutdownListener implements ApplicationListener<ContextClosedEvent> {
    private static final List<ExecutorService> POOLS = Collections.synchronizedList(new ArrayList<>(12));


    /**
     * 线程中的任务在接收到应用关闭信号量后最多等待多久就强制终止,其实就是给剩余任务预留的时间, 到时间后线程池必须销毁
     */
    private static final long AWAIT_TERMINATION = 8;


    /**
     * awaitTermination的单位
     */
    private final TimeUnit TIME_UNIT = TimeUnit.SECONDS;


    /**
     * description: 添加自定义线程池,可以在线程池实例化的地方把它注册进去,
     * 一般而言,我们开发中的线程池都是推荐自己去实现的
     *
     * @author luhui
     * @date 2022/11/4 14:37
     */
    public static void registryExecutor(ExecutorService executor) {
        POOLS.add(executor);
    }


    @Override
    public void onApplicationEvent(ContextClosedEvent event) {
        log.info("线程池优雅关闭开始, 当前要处理的线程池数量为: {} >>>>>>>>>>>>>>>>", POOLS.size());
        if (CollectionUtils.isEmpty(POOLS)) {
            return;
        }
        for (ExecutorService pool : POOLS) {
            // 这里会使线程池进入一个关闭状态,拒绝外部请求的同时,阻塞等待池中的线程跑完
            pool.shutdown();
            try {
                if (!pool.awaitTermination(AWAIT_TERMINATION, TIME_UNIT)) {
                    if (log.isWarnEnabled()) {
                        log.warn("Timed out while waiting for executor [{}] to terminate", pool);
                    }
                    // 如果到时了没跑完,则执行这个方法
                    interruptTask();
                }
            } catch (InterruptedException ex) {
                if (log.isWarnEnabled()) {
                    log.warn("Interrupted while waiting for executor [{}] to terminate", pool);
                }
                Thread.currentThread().interrupt();
            }
        }
    }


    /**
     * description: 中断超时未完成的任务,并且优先级设为最高,以便下次启动进行补偿
     *
     * @author luhui
     * @date 2022/11/4 18:26
     */
    private void interruptTask() {
        log.info("开始保存未完成的任务>>>>>>>>>>>>>>>>");
        Long taskId = TaskQueueManager.getInstance().pollTask();
        while (taskId != null) {
            log.info("中断一个任务{}", taskId);
            taskId = TaskQueueManager.getInstance().pollTask();
        }
        log.info("保存完成>>>>>>>>>>>>>>>>");
    }

代码中已经给出详细的注释,这也是我用在之前的任务系统中的一个优雅停机方式,也是我觉得比较灵活且简单的。

    上述的停机方式加上上一篇文章中的启动补偿,基本能解决当下停机所可能带来的问题了。

- 结束语 -

    当然可能有更好更健壮的停机方式,甚至一些分布式应用中有专门的组件去做这个(当然我觉得最后的实现也大同小异,无非是截流的方式改成网关而不是线程池),但是还是那句话,没有最好的设计,只有最合适的设计。

猜你喜欢

转载自blog.csdn.net/qq_31363843/article/details/128031181