kakfa发版丢消息事件分析

背景

其他部门同事反馈在项目发版/重启(kill -15)的那段时间,经常会出现导致 C 端业务出现问题,从而产生资损

一听资损,赶紧应答下来,了解了下具体情况,然后立马去排查了

问题分析

结合同事的描述以及对业务的了解,很快就定位到是 kafka 消息丢失导致 C 端业务出现问题

业务当前消费架构图

从上图可以了解到几个点会导致目前这个场景消息丢失

  1. kafka 一秒一次的位移提交
  2. Queue 队列没消费完任务
  3. work 线程池从 Queue 中拉取的任务没消费完(每次拉取一个)

问题所在 :因C端业务特性,非准实时的消息是没有意义的(分钟级),所以kafka的自动提交位移实际上是符合业务需求,三点结合起来看问题应该是出在:在发版时 消费单线程 依旧在拉取消息写入 Queue,并且后续的 线程池也没有将 Queue中的任务给处理完

消费架构改造

  1. 改造消费流程
  2. 启动时增加JVM关闭钩子,在关闭前将 isRunning 修改为fale,从而停止 消费单线程 继续拉取kafka消息
  3. 优雅关闭 work线程池
复制代码
// shutdown() 与 shutdownNow()这里也给到一段shutdown测试代码
ThreadPoolExecutor executorService =
    new ThreadPoolExecutor(1, 1, 30, TimeUnit.SECONDS, new LinkedBlockingQueue<>());
AtomicInteger integer = new AtomicInteger();
for (int i = 0; i < 100; i++) {
    executorService.execute(() -> {
        try {
            System.out.println(new Date() + "=====>" + integer.incrementAndGet());
            Thread.sleep(1000L);
        } catch (Exception e) {
            e.printStackTrace();
        }
    });
}

Thread.sleep(5000L);
executorService.shutdown();
// executorService.shutdownNow();
System.out.println("线程池已触发shutdown");

随之而来的另一个问题,若在JVM关闭钩子中对 work线程池 操作shutdown,在任务中是有使用到Spring容器中的bean,若bean销毁了,那么work线程池中的任务都无法再执行成功(具体销毁优先级细则可自行百度,这里不做延伸)。

基于这个问题,回想到之前常用的一个注解 @PostConstruct 的一个孪生兄弟 @PreDestroy ,这是在Java规范JSR-250引入的注解,定义了对象的创建和销毁工作,那么Spring必然对它有做支持,测试代码如下

复制代码
ThreadPoolExecutor executorService =
        new ThreadPoolExecutor(1, 1, 30, TimeUnit.SECONDS, new LinkedBlockingQueue<>());

@PostConstruct
public void postConstruct(){
    AtomicInteger integer = new AtomicInteger();
    for (int i = 0; i < 100; i++) {
        executorService.execute(() -> {
            try {
                System.out.println(new Date() + "=====>" + integer.incrementAndGet());
                Thread.sleep(1000L);
            } catch (Exception e) {
                e.printStackTrace();
            }
        });
    }
}

@PreDestroy
public void preDestroy(){
    executorService.shutdown();
}

// 增加一个测试关闭的接口
@GetMapping("/shutdown")
public void shutdown() {
    System.exit(0);
}

测试结果依旧失败,看日志打印是正在处理线程池中已被接收的任务时挂掉的(这不科学,上面shutdown()测试案例结果明明会等待所有任务结束以后再结束),心里一群 草姓的马 飘过-_-

转念一想:其实这样也对,若一个池任务过多导致一直无法kill掉进程,这种行为也不对...那有没有什么补偿机制可以用,emm,山重水复疑无路,柳暗花明又一村哇,Doug Lea大神名不虚传,早就为我们考虑好了

复制代码
// 贴出改动方法
@PreDestroy
public void preDestroy(){
    executorService.shutdown();
    try {
        if(executorService.awaitTermination(5, TimeUnit.SECONDS)){
            System.out.println("任务执行完毕结束");
        } else {
            System.out.println("time out 结束");
        }
    } catch (InterruptedException e) {
        System.out.println("Interrupted while waiting for executor");
        Thread.currentThread().interrupt();
        executorService.shutdownNow();
    }
}

嘿嘿,这么一改顺眼多了,线程池在shutdown后再至多等待N秒(若无任务则直接返回true),业务可以根据特性去决定此值配置

但是这么写多麻烦,那么多重要的线程池各个都要在这里写,那Spring如何实现线程池的优雅停的呢?想到Spring的生命周期中的 销毁回调,实现 DisposableBean 即可,那看看ThreadPoolTaskExecutor,其父类ExecutorConfigurationSupport 在处理销毁时,会判定其 waitForTasksToCompleteOnShutdown 参数是否为true来决定是否要调用shutdown(),并且根据其 awaitTerminationSeconds 参数来决定是否需要调用 ExecutorService.awaitTermination 去等待线程池处理一定时间

那让我们来改造改造现在的work线程池,指定业务指定配置以后,交给spring去帮我们去做这些重复的销毁动作

写到最后

若使用Spring提供线程池,并指定以下两个参数即可实现线程池优雅停

  1. waitForTasksToCompleteOnShutdown 参数,在销毁时会帮我们调用一次线程池shutdown()
  2. awaitTerminationSeconds 参数,在调用shutdown以后可以等等一段时间,从而尽可能的将线程池中任务给执行完毕

ExecutorService.awaitTermination 虽好,可不要贪杯(滥用)哦,多个线程池都指定此参数并在销毁时都存在大量的任务,可能会导致 kill -15 的时间增加,从而出现一种 "kill不掉" 的现象

相关推荐
Hello.Reader25 分钟前
基于 Flink CDC 的 MySQL → Kafka Streaming ELT 实战
mysql·flink·kafka
资深web全栈开发1 小时前
[特殊字符]图解 Golang 反射机制:从底层原理看动态类型的秘密
开发语言·后端·golang
西岭千秋雪_1 小时前
Zookeeper实现分布式锁
java·分布式·后端·zookeeper·wpf
码事漫谈8 小时前
智能体颠覆教育行业调研报告:英语、编程、语文、数学学科应用分析
后端
蓝-萧8 小时前
使用Docker构建Node.js应用的详细指南
java·后端
码事漫谈8 小时前
《C语言点滴》——笑着入门,扎实成长
后端
Tony Bai8 小时前
【Go模块构建与依赖管理】09 企业级实践:私有仓库与私有 Proxy
开发语言·后端·golang
咖啡教室9 小时前
每日一个计算机小知识:ICMP
后端·网络协议
间彧9 小时前
OpenStack在混合云架构中通常扮演什么角色?
后端
咖啡教室9 小时前
每日一个计算机小知识:IGMP
后端·网络协议