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 ขัดข้อง

ฉันมีบันทึก VehicleEntity ประมาณ 3200 รายการ (สมมติว่าเป็น 3200 พอดี) ดังนั้นฉันจึงค้นหา VehicleDataUpdater - มีวัตถุอยู่ในหน่วยความจำจำนวนเท่าใด หลังจากการวนซ้ำครั้งแรก (เมื่อฉันเพิ่งเริ่มแอป) มันน้อยกว่า 3200 แต่ไม่ใช่ศูนย์ - อาจจะประมาณ 3,000-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()));
    }
}

ยานพาหนะดาว:

@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 มันซับซ้อนกว่านิดหน่อย ... เพิ่มโค้ดพร้อมบริการ dao ฯลฯ   -  person zhoriq    schedule 23.04.2020
comment
@ M.Deinum ฉันจะทำมันกับชิ้นได้อย่างไร? นอกจากนี้ฉันไม่เห็นปัญหากับรายการเพราะมันไม่ใหญ่มาก 3000 บันทึกดูเหมือนจะไม่มากเกินไปสำหรับฉัน ใช่ ฉันใช้ JPA และอัปเดตคำถามด้วยรหัสของมัน ฉันกำลังคิดถึงปัญหาหน่วยความจำ EntityManager ดังนั้นฉันจึงแยกเอนทิตีพิเศษจาก EM ไปยังโค้ดหลักด้วย return new ArrayList‹›(...) ใน VehicleService   -  person zhoriq    schedule 23.04.2020
comment
การสร้างรายการใหม่ไม่ได้ช่วยอะไร แต่ยังคงเป็นเอนทิตีที่ได้รับการจัดการ มันจะโหลดทั้งหมด 3,000 รายการพร้อมกันในหน่วยความจำ (และตอนนี้คือ 3,000 หรือประมาณ 30,000) และยังพิจารณาว่าไม่มีกระบวนการทำงานเพียงอย่างเดียว ScheduledVehicleDataUpdate ของคุณควรเป็น bean ที่จัดการด้วยสปริงโดยมี @Scheduled เพื่อให้ Spring ฉีดการขึ้นต่อกันและใช้ @Scheduled เพื่อกำหนดเวลาสิ่งต่าง ๆ คุณควรทำงานกับกรอบงานที่คุณกำลังทำงานอยู่ เกี่ยวกับชิ้นส่วนต่างๆ นั่นอาจไม่ได้ผลทั้งหมดกับโซลูชันปัจจุบันของคุณ คุณต้องการงานเล็กๆ เหล่านั้นทั้งหมดด้วยหรือเปล่า? ทำไมไม่ทำการอัพเดตตามลำดับล่ะ?   -  person M. Deinum    schedule 23.04.2020
comment
ดูเหมือนทุกอย่างจะเหมือนกับการพยายามปรับบางสิ่งให้เหมาะสมซึ่งไม่จำเป็นต้องปรับให้เหมาะสมและมีแต่ทำให้สิ่งต่างๆ ซับซ้อนเกินไปเท่านั้น   -  person M. Deinum    schedule 23.04.2020
comment
@ M.Deinum ใช่ การสร้างรายการใหม่ไม่สมเหตุสมผล ฉันเห็นด้วย ฉันใช้ตัวกำหนดเวลาที่ซับซ้อนเนื่องจากฉันกำลังโหลดค่าความล่าช้าจากไฟล์คุณสมบัติที่สามารถเปลี่ยนแปลงได้ทันที ตัวกำหนดเวลาของฉันอาจไม่เหมาะสมแต่ไม่สามารถส่งผลต่อหน่วยความจำรั่วได้ หรือสามารถ? การอัปเดตตามลำดับจะใช้เวลานานกว่า (ตอนนี้เป็น 23 เธรด = 0.5 นาที โดยมี 1 เธรด = ประมาณ 11.5 นาที)   -  person zhoriq    schedule 23.04.2020
comment
ฉันขาดบริการ @ เหนือบริการยานพาหนะของคุณ ฉันเห็นว่าคุณทำรูปแบบ Singleton ที่นั่น แต่นั่นไม่ได้สร้างความแตกต่างเลย: แม้แต่บริการ Singleton ก็ถูกฉีดโดยใช้ @ Autowire และ @ Service --- Spring ทำให้แน่ใจว่าจริง ๆ แล้วมีเพียงอินสแตนซ์เดียวเท่านั้นที่ทำงาน ดู: stackoverflow.com/questions/2173006/ ดังนั้นฉันคิดว่ามันเป็นการผสมผสานระหว่างการสร้างอินสแตนซ์ด้วยตนเอง วงจรชีวิตของสปริง และมัลติเธรด ซึ่งนำไปสู่การรั่วไหล - ยากที่จะบอกได้ว่ามีอะไรรั่วอย่างแน่นอน แต่ตราบใดที่คุณไม่ได้ใช้ 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 อย่างไรก็ตามคุณกำลังพยายามใช้ Canon (มัลติเธรด) แทนที่จะแก้ไขปัญหาจริง เพื่อภาพรวมที่ดี ฉันขอแนะนำให้อ่าน [โพสต์บล็อกนี้] [1]

  1. ใช้การประมวลผลก้อนและล้างตัวจัดการเอนทิตีหลังจากบันทึก x แล้วล้างข้อมูล สิ่งนี้จะป้องกันไม่ให้คุณทำการตรวจสอบสกปรกจำนวนมากในแคชระดับแรก
  2. เปิดใช้งานคำสั่ง Batch ในไฮเบอร์เนตตลอดจนการสั่งซื้อส่วนแทรกและการอัปเดต

ก่อนอื่นให้เริ่มต้นด้วยคุณสมบัติ ตรวจสอบให้แน่ใจว่า 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 ได้ด้วยการทำให้ขี้เกียจขึ้นอีกเล็กน้อย (เช่น ดึงข้อมูลเมื่อคุณต้องการเท่านั้น) คุณสามารถทำได้โดยการแตะเข้าสู่โหมดไฮเบอร์เนตและใช้วิธี 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 loop เพื่อทำงานกับสตรีม และคอยนับตัวเองและยังคงล้างข้อมูลเป็นระยะ การใช้สตรีมไม่น่าจะช่วยปรับปรุงประสิทธิภาพได้ (อาจทำให้ประสิทธิภาพลดลงด้วยซ้ำ) แต่จะใช้หน่วยความจำน้อยกว่าเมื่อดึงองค์ประกอบทั้งหมดในครั้งเดียว เนื่องจากคุณมีวัตถุในหน่วยความจำมากเท่ากับที่คุณใช้สำหรับขนาดแบตช์!

@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