繁体   English   中英

Spring 启动线程池任务执行器 memory 泄漏

[英]Spring Boot ThreadPoolTaskExecutor memory leak

我有 Spring 在 Wildfly 18.0.1 上运行的启动应用程序。 该应用程序的主要目的是:每 5 分钟运行一次作业。 所以我做:

TaskScheduler:初始化调度器

@Autowired
ThreadPoolTaskScheduler taskScheduler;
taskScheduler.scheduleWithFixedDelay(new ScheduledVehicleDataUpdate(), 300000);

ScheduledVehicleDataUpdate:运行更新程序的调度程序

public class ScheduledVehicleDataUpdate implements Runnable {
    @Autowired
    TaskExecutor taskExecutor;

    @Override
    public void run() {
        try {
            CountDownLatch countDownLatch;
            List<VehicleEntity> vehicleList = VehicleService.getInstance().getList();
            if (vehicleList.size() > 0) {
                countDownLatch = new CountDownLatch(vehiclesList.size());
                vehicleList.forEach(vehicle -> taskExecutor.execute(new VehicleDataUpdater(vehicle, countDownLatch)));
                countDownLatch.await();
            }
        }
        catch (InterruptedException | RuntimeException e) {
            System.out.println(e.getMessage())
        }
    }
}

任务执行器:

@Bean
public TaskExecutor taskExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setCorePoolSize(23);
    executor.setMaxPoolSize(23);
    executor.setQueueCapacity(5000);
    executor.setThreadNamePrefix("VehicleService_updater_thread");
    executor.initialize();
    return executor;
}

VehicleDataUpdater:主更新程序 class

public class VehicleDataUpdater implements Runnable {
    private final VehicleEntity vehicle;
    private final CountDownLatch countDownLatch;

    public VehicleDataUpdater(VehicleEntity vehicle, CountDownLatch countDownLatch) {
        this.vehicle = vehicle;
        this.countDownLatch = countDownLatch;
    }

    @Override
    public void run() {    
        try {
            this.updateVehicleData();
        }
        catch (Exception e) {
            System.out.println(e.getMessage());
        }
        finally {
            countDownLatch.countDown();
        }
    }

    public void updateVehicleData() {
        // DO UPDATE ACTIONS;
    }
}

问题是完成ScheduledVehicleDataUpdate后 memory 没有清除。 它看起来像这样: 在此处输入图像描述

memory 的每一步都在增长、增长、增长,在不可预测的时刻,所有 memory 都被释放。 以及来自第一次迭代的对象,以及来自最后一次迭代的 object。 在最坏的情况下,它会占用所有可用的 memory (120Gb) 和 Wildfly 崩溃。

我有大约 3200 条 VehicleEntity 记录(假设正好是 3200 条)。 所以我寻找了 VehicleDataUpdater - memory 中有多少对象。 第一次迭代后(当我只启动应用程序时)它小于 3200 但不为零 - 可能约为 3000-3100。 它的每一步都在增长,但并不完全是 3200 条记录。 这意味着一些对象从 memory 中清除,但其中大部分仍然存在。

Next:正常的迭代持续时间约为 30sec - 1min。 当 memory 没有清理并继续增长时,每次迭代都会获得越来越多的时间:我看到的最长的是 30 分钟。 池中的线程大多在“监视器”state 中,即有一些锁等待释放。 可能是之前未释放的迭代中的锁定 - 并再次质疑 - 为什么所有 memory 都没有在上一步释放?

如果我在一个线程中执行更新(没有 taskExecutor,只需vehicleList.foreach(vehicle -> VehicleDataUpdater(vehicle)); ),那么我没有看到任何 memory 增长。 更新后每辆车 memory 被清除。

我没有发现 ThreadPoolTaskExecutor 或 ThreadPoolTaskScheduler 的 memory 泄漏有任何问题,所以我不知道如何解决它。

完成调度程序任务后不清除 memory 的可能方法是什么? 完成后如何查看谁在锁定 object? 我正在使用 VisualVM 2.0.1 并没有发现这样的可能性。

编辑1:

车辆服务:

public class VehicleService {
    private static VehicleService instance = null;
    private VehicleDao dao;

    public static VehicleService getInstance(){
        if (instance == null) {
            instance = new VehicleService();
        }
        return instance;
    }

    private VehicleService(){}

    public void setDao(VehicleDao vehicleDao) { this.dao = vehicleDao; }

    public List<VehicleEntity> list() {
        return new ArrayList<>(this.dao.list(LocalDateTime.now()));
    }
}

车道:

@Repository
public class VehicleDao {
    @PersistenceContext(unitName = "entityManager")
    private EntityManager entityManager;

    @Transactional("transactionManager")
    public List<VehicleRegisterEntity> list(LocalDateTime dtPeriod) {
        return this.entityManager.createQuery("SOME_QUERY", VehicleEntity.class).getResultList();
    }
}

初始化服务:

@Service
public class InitHibernateService {
    private final VehicleDao vehicleDao;

    @Autowired
    public InitHibernateService(VehicleDao vehicleDao){
        this.vehicleDao = vehicleDao;
    }

    @PostConstruct
    private void setDao() {
        VehicleService.getInstance().setDao(this.vehicleDao);
    }
}

实体管理器:

@Bean(name = "entityManager")
@DependsOn("dataSource")
public LocalContainerEntityManagerFactoryBean entityManagerFactory() throws NamingException {
    LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
    em.setPersistenceProviderClass(HibernatePersistenceProvider.class);
    em.setDataSource(dataSource());
    em.setPackagesToScan("MY_PACKAGE");
    em.setJpaVendorAdapter(vendorAdapter());
    em.setJpaProperties(hibernateProperties());
    em.setPersistenceUnitName("customEntityManager");
    em.setJpaDialect(new CustomHibernateJpaDialect());
    return em;
}

在使用 JPA 时,查看您想要实现的基本上是最佳批处理。 但是,您正在尝试使用规范(多线程)而不是解决实际问题。 对于一个很好的概述,我强烈建议阅读 [this blog post][1]。

  1. 使用块处理并在 x 记录后刷新实体管理器,然后清除。 这可以防止您在一级缓存中进行大量脏检查
  2. 在 hibernate 上启用批处理语句以及订购插入和更新

首先从属性开始,确保您的hibernateProperties包含以下内容

hibernate.jdbc.batch_size=25
hibernate.order_inserts=true
hibernate.order_updates=true

然后重写您的ScheduledVehicleDataUpdate以利用这一点并定期刷新/清除实体管理器。

@Component
public class ScheduledVehicleDataUpdate {
    @PersistenceContext
    private EntityManager em;

    @Scheduled(fixedDelayString="${your-delay-property-here}")
    @Transactional
    public void run() {
        try {
            List<VehicleEntity> vehicleList = getList();
            for (int i = 0 ; i < vehicleList.size() ; i++) {
              updateVehicle(vehicleList.get(i));
              if ( (i % 25) == 0) {
                em.flush();
                em.clear();
              }
            }
        }
    }

    private void updateVehicle(Vehicle vehicle) {
       // Your updates here
    }

    private List<VehicleEntity> getList() {
        return this.entityManager.createQuery("SOME_QUERY", VehicleEntity.class).getResultList();
    }
}

现在,您还可以通过使getList更懒一些(即仅在需要时检索数据)来减少 getList 的 memory 消耗。 您可以通过点击 hibernate 并使用stream方法(从 Hibernate 5.2 开始)或使用旧版本时(做更多工作并使用ScrollableResult 滚动查看结果 如果您已经使用 JPA 2.2(即 Hibernate 5.3),您可以直接使用getResultStream

private Stream<VehicleEntity> getList() {
  Query q = this.entityManager.createQuery("SOME_QUERY", VehicleEntity.class);
  org.hibernate.query.Query hq = q.unwrap(org.hibernate.query.Query.class);
  return hq.stream();
}

或与 JPA 2.2

private Stream<VehicleEntity> getList() {
  Query q = this.entityManager.createQuery("SOME_QUERY", VehicleEntity.class);
  return q.getResultStream();
}

在您的代码中,您需要更改 for 循环以使用 stream,并自己保留一个计数器并仍然定期刷新。 使用 stream 不太可能提高性能(甚至可能会降低性能),但在一次检索所有元素时会使用更少的 memory。 因为您在内存中只有与批量大小一样多的对象..

@Scheduled(fixedDelayString="${your-delay-property-here}")
    @Transactional
    public void run() {
        try {
            Stream<VehicleEntity> vehicles = getList();
            LongAdder counter = new LongAdder();
            vehicles.forEach(it -> {
              counter.increment();
              updateVehicle(it);
              if ( (counter.longValue() % 25) == 0) {
                em.flush();
                em.clear();
              }
            });
            }
        }
    }

像这样的东西应该可以解决问题。

注意:我输入了代码,这可能由于缺少括号、导入等而无法编译。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM