[Java- Thread] 스레드 풀 포화 정책 이란?
스레드란?
Cpu Core 하나당 하나의 Thread를 실행시킬 수 있다. 하나의 프로세스 안에서 여러개의 쓰레드를 작업하여 업무를 분산시킬 수 있음
단순한 Thread 사용 과정 및 발생 가능한 문제점1
1. Java는 1:1 Threading-Model로 Thread를 생성한다.
2. User Thread(Process Thread) 생성시 OS Thread(OS Thread)와 연결해야 한다.
3. 즉 Thread를 새로 생성할때마다 OS Kernal과 통신이 필요하다.
4. Thread는 생성비용이 많이 발생하는 작업이며 작업 요청이 들어올 때 마다 생성하게되면 최종 요청 처리 시간이 증가한다.
단순한 Thread 사용 과정 및 발생 가능한 문제점 2
1. Process의 처리 속도보다 빠르게 요청이 쏟아지면 새로운 Thread가 무제한적으로 계속 생성된다.
2. Thread가 많아질수록 메모리를 차지하고 Context-Swtiching이 더 자주 발생한다.
3. 즉, 메모리 문제가 발생할 수 있고 CPU 오버헤드가 증가한다.
그래서 사용하는 것은 ? 스레드 풀!
핵심 설정은 maximumPoolSize, keepAliveTime, corePoolSize이다.
최대 maximumPoolSize만큼 유지가 가능하고 corePoolSize만큼 유지하게된다.
우리는 내장 엔진인 Tomcat을 사용하게 되는데 Java기반의 WAS이므로 ThreadPool과 매우 유사한 스레드 풀 구현체를 갖고 있다.
해당 스레드 풀은 Java에서 알던것과 유사하지만 추가로 알아둬야 할 것들이 있다.
Max-Connections
Tomcat이 초대로 동시에 처리할 수 있는 커넥션의 수이다. 웹에서 요청이 들어오면 Tomcat의 Connector가 Connection을 생성하면서 요청된 작업을 스레드 풀의 스레드에 연결한다.
AcceptCount
Max-Connections이상의 요청이 들어왔을 때 사용하는 대기열 Queue 사이즈로 Max-Connections와 AcceptCount 이상의 요청이 들어오면 추가적으로 들어오는 요청은 거절 될 수 있다.
Spring boot Tomcat Thread Pool 설정
1. Server.tomcat.thread.max
- 기본 값 200
- 서버 어플리케이션이 동시에 처리할 수 있는 요청 개수와 관련이 있다.
- 요청 수에 비해 너무 많이 설정하면 놀고 있는 스레드가 많아져서 비효율적
- 너무 적게 설정하면 동시 처리 요청 수가 줄어든다. TPS가 감소하게 됨
2.
스레드 풀과 포화 상태란?
Java 에서 ThreadPoolExecutor는 다음 요소로 구성이 된다.
- corePoolSize : 항상 유지할 최소 스레드 수
- maximumPoolSize : 생성 가능한 최대 스레드 수
- workQueue : 대기열, 작업이 들어오면 이곳에 쌓이게 됨(LinkedBlockingQueue)
- RejectedExecutionHandler : 포화 상태일 때 동작할 정책
포화 상태란?
다음 3가지 조거닝 만족 되면 스레드 풀 포화 상태가 된다.
- 실행 중인 스레드 수가 maximumPoolSize에 도달한다.
- 대기열(workQueue)가 가득찬다.
- 새로운 작업을 제출한다.
이때 실행할 수 있는 스레드도 없고, 대기열에 넣을 공간도 없으므로, 포화 상태 정책이 실행된다.
기본 포화 정책 4가지
AbortPolicy(기본값) , CallerRunPolicy, DiscardPolicy, DiscardOldestPolicy
AbortPolicy
- 작업을 거부하고 RejectedExecutionException을 던진다.
- 예외로 즉시 알 수 있어 디버깅에 유리하다.
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
CallerRunsPolicy
- 요청한 스레드가 직접 작업을 실행한다.
- 일시적인 트래픽 급증 시 백프레셔(처리 지연)을 유도하여 시스템을 보호한다.
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
DiscardPolicy
- 아무 일도 하지 않고 작업을 무시한다.
- 조용히 실패하므로 중요한 작업에선 사용하지 않는것을 추천한다.
DiscardOldestPolicy
- 대기열에서 가장 오래된 작업을 버리고 새 작업을 추가한다.
- 신규 요청 우선 처리시 해당 전략을 사용하며 대기열이 FIFO인 경우에만 유효하다.
실제로 다음과 같이 ExecutorService를 만들어서 쓰레드에 대한 사이즈와 정책을 설정할 수 있다.
아래 코드와 같이 직접 핸들러를 구현하여 쓰레드 풀 포화 현상시 적용할 정책에 대해 직접 제어할 수도 있다.
@Slf4j
@Component
public class CustomRejectPolicy implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable r,
ThreadPoolExecutor executor) {
log.error("작업이 거부됨 : " + r.toString());
// Monitoring System으로 로그 전송
}
}
내 프로젝트의 문제는 Thread10000명을 돌린다면 가끔 Thread Pool 개수가 부족해서 예외를 발생시키곤 하는데 이를 CallRunPolicy 정책을 사용해서 백프레셔를 적용시킬 수 있을듯하다.