Thread Pool을 효율적으로 조정하기 위해선 Thread Pool의 동작 방식과 내부 처리되는 방식에 대한 기본 지식이 필요하다.
제대로 이해하지도 않고 무작정 부하테스트 계속 진행하다가 노트북이 살짝 맛이 가버렸음... 다음부턴 제대로 사전 조사하고 테스트를 하자는 마음으로 내용 정리
스프링 부트 내장 Tomcat 역사
스프링 부트는 내장 서블릿 컨테이너인 Tomcat을 이용한다
스프링 부트 2.5.4 기준으로 9.0.52버전 Tomcat을 내장하고 있다
Tomcat의 Thread Pool 동작 Flow
Tomcat 부팅 시 다중 요청 처리를 위해 Thread Pool을 생성한다
유저 요청(HttpServletRequest)가 들어오면 Thread Pool의 Thread를 하나씩 할당한다
할당 받은 Thread에서 Dispatcher Servlet을 거쳐 요청을 처리한다
작업 수행 후 Thread Pool로 반환된다
Thread Pool의 기본 동작 Flow
첫 task가 들어오면 core size만큼 thread 생성
task(Connection, Server socket에서 accept한 소켓 객체)가 task queue에 저장
idle(유휴, 대기)상태인 Thread가 있으면 task queue에서 polling하여 Thread에 작업 할당 및 처리
3-1. idle 상태인 thread가 없으면 task queue에서 대기
3-2. task queue가 가득 차게되면 thread 생성
3-3. task queue가 가득 차있고, thread도 최대 사이즈라면 이후 요청에 대해선 connection-refused 오류 반환
task 완료 시 Thread는 다시 idle상태로 돌아감
만약 task queue가 비어있고 core size 이상의 thread가 생성되어있으면 thread를 소멸
ps. Connection, Server socket에서 accept한 소켓 객체 -> 자바 소켓 프로그래밍의 서버와 클라이언트가 연결되는 개념을 이해하면 됨
스프링 부트 내장 Tomcat Default 값
내장 톰캣과 아파치 톰캣의 Default 값은 다르다
아래 설정이 Tomcat ThreadPoolExcutor와 Connector에 적용된다
server:
tomcat:
threads:
max: 200 # 최대 생성 가능한 Thread
min-spare: 10 # idle(유휴) thread (활성화, 대기중인 쓰레드라는 뜻)
accept-count: 100 # task queue (작업큐) 크기
max-connections: 8192 # 최대 허용할 connection
connection-timeout: 20000 # timeout(ms)
port: 8080
Tomcat이 Thread Pool을 사용하게 된 이유
Tomcat 3.2버전 이전에는 서블릿 실행 시 Thread를 생성/소멸하는 방식이였다.
이 방식은 요청마다 Thread를 생성/소멸함에 있어서 많은 비용을 사용하게 되었고,
동시에 대용량의 요청이 들어왔을 경우 리소스 소모를 억제하기 어려워져 서버가 다운되는 문제를 야기하게 될 수 있었다.
이러한 문제 때문에 Thread Pool을 사용하게 된다
Thread Pool 설정 시 유의 사항
Thread는 너무 많이 할당하면 불필요한 리소스를 낭비하게 되고, cpu의 자원을 두고 경합하게 되어 오히려 성능이 저하될 수 있다.
반대로 너무 적게 할당한다면 cpu의 자원을 활용하지 못하여 성능이 저하될 수 있다.
~~Tomcat 설정 중 accept-count의 Default는 "무한 대기열 전략"을 사용하고 있어 Integer.MAX로 설정되어있다.
즉 이렇기에 accept-count를 설정하지 않는다면 max를 설정하는 것은 의미가 없어진다.
(task queue가 꽉 차게 되었을 때 Thread를 추가 생성하기 때문)~~ -> 확인해보니 200으로 변경되었음
적정 Thread 계산 법
스레드 수 = 사용 가능한 코어 수 * (1 + 대기,유휴 시간/서비스 시간)
서비스 시간 -> 프로세스가 결과를 산출하기까지 소요되는 시간
대기,유휴 시간 -> task queue에서 대기하는 시간 -> 응답 시간 - 서비스 시간
ex) 코어가 2개, 대기시간이 50ms, 서비스 시간이 5ms라면 2*(1+50/5)가 계산되어 22
하지만 만약 DB Connection Pool이나 JMS(자바 메세지 서비스) 등 어플리케이션 외의 영역을 고려한다면
스레드 수 = 사용 가능한 코어 수 CPU 목표 사용량 (1 + 대기,유휴 시간/서비스 시간)
리틀의 법칙을 이용한 병렬 work 스레드의 수를 계산 (TPS)
리틀의 법칙 L = λ * W
L -> 동시에 처리된 요청 수 (큐 안에 커넥션 수)
λ -> 평균 도착 시간
W -> 요청이 처리되기까지 평균 대기 시간
ex) 평균 응답 시간이 55ms, Thread Pool의 사이즈가 22인 서비스가 있다면
22 / 0.055 = 400TPS 가 계산된다
여기부턴 Blocking, Non-Blocking 내용
Blocking, Non-Blocking I/O와 연결되는 이유
위와 같이 설정하고 5개의 요청을 보낼 경우 4번째부터 connection-refused오류가 발생할 것 같지만 정상적으로 처리가 된다.
그 이유는 Tomcat이 NIO Connector을 채택하여 사용하고 있기 때문이다.
Thread Pool 기본 지식과 Blocking, Non-Blocking 이해
Thread Pool을 효율적으로 조정하기 위해선 Thread Pool의 동작 방식과 내부 처리되는 방식에 대한 기본 지식이 필요하다.
스프링 부트 내장 Tomcat 역사
Tomcat의 Thread Pool 동작 Flow
Thread Pool의 기본 동작 Flow
ps. Connection, Server socket에서 accept한 소켓 객체 -> 자바 소켓 프로그래밍의 서버와 클라이언트가 연결되는 개념을 이해하면 됨
스프링 부트 내장 Tomcat Default 값
Tomcat이 Thread Pool을 사용하게 된 이유
이 방식은 요청마다 Thread를 생성/소멸함에 있어서 많은 비용을 사용하게 되었고,
동시에 대용량의 요청이 들어왔을 경우 리소스 소모를 억제하기 어려워져 서버가 다운되는 문제를 야기하게 될 수 있었다.
이러한 문제 때문에 Thread Pool을 사용하게 된다
Thread Pool 설정 시 유의 사항
반대로 너무 적게 할당한다면 cpu의 자원을 활용하지 못하여 성능이 저하될 수 있다.
즉 이렇기에 accept-count를 설정하지 않는다면 max를 설정하는 것은 의미가 없어진다.
(task queue가 꽉 차게 되었을 때 Thread를 추가 생성하기 때문)~~ -> 확인해보니 200으로 변경되었음
적정 Thread 계산 법
하지만 만약 DB Connection Pool이나 JMS(자바 메세지 서비스) 등 어플리케이션 외의 영역을 고려한다면
리틀의 법칙을 이용한 병렬 work 스레드의 수를 계산 (TPS)
22 / 0.055 = 400TPS 가 계산된다
여기부턴 Blocking, Non-Blocking 내용
Blocking, Non-Blocking I/O와 연결되는 이유
그 이유는 Tomcat이 NIO Connector을 채택하여 사용하고 있기 때문이다.
BIO(Blocking I/O) Connector
NIO(Non-Blocking I/O) Connector
추후 참조링크를 다시 읽어보려한다
그럼 Blocking I/O, Non-Blocking I/O가 무엇이냐 (위 내용들은 해당 Model을 사용한 방식임 헷갈리지 말자)
Blocking I/O Model
Non-Blocking I/O Model
"처리 됐어?" -> "없으니깐 잠깐 다른것좀 할게" -> "처리됐어?" 같은 방식
I/O 이벤트 통지 모델이란
Synchronouse Model (동기 모델)
결국, 그냥 기다리든 다른걸 하면서 기다리든 기다린다는 것
Asynchronous Model (비동기 모델)
Blocking, Non-Blocking, Synchronous, Asynchronous 정리 그림
참조 사이트