풀 (Pool)이란?
컴퓨터 과학에서 풀은 사용할 때 획득한 메모리와 나중에 해제되는 메모리가 아닌, 사용할 준비가 된 메모리에 유지되는 리소스 모음이다.
- 이미 사용할 준비가 된 자원을 메모리 위에 일정량 미리 생성해둔 자원의 집합
- 클라이언트가 리소스 사용을 완료하면 해제 및 손실되지 않고 풀로 반환된다.
들어가기 전에 Connection Pool과 Thread Pool은 다르다는 점을 알고 가셨으면 좋겠습니다.
애플리케이션에 대한 모든 요청이 DB에 접근하는 것은 아니기 때문에 WAS의 Thread는 Connection Pool의 갯수보다 여유있게 설정하는것이 좋다.
- Connection Pool은 DB와 커넥션을 맺기 위한 드라이버를 로드하고 커넥션 객체를 생성하는 등의 과정에서 발생하는 비용을 절약할 수 있다.
- Thread Pool은 스레드를 미리 생성해놓음으로써 스레드의 생성 및 수거가 빈번하게 발생하지 않아 메모리 할당에 소모되는 비용을 절약할 수 있다.
Spring Thread Pool
스레드 풀이 없다면...
서버는 동시에 여러 사용자가 접속할 수 있습니다. 자바에서는 스레드를 운영 체제의 자원으로 사용하는데 모든 요청에 대해 스레드를 생성하고 소멸하는 것은 OS와 JVM에 대해 많은 부담을 안겨줍니다.
1. 스레드 풀이 없다면 서버에 들어오는 요청마다 스레드를 새로 만들어서 처리하고 처리가 끝난 스레드는 버리는 방식으로 스레드 생성에 소요되는 시간 때문에 요청 처리가 더 오래 걸린다.
2. 요청 마다 스레드를 생성하는 경우에 처리 속도보다 더 빠르게 요청이 늘어나면 스레드가 계속 생성되면서 컨테스트 스위칭이 더 자주 발생하게 된다.
- CPU 오버헤드 증가로 CPU time 낭비
3. 동시에 일정 이상의 다수 요청이 들어오면 리소스(CPU와 메모리 자원) 소모에 대한 억제가 어려워진다.
- 순간적으로 서버가 다운되거나 동시다발적인 요청을 처리하지 못해서 스레드는 계속 생성되고 메모리 고갈로 인한 문제들이 생길 수 있습니다.
스레드 풀이란?
스레드 풀은 미리 일정 개수의 스레드를 생성하여 관리하는 기법입니다.
- 작업이 발생해서 작업 큐에 작업들이 쌓이게 되면 대기 상태에 있는 스레드 중 하나를 선택하여 작업을 수행한다.
- 작업이 완료되면 스레드를 제거하지 않고 해당 스레드는 다시 대기 상태로 돌아가고, 새로운 작업을 할당받을 준비를 합니다.
스레드 풀의 장점
1. 쓰레드 풀은 미리 정해진 개수의 스레드를 생성하여 관리하기 때문에, 쓰레드 생성 및 삭제에 따른 오버헤드를 줄일 수 있습니다.
2. 쓰레드 풀은 작업을 대기 상태로 유지하여 작업 처리 속도를 향상시킬 수 있습니다.
- 작업이 발생하면 대기 중인 쓰레드 중 하나를 선택하여 작업을 할당하므로, 작업 처리를 병렬로 진행할 수 있습니다.
3. 쓰레드 풀을 사용하면 동시에 처리할 수 있는 작업의 개수를 제한할 수 있습니다.
- 쓰레드 풀의 크기를 조절하여 시스템의 부하를 조절하고, 과도한 작업 요청으로 인한 성능 저하를 방지할 수 있습니다.
4. 쓰레드 풀을 사용하면 쓰레드의 생명 주기를 관리할 수 있습니다.
- 쓰레드 풀은 스레드의 생성, 재사용, 종료 등을 관리하므로, 스레드의 안전한 운영을 도와줍니다.
사용자의 요청을 어떻게 처리하는 걸까?
- Tomcat은 다중 요청을 처리하기 위해서, 부팅할 때 Thread의 컬렉션인 Thread Pool을 생성한다.
- 유저 요청(HttpServletRequest)가 들어오면 Thread Pool에서 하나씩 Thread를 할당한다. 해당 Thread에서 스프링부트에서 작성한 Dispathcer Servlet을 거쳐 유저 요청 을 처리한다.
- 작업을 모두 수행하고 나면 Thread는 Thread Pool로 반환한다.
SpringBoot를 MVC로 사용하면 내장되어있는 서블릿 컨테이너(Tomcat)에서 다중 요청을 처리해주게 됩니다.
- 요즘 버전의 내장 톰캣들은 스레드풀을 활용하고 있으며 .yml 혹은 .properties 파일을 통해서 쉽게 스레드 개수를 제어할 수 있습니다.
.yml, .properties에서 설정하면 SpringBoot Auto Configuration을 통해서 부팅할 때 우리가 설정한 Thread Pool을 생성하게 됩니다.
# application.yml
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 # 서버를 띄울 포트번호
- 작성하지 않으면 SpringBoot AutoConfiguration에서 정의한 디폴트값을 주입
- org.springframework.boot.autoconfigure.web.ServerProperties에서 디폴트 값을 확인할 수 있습니다.
- 작업 큐에 사이즈, thread에 개수를 너무 많이 늘리면 메모리 고갈 문제를 야기할 수 있으니 주의하기!
스레드는 적절한 수로 유지되는 것이 가장 좋다.
- 스레드풀 전략 혹은 적정 스레드 개수로 검색해보자
스레드는 많으면 너무 많은 스레드가 cpu의 자원을 두고 경합하게 되므로 처리속도가 느려질 수 있고, 적으면 cpu자원을 최적으로 활용하지 못하여 마찬가지로 처리속도가 느려질 수 있습니다.
- 스레드 풀은 최대한 core size를 유지하려고 합니다.
- CPU 연산이 많이 필요한 task와 Blocking I/O되는 task가 많은지도 고려해보자
Thread Pool Flow - Thread를 미리 만들어 놓고 필요한 작업에 할당했다가 돌려 받는다
1. 첫 작업이 들어오면 core size만큼의 스레드를 생성한다.
2. 유저 요청(Connection, Server socket에서 accept 한 소캣 객체)이 들어올 떄 마다 작업 큐(queue)에 담아둔다.
3. core size의 Thread 중, 유휴상태(idle)인 Thread가 있다면 작업 큐(queue)에서 작업을 꺼내 Thread에 작업을 할당하여 작업을 처리한다.
- 만약 유휴상태인 Thread 가 없다면 작업은 큐(queue)에서 대기한다.
- 그 상태가 지속되어 작업 큐(queue)가 꽉 찬다면 Thread를 새로 생성한다.
- 3번의 과정을 반복하다 Thread Max size에 도달하고 작업큐(queue)도 꽉 차게 되면 추가요청에 대해선 connection-refused 오류를 반환한다.
4.Task가 완료되면 Thread 는 다시 유휴상태로 돌아간다.
- 작업큐(queue)가 비어있고 core size이상의 Thread가 생성되어있다면 Thread를 Destory한다.
스레드는 많으면 너무 많은 스레드가 cpu의 자원을 두고 경합하게 되므로 처리속도가 느려질 수 있고, 적으면 cpu자원을 최적으로 활용하지 못하여 마찬가지로 처리속도가 느려질 수 있어 적절한 스레드 개수로 유지되는 것이 가장 좋다.
스레드풀 테스트
1. 유저 요청이 들어올 때(Connection)마다 스레드가 하나씩 할당한다.
2. 작업큐가 가득차면 최대 스레드 개수만큼 스레드가 늘어난다.
3. 스레드도 가득 차면 유저 요청이 거절된다.
동시에 요청이 5개가 왔을 때를 가정한다. (Thread.sleep을 줘서 동시에 접근한 것처럼 테스트)
#application.yml 파일
max: 2
min-spare: 2
accept-count: 1
connections-timeout: 20000(20초)
max-connections: 8192(default)
- 1,2 요청은 스레드가 작업을 하고 3 요청은 작업 큐에 들어가고 4,5 요청은 거절하는 것으로 예상한다.
RestTemplate로 서버2 -> 서버1로 요청
Console을 자세히 확인해보자
- nio-xxxx-xxxx-xx ??? 이게 뭘까요
4,5번 째 요청은 받을 수 없었을텐데 왜 처리가 되었을까요?
우리가 생각했던 테스트는 BIO Connector(Blocking I/O) 일때 유효한 이야기 입니다.
- 톰캣 8.0부터 NIO(NonBlocking I/O) Connector이 기본으로 채택되고 9.0 부터는 BIO Connector가 deprecate 됨 으로써 위의 설명과는 다른 방식으로 진행되게 된다.
BIO Connector와 NIO Connector
NIO 기반의 Connector는 하나의 Connection이 하나의 스레드를 할당받는 BIO Connector에 비해, Selector를 활용해 Socket을 관리하므로 더 적은 스레드를 사용합니다. 또한 max-connections값까지 접속을 유지하고, 스레드가 모자라다면 max 사이즈까지 스레드를 추가합니다. time-wait시간 안에 처리가 가능하다면 처리할 수 있습니다.
- yml에서 max-connections 개수를 줄이거나, connections-timeout을 짧게하면 서버2에서 거절당한것을 확인할 수 있었습니다.
추가로 주의할 점
자바의 Executors 클래스는 static 메서드로 다양한 형태의 스레드 풀을 제공한다.
문제는 생성자의 new LinkedBlockingQueue를 보자
생성자로 Integer.MAX_VALUE 약 20억개 정도로 사실상 제한이 없는 queue 사이즈를 넣어놓는것을 볼 수 있다.
- size 제한 없는 큐는 상황에 따라서는 메모리를 고갈시키는 잠재적인 위험 요인이 될 수 있으니 편하게 제공 받는 메서드도 확인해서 써야겠다.
참조
'개발 > Spring Boot' 카테고리의 다른 글
Spring Batch JpaItemWriter에서 List<Entity> 처리하기 (0) | 2024.05.25 |
---|---|
회원 탈퇴 로직 Spring Event로 처리하기 (0) | 2024.04.06 |
토큰 재발급 로직을 테스트하면서 발생한 문제 (2) | 2024.01.18 |
Test에서 deleteAll과 deleteAllInBatch() (0) | 2023.07.18 |
UnknownEntityException 문제.. (0) | 2023.06.22 |