쓰레드 풀

김파란·2025년 1월 8일

Spring

목록 보기
9/10

개요

  • 스프링부트는 3.3.7 기준으로 Tomcat 10.1.34버전을 내장하고 있다
  • Tomcat은 다중 요청을 처리하기 위해서 부팅할 때 Thread pool을 생성한다
  • 요청이 들어오면 Thread Pool에서 하나씩 Thread를 할당하고, 해당 Thread는 Dispatcher Servlet을 거쳐 유저 요청을 처리한다
  • 작업을 모두 수행하고 나면 스레드는 스레드풀로 반환된다
  • 스프링의 톰캣버전

내장 톰캣

  • 스프링과 스프링부트의 주요한 차이점 중 하나는 스프링 부트에서는 내장 서블릿 컨테이너(Tomcat)을 지원한다는 것이다
  • 환경설정만으로 간편하게 Tomcat의 설정을 바꿔줄 수 있다
  • 디폴트 값은 ServerProperties에서 확인할 수 있다
server:
  tomcat:
    threads:
      max: 200 # 생성할 수 있는 thread의 총 개수
      min-spare: 10 # 항상 활성화 되어있는(idle) thread의 개수
    max-connections: 8192 # 수립가능한 connection의 총 개수
    accept-count: 100 # 작업큐의 사이즈
    connection-timeout: 20000 # timeout 판단 기준 시간, 20초

기본 설정

  • 대기큐: 연결 대기 중인 요청을 처리할 수 있는 큐로, 커넥션이 받아들여졌지만 아직 처리가 이루어지지 않은 요청이다
    • 이 큐가 꽉 차면 새로운 연결요청이 거부된다
  • 작업큐: 요청을 실제로 처리하는 스레드가 처리할 요청을 저장하는 큐이다.
    • 이 큐가 꽉 차면 작업을 처리할 수 없으므로 추가 요청을 거부하거나 대기시킬 수 있다
  • 대기큐와 작업큐는 다른 의미이다
    • 예를 들어 8300개의 동시요청이 들어오면 8192개는 요청을 하고 100개는 대기를 하고 8개는 거절한다
    • 200개의 스레드가 8192개의 요청을 처리하는 개념이다
    • 작업큐의 용량은 무한이지만 최대 커넥션수의 8192이므로 그 이상 연결 요청이 오면 대기를 하거나 거절을 한다
public Tomcat() {
            this.maxConnections = 8192; // 최대 커넥션 수
            this.acceptCount = 100; // 대기큐의 크기
            this.maxKeepAliveRequests = 100; // 최대 연결유지 요청 수
        }
public static class Threads {
            private int max = 200; // 최대 스레드 수
            private int minSpare = 10; // 최소 활성화 스레드 수
            private int maxQueueCapacity = Integer.MAX_VALUE; // 작업큐의 최대 용량
 }

스레드풀

  • 프로그램 실행에 필요한 Thread를 미리 생성해놓는다는 개념이다
  • 요청이 들어올 때마다 쓰레드를 생성하고 소멸하게 된다면 OS와 JVM에게 큰 부담을 안겨준다
    • 또한 동시에 일정 이사의 다수요청이 들어온다면 리소스(CPU와 메모리 자원) 소모에 대한 억제가 어렵다. 즉 순간적으로 서버가 다운되거나 동시다발적인 요청을 처리하지 못하는 문제가 발생할 수 있다
  • 해당 문제를 해결하기 위해 톰캣은 Tomcat 3.2 이후부터 스레드풀을 활용했다
  • 스레드가 너무 많으면 CPU의 자원을 두고 경합하게 되므로 처리속도가 느려질 수 있다
  • 스레드가 적으면 CPU자원을 최적으로 활용하지 못하여 처리속도가 느려질 수 있다. 스레드는 적절한 수로 유지되는 것이 좋다

동작원리

  1. 요청이 들어올 때 스레드를 생성한다
    1-1. core-size(내가 설정한 기본 스레드수)만큼 스레드를 생성한다
  2. 유저 요청이 들어올 때마다 작업큐에 담아둔다
  3. core-size의 스레드 중 유후상태(idle)인 스레드가 있다면 작업 큐에서 작업을 꺼내 스레드에 작업을 할당하여 작업을 처리한다
    3-1. 만약 유휴상태인 스레드가 없다면 작업은 작업 큐에서 대기한다
    3-2. 그 상태가 지속되어 작업큐가 꽉 찬다면, 스레드를 새로 생성한다
    3-3. 스레드 최대 사이즈에 도달하고 작업큐도 꽉 차게 된면 추가 요청에 대해서는 설정에 따라 거절, 예외 등을 한다
  4. 태스크가 완료되면 스레드는 다시 유휴상태로 돌아간다
    4-1. 작업큐가 비어있고 core-size이상의 스레드가 생성되어 있다면 스레드를 제거한다

BIO Connector와 NIO Connector

  • Tomcat 8.0부터 NIO Connector이 기본으로 채택되었다
  • Tomcat 9.0부터 BIO Connector가 deprecate되었다

1). BIO Connector

  • Blocking I/O Connector
  • 커넥션을 처리할 때 Java의 기본적인 I/O기술을 사용한다
  • Thread Pool에 의해 관리되는 Thread는 요청을 처리하고 응답한 후 소켓 연결이 종료되면 Pool에 다시 돌아온다
    • 즉 conneciton이 닫힐 때까지 하나의 thread는 특정 connection에 계속 할당된다
  • 이렇게 되면 동시에 사용되는 Thread 수가 동시 접속할 수 있는 사용자의 수가 될 것이다
    • Thread들이 충분히 사용되지 않고 유휴상태로 낭비되는 시간이 많이 발생한다
    • 이러한 문제점을 해결하고 Thread를 효율적으로 사용하기 위해 NIO Connector가 등장했다

2). NIO Connector

  • NonBlocking I/O Connector
  • I/O가 아니라 Http11NioProtocol을 사용한다. 자바 NIO를 확인해보자
  • NIO Connector에선 Poller라고 하는 별도의 스레드가 커넥션을 처리한다
    • Poller는 Socket들을 캐시로 들고 있다가 해당 Socket에서 data에 대한 처리가 가능한 순간에만 Thread를 할당하는 방식을 사용해서 Thread가 유휴상태로 낭비되는 시간을 줄여준다
  • Selector를 활용해 Socket을 관리하므로 더 적은 스레드를 사용한다. 또한 max-connections값까지 접속을 유지하고, 스레드가 모자라다면 max 사이즈까지 스레드를 추가한다.
    • time-wait시간 안에 처리가 가능하다면 처리할 수 있다

동작 방식

  1. Acceptor: Socket Connection을 Accept한다
    1-1. Socket에서 Socket Channel 객체를 얻어서 톰캣의 NioChannel 객체로 변환한다
    1-2. NioChannel 객체를 PollerEvent라는 객체로 캡슐화하여 Event Queue에 넣는다
    1-3. Acceptor는 Event Queue의 공급자, Poller Thread는 Event Queue의 사용자이다
  2. Poller는 NIO Selector를 사용하여 등록된 여러 채널을 관리하고, select 동작을 통해 읽을 수 있는 채널을 식별합니다.
    2-1. Selector에는 다수의 채널이 등록되어 있고, Select 동작을 수행하여 데이터를 읽을 수 있는 소켓을 얻는다
    2-2. Worker Thread Pool에서 이용할 수 있는 Worker Thread를 얻어서 해당 소켓을 Worker Thread에게 넘긴다
  3. Poller에선 Max Connection까지 연결을 수락하고, Selector을 통해 채널을 관리하므로 작업큐 사이즈와 관계없이 추가로 커넥션을 거절하지 않고 받아 놓을 수 있다
  4. 스레드 또한 모자라다면 Max-Size까지 스레드를 추가한다

참고) https://velog.io/@sihyung92/how-does-springboot-handle-multiple-requests

0개의 댓글