대기열을 처리가 지연되는 이슈
원인
스케쥴러가 스레드가 1개여서 느리다고 판단
- SchedulingConfigurer로 Pool Size 10개로 증가.
- @Async로 비동기로 스케쥴러 실행
스케줄러 로직상 문제라고 판단.
- 레디스에서 큐에 담겨져있는 모든 사람들을 가져와서 반복문을 실행 → O(n)
- 그 후에 일치 한 키가 있는 경우 Rank를 가져와서 SSE에 Send → O(n) * O(logn)
해결
여기서 1번에서 10만명이 큐에 담겨져 있으면 부하가 엄청나게 발생하게 됩니다.
Rank를 바로 가져와서 SSE에 Send 할 수 있도록 개선
@Component
@RequiredArgsConstructor
public class ReservationScheduler {
private static final long delay = 1000L;
private final ReservationQueue reservationQueue;
private final EmitterService emitterService;
@Scheduled(fixedRate = delay)
public void reservationScheduler() {
// 입장처리할 사용자들
long joinSize = 50L;
Set<ReservationDto> enterQueue = reservationQueue.getQueue(joinSize - 1);
enterQueue.parallelStream().forEach(reservationDto -> {
emitterService.sendSeatListToClient(reservationDto);
reservationQueue.deleteQueue(reservationDto);
});
if (enterQueue.size() == joinSize) {
// 나머지 대기열 가져오기, 사용자에게 대기 순위 보내주기 -> 문제가 발생하는 로직
Set<ReservationDto> waitQueue = reservationQueue.getQueue(-1L);
waitQueue.parallelStream().forEach(reservationDto -> {
Long rank = reservationQueue.getRank(reservationDto) + 1;
emitterService.sendWaitNumberToClient(reservationDto, rank);
});
}
}
}
@Scheduled(fixedRate = 1000)
public void sendQueueNum() {
Map<String, SseEmitter> sseEmitters = emitterRepository.findAllEmitters();
sseEmitters.forEach((key, emitter) -> {
ReservationDto reservationDto = new ReservationDto(key.split("_")[0], Long.parseLong(key.split("_")[1]));
long rank = reservationQueue.getRank(reservationDto) + 1;
if (rank > 50) {
sendToClient(emitter, key, rank);
}
});
}
O(n) * O(logn)
sse에 연결되어 있는 사용자가 실제 대기 사용자과 같기 때문에 기존의 redis에서 조회해서 가져오는 로직에서 sseRepository에서 연결되어 있는 사용자를 가져와 redis에서 대기 순서를 조회해 send해주는 방식으로 변경했습니다.
대용량 데이터 생성 시, OutOfMemoryError 발생 되는 이슈
원인
- 행사장의 좌석의 수(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의 걸린 것을 확인.
- 하지만 100만건의 데이터를 전송했을 때, 데이터가 저장되었지만 Reponse(응답 값으로)로 Time-Out이 발생.
- 확인해본 결과 nginx connection의 Time-Out이 60s이기 때문에 postman으로 Reponse를 받을 때 Time-Out이 발생.
- Time-Out의 Default 값이 60s로 설정 되어 있어, 저장 되는데 문제는 없음.
'항해99' 카테고리의 다른 글
항해99 실전 프로젝트 (31일차) (0) | 2024.03.09 |
---|---|
항해99 실전 프로젝트 (30일차) (0) | 2024.03.05 |
항해99 실전 프로젝트 (28일차) (0) | 2024.03.02 |
항해99 실전 프로젝트 (27일차) (0) | 2024.03.01 |
항해99 실전 프로젝트 (26일차) (0) | 2024.02.28 |
대기열을 처리가 지연되는 이슈
원인
스케쥴러가 스레드가 1개여서 느리다고 판단
- SchedulingConfigurer로 Pool Size 10개로 증가.
- @Async로 비동기로 스케쥴러 실행
스케줄러 로직상 문제라고 판단.
- 레디스에서 큐에 담겨져있는 모든 사람들을 가져와서 반복문을 실행 → O(n)
- 그 후에 일치 한 키가 있는 경우 Rank를 가져와서 SSE에 Send → O(n) * O(logn)
해결
여기서 1번에서 10만명이 큐에 담겨져 있으면 부하가 엄청나게 발생하게 됩니다.
Rank를 바로 가져와서 SSE에 Send 할 수 있도록 개선
@Component
@RequiredArgsConstructor
public class ReservationScheduler {
private static final long delay = 1000L;
private final ReservationQueue reservationQueue;
private final EmitterService emitterService;
@Scheduled(fixedRate = delay)
public void reservationScheduler() {
// 입장처리할 사용자들
long joinSize = 50L;
Set<ReservationDto> enterQueue = reservationQueue.getQueue(joinSize - 1);
enterQueue.parallelStream().forEach(reservationDto -> {
emitterService.sendSeatListToClient(reservationDto);
reservationQueue.deleteQueue(reservationDto);
});
if (enterQueue.size() == joinSize) {
// 나머지 대기열 가져오기, 사용자에게 대기 순위 보내주기 -> 문제가 발생하는 로직
Set<ReservationDto> waitQueue = reservationQueue.getQueue(-1L);
waitQueue.parallelStream().forEach(reservationDto -> {
Long rank = reservationQueue.getRank(reservationDto) + 1;
emitterService.sendWaitNumberToClient(reservationDto, rank);
});
}
}
}
@Scheduled(fixedRate = 1000)
public void sendQueueNum() {
Map<String, SseEmitter> sseEmitters = emitterRepository.findAllEmitters();
sseEmitters.forEach((key, emitter) -> {
ReservationDto reservationDto = new ReservationDto(key.split("_")[0], Long.parseLong(key.split("_")[1]));
long rank = reservationQueue.getRank(reservationDto) + 1;
if (rank > 50) {
sendToClient(emitter, key, rank);
}
});
}
O(n) * O(logn)
sse에 연결되어 있는 사용자가 실제 대기 사용자과 같기 때문에 기존의 redis에서 조회해서 가져오는 로직에서 sseRepository에서 연결되어 있는 사용자를 가져와 redis에서 대기 순서를 조회해 send해주는 방식으로 변경했습니다.
대용량 데이터 생성 시, OutOfMemoryError 발생 되는 이슈
원인
- 행사장의 좌석의 수(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의 걸린 것을 확인.
- 하지만 100만건의 데이터를 전송했을 때, 데이터가 저장되었지만 Reponse(응답 값으로)로 Time-Out이 발생.
- 확인해본 결과 nginx connection의 Time-Out이 60s이기 때문에 postman으로 Reponse를 받을 때 Time-Out이 발생.
- Time-Out의 Default 값이 60s로 설정 되어 있어, 저장 되는데 문제는 없음.
'항해99' 카테고리의 다른 글
항해99 실전 프로젝트 (31일차) (0) | 2024.03.09 |
---|---|
항해99 실전 프로젝트 (30일차) (0) | 2024.03.05 |
항해99 실전 프로젝트 (28일차) (0) | 2024.03.02 |
항해99 실전 프로젝트 (27일차) (0) | 2024.03.01 |
항해99 실전 프로젝트 (26일차) (0) | 2024.02.28 |