꿈꾸는 개발자 박상호입니다.
Devhoyas
꿈꾸는 개발자 박상호입니다.
전체 방문자
오늘
어제
  • ALL (17)
    • Algorithm (7)
    • Java (2)
    • Go (2)
    • Spring (3)
    • Database (1)
      • MySQL (1)
      • ElasticSearch (0)
    • Http (1)
    • 일상 (1)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

인기 글

최근 글

티스토리

hELLO · Designed By 정상우.
꿈꾸는 개발자 박상호입니다.

Devhoyas

Spring

[Spring] 비동기 프로그래밍 TaskRejectedException

2022. 4. 20. 21:34

오늘 진행하고 있는 프로젝트에서 비동기 관련 오류가 발생했다.

해당 프로젝트의 로그를 확인했더니 아래와 같은 Exception을 날리고 있었다.

around search is failed. parameter: { *** }, e: 

org.springframework.core.task.TaskRejectedException: Executor [java.util.concurrent.ThreadPoolExecutor@76e0dac1[Running, pool size = 150, active threads = 10, queued tasks = 38, completed tasks = 118591]] did not accept task: org.springframework.aop.interceptor.AsyncExecutionInterceptor$$Lambda$1930/1632145409@31f653cb
	at org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor.submit(ThreadPoolTaskExecutor.java:344)
	at org.springframework.aop.interceptor.AsyncExecutionAspectSupport.doSubmit(AsyncExecutionAspectSupport.java:287)
    ... (중간 생략)
	... 86 common frames omitted

위 로그를 확인해 보면 TaskRejectedException이 발생한걸 알 수 있다.

해당 Exception은 비동기 프로그래밍시 최대로 가용할 수 있는 Thread 수 + Queue 가용 가능 수를 벗어나 생긴 Exception이다.

프로젝트 내에 Executor 설정은 아래와 같이 설정되어 있다.

@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {

    public static final int CORE_POOL_SIZE = 100;
    public static final int MAX_POOL_SIZE = 150;
    public static final int QUEUE_CAPACITY = 50;

    ThreadPoolTaskExecutor executor;

    @PostConstruct
    public void init(){
        this.executor = new ThreadPoolTaskExecutor();
        this.executor.setCorePoolSize(CORE_POOL_SIZE);
        this.executor.setMaxPoolSize(MAX_POOL_SIZE);
        this.executor.setQueueCapacity(QUEUE_CAPACITY);
        this.executor.initialize();
    }

    @Override
    public Executor getAsyncExecutor() {
        return this.executor;
    }

    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return new SimpleAsyncUncaughtExceptionHandler();
    }
}

위 코드의 Executor 설정을 토대로 210개의 요청이 한꺼번에 들어와 시간이 꽤 걸리는 (중간에 Blocking되는 작업이 있거나 지연이 많이 되는 작업이 있는) 비동기 작업을 한다고 가정해보자.

1. 100개의 요청까지는 쓰레드풀에서 100개의 Thread를 생성해 비동기로 처리한다. (처리 요청 수 : 100개)

2. 50개의 요청은 Queue에 담는다. (처리 요청 수 : 150개)

쓰레드 풀에 CorePoolSize 만큼 Thread를 생성했다면 먼저, Queue에 적재한다.
해당 Queue는 서버가 종료되면 데이터가 날라가므로 주의하자. 

3. Queue가 꽉 찼다면 (MaxPoolSize - CorePoolSize = 50) 만큼 임시 풀에 Thread를 쌓는다.

4. 나머지 10개의 요청은 더이상 수용할 수 없어 TaskRejectException을 던진다.

 


왜 빠르게 처리하지 못했을까?

현재 프로젝트 내에는 CompletableFuture로 처리시 DB에서 SELECT하는 코드가 섞여있었다.

많은 요청이 한꺼번에 DB에 쿼리를 날려 접근하려 했고 이에따라 네트워크 지연이 발생해 결과를 얻지 못하고 쓰레드만 생성하고 있던것 이다.

 


해결 방안

1. Executor의 설정에서 CorePoolSize, QueueCapacity, MaxPoolSize를 늘린다.

-> 하드웨어에 비례하여 설정하는 것이기에 보수적으로 접근해야하며 리스크가 매우 크기에 올바르지 않은 방안이다.

2. 무제한 큐 전략으로 변경한다.

-> 어차피 처리하지 못하고 큐에 요청들만 줄 세우는것이기에 성능면에서 절대 올바르지 않다.

서울로 들어가는 톨게이트가 하나라고 가정했을때 수많은 서울로 가는 차들이 줄지어 기다려야만 한다.

3. TaskRejected 전략을 바꾼다.

-> TaskRejected의 기본 전략인 ThreadPoolExecutor.AbortPolicy를 작업이 거절되면 가장 오래된 (가장 처음 등록된) 작업을 제거하고 다시 실행하도록 하는 전략인 ThreadPoolExecutor.DiscardOldestPolicy 으로 바꾸는 것이다.

그럼 가장 오래 기다리고 있던 작업은 무슨 죄야? 불공평 하잖아!

4. DB에 접근하는 (INSERT SELECT나 UPDATE SELECT가 아닌) 로직을 수정한다.

-> 근본적인 원인이다. 현재 접근하는 DB는 비동기 방식을 지원하지 않고 동기로만 처리하는 RDB이다. 

해당 RDB를 비동기를 지원하는 DB로 전환하거나, 해당 비동기 로직에서 DB에서 SELECT하는 구절만 빼 따로 수행하는 방법이다.

 


결론

해당 오류가 났다면 100이면 100 설계가 잘못된것이다.

근본적인 원인부터 파악을 하고 차근차근 설계를 다시 하거나 비즈니스 로직을 철저히 세우는 방법뿐이다.

저작자표시 비영리 변경금지

'Spring' 카테고리의 다른 글

[Spring] Spring WebClient의 사용  (0) 2022.06.04
[Spring] Annotation과 Reflection을 이용한 확장성이 좋은 AOP 만들기  (0) 2022.05.20
    'Spring' 카테고리의 다른 글
    • [Spring] Spring WebClient의 사용
    • [Spring] Annotation과 Reflection을 이용한 확장성이 좋은 AOP 만들기
    꿈꾸는 개발자 박상호입니다.
    꿈꾸는 개발자 박상호입니다.
    취미를 특기로, 특기를 꿈으로, 꿈을 직업으로!

    티스토리툴바