Spring Boot ThreadPoolTaskExecutor утечка памяти - PullRequest
1 голос
/ 23 апреля 2020

У меня приложение Spring Boot, работающее на 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())
        }
    }
}

TaskExecutor:

@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: основной класс обновления

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;
    }
}

Проблема в том, что после fini sh ScheduledVehicleDataUpdate память НЕ очищается. Это выглядит так: enter image description here

Каждый шаг памяти растет, растет, растет, и в непредсказуемый момент вся память освобождается. И объекты из первой итерации, и объекты из последней итерации. В самом плохом случае это занимает всю доступную память (120 Гб) и происходит сбой Wildfly.

У меня около 3200 записей VehicleEntity (допустим, ровно 3200). Итак, я искал VehicleDataUpdater - сколько объектов в памяти. После первой итерации (когда я только запускал приложение) оно меньше 3200, но не ноль - может быть, около 3000-3100. И каждый шаг растет, но не точно на 3200 записей. Это означает, что некоторые объекты удаляются из памяти, но большинство из них остается там.

Далее: обычная продолжительность итерации составляет около 30 с c - 1 мин. Когда память не очищается и продолжает расти, каждая итерация получает все больше и больше времени: самое длинное, что я видел, было 30 минут. И потоки из пула в основном находятся в состоянии «мониторинга», то есть существуют некоторые блокировки, ожидающие освобождения. Возможно, блокирует от предыдущих итераций, которые не были освобождены - и снова вопрос - почему не была освобождена вся память на предыдущем шаге?

Если я выполняю обновление в одном потоке (без taskExecutor, просто vehicleList.foreach ( транспортное средство -> VehicleDataUpdater (vehicle)); ), чем я не видел увеличения памяти. После обновления очищается каждая память автомобиля.

Я не обнаружил никаких проблем с утечками памяти для ThreadPoolTaskExecutor или ThreadPoolTaskScheduler, поэтому я понятия не имею, как это исправить.

Какие возможные способы не очистка памяти после финишного sh задания планировщика? Как я могу посмотреть, кто блокирует объект после fini sh? Я использую VisualVM 2.0.1 и не нашел там таких возможностей.

РЕДАКТИРОВАТЬ 1:

VehicleService:

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()));
    }
}

VehicleDao:

@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();
    }
}

InitService:

@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);
    }
}

EntityManager:

@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;
}

1 Ответ

1 голос
/ 23 апреля 2020

Глядя на то, что вы пытаетесь достичь, в принципе, оптимальная пакетная обработка при использовании JPA. Однако вы пытаетесь использовать канон (многопоточность) вместо решения реальной проблемы. Для хорошего обзора я настоятельно рекомендую прочитать [этот пост] [1].

  1. Используйте обработку чанка и flu sh менеджер сущностей после x записей и затем очистите. Это не позволяет выполнять много грязных проверок в кэше первого уровня
  2. Включить пакетные операторы в спящем режиме, а также упорядочивать вставки и обновления

Прежде всего начните со свойств make убедитесь, что ваш 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, сделав его немного более ленивым (т.е. извлекайте данные только тогда, когда вам это нужно). Вы можете сделать это, нажав на hibernate и используя метод stream (по состоянию на Hibernate 5.2) или при использовании более старых версий сделайте немного больше работы и используйте ScrollableResult (см. . Есть ли способ прокрутить результаты с помощью JPA / спящий режим? ). Если вы уже используете 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();
}

В вашем коде вам необходимо изменить значение l oop для работы с потоком и сохранить счетчик самостоятельно и при этом гриппом sh периодически. Использование потока вряд ли улучшит производительность (может даже ухудшить ее), но будет использовать меньше памяти, чем при извлечении всех элементов одновременно. Поскольку у вас есть только столько объектов в памяти, сколько вы используете для размера пакета !.

@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();
              }
            });
            }
        }
    }

Нечто подобное должно сработать.

ПРИМЕЧАНИЕ: Я набрал код по ходу дела, он может не скомпилироваться из-за некоторых пропущенных скобок, импортирует et c.

...