본문 바로가기
개발 이야기/Springboot

httpclient thread limit exceeded replacing blocked worker 에러

by 농개 2025. 4. 17.

에러 관련 경험을 포스팅합니다.

 

목차

    1. 이슈 및 운영 환경

    이슈 내용은 외부 API 호출 시, 간헐적으로 실패하는 이슈였습니다.

    서버 환경은 아래와 같았습니다.

    • Springboot 2.5
    • Java 11

    특히, 요청량이 과도한 경우에 잦은 빈도로 에러가 발생했습니다.

    에러 로그는 대략 아래와 같았습니다.

    ...(중략)
    Caused by: java.util.concurrent.RejectedExecutionException: Thread limit exceeded replacing blocked worker
    	at java.util.concurrent.ForkJoinPool.tryCompensate(ForkJoinPool.java:1819) ~[?:?]
    	at java.util.concurrent.ForkJoinPool.compensatedBlock(ForkJoinPool.java:3448) ~[?:?]
    	at java.util.concurrent.ForkJoinPool.managedBlock(ForkJoinPool.java:3434) ~[?:?]
    	at java.util.concurrent.CompletableFuture.waitingGet(CompletableFuture.java:1898) ~[?:?]
    	at java.util.concurrent.CompletableFuture.get(CompletableFuture.java:2072) ~[?:?]
    	at jdk.internal.net.http.HttpClientImpl.send(HttpClientImpl.java:554) ~[java.net.http:?]
    	at jdk.internal.net.http.HttpClientFacade.send(HttpClientFacade.java:123) ~[java.net.http:?]
    ...(중략)

     

    2. 원인

    과도한 스레드, 워커 교체, ForkJoinPool... 등의 키워드로 파악해 봤을 때

    외부 요청 시, 동시성(Concurrency) 확보를 위한 비동기 호출 과정에서 문제가 생긴걸로 유추했습니다.

     

    그리고 대상 코드에 로깅을 추가(Thread name 출력 추가)하여 재현하던 중

    특이점을 발견했습니다.

    Thread name: ForkJoinPool.commonPool-worker-62
    Thread name: ForkJoinPool.commonPool-worker-63
    Thread name: ForkJoinPool.commonPool-worker-64
    Thread name: ForkJoinPool.commonPool-worker-65
    Thread name: ForkJoinPool.commonPool-worker-66
    Thread name: ForkJoinPool.commonPool-worker-67
    Thread name: ForkJoinPool.commonPool-worker-68
    Thread name: ForkJoinPool.commonPool-worker-69
    ...

     

    이슈 가능성이 있는 기능 구현부에서 ForkJoinPool에서 워커 스레드가 과도하게 생성되었습니다.

    스레드 개수가 약 300개를 초과하는 순간, 아래의 에러로그와 함께 기능이 중단되었습니다.

    Thread limit exceeded replacing blocked worker

     

     

    3. 간단한 해결

    CompletableFuture를 사용하고 있었습니다.

    이를 제한된 Thread Pool을 사용하도록 할 수 있습니다.

    다음은 예시 코드입니다.(실제 코드아님)

    Executor testExecutor = Executors.newFixedThreadPool(10);
    CompletableFuture<String> name = CompletableFuture.supplyAsync(() -> "Baeldung");
    
    CompletableFuture<Integer> nameLength = name.thenApplyAsync(value -> {
        printCurrentThread(); // will print "pool-2-thread-1"
        return value.length();
    }, testExecutor);

    참고: https://www.baeldung.com/java-completablefuture-threadpool

     

    고정된 스레드풀로 CompletableFuture를 사용하고 난 후

    아래와 같이 로그가 출력되었습니다.

    Thread name: ForkJoinPool.commonPool-worker-5
    Thread name: ForkJoinPool.commonPool-worker-1
    Thread name: ForkJoinPool.commonPool-worker-4
    Thread name: ForkJoinPool.commonPool-worker-3
    Thread name: ForkJoinPool.commonPool-worker-6
    Thread name: ForkJoinPool.commonPool-worker-2
    ...(중략)