문득 궁금해졌다.
보통 애플리케이션에서 쓰레드 풀 설정을 할것이다.
그런데 설정 값 이상의 트래픽이 몰리고, 쓰레드에서 지연이 발생한다면 어떤 현상이 발생할까?
요청이 유실될까? 아니면 지연저장될까? 내부적으로 어떻게 처리되지? 너무나도 궁금해졌다. 테스트해보자
우리의 최대 쓰레드 개수는 30개 이며, 작업 큐 크기는 10개이다.
쓰레드 작업에 20초 지연시간이 걸리도록 했다.
최초 1000개의 트래픽 요청에서 시작해서 1초마다 500씩 증가하는 트래픽 부하를 걸어봤다
@EnableAsync
@Configuration
//@ConditionalOnProperty(name = "async.enabled", havingValue = "true", matchIfMissing = true)
public class AsyncConfig {
@Bean(name = "threadPoolTaskExecutor")
public Executor threadPoolTaskExecutor() {
ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
threadPoolTaskExecutor.setCorePoolSize(3); // 기본 스레드 수
threadPoolTaskExecutor.setMaxPoolSize(30); // 최대 스레드 수
threadPoolTaskExecutor.setQueueCapacity(10); // Queue 수
threadPoolTaskExecutor.setThreadNamePrefix("Executor-");
threadPoolTaskExecutor.initialize();
return threadPoolTaskExecutor;
}
}
@Async
@EventListener
public void update(UserProfileUpdateEvent update){
try {
Thread.sleep(20000); // 20초 지연 시뮬레이션
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
log.info("update event");
csvReader.update(UserProfileEntity.update(update));
}
결과값
Executor [java.util.concurrent.ThreadPoolExecutor@397b9c2f[Running, pool size = 30, active threads = 30, queued tasks = 10]] did not accept task
원인 : 스레드 풀이 포화 상태가 되었고, 더 이상 작업을 받아들일 수 없어 예외가 발생한 것이다.
ThreadPoolExecutor
는 다음 조건을 만족할 경우 태스크(작업)를 거절합니다:
- 현재 실행 중인 스레드 수가 최대 풀 크기에 도달했고
- 큐에 대기 중인 작업도 큐 용량 한도를 초과했을 때
즉, active thread == maxPoolSize
이고 queue size == queue capacity
이면, 새 작업을 더 이상 수용할 수 없어 RejectedExecutionException을 던진다.
결국 30개 이외의 요청들은 처리되지 않고 유실된다.
유실되지 않기 위한 대안은?
1. CallerRunsPolicy 정책 도입
@Bean(name = "taskExecutor")
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10); // 기본 스레드 수
executor.setMaxPoolSize(100); // 최대 스레드 수
executor.setQueueCapacity(500); // 큐 크기
executor.setThreadNamePrefix("Async-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); // 거절 시 대체 정책
executor.initialize();
return executor;
}
CallerRunsPolicy 정책은 거절된 작업을 현재 작업을 요청한 스레드(Main Thread 등) 가 직접 실행합니다.
📌장점
- 메인 쓰레드도 투입되기 때문에 시스템이 더 이상 태스크를 수용하지 못할 작업을 실행시킬 수 있다.(유실되지 않을수도..)
📌단점
- 비동기 작업이 동기적으로 실행되므로, 요청 처리 속도가 느려지고 시스템 응답성이 떨어질 수 있음.
- 로직 자체에 지연이 발생한다면, 메인 쓰레드를 투입해도 지연이 될것이고 메인 쓰레드 조차 작업에 붙잡혀 있기 때문에 이는 전체 시스템 장애문제로 이어질 수 있다.
📌 다른 RejectedExecutionHandler 정책들
정책 이름 | 설명 |
---|---|
AbortPolicy (기본값) |
작업이 거절되면 RejectedExecutionException 발생 |
DiscardPolicy |
거절된 작업을 그냥 버림 |
DiscardOldestPolicy |
큐에서 가장 오래된 작업을 버리고 새 작업을 큐에 추가 |
CallerRunsPolicy |
거절된 작업을 호출한 쓰레드에서 실행 |
2.거절된 요청들을 큐에 임시저장하고 작업이 끝나고 반환된 쓰레드가 있으면 바로 할당하자.(직접생각해봄)
최대한 요청 유실을 방지하는 방법으로 생각해봤다.
큐는 내부 자료구조 큐가 될수도 있고, 트래픽이 엄청나다면 외부 MQ 솔루션이 될수도 있을거 같다.
일단은 내부 자료구조 큐 방법을 도입해보자
@Configuration
@EnableAsync
public class AsyncConfig {
// 임시 작업 큐 (전역)
private final BlockingQueue<Runnable> retryQueue = new LinkedBlockingQueue<>(100000);
@PostConstruct
public void startRetryWorker() {
Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(() -> {
Runnable r = retryQueue.poll();
if (r != null) {
try {
threadPoolTaskExecutor().execute(r);
} catch (RejectedExecutionException ex) {
retryQueue.offer(r); // 아직 안 되면 다시 저장
}
}
}, 100, 100, TimeUnit.MILLISECONDS);
}
@Bean(name = "threadPoolTaskExecutor")
public ThreadPoolTaskExecutor threadPoolTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(3);
executor.setMaxPoolSize(30);
executor.setQueueCapacity(10);
executor.setThreadNamePrefix("Executor-");
// 커스텀 거절 정책 설정
executor.setRejectedExecutionHandler((r, exec) -> {
System.out.println("거절됨 → 임시 큐에 저장");
retryQueue.offer(r); // 즉시 처리 불가 → 내부에 보관
});
executor.initialize();
return executor;
}
}
결과값
2025-05-30T07:22:15.390Z INFO 1 --- [ Executor-2] c.h.t.a.event.UserProfileEventHandler : update event
2025-05-30T07:22:15.392Z INFO 1 --- [ Executor-3] c.h.t.a.event.UserProfileEventHandler : update event
2025-05-30T07:22:15.393Z INFO 1 --- [ Executor-1] c.h.t.a.event.UserProfileEventHandler : update event
2025-05-30T07:22:15.526Z INFO 1 --- [ Executor-5] c.h.t.a.event.UserProfileEventHandler : update event
2025-05-30T07:22:15.526Z INFO 1 --- [ Executor-4] c.h.t.a.event.UserProfileEventHandler : update event
2025-05-30T07:22:15.530Z INFO 1 --- [ Executor-6] c.h.t.a.event.UserProfileEventHandler : update event
2025-05-30T07:22:15.554Z INFO 1 --- [ Executor-7] c.h.t.a.event.UserProfileEventHandler : update event
2025-05-30T07:22:15.570Z INFO 1 --- [ Executor-8] c.h.t.a.event.UserProfileEventHandler : update event
2025-05-30T07:22:15.577Z INFO 1 --- [ Executor-11] c.h.t.a.event.UserProfileEventHandler : update event
2025-05-30T07:22:15.578Z INFO 1 --- [ Executor-12] c.h.t.a.event.UserProfileEventHandler : update event
2025-05-30T07:22:15.579Z INFO 1 --- [ Executor-16] c.h.t.a.event.UserProfileEventHandler : update event
2025-05-30T07:22:15.591Z INFO 1 --- [ Executor-15] c.h.t.a.event.UserProfileEventHandler : update event
2025-05-30T07:22:15.645Z INFO 1 --- [ Executor-17] c.h.t.a.event.UserProfileEventHandler : update event
2025-05-30T07:22:15.744Z INFO 1 --- [ Executor-14] c.h.t.a.event.UserProfileEventHandler : update event
2025-05-30T07:22:15.845Z INFO 1 --- [ Executor-10] c.h.t.a.event.UserProfileEventHandler : update event
2025-05-30T07:22:15.944Z INFO 1 --- [ Executor-18] c.h.t.a.event.UserProfileEventHandler : update event
2025-05-30T07:22:16.044Z INFO 1 --- [ Executor-9] c.h.t.a.event.UserProfileEventHandler : update event
2025-05-30T07:22:16.144Z INFO 1 --- [ Executor-13] c.h.t.a.event.UserProfileEventHandler : update event
2025-05-30T07:22:16.244Z INFO 1 --- [ Executor-19] c.h.t.a.event.UserProfileEventHandler : update event
2025-05-30T07:22:16.344Z INFO 1 --- [ Executor-22] c.h.t.a.event.UserProfileEventHandler : update event
2025-05-30T07:22:16.444Z INFO 1 --- [ Executor-20] c.h.t.a.event.UserProfileEventHandler : update event
2025-05-30T07:22:16.544Z INFO 1 --- [ Executor-21] c.h.t.a.event.UserProfileEventHandler : update event
2025-05-30T07:22:16.644Z INFO 1 --- [ Executor-23] c.h.t.a.event.UserProfileEventHandler : update event
2025-05-30T07:22:16.744Z INFO 1 --- [ Executor-24] c.h.t.a.event.UserProfileEventHandler : update event
2025-05-30T07:22:16.844Z INFO 1 --- [ Executor-25] c.h.t.a.event.UserProfileEventHandler : update event
2025-05-30T07:22:16.944Z INFO 1 --- [ Executor-27] c.h.t.a.event.UserProfileEventHandler : update event
2025-05-30T07:22:17.044Z INFO 1 --- [ Executor-26] c.h.t.a.event.UserProfileEventHandler : update event
2025-05-30T07:22:17.144Z INFO 1 --- [ Executor-28] c.h.t.a.event.UserProfileEventHandler : update event
2025-05-30T07:22:17.244Z INFO 1 --- [ Executor-29] c.h.t.a.event.UserProfileEventHandler : update event
2025-05-30T07:22:17.344Z INFO 1 --- [ Executor-30] c.h.t.a.event.UserProfileEventHandler : update event
...
거절됨 → 임시 큐에 저장
거절됨 → 임시 큐에 저장
거절됨 → 임시 큐에 저장
거절됨 → 임시 큐에 저장
거절됨 → 임시 큐에 저장
...
2025-05-30T07:22:35.390Z INFO 1 --- [ Executor-2] c.h.t.a.event.UserProfileEventHandler : update event
2025-05-30T07:22:35.392Z INFO 1 --- [ Executor-3] c.h.t.a.event.UserProfileEventHandler : update event
2025-05-30T07:22:35.393Z INFO 1 --- [ Executor-1] c.h.t.a.event.UserProfileEventHandler : update event
2025-05-30T07:22:35.526Z INFO 1 --- [ Executor-5] c.h.t.a.event.UserProfileEventHandler : update event
2025-05-30T07:22:35.526Z INFO 1 --- [ Executor-4] c.h.t.a.event.UserProfileEventHandler : update event
2025-05-30T07:22:35.530Z INFO 1 --- [ Executor-6] c.h.t.a.event.UserProfileEventHandler : update event
2025-05-30T07:22:35.554Z INFO 1 --- [ Executor-7] c.h.t.a.event.UserProfileEventHandler : update event
2025-05-30T07:22:35.570Z INFO 1 --- [ Executor-8] c.h.t.a.event.UserProfileEventHandler : update event
2025-05-30T07:22:35.577Z INFO 1 --- [ Executor-11] c.h.t.a.event.UserProfileEventHandler : update event
2025-05-30T07:22:35.578Z INFO 1 --- [ Executor-12] c.h.t.a.event.UserProfileEventHandler : update event
2025-05-30T07:22:35.579Z INFO 1 --- [ Executor-16] c.h.t.a.event.UserProfileEventHandler : update event
2025-05-30T07:22:35.591Z INFO 1 --- [ Executor-15] c.h.t.a.event.UserProfileEventHandler : update event
2025-05-30T07:22:35.645Z INFO 1 --- [ Executor-17] c.h.t.a.event.UserProfileEventHandler : update event
2025-05-30T07:22:35.744Z INFO 1 --- [ Executor-14] c.h.t.a.event.UserProfileEventHandler : update event
2025-05-30T07:22:35.846Z INFO 1 --- [ Executor-10] c.h.t.a.event.UserProfileEventHandler : update event
2025-05-30T07:22:35.945Z INFO 1 --- [ Executor-18] c.h.t.a.event.UserProfileEventHandler : update event
2025-05-30T07:22:36.045Z INFO 1 --- [ Executor-9] c.h.t.a.event.UserProfileEventHandler : update event
2025-05-30T07:22:36.145Z INFO 1 --- [ Executor-13] c.h.t.a.event.UserProfileEventHandler : update event
2025-05-30T07:22:36.245Z INFO 1 --- [ Executor-19] c.h.t.a.event.UserProfileEventHandler : update event
2025-05-30T07:22:36.345Z INFO 1 --- [ Executor-22] c.h.t.a.event.UserProfileEventHandler : update event
2025-05-30T07:22:36.445Z INFO 1 --- [ Executor-20] c.h.t.a.event.UserProfileEventHandler : update event
2025-05-30T07:22:36.544Z INFO 1 --- [ Executor-21] c.h.t.a.event.UserProfileEventHandler : update event
2025-05-30T07:22:36.644Z INFO 1 --- [ Executor-23] c.h.t.a.event.UserProfileEventHandler : update event
2025-05-30T07:22:36.744Z INFO 1 --- [ Executor-24] c.h.t.a.event.UserProfileEventHandler : update event
2025-05-30T07:22:36.844Z INFO 1 --- [ Executor-25] c.h.t.a.event.UserProfileEventHandler : update event
2025-05-30T07:22:36.944Z INFO 1 --- [ Executor-27] c.h.t.a.event.UserProfileEventHandler : update event
2025-05-30T07:22:37.044Z INFO 1 --- [ Executor-26] c.h.t.a.event.UserProfileEventHandler : update event
2025-05-30T07:22:37.144Z INFO 1 --- [ Executor-28] c.h.t.a.event.UserProfileEventHandler : update event
2025-05-30T07:22:37.244Z INFO 1 --- [ Executor-29] c.h.t.a.event.UserProfileEventHandler : update event
2025-05-30T07:22:37.344Z INFO 1 --- [ Executor-30] c.h.t.a.event.UserProfileEventHandler : update event
예외나 장애가 터지지 않고! 트래픽 유실 없이 작업을 처리하고 있다ㅠㅠ
주의사항은 내부 자료구조로 처리하게되면 아무래도 new LinkedBlockingQueue<>(100000);
크기가 무한이 아니기 때문에 엄청난 트래픽을 감당할 수는 없을것이고, 메모리가 overflow 될 위험이 크다.
이에 대한 대응방안은 앞단에서 선조치해서 일정이상의 트래픽은 막아버리고 클라이언트 단에는 대기 메시지를 알려준다던지,
요청 저장을 위한 외부 MQ 솔루션을 사용해야될거같다.
'나의 주니어 개발 일기 > 트러블슈팅' 카테고리의 다른 글
Spring Integration Udp 통신중 버퍼 크기로 인한 트러블 슈팅 (1) | 2024.07.05 |
---|---|
코틀린 JPA 순환참조 문제 발생 (1) | 2024.01.30 |
JAVA에서 C의 네트워크 패킷을 해석할때의 트러블슈팅 (0) | 2024.01.23 |