• 记录一次OOM排查经历(一)


    一、经历概要

    程序里有个跑数据的job,这个job的主要功能是往数据库写假数据。

    既需要跑历史数据(传给job的日期是过去的时间),也需要能够上线后,实时跑(十秒钟触发一次,传入触发时的当前时间)。

    其中一个job比较奇葩点,要写入的数据比较难以随机生成,是产品的同事从互联网上找的数据,比如当前网络上的热门话题,然后导入到数据库中。所以,

    我这边随机的时候,不能乱造。因此我的策略是,从数据库将已经存在的那几条真实数据查询出来,然后job中根据随机数,选择其中一条来仿造一条新的,

    随机生成新记录的其他字段,再写入数据库中。

    我单元测试一直这么跑的,没有任何问题,直到,将定时触发器打开,然后上线运行。。。悲剧来了。

    二、程序大体逻辑

    1、job接口定义:

    /**
     * desc:
     * 造数据的job,可按表来划分。一个表一个job
     * @author : 
     * creat_date: 2018/6/11 0011
     * creat_time: 14:46
     **/
    public interface DataProduceJob {
        /**
         * job的初始化
         * @param date
         */
        void jobInit(Date date);
    
        /**
         * 具体的job运行细节
         */
        void jobDetail(Integer recordNum);
    }

    job之所以分了上面两个接口,只是因为设计失误,完全可以融合为一个方法。jobInit的内容,后来我改写到job的afterPropertiesSet中了。

    (job实现了org.springframework.beans.factory.InitializingBean接口,保证初始化数据只被调用一次,所谓的初始化数据是指:

    读文件,读数据库之类的准备工作,后续的假数据都从这里面取)

    这边是出问题的job的源码:

    package com.ceiec.datavisual.quartz.job;
    
    import com.ceiec.common.utils.FileUtils;
    import com.ceiec.common.utils.MathUtils;
    import com.ceiec.datavisual.dao.GpsLocationSampleMapper;
    import com.ceiec.datavisual.dao.TopicAccountMapper;
    import com.ceiec.datavisual.dao.TopicMapper;
    import com.ceiec.datavisual.dao.TopicWebsiteMapper;
    import com.ceiec.datavisual.model.GpsLocationSample;
    import com.ceiec.datavisual.model.Topic;
    import com.ceiec.datavisual.model.TopicAccount;
    import com.ceiec.datavisual.model.TopicWebsite;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.core.io.ClassPathResource;
    import org.springframework.stereotype.Component;
    
    import java.math.BigDecimal;
    import java.util.Date;
    import java.util.List;
    import java.util.Random;
    
    @Component
    public class TopicWebsiteJob extends BaseJob implements DataProduceJob {
        @Autowired
        private TopicWebsiteMapper topicWebsiteMapper;
    
        private Date date;
    
        Random random = new Random();
    
        private List<TopicWebsite> topicWebsites;
    
        /**
         * 当前job执行时的时间,会作为创建时间写入数据库表
         *
         * @param date
         */
        @Override
        public void jobInit(Date date) {
            this.date = date;
            topicWebsites = topicWebsiteMapper.selectAll();
        }
    
        @Override
        public void jobDetail() {
            for (TopicWebsite website : topicWebsites) {
                for (int i = 0; i < 5; i++) {
                    TopicWebsite topicWebsite = new TopicWebsite();
    
                    topicWebsite.setWebsiteName(website.getWebsiteName());
                    topicWebsite.setIconUrl(website.getIconUrl());
                    topicWebsite.setHotValue((long) random.nextInt(6354147));
                    //设置时间
                    topicWebsite.setCreateTime(date);
    
                    topicWebsiteMapper.insert(topicWebsite);
                }
            }
        }
    
    }

    2、job的历史数据初始化器

    初始化器,主要是用于生成历史数据,用的是随机生成的过去30天内的时间,去new一个job。

    然后调用job的init,设置date;然后调用job的细节。

    上面我也说了,没必要搞两个,只是最初设计失误了。

    总体逻辑,就是传入日期,然后根据那个日期,去造假数据。

    package com..datavisual.quartz.init;
    
    /**
     * desc:
     * 用于造初始化数据
     * @author : 
     * creat_date: 2018/6/11 0011
     * creat_time: 14:29
     **/
    public interface Initer {
        /**
         * 具体的初始化逻辑,可参考
         * @return 成功或失败
         */
        Boolean init();
    }

    出问题的初始化器的源码:

    package com.ceiec.datavisual.quartz.init;
    
    import com.ceiec.datavisual.quartz.job.TopicWebsiteJob;
    import org.joda.time.DateTime;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    
    import java.util.Date;
    
    /**
     * desc:
     *
     * @author: 
     * creat_date: 2018/6/11 0011
     * creat_time: 14:28
     **/
    @Component
    public class TopicWebsiteIniter implements Initer {
        @Autowired
        private TopicWebsiteJob job;
    
        @Override
        public Boolean init() {
            DateTime now = DateTime.now();
            //日期循环,30天
            for (int a = -29; a < 1; a++) {
                for (int b = 0; b < 24; b++) {
                    int minutes = (int) (Math.random() * 60);
                    Date date = com.ceiec.datavisual.quartz.DateUtils.getNeedTime(b, minutes, 0, a);
                    if (a == 0 && date.after(now.toDate())) {
    
                    } else {
                        job.jobInit(date);
                        job.jobDetail(360);
                    }
                }
            }
    
            return true;
        }
    
    }

    3、目前为止,运行正常?

    到目前为止,运行没什么问题,因为我都是用单元测试的方式去调用上面的initer.init方法。

    真的吗?

    4、加上定时触发机制

    这些job,在上线后,还是需要继续运行。具体的间隔,是每十秒触发一次。

    code如下:

    package com..datavisual.quartz.schedule;
    
    import com..datavisual.quartz.job.TopicWebsiteJob;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.scheduling.annotation.Scheduled;
    import org.springframework.stereotype.Component;
    
    import java.util.Date;
    
    @Component
    public class TopicWebsiteScheduler implements DataProduceScheduler {
        private static final Logger logger = LoggerFactory.getLogger(TopicWebsiteScheduler.class);
    
        @Autowired
        private TopicWebsiteJob job;
      
        @Override
        @Scheduled(cron = "0/10 * * * * ?}")
        public Boolean schedule() {
            logger.info("start...");
            job.jobInit(new Date());
            job.jobDetail(1);
    
            return true;
        }
    
    }

    5、问题出来了

    就上面的代码,上线一运行,因为job比较多,说实话,也没注意一些细节,没去查看数据库的数据条数。

    我一直以为没啥问题,直到运行了没一会,程序假死了,卡着不动了。

    后来将堆转储拿出来分析,才发现,是因为每次init被多次调用了,每次调用都会从表里面查所有数据(一直以为只有10条真实数据)。

    然后根据这些数据,去生成新的假数据。再插回表里。这时候表里的数据,差不多翻倍了。

    再过10s后,再次查询,这次查到20条,然后,又造了20条假数据,写到表里,变成了40条。

    再过10s后,再次查询,这次查到40条,然后,又造了40条假数据,写到表里,变成了80条。

    。。。

    然后就越来越慢,越来越卡。。。直到发现表里竟然变成了千万条数据,然后将java程序的内存撑爆了。

    三、总结

    其实这次主要的坑,在于自己设计功力不够,没有考虑清楚。数据库的数据是变化的,而我拿变化的东西作为基准,来生成假数据,再将假数据写入到原表,造成了

    表里数据的指数级增长,然后撑爆了内存。

    抛开这块不说,比较有意思的是,查找这个bug背后原因的过程,后边单独写。

  • 相关阅读:
    制定并分享愿景 领导的艺术之一
    不要非黑即白,有些数据即使只有90%的准确,也是有用的
    双赢的思维考虑问题
    利用一切机会丰富自己的知识,利用一切机会调整自己的行为,为了达成目标而与他人合作,取得共赢 update by June 2012
    数据说话 说服别人
    对重要的事情,要很快做出反应
    You can if you think you can
    宽容的心态,开明的头脑
    DataGridView上下移动行及设置当前行
    sql 数据库、表
  • 原文地址:https://www.cnblogs.com/grey-wolf/p/9179113.html
Copyright © 2020-2023  润新知