티스토리 뷰

Spring

[SpringBoot] 요청량이 급증하여 톰캣의 초과 스레드가 활용될 때, TIMED_WAITING 상태의 스레드가 급증하는 이유

망나니개발자 2025. 5. 6. 10:00
반응형

 

 

 

1. 요청량이 급증하여 톰캣의 초과 스레드가 활용될 때, 
TIMED_WAITING 상태의 스레드가 급증하는 이유


[ TIMED_WAITING 스레드가 급증하는 상황 ]

기본 스레드 만을 활용하는 상황

이전 포스팅에서 살펴보았듯, 톰캣은 내부적으로 자바와 거의 유사한 ThreadPoolExecutor를 활용하는데, 해당 과정의 동작 방식은 다음과 같다. (참고로 이러한 부분은 톰캣에 종속적이니, 보다 자세한 내용을 위해서는 반드시 이전 포스팅을 참고하도록 하자.)

  1. 작업 요청이 들어오면, core 사이즈 만큼 스레드를 생성함
  2. core 사이즈를 초과하면 max 사이즈 만큼 스레드(초과 스레드)를 만들어 실행시키는데, 이는 설정된 시간을 넘어서도 활용되지 않으면 소멸됨
  3. 스레드의 수가 max 사이즈를 초과하면, 더 이상 초과 스레드를 생성하지 않고 큐에서 작업들이 대기함
  4. 큐가 꽉 차면 정책에 따라 처리함(기본적으로 예외를 발생시킴)

 

 

먼저, 요청량이 많지 않아서 기본 스레드로도 충분히 감당 가능한 상황이라고 하자.

  • 요청량: 6
  • 기본 스레드 크기(core, threads.min-spare): 10
  • 최대 스레드 크기(max, threads.max): 15

 

 

이러한 상황에서는 기본 스레드 만으로도 모든 작업이 처리되어 초과 스레드가 생성되지 않는다. 위의 상황을 기준으로 스레드 상태를 분석하면 다음과 같다.

  • 전체 스레드의 수: 10개
  • 요청 처리중인 6개의 스레드
    • 스레드가 어떠한 작업을 처리중임
    • 처리중이라면 RUNNABLE, 대기중이라면 TIMED_WAITING 상태임
  • 대기중인 4개의 스레드 상태
    • 처리할 요청이 없으므로 대기중임
    • WAITING 상태로 스레드 풀에서 대기중

 

 

실제로 이러한 부분을 스레드 덤프를 통해 확인하면, 다음과 같이 WAITING 상태임을 확인할 수 있다.

"http-nio-8080-exec-2" #86 daemon prio=5 os_prio=31 cpu=4.62ms elapsed=2660.14s tid=0x000000010f028400 nid=0xa207 waiting on condition  [0x000000030188a000]
   java.lang.Thread.State: WAITING (parking)
	at jdk.internal.misc.Unsafe.park(java.base@17.0.12/Native Method)
	- parking to wait for  <0x00000004048005e0> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
	at java.util.concurrent.locks.LockSupport.park(java.base@17.0.12/LockSupport.java:341)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionNode.block(java.base@17.0.12/AbstractQueuedSynchronizer.java:506)
	at java.util.concurrent.ForkJoinPool.unmanagedBlock(java.base@17.0.12/ForkJoinPool.java:3465)
	at java.util.concurrent.ForkJoinPool.managedBlock(java.base@17.0.12/ForkJoinPool.java:3436)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(java.base@17.0.12/AbstractQueuedSynchronizer.java:1625)
	at java.util.concurrent.LinkedBlockingQueue.take(java.base@17.0.12/LinkedBlockingQueue.java:435)
	at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:117)
	at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:33)
	at org.apache.tomcat.util.threads.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1114)
	at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1175)
	at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)
	at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:63)
	at java.lang.Thread.run(java.base@17.0.12/Thread.java:840)

 

 

 

초과 스레드를 생성 및 활용하는 상황

그렇다면 요청량이 많아지는 상황은 어떻게 될까? 현재의 상황이 다음과 같다고 하자.

  • 요청량: 15
  • 기본 스레드 크기(core, threads.min-spare): 10
  • 최대 스레드 크기(max, threads.max): 15

 

 

현재는 기본 스레드 만으로는 요청 처리가 불가능하므로, 초과 스레드가 생성 및 사용된다. 따라서 위의 상황을 기준으로 스레드 상태를 분석하면 다음과 같다.

  • 전체 스레드의 수: 15개
  • 요청 처리중인 10개의 기본 스레드와 5개의 초과 스레드
    • 모든 스레드가 어떠한 작업을 처리중임
    • 처리중이라면 RUNNABLE, 대기중이라면 TIMED_WAITING 상태임

 

 

이후에 요청이 완료되면 각각의 스레드는 스레드 풀로 반납되고, 생성되었던 5개의 스레드는 일정 시간을 대기했다가 활용되지 않으면 제거된다. 문제는 이때 스레드 풀에서 대기중인 스레드의 상태들이 모두 TIMED_WAITING 상태라는 것이다.

"http-nio-8080-exec-2" #86 daemon prio=5 os_prio=31 cpu=3.75ms elapsed=7.33s tid=0x000000010f028400 nid=0xa207 waiting on condition  [0x000000030188a000]
   java.lang.Thread.State: TIMED_WAITING (parking)
	at jdk.internal.misc.Unsafe.park(java.base@17.0.12/Native Method)
	- parking to wait for  <0x000000040a5c88d0> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
	at java.util.concurrent.locks.LockSupport.parkNanos(java.base@17.0.12/LockSupport.java:252)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos(java.base@17.0.12/AbstractQueuedSynchronizer.java:1674)
	at java.util.concurrent.LinkedBlockingQueue.poll(java.base@17.0.12/LinkedBlockingQueue.java:460)
	at org.apache.tomcat.util.threads.TaskQueue.poll(TaskQueue.java:99)
	at org.apache.tomcat.util.threads.TaskQueue.poll(TaskQueue.java:33)
	at org.apache.tomcat.util.threads.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1113)
	at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1175)
	at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)
	at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:63)
	at java.lang.Thread.run(java.base@17.0.12/Thread.java:840)

 

 

 

[ 초과 스레드가 생성된 경우, 스레드가 TIMED_WAITING 상태인 이유 ]

케이스를 정리하면 기본 스레드 이상의 요청이 인입되었고, 이로 인해 초과 스레드까지 생성 및 활용된 상황이다. 그리고 스레드 풀에서 대기중인 모든 스레드들은 WAITING이 아닌 TIMED_WAITING 상태로 존재하는 상황이다.

 

 

 

추가적으로 처리가 필요한 부분은, 일정 시간을 넘어서도 초과 스레드가 활용되지 않는다면, 이를 제거하고 스레드 풀의 개수를 기본 스레드의 수로 맞춰주어야 한다.

그렇다면 이를 어떻게 구현할 수 있을까? 스레드들로 하여금 처리할 요청들이 담긴 작업 큐로부터 일정 시간 작업을 가져오도록 대기시키고, 일정 시간 이후에도 처리할 작업이 없으면 이를 종료시키면 될 것이다. 톰캣의 로직을 보면 톰캣 역시도 내부적으로 이러한 구현을 따르고 있다.

 

 

위의 코드를 보면 getTask 내부에서 기본 스레드 풀의 크기 이상의 스레드가 존재하는 경우(초과 스레드가 생성된 경우)에는 timed 변수가 true가 되고, 작업 큐(WorkQueue)에서 일정 시간 대기하게 된다. 그리고 꺼낼 작업이 없으면, 최종적으로 반환값이 null이 되고 ThreadPoolExecutor가 해당 스레드를 종료시키는 것이다.

 

즉, 요청량이 급증하여 초과 스레드가 생성 및 활용 되었다면 요청 처리를 완료한 후에 스레드 풀의 스레드 갯수를 기본 스레드로 조절해야 한다. 이를 위해 스레드 풀에서 관리되는 스레드들로 하여금 일정 시간 대기시키고, 일이 없어서 자동으로 깨워지도록 하여 스레드를 종료시키고자 TIMED_WAITING 상태의 스레드가 급증한 것처럼 보이는 것이다. 따라서 TIMED_WAITING이 급증한 경우에는 무언가 대기중인 상황 외에도 초과 스레드가 활용되지는 않았는지 같이 확인해볼 필요가 있다.

 

 

 

 

 

 

관련 포스팅

  1. 멀티 스레드 기반으로 다중 요청을 처리하는 톰캣(Tomcat)의 구조와 동작 방식
  2. 올바른 스프링 부트 톰캣 애플리케이션 설정 가이드(SpringBoot Tomcat Configuration)
  3. 스프링 부트 톰캣 애플리케이션의 Graceful Shutdown 동작 방식(Spring Boot Tomcat Graceful Shutdown)
  4. 요청량이 급증하여 톰캣의 초과 스레드가 활용될 때, TIMED_WAITING 상태의 스레드가 급증하는 이유

 

 

 

반응형
댓글
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG more
«   2025/06   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
글 보관함