본문으로 바로가기
728x90
반응형
SMALL

문득 궁금해졌다.

보통 애플리케이션에서 쓰레드 풀 설정을 할것이다.

그런데 설정 값 이상의 트래픽이 몰리고, 쓰레드에서 지연이 발생한다면 어떤 현상이 발생할까?

요청이 유실될까? 아니면 지연저장될까? 내부적으로 어떻게 처리되지? 너무나도 궁금해졌다. 테스트해보자

우리의 최대 쓰레드 개수는 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는 다음 조건을 만족할 경우 태스크(작업)를 거절합니다:

  1. 현재 실행 중인 스레드 수가 최대 풀 크기에 도달했고
  2. 큐에 대기 중인 작업도 큐 용량 한도를 초과했을 때

즉, 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 솔루션을 사용해야될거같다.

728x90
반응형
LIST