Kebocoran memori Spring Boot ThreadPoolTaskExecutor

Saya menjalankan aplikasi Spring Boot di Wildfly 18.0.1. Tujuan utama dari aplikasi ini adalah: setiap 5 menit menjalankan beberapa pekerjaan. Jadi saya membuat:

Penjadwal Tugas: menginisialisasi penjadwal

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

ScheduledVehicleDataUpdate: penjadwal yang menjalankan pembaru

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

Pelaksana Tugas:

@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: kelas pembaru utama

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

Masalahnya adalah setelah ScheduledVehicleDataUpdate selesai, memori TIDAK terhapus. Tampilannya seperti ini: masukkan deskripsi gambar di sini

Setiap langkah memori tumbuh, berkembang, berkembang dan pada saat yang tidak terduga semua memori dilepaskan. Dan objek dari iterasi pertama, dan objek dari iterasi terakhir. Dalam kasus yang paling buruk, dibutuhkan semua memori yang tersedia (120Gb) dan Wildfly mogok.

Saya memiliki sekitar 3200 catatan VehicleEntity (anggap saja 3200). Jadi saya mencari VehicleDataUpdater - berapa banyak objek yang ada di memori. Setelah iterasi pertama (saat saya baru memulai aplikasi) nilainya kurang dari 3200 tetapi tidak nol - mungkin sekitar 3000-3100. Dan setiap langkahnya bertambah tetapi tidak persis pada 3200 catatan. Artinya, beberapa objek terhapus dari memori namun sebagian besar tetap berada di sana.

Berikutnya: durasi iterasi normal adalah sekitar 30 detik - 1 menit. Ketika memori tidak jernih dan terus bertambah maka setiap iterasi mendapatkan lebih banyak waktu: waktu terlama yang saya lihat adalah 30 menit. Dan thread dari pool sebagian besar berada dalam status "monitor", yaitu ada beberapa kunci yang menunggu untuk dilepaskan. Mungkin mengunci dari iterasi sebelumnya yang tidak dirilis - dan bertanya lagi - mengapa semua memori tidak dikosongkan pada langkah sebelumnya?

Jika saya menjalankan pembaruan dalam satu utas (tanpa taskExecutor, cukup vehicleList.foreach(vehicle -> VehicleDataUpdater(vehicle)); ) maka saya tidak melihat memori bertambah. Setelah pembaruan, setiap memori kendaraan dihapus.

Saya tidak menemukan masalah kebocoran memori untuk ThreadPoolTaskExecutor atau ThreadPoolTaskScheduler, jadi saya tidak tahu cara memperbaikinya.

Apa cara yang mungkin untuk tidak mengosongkan memori setelah menyelesaikan tugas penjadwal? Bagaimana saya bisa melihat siapa yang mengunci objek setelah selesai? Saya menggunakan VisualVM 2.0.1 dan tidak menemukan kemungkinan seperti itu.

EDIT 1:

Layanan Kendaraan:

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

Dao Kendaraan:

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

Layanan Init:

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

Manajer Entitas:

@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 sumber
comment
VehicleService.getInstance().getList() .. menurut saya Anda harus memasukkan VehicleService dengan @autowire   -  person Gewure    schedule 23.04.2020
comment
Apa yang sebenarnya dilakukan VehicleService.getInstance().getList()? Anda juga mungkin harus memperbarui/membaca sesuatu dalam potongan/malas, bukan daftar. Dengan asumsi Anda menggunakan sesuatu seperti JPA, Anda mungkin memiliki masalah lain dengan entitas yang terpisah. Secara keseluruhan, tidak ada cukup informasi dalam pertanyaan Anda untuk menjawabnya.   -  person M. Deinum    schedule 23.04.2020
comment
@Gewure ini sedikit lebih rumit... menambahkan kode dengan layanan, dao, dll.   -  person zhoriq    schedule 23.04.2020
comment
@ M.Deinum bagaimana saya bisa melakukannya dengan potongan? Saya juga tidak melihat masalah dengan daftarnya karena tidak terlalu besar. 3000 catatan sepertinya tidak terlalu banyak bagi saya. Ya, saya menggunakan JPA dan saya memperbarui pertanyaan dengan kodenya. Saya sedang memikirkan masalah memori EntityManager jadi saya secara khusus memisahkan entitas dari EM ke kode utama dengan return new ArrayList‹›(...) di VehicleService.   -  person zhoriq    schedule 23.04.2020
comment
Membuat daftar baru tidak akan membantu, daftar tersebut tetap merupakan entitas yang dikelola. Ini akan memuat 3000 sekaligus dalam memori (dan sekarang menjadi 3000, bagaimana dengan 30000) dan juga mempertimbangkan bahwa tidak ada proses yang berjalan sendiri. ScheduledVehicleDataUpdate Anda harus berupa kacang yang dikelola pegas dengan @Scheduled sehingga Spring memasukkan dependensi dan menggunakan @Scheduled untuk menjadwalkan berbagai hal. Anda harus bekerja DENGAN kerangka kerja yang sedang Anda kerjakan. Mengenai potongannya, itu mungkin tidak sepenuhnya berfungsi dengan solusi Anda saat ini. Apakah Anda juga memerlukan semua tugas kecil itu? Mengapa tidak melakukan pembaruan secara berurutan saja?   -  person M. Deinum    schedule 23.04.2020
comment
Semuanya terlihat seperti mencoba mengoptimalkan sesuatu yang tidak perlu dioptimalkan dan hanya membuat segalanya menjadi terlalu rumit.   -  person M. Deinum    schedule 23.04.2020
comment
@M.Deinum ya, membuat daftar baru tidak masuk akal, saya setuju. Saya telah menggunakan penjadwal yang kompleks karena saya memuat nilai penundaan dari file properti yang dapat diubah saat itu juga. Mungkin penjadwal saya tidak optimal tetapi tidak mempengaruhi kebocoran memori. Atau bisa? Pembaruan berurutan akan memakan waktu lebih lama (sekarang 23 thread = 0,5 menit, dengan 1 thread = kira-kira 11,5 menit)   -  person zhoriq    schedule 23.04.2020
comment
saya kehilangan layanan @ di atas VehicleService Anda. Saya melihat Anda melakukan pola Singleton di sana, tetapi itu tidak membuat perbedaan: bahkan layanan Singleton disuntikkan menggunakan @ Autowire dan @ Service --- Spring memastikan bahwa sebenarnya hanya SATU contoh yang berjalan. lihat: stackoverflow.com/questions/2173006/ jadi menurut saya ini merupakan campuran dari pembuatan instance secara manual, siklus hidup pegas, dan multithreading, yang menyebabkan kebocoran - sulit untuk mengetahui apa yang sebenarnya bocor, tetapi selama Anda tidak menggunakan Constructor-Injection, @ Service, dan @ autowire ..!   -  person Gewure    schedule 23.04.2020
comment
Saat melakukan 1 thread, lakukan pemrosesan potongan, (yaitu siram setelah x record dan hapus cache). Baca streaming alih-alih daftar lengkap. Ini mungkin masih sedikit lebih lambat tetapi lebih mudah untuk dipertahankan. Masalah utama dengan satu thread adalah pemeriksaan JPA yang kotor yang menjadi hambatan (karenanya flush dan clear setelah catatan x). Anda juga tidak dapat mengubah apa pun di sini setelah startup, Anda dapat menggunakan hal yang sama dengan @Scheduled dan membaca dari file properti, jadi tidak ada yang menghalangi Anda melakukan desain yang benar.   -  person M. Deinum    schedule 23.04.2020


Jawaban (1)


Melihat apa yang ingin Anda capai pada dasarnya adalah pemrosesan batch yang optimal saat menggunakan JPA. Namun Anda mencoba menggunakan kanon (multi-threading) alih-alih menyelesaikan masalah sebenarnya. Untuk ikhtisar yang bagus, saya sangat menyarankan untuk membaca [entri blog ini] [1].

  1. Gunakan pemrosesan potongan dan bersihkan manajer entitas setelah x catatan lalu hapus. Ini mencegah Anda melakukan banyak pemeriksaan kotor di cache tingkat pertama
  2. Aktifkan pernyataan Batch saat hibernasi serta pemesanan sisipan dan pembaruan

Pertama-tama mulailah dengan properti, pastikan hibernateProperties Anda berisi yang berikut ini

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

Kemudian tulis ulang ScheduledVehicleDataUpdate Anda untuk memanfaatkan ini dan siram/hapus manajer entitas secara berkala.

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

Sekarang Anda juga dapat mengurangi konsumsi memori getList dengan membuatnya sedikit lebih malas (yaitu hanya mengambil data saat Anda membutuhkannya). Anda dapat melakukan ini dengan memasuki hibernasi dan menggunakan metode stream (mulai Hibernate 5.2) atau ketika menggunakan versi yang lebih lama, lakukan lebih banyak pekerjaan dan gunakan ScrollableResult (lihat Apakah ada cara untuk menggulir hasil dengan JPA/hibernate?). Jika Anda sudah menggunakan JPA 2.2 (yaitu Hibernate 5.3), Anda dapat menggunakan getResultStream secara langsung.

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

atau dengan JPA 2.2

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

Dalam kode Anda, Anda perlu mengubah loop for agar berfungsi dengan aliran, dan menyimpan penghitung sendiri dan tetap melakukan flush secara berkala. Menggunakan aliran tidak akan meningkatkan kinerja (bahkan mungkin menurunkannya) tetapi akan menggunakan lebih sedikit memori dibandingkan saat mengambil semua elemen sekaligus. Karena Anda hanya memiliki objek di memori sebanyak yang Anda gunakan untuk ukuran batch!.

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

Sesuatu seperti ini seharusnya berhasil.

CATATAN: Saya mengetik kode sambil melanjutkan, ini mungkin tidak dapat dikompilasi karena beberapa tanda kurung hilang, impor, dll.

person M. Deinum    schedule 23.04.2020