본문으로 건너뛰기

Tomcat 스레드 풀 튜닝: maxThreads와 Executor

Tomcat의 스레드 풀은 동시 요청을 처리하는 핵심 자원입니다. 너무 적으면 요청이 큐에 쌓이고, 너무 많으면 컨텍스트 스위칭과 메모리 문제가 발생합니다. 서비스 특성에 맞는 최적값을 찾는 방법을 알아봅니다.


Tomcat 스레드 처리 모델

요청 도착


[Accept Queue] ← OS 소켓 큐 (acceptCount)
│ 큐 초과 시 → TCP 거절

[Thread Pool] ← maxThreads, minSpareThreads
│ 스레드 부족 시 → acceptCount 대기

[Servlet 처리]


응답 반환

server.xml 스레드 풀 기본 설정

<!-- conf/server.xml -->
<Connector port="8080"
protocol="HTTP/1.1"

<!-- 최대 스레드 수: 동시 처리 가능한 요청 -->
maxThreads="200"

<!-- 최소 유휴 스레드 수: 항상 대기 중인 스레드 -->
minSpareThreads="10"

<!-- 연결 수락 큐 크기 (OS backlog) -->
acceptCount="100"

<!-- 최대 연결 수 (NIO: 기본 10000) -->
maxConnections="10000"

<!-- 연결 타임아웃 (ms) -->
connectionTimeout="20000"

<!-- 요청 파라미터 최대 수 -->
maxParameterCount="1000"

<!-- 요청 헤더 최대 크기 (bytes) -->
maxHttpHeaderSize="8192"
/>

공유 Executor로 여러 Connector 스레드 관리

여러 Connector(HTTP, HTTPS, AJP)가 있을 때 스레드 풀을 공유합니다.

<!-- conf/server.xml -->

<!-- 1. Executor 정의 -->
<Executor name="tomcatThreadPool"
namePrefix="catalina-exec-"

<!-- 최대 스레드 -->
maxThreads="400"

<!-- 최소 유휴 스레드 -->
minSpareThreads="10"

<!-- 최대 유휴 시간 (ms): 이 시간 초과 유휴 스레드 제거 -->
maxIdleTime="60000"

<!-- 큐 용량 (0 = 무제한, -1 = 직접 스레드 거부) -->
maxQueueSize="100"

<!-- 스레드 데몬 여부 -->
daemon="true"

<!-- 스레드 우선순위 -->
threadPriority="5"
/>

<!-- 2. Connector에서 Executor 참조 -->
<Connector port="8080"
protocol="HTTP/1.1"
executor="tomcatThreadPool"
acceptCount="100"
maxConnections="10000"
connectionTimeout="20000"
/>

<Connector port="8443"
protocol="org.apache.coyote.http11.Http11NioProtocol"
executor="tomcatThreadPool" 같은 공유
SSLEnabled="true"
scheme="https"
secure="true"
<!-- ... SSL 설정 ... -->
/>

NIO vs NIO2 프로토콜 선택

<!-- HTTP/1.1 NIO (권장, Java NIO 기반) -->
<Connector protocol="HTTP/1.1" .../>
<!-- = org.apache.coyote.http11.Http11NioProtocol -->

<!-- HTTP/1.1 NIO2 (비동기 I/O, Java 7+) -->
<Connector protocol="org.apache.coyote.http11.Http11Nio2Protocol" .../>

<!-- HTTP/2 (Upgrade 방식, Tomcat 8.5+) -->
<UpgradeProtocol className="org.apache.coyote.http2.Http2Protocol"/>

Spring Boot 내장 Tomcat 튜닝

# application.yml
server:
tomcat:
# 최대 스레드 수
threads:
max: 200
min-spare: 10
# 연결 큐
accept-count: 100
# 최대 연결 수
max-connections: 10000
# 연결 타임아웃
connection-timeout: 20s
# 요청 바디 최대 크기
max-http-form-post-size: 10MB
// 또는 Java Config로 세밀하게 설정
@Configuration
public class TomcatConfig {

@Bean
public TomcatServletWebServerFactory tomcatFactory() {
TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();

factory.addConnectorCustomizers(connector -> {
ProtocolHandler handler = connector.getProtocolHandler();
if (handler instanceof AbstractHttp11Protocol<?> http11) {
http11.setMaxThreads(400);
http11.setMinSpareThreads(20);
http11.setAcceptCount(100);
http11.setConnectionTimeout(20000);
}
});

return factory;
}
}

최적 스레드 수 계산

스레드 수는 서비스의 I/O 특성에 따라 달라집니다.

CPU Bound 서비스 (이미지 처리, 암호화 등):
maxThreads ≈ CPU 코어 수 × 1~2
예: 8 코어 → maxThreads = 8~16

I/O Bound 서비스 (DB 쿼리, 외부 API 호출):
maxThreads ≈ CPU 코어 수 × (1 + 평균 대기 시간 / 처리 시간)
예: 8 코어, 응답 200ms 중 I/O 대기 180ms →
= 8 × (1 + 180/20) = 8 × 10 = 80~100

혼합 서비스 (일반적인 웹앱):
maxThreads = 100~200 (일반적인 권장값)
maxThreads = 200~400 (고트래픽, 여러 Connector 공유 시)

스레드 상태 모니터링

# JVM 스레드 덤프 (각 스레드 상태 확인)
kill -3 $(pgrep -f tomcat) # SIGQUIT → catalina.out에 덤프 출력
# 또는
jstack <PID> | grep -E "catalina-exec|WAITING|BLOCKED" | head -50

# 바쁜 스레드 수 확인
jstack <PID> | grep "catalina-exec" | grep -v "WAITING" | wc -l

# Spring Boot Actuator + Micrometer로 스레드 메트릭
curl http://localhost:8080/actuator/metrics/tomcat.threads.busy
curl http://localhost:8080/actuator/metrics/tomcat.threads.current
curl http://localhost:8080/actuator/metrics/tomcat.connections.current
# application.yml: Actuator 활성화
management:
endpoints:
web:
exposure:
include: health,metrics,threaddump
metrics:
tags:
application: ${spring.application.name}

스레드 풀 고갈 감지 및 대응

# 증상: 응답 지연 급증, 503 에러 증가, 큐 대기 스레드 증가
# 원인: maxThreads 초과, 느린 DB 쿼리, 외부 API 타임아웃 미설정

# 진단 1: 현재 active 스레드 vs maxThreads
curl http://localhost:8080/actuator/metrics/tomcat.threads.busy

# 진단 2: 어떤 스레드가 막혔는지 확인
jstack <PID> | awk '/catalina-exec/{p=1} p{print; if (/^$/) {p=0}}' | head -100

# 임시 조치: maxThreads 증가 (JMX 사용)
# 영구 조치: 느린 쿼리 최적화, 외부 API 타임아웃 설정

프로덕션 권장 설정 요약

<Executor name="tomcatThreadPool"
maxThreads="300"
minSpareThreads="20"
maxIdleTime="60000"
maxQueueSize="100"/>

<Connector port="8080"
protocol="HTTP/1.1"
executor="tomcatThreadPool"
acceptCount="100"
maxConnections="10000"
connectionTimeout="20000"
compression="on"
compressionMinSize="2048"/>