요청이 WAS 안에서 겪는 세 개의 서로 다른 한도.
- ① OS 가 SYN 을 받아 accept 큐에 넣는다 — ← acceptCount
- ② WAS 가 accept 해서 연결로 인정한다 — ← maxConnections
- ③ 처리할 스레드를 할당한다 — ← maxThreads
Tomcat 11.0.24 기본값
- maxThreads — 200
- maxConnections — 8192
- acceptCount — 100
- connectionTimeout — 60000
- keepAliveTimeout — connectionTimeout 값을 따름
- maxKeepAliveRequests — 100
함정 —
connectionTimeout의 문서 기본값은 60000 이지만 배포되는server.xml은 20000 으로 설정한다. 값을 논할 때는 항상 "무엇 기준인지" 를 밝힌다.
maxConnections 가 maxThreads 보다 훨씬 큰 이유
- 연결을 8천 개 유지하면서 실제 처리는 200개만 동시에 한다
- keep-alive 로 붙어만 있는 연결이 스레드를 잡지 않는다
- NIO 커넥터라서 가능하다 (연결과 스레드가 1:1 이 아니다)
예전 BIO 커넥터는 연결 = 스레드였다 그때는 maxConnections 를 따로 둘 이유가 없었다
스레드는 몇 개가 맞나
하한 — 리틀의 법칙 필요한 동시 처리 수 = 처리량(TPS) × 평균 응답시간(초) 1,000 TPS · 평균 50ms → 1000 × 0.05 = 50 여유를 감안해 2~3배로 시작한다 → 100~150
상한 — 뒤에 있는 가장 좁은 자원 스레드 200개 · DB 커넥션 20개라면 180개는 커넥션을 기다린다 스레드를 400개로 늘려도 DB 앞의 줄만 길어진다
- 앞을 넓히면 병목이 사라지는 게 아니라 줄이 옮겨갈 뿐이다
메모리도 함께 본다
스레드 하나 = 네이티브 스택 (리눅스 기본 1MB 예약)
maxThreads 2000 → 스택만으로 최대 2GB 규모의 주소 공간 컨테이너 limit 을 넘기면 커널이 프로세스를 죽인다 (OOMKilled — 애플리케이션 로그에 아무것도 안 남는다)
큐가 어디에 생기는지가 설계다
-
maxConnections 도달 — → OS accept 큐(acceptCount)에서 대기
-
acceptCount 도 참 — → OS 가 연결을 거부한다
-
빠른 실패를 원한다 — acceptCount 를 작게
- 거부가 빨리 나고 앞단이 다른 서버로 보낸다
-
버티기를 원한다 — 크게 — 대기는 늘지만 끊기지는 않는다
-
큐를 크게 잡는 것은 대체로 나쁜 선택이다
- 오래 기다린 요청은 클라이언트가 이미 포기했을 가능성이 높다
- 처리해 봐야 버려진다 (일은 하고 값은 못 받는다)
keep-alive 설정이 연결 수를 좌우한다
-
maxKeepAliveRequests — 한 연결에서 처리할 최대 요청 수 (기본 100)
- 초과하면 연결을 닫는다
-
keepAliveTimeout — 다음 요청을 기다리는 시간
-
값이 작으면 — 연결을 자주 새로 만든다 (핸드셰이크 비용)
-
값이 크면 — 유휴 연결이 maxConnections 를 잠식한다
앞단 nginx 가 커넥션 풀을 유지한다면 백엔드의 keep-alive 는 넉넉해도 된다 — 연결 수가 예측 가능하기 때문이다 반대로 클라이언트가 직접 붙는 구조라면 보수적으로 잡는다
관측 없이 튜닝하지 않는다
봐야 할 지표
활성 / 전체 스레드 수 상시 포화면 상한이 낮거나 뒤가 느리다
큐 대기 수 0 보다 크게 지속되면 병목 신호
현재 연결 수 / maxConnections
accept 큐 ss -lnt 의 Recv-Q(대기) · Send-Q(백로그 한도)
숫자를 바꾸기 전에 '어디가 포화인지' 를 먼저 확인한다 포화가 아닌 값을 올리면 아무 일도 일어나지 않는다
면접 함정
- ❌ "스레드를 늘리면 처리량이 는다" → 뒤가 좁으면 대기만 늘어난다.
- ❌ "maxConnections와 maxThreads는 같은 말" → 수용과 처리는 다른 층이다.