关于一次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. 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);
    }
}

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

输出间隔都为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);

    }
}

执行结果如下

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

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

单机框架的定时任务

Quartz任务调度框架

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

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

分布式框架的任务调度

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

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

自研定时任务框架

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

猜你喜欢

转载自blog.csdn.net/qq_31363843/article/details/126361843
今日推荐