트러블슈팅

대용량 데이터 생성 후 저장, OutOfMemoryError

계양 꿀주먹 2024. 3. 15. 07:26

원인

  • 행사장의 좌석의 수(3,000,000)만큼 데이터 생성 시 , java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "http-nio-5000-Acceptor” 가 발생.

[ 데이터 생성 코드 ]

@Transactional
	public void createEventTimes(EventTimesRequestDto eventTimesRequestDto, Long eventId) {
		Event event = eventRepository.findById(eventId)
			.orElseThrow(() -> new GlobalException(HttpStatus.NOT_FOUND, ExceptionCode.NOT_EXIST_EVENT));
		EventTimes eventTimes = new EventTimes(eventTimesRequestDto, event);
		eventTimesRepository.save(eventTimes);

		List<Seat> seatList = seatRepository.findByStageId(event.getStage().getId());
		List<ElasticSearchReservation> elasticSearchReservationList = new ArrayList<>();

		for (Seat seat : seatList) {
			for (int j = 1; j <= seat.getSeatCapacity(); j++) {
				String uuid = UUID.randomUUID().toString();
				ElasticSearchReservation elasticSearchReservation = new ElasticSearchReservation(uuid, seat.getId(),
					event.getId(), eventTimes.getId(), j);
				elasticSearchReservationList.add(elasticSearchReservation);
			}
		}
		elasticSearchReservationRepository.saveAll(elasticSearchReservationList);
	}

분석

  • 2만건의 좌석을 생성 시, 에러가 발생하지 않음.
  • 원인은 elasticSearchReservationList에 대량의 ElasticSearchReservation 객체를 한 번에 추가하고, 이를 한 번에 저장하려고 하기 때문에 Heap메모리에서 에러가 발생되는 것으로 분석.

해결

[ 배치 처리 방식을 이용한 코드 ]

@Transactional
public void createEventTimes(EventTimesRequestDto eventTimesRequestDto, Long eventId) {
    Event event = eventRepository.findById(eventId)
        .orElseThrow(() -> new GlobalException(HttpStatus.NOT_FOUND, ExceptionCode.NOT_EXIST_EVENT));
    EventTimes eventTimes = new EventTimes(eventTimesRequestDto, event);
    eventTimesRepository.save(eventTimes);

    List<Seat> seatList = seatRepository.findByStageId(event.getStage().getId());
    List<ElasticSearchReservation> elasticSearchReservationList = new ArrayList<>();
    int batchSize = 10000;

    for (Seat seat : seatList) {
        for (int j = 1; j <= seat.getSeatCapacity(); j++) {
            String uuid = UUID.randomUUID().toString();
            ElasticSearchReservation elasticSearchReservation = new ElasticSearchReservation(uuid, seat.getId(),
                event.getId(), eventTimes.getId(), j);
            elasticSearchReservationList.add(elasticSearchReservation);

            // If the list has reached the batch size, save and clear it
            if (elasticSearchReservationList.size() >= batchSize) {
                elasticSearchReservationRepository.saveAll(elasticSearchReservationList);
                elasticSearchReservationList.clear();
            }
        }
    }

    // Save any remaining items in the list
    if (!elasticSearchReservationList.isEmpty()) {
        elasticSearchReservationRepository.saveAll(elasticSearchReservationList);
    }
}
  • 배치 처리 방식을 사용하여 데이터를 일정 크기의 작은 덩어리로 나눠서 전송하여 각각의 작은 배치가 전송되고 clear()를 하여 메모리 부담 줄임.
  • 부분 코드 추가.

[ CPU 그래프 ]

  • 빨간박스 안에 있는 그래프는 한 번에 데이터를 전송 했을 때, 파란색박스 안에 있는 그래프는 배치를 사용하여 데이터를 전송 했을 때 그래프.
  • 사용량이 두 배정도 줄인 것을 확인.
  • 흰색박스는 10만건, 50만건의 데이터를 전송해본 결과로, 각각 8s, 37s의 걸린 것을 확인.