• 关于一次fullgc告警分析与分布式任务调度框架的选型调研


    微信公众号:java排坑日记,当你厌烦了长篇大论的面经,没时间系统的读书,可以利用茶余饭后地铁上马桶上几分钟的碎片时间来提升一下自己,坚持下来会有惊喜!
    最近记录了一些java中常踩的坑、设计思路和小知识点,大家可以看看
    详细记录一次接入xxl-job的踩坑路径
    30s快速解决循环依赖
    idea中一个小小的操作竟能解决如此多的问题
    docker中的服务接入xxljob需要注意的一点
    关于一次fullgc的告警分析
    mysql中的int类型竟变成了它?
    jpa中的字段总是自己莫名更新?
    获取不到类上的注解?空指针?
    学会这招,再也不怕依赖冲突!
    redis的热点key还能这么处理?
    领导让我设计一个任务系统
    当服务重启时,大部分人没考虑这点
    参数还能这么优雅校验?
    文件上传报错,全局异常处理!
    常见的点赞功能如何实现,如何防止刷赞

    一次fullGc

    整个思路如下

    1. 起因是这样的,最近收到的告警信息中,发现fullgc出现的频率突然高起来了。
    2. 然后想到,jvm配置近期无改动,也就是说服务的堆栈大小是正常的。
    3. 然后查看了一下最近的业务使用情况,发现qps也没有明显的抖动。
    4. 所以初步怀疑是代码问题。
    5. 然后仔细看了下cat中的内存使用柱状图,发现一个奇怪的特征,两台机器,基本都是一台机器在频繁做fullgc,另一台则不会,而且出现fullgc之前的内存使用情况是老年代呈现梯度递增的,也就是说在某一个时间点会突然暴增4g直接进入老年代的大对象。
    6. 经过我们的初步猜测,应该是某个时间点内存突然加载了很大的对象,其实这时候已经大概猜到可能是定时任务了。
    7. 不过还是去看了下gc日志,发现有一个对象的内存占用率是显著增加的,也就是所谓的大对象。
    8. 根据这个对象,找到相应代码,发现确实是定时任务中的一个对象,会定时从库中查询,库中的数据量其实还好,但是数据中有大字段,所以开发同学没注意,load了整个对象,然后导致了频繁的fullgc。
    9. 解决方法倒是好说,根据业务需要,对查询的字段做精简即可。

    产生的想法

    但是这次定时任务导致的fullgc引发了我的思考,其实在当今分布式应用的大环境下,我们还用之前老一套的定时任务去处理,确实会带来很多不便,比如这次的问题,如果采用分布式任务调度框架的话,是不是就能避免或者更早的发现这样的问题了?
    所以决定调研一下定时任务以及分布式调度框架相关的东西,选择一个适合我们业务的框架。

    无框架的定时任务

    以下按照技术的发展顺序,依次介绍

    定时器类Timer

    早期没有任何框架的时候,是使用JDK中的Timer机制和多线程机制(Runnable+线程休眠)来实现定时或者间隔⼀段时间执⾏某⼀段程序。

    import java.util.Timer;
    import java.util.TimerTask;
    import java.util.Date;
    import java.text.SimpleDateFormat;
    
    class MyTask extends TimerTask
    {
    	@Override
        public void run()
    	{       
    		Date date =new Date();
    		SimpleDateFormat ft=new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
    		System.out.println("当前时间:"+ft.format(date));	
        }
    }
    class TimerDemo 
    {
        public static void main(String[] args) 
    	{       
            Timer t=new Timer();
    		MyTask task=new MyTask();
    		t.scheduleAtFixedRate(task,0, 1000); 
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    其实就是在主线程中开启一个子线程,按时的执行某个任务。
    他也有缺点

    1. Timer类内部是维护了一个TaskQueue.TimerTask[]数组,数组中放了要执行的TimerTask,然后Timer执行器按照顺序去依次唤醒,这就导致了,如果两个任务的触发时间一致,那么必然有一个任务会被另一个任务耽搁。
    2. 还是基于上述的原理,在Timer是单线程的前提下,如果一个任务出了异常,那么别的任务也会被中断
      比如如下代码
    package com.example.demo.test;
    
    import java.text.SimpleDateFormat;
    import java.util.Date;
    import java.util.Timer;
    import java.util.TimerTask;
    import java.util.concurrent.TimeUnit;
    
    /**
     * description
     *  * @author luhui
     * @date 2022/8/17
     */
    public class TimerTest {
        public static void main(String[] args) {
            TimerTask task1 = new TimerTask() {
                @Override
                public void run() {
                    System.out.println("task1:" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
                    try {
                        TimeUnit.SECONDS.sleep(2);
                    } catch (Exception e) {
                        throw new RuntimeException();
                    }
                }
            };
    
            TimerTask task2 = new TimerTask() {
                @Override
                public void run() {
                    System.out.println("task2:" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
                    try {
                        TimeUnit.SECONDS.sleep(3);
                    } catch (Exception e) {
                        throw new RuntimeException();
                    }
                }
            };
    
            Timer timer = new Timer();
            timer.schedule(task1, 0, 1000);
            timer.schedule(task2, 0, 1000);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    task1与task2的调度间隔为1s,但是因为task1与task2的执行时间都超过了1s,所以预期的时间间隔应该是

    • task1为2s
    • task2为3s
      但实际输出如下
    task1:2022-08-17 17:06:59
    task2:2022-08-17 17:07:01
    task1:2022-08-17 17:07:04
    task2:2022-08-17 17:07:06
    task1:2022-08-17 17:07:09
    task2:2022-08-17 17:07:11
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    输出间隔都为5s,很显然task1和task2互相阻塞。
    其实在编译的时候编译器也会提醒
    在这里插入图片描述

    定时器任务池ScheduledExecutorService

    承接上文,直接上代码

    package com.example.demo.test;
    
    import java.text.SimpleDateFormat;
    import java.util.Date;
    import java.util.concurrent.ScheduledExecutorService;
    import java.util.concurrent.ScheduledThreadPoolExecutor;
    import java.util.concurrent.TimeUnit;
    
    /**
     * description
     *
     * @author luhui
     * @date 2022/8/17
     */
    public class ScheduledExecutorTest {
    
        public static void main(String[] args) {
            Runnable task1 = () -> {
                System.out.println("task1:" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (Exception e) {
                    throw new RuntimeException();
                }
            };
            Runnable task2 = () -> {
                System.out.println("task2:" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
                try {
                    TimeUnit.SECONDS.sleep(3);
                } catch (Exception e) {
                    throw new RuntimeException();
                }
            };
    
            ScheduledExecutorService scheduledExecutorService = new ScheduledThreadPoolExecutor(2);
            scheduledExecutorService.scheduleAtFixedRate(task1, 0, 1, TimeUnit.SECONDS);
            scheduledExecutorService.scheduleAtFixedRate(task2, 0, 1, TimeUnit.SECONDS);
    
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    执行结果如下

    task1:2022-08-17 17:09:54
    task2:2022-08-17 17:09:54
    task1:2022-08-17 17:09:56
    task2:2022-08-17 17:09:57
    task1:2022-08-17 17:09:58
    task2:2022-08-17 17:10:00
    task1:2022-08-17 17:10:01
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    因为我们在new ScheduledThreadPoolExecutor的时候已经指定了两个核心线程了,所以不会出现单线程的问题了。

    单机框架的定时任务

    Quartz任务调度框架

    Quartz是很常用很经典的一个任务框架,使用起来也很简单,比较容易集成到项目中。
    单机项目就不用说了,在分布式情况下,其实Quartz也是可以做成集群的。
    但是它的集群有以下缺点:
    缺点

    1. 不适合大量的短任务 & 不适合过多节点部署;
    2. 解决了高可用的问题,并没有解决任务分片的问题,存在单机处理的极限(即:不能实现水平扩展)。
    3. 需要把任务信息持久化到业务数据表,和业务有耦合
    4. 调度逻辑和执行逻辑并存于同一个项目中,在机器性能固定的情况下,业务和调度之间不可避免地会相互影响。
    5. quartz集群模式下,是通过数据库独占锁来唯一获取任务,任务执行并没有实现完善的负载均衡机制。
    6. 管理不便,多机器容易出现时钟问题。

    分布式框架的任务调度

    ## Elastic-Job
    对我们的业务场景而言,分布式调度框架的优点就是

    1. 因为业务系统众多,所以需要一个解耦的分布式调度框架去做定时任务
    2. 业务系统接入简单
    3. 有成熟稳定的社区去迭代,文档周全
    4. 最好能有报警的机制
    5. 能够实时的监控任务的执行情况(如时间、结果等)
      所以最后选定了xxl-job

    自研定时任务框架

    这里没有写每种分布式框架的原理,其实分布式调度框架的目的是一样的,所以注定了它的原理不会有太大的差异。
    基于这种原理,我们也可以研制自己的定时任务框架,只不过对于大部分场景而言,没必要重复造轮子。

  • 相关阅读:
    讯飞星火大模型V3.0 WebApi使用
    AutoDL使用手册
    【2021集创赛】Arm杯一等奖作品—基于 Cortex-M3 内核 SOC 的动目标检测与跟踪系统
    1001 害死人不偿命的(3n+1)猜想Java
    吃透Chisel语言.21.Chisel时序电路(一)——Chisel寄存器(Register)详解
    为了买个硬盘,我专门写了篇笔记
    Qt-QTransform-内存结构-仿射变换-工作原理-C++
    Docker 的数据管理与网络通信以及Docker镜像的创建
    Vite快速创建Vue3项目
    HTML+CSS大作业:使用html设计一个简单好看的公司官网首页 浮动布局
  • 原文地址:https://blog.csdn.net/qq_31363843/article/details/126361843