[Java Spring] 스프링으로 Thread Pool 원리 이해하기 ( 다중 유저 요청 처리 등)
안녕하세요 최근 학교 수업으로 진행하는 프로젝트 때문에 Thread에 관련된 요청을 수행해야하는 일이 생겼습니다.
그래서 오늘은 자바 스프링의 Thread pool을 학습하는 시간을 가져보겠습니다.
결론 3줄 요약
1.스프링 부트는 내장 서블릿 컨테이너인 Tomcat을 사용합니다.
2.Tomcat은 다중 처리 요청을 처리하기 위해서, 부팅할 때 Thread의 컬렉션인 Thread pool을 생성합니다.
3.유저 요청(HttpServletRequest)가 들어오면 Thread Pool에서 하나씩 Thread를 할당해서 요청을 Dispatcher Servlet을 거쳐 유저 요청을 처리한 후 작업을 모두 수행하고 나서 Thread는 Thread Pool로 반환됩니다.
Thread Pool이 등장하게 된 배경
1.스레드를 한 번 생성할 때 마다 OS가 새로 생성된 스레드를 위한 메모리 영역을 확보 및 할당을 진행하고, 스레드가 더 이상 필요없을 땐 다시 이 메모리 영역을 회수하는 작업이 발생합니다.
2.이것은 비용이 큰 작업이기 때문에, 계속 생성하고 수거하는 것은 자원의 소모도 크고 퍼포먼스에 악영향을 끼칩니다.
3.그래서 Thread Pool이라는 개념을 이용해서 스레드를 미리 만들어두고 작업이 들어올 때 마다 스레드를 적절히 분배해주자는 개념이 등장하였습니다.
4.즉, 부팅할 때 Thread Pool을 생성해서 미리 만들어져 있는 쓰레드들을 적절하게 할당해주고 작업을 처리하자는 개념입니다.
Spring boot에서 application.yml 설정으로 스레드 관리하기
# application.yml (적어놓은 값은 default) 출처: https://russell-seo.tistory.com/9
server:
tomcat:
threads:
max: 200 # 생성할 수 있는 thread의 총 개수
min-spare: 10 # 항상 활성화 되어있는(idle) thread의 개수
max-connections: 8192 # 수립가능한 connection의 총 개수
accept-count: 100 # 작업큐의 사이즈
connection-timeout: 20000 # timeout 판단 기준 시간, 20초
port: 8080 # 서버를 띄울 포트번호
Thread Pool 흐름 이해하기
1.첫 작업이 들어오면, core size만큼의 스레드를 생성
2.유저 요청이(Connection, Server socket에서 accept한 소켓 객체)이 들어올 때마다 작업 큐(queue)에 담아둠.
3.core size의 스레드 중, 유휴상태(idle,작업이 가능한 스레드)인 스레드가 있다면 작업 큐에서 작업을 꺼내 스레드에 작업을 할당하여
작업을 처리함.
3-1. 만약 유후상태인 스레드가 없다면, 작업은 작업 큐에서 대기
3-2. 그 상태가 지속되어 작업 큐가 가득 찼다면, 스레드를 새로 생성
3-3. 3번 과정을 반복하다 스레드 최대 사이즈에 도달하고 작업큐도 꽉 차게 되면, 추가 요청에 대해선
connection-refused 오류를 반환
4. task가 완료되면 스레드는 다시 유휴상태로 돌아갑니다.
4-1. 작업큐가 비어있고 core size 이상의 스레드가 생성되어있다면 스레드를 destroy를 합니다.
유저 요청이 들어올 때(Connection) 마다
스레드가 하나씩 할당-> 작업큐가 가득차면 스레드가 늘어남 -> 스레드도 가득차면 유저 요청이 거절됨
하지만, 이것은 BIO Connector(Blocking I/O) 일 때의 상황입니다.
현재는 톰캣이 8.0부터 NIO(NonBlocking I/O) Connector이 기본으로 채택되고
톰캣 9.0부터는 BIO Connector가 deprecate됨으로써 위의 동작 과정과는 다른 방식으로 진행됩니다.
Connector이란?
Connector은 소켓 연결을 수입하고 데이터 패킷을 획득하여 HttpServletRequest 객체로 변환한 뒤 Servlet 객체에 전달하는 역할을 합니다.
- Acceptor에서 while문으로 대기하며 port listen을 통해 Socket Connection을 얻게 됩니다.
- Socket Connection으로부터 데이터를 획득합니다.
데이터 패킷을 파싱해서 HttpServletRequest 객체를 생성합니다. - Servlet Container에 해당 요청객체를 전달합니다. ServletContainer는 알맞은 서블릿을 찾아 요청을 처리합니다.
blocking I/O vs Non - Blocking I/O 간단 개념 설명
blocking I/O : Blocking I/O는 I/O 작업이 완료될 때까지 프로그램 실행을 멈추는 방식
Non-Blocking I/O : Non-Blocking I/O는 I/O 작업이 완료될 때까지 프로그램 실행이 멈추지 않고 다른 작업을 수행하는 방식
BIO Connector란
BIO Connector는 Socket Connection 처리를 위해 Java의 기본적인 I/O 기술을 사용하는 방식입니다. BIO Connector는 다음과 같은 특징을 가지고 있습니다.
- Thread 기반 처리: BIO Connector는 각 Socket Connection을 처리하기 위해 하나의 Thread를 할당합니다.
- 연결 유지: Thread는 연결이 닫힐 때까지 특정 Connection에 계속 할당됩니다.
- 동시 접속 제한: 동시에 사용될 수 있는 Thread 수는 동시 접속 가능한 사용자 수와 동일합니다.
- Thread 낭비: Thread가 사용되지 않고 Idle 상태로 낭비되는 시간이 발생할 수 있습니다.
BIO Connector의 문제점
BIO Connector는 다음과 같은 문제점을 가지고 있습니다.
- 비효율적인 리소스 사용: Thread 기반 처리 방식은 많은 Thread를 사용하여 리소스를 비효율적으로 사용할 수 있습니다.
- 확장성 제한: 동시 접속 가능한 사용자 수는 사용 가능한 Thread 수에 제한됩니다.
- 높은 CPU 사용량: 많은 Thread가 동시에 실행되면 CPU 사용량이 높아질 수 있습니다.
NIO Connector의 개념 알아보기
NIO Connector는 Poller라는 별도의 스레드를 사용하여 여러 개의 Socket 연결을 처리합니다.
Poller는 Socket들을 캐시로 들고 있다가 해당 Socket에서 data에 대한 처리가 가능한 순간에만 스레드를 할당하는
방식을 사용해서 스레드가 idle 상태로 낭비되는 시간을 줄여줍니다.
- Acceptor: Acceptor는 명칭 그대로 Socket Connection을 accept를 합니다. 또한,
새로운 Socket 연결 요청을 수락하고 NioChannel 객체로 변환합니다.
소켓에서 Channel 객체를 얻어서 톰캣의 NioChannel 객체로 변환합니다. 그리고 추가로 NioChannel 객체를 PollerEvent라는 객체로 한번 더 캡슐화 해서 event queue에 넣게됩니다. 즉, Acceptor는 event Queue의 공급자입니다. - Poller: Poller thread는 event queue의 사용자입니다. Poller는 NIO의 Selector를 가지고 있습니다.
Selector는 다수의 채널이 등록되어 있고, select 동작을 수행하여 데이터를 읽을 수 있는 소켓을 획득합니다.
그리고 Worker Thread pool에서 이용할 수 있는 Worker Thread를 얻어서 해당 소켓을 worker thread에게 넘기게 됩니다.
또한, Java Nio Selector를 사용해서 data 처리가 가능할 때만 Thread를 사용하기 때문에 idle 상태로 낭비되는 Thread가 줄어들게 됩니다. - Worker Thread: Poller는 Worker Thread Pool에서 이용 가능한 Worker Thread를 선택하고 해당 Socket을 Worker Thread에게 넘깁니다.
- Worker Thread: Worker Thread는 Socket으로부터 데이터를 읽고 처리합니다.
- 응답 전송: Worker Thread는 처리 결과를 기반으로 응답을 클라이언트에게 전송합니다.
요약
NIO 기반의 Connector은 하나의 Connection이 하나의 스레드를 할당받아 사용하는 BIO Connector에 비해,
Selector를 활용해 Socket을 관리하므로 더 적은 스레드를 사용합니다.
출처:
https://velog.io/@haero_kim/Java-Thread-Pool-개념과-동작원리
https://russell-seo.tistory.com/9
https://velog.io/@sihyung92/how-does-springboot-handle-multiple-requests