Утечка памяти Spring Boot ThreadPoolTaskExecutor

У меня есть приложение 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())
        }
    }
}

Исполнитель задач:

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

Проблема в том, что после завершения ScheduledVehicleDataUpdate память НЕ очищается. Это выглядит так: введите здесь описание изображения

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

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

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

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

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

Какие возможные способы не очищать память после завершения задачи планировщика? Как я могу посмотреть, кто блокирует объект после завершения? Я использую 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()));
    }
}

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

ИнитСервис:

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

person zhoriq    schedule 22.04.2020    source источник
comment
VehicleService.getInstance().getList() .. я думаю, вам следует внедрить VehicleService с помощью @autowire   -  person Gewure    schedule 23.04.2020
comment
Что на самом деле делает VehicleService.getInstance().getList()? Также вам, вероятно, следует обновлять/читать информацию по частям/лениво вместо списков. Предполагая, что вы используете что-то вроде JPA, у вас может возникнуть другая проблема с отсоединенными объектами. В общем, в вашем вопросе недостаточно информации для ответа на него.   -  person M. Deinum    schedule 23.04.2020
comment
@Gewure, это немного сложнее ... добавлен код с сервисом, дао и т. Д.   -  person zhoriq    schedule 23.04.2020
comment
@ M.Deinum, как я могу сделать это с кусками? Также я не видел проблем со списком, потому что он не такой большой. 3000 записей для меня не слишком много. Да, я использую JPA и обновил вопрос с его кодом. Я думал о проблеме с памятью EntityManager, поэтому я специально отделил сущности от EM к основному коду с помощью return new ArrayList‹›(...) в VehicleService.   -  person zhoriq    schedule 23.04.2020
comment
Создание нового списка не поможет, это все еще управляемые объекты. Он загрузит сразу все 3000 в память (а сейчас это 3000, что насчет 30000) и также учтет, что no-process работает в одиночку. Ваш ScheduledVehicleDataUpdate должен быть управляемым bean-компонентом spring с @Scheduled, чтобы Spring внедрял зависимости и использовал @Scheduled для планирования. Вы должны работать с фреймворком, над которым вы сейчас работаете. Что касается фрагментов, это может не полностью работать с вашим текущим решением. Вам также нужны все эти мелкие задачи? Почему бы просто не делать обновления последовательно?   -  person M. Deinum    schedule 23.04.2020
comment
Все это очень похоже на попытку оптимизировать что-то, что не нуждается в оптимизации и только делает вещи чрезмерно сложными.   -  person M. Deinum    schedule 23.04.2020
comment
@M.Deinum M.Deinum да, создавать новый список не имеет смысла, я согласен. Я использовал сложный планировщик, потому что я загружаю значение задержки из файла свойств, которое можно изменить на лету. Возможно, мой планировщик не оптимален, но это не может повлиять на утечки памяти. Или может? Последовательное обновление займет больше времени (сейчас 23 потока = 0,5 мин, 1 поток = примерно 11,5 мин)   -  person zhoriq    schedule 23.04.2020
comment
мне не хватает службы @ над вашим VehicleService. Я вижу, вы сделали там шаблон Singleton, но на самом деле это не имеет значения: даже службы Singleton внедряются с использованием @ Autowire и @ Service --- Spring гарантирует, что на самом деле работает только ОДИН экземпляр. см.: stackoverflow.com/questions/2173006/ поэтому я думаю, что это немного смесь создания экземпляров вручную, жизненного цикла Spring и многопоточности, что приводит к утечкам - трудно сказать, что именно протекает, но пока вы не используете Constructor-Injection, @ Service и @ autowire ..!   -  person Gewure    schedule 23.04.2020
comment
При выполнении 1 потока выполняйте обработку фрагментов (т.е. очищайте после x записей и очищайте кеш). Читайте потоковое вместо полного списка. Это все еще может быть немного медленнее, но проще в обслуживании. Основная проблема с одним потоком — грязная проверка JPA, которая становится узким местом (отсюда сброс и очистка после x записей). Также вы не можете ничего изменить здесь после запуска, вы можете использовать то же самое с @Scheduled и читать из файла свойств, поэтому ничто не мешает вам сделать правильный дизайн.   -  person M. Deinum    schedule 23.04.2020


Ответы (1)


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

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

Прежде всего, начните со свойств, убедитесь, что ваш hibernateProperties содержит следующее

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

Затем перепишите свой ScheduledVehicleDataUpdate, чтобы воспользоваться этим и периодически сбрасывать/очищать entitymanager.

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

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

Что-то вроде этого должно помочь.

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

person M. Deinum    schedule 23.04.2020