Tomcat 은 어떻게 동작할까? - Spring 과의 연동을 중점으로 (4)

정원식·2023년 12월 16일
0

23년 5월에 작성한 글입니다.

개요

  • 본 시리즈에서는 사용자의 요청이 톰캣에서 어떻게 처리되어 우리가 작성한 비즈니스 로직에 도달하는지에 대해 다룹니다.
  • 마지막 편에서는 만약 스레드가 한개인 경우, 무슨 일이 일어나는지 살펴봅니다. 그리고 Virtual Thread 를 적용해봅니다.

사용한 버전

  • Servlet: 4.0.1
  • Tomcat: 9.0.60
  • Spring Boot: 2.6.6
  • Spring WebMvc: 5.3.18

IF: 스레드가 한개라면?

  • 만약 스레드풀의 스레드가 한개라면 동시에 많은 요청이 들어왔을때 무슨일이 일어날까요?
    스레드가 한개인 상태에서 많은 요청이 들어온다면 커넥션 생성이 실패하지 않을까요?
  • 크게 두단계로 구분지을수 있습니다.
    1. ThreadPool 레벨
    2. Acceptor 레벨

ThreadPool 레벨

  • 아래 프로퍼티에 따라 스레드풀이 생성됩니다. (기본적으로 org.apache.tomcat.util.thread.ThreadPoolExecutor)
    • server.tomcat.threads.max: 최대 스레드 갯수
    • server.tomcat.threads.min-spare: 최소 스레드 갯수
  • 기본적으로 org.apache.tomcat.util.thread.ThreadPoolExecutor 을 사용합니다.
    • 해당 Executor 내에서 처리해야하는 요청을 org.apache.tomcat.util.threads.TaskQueue 통해 관리합니다.
      • TaskQueueLinkedBlockingQueue 을 상속하여 요청량이 스레드 갯수를 넘어도 문제없이 큐에 요청을 저장합니다.
  • 결론
    • 스레드풀은 요청에 대한 처리량과 관련이 있습니다.

Acceptor 레벨

  • 아래 프로퍼티에 따라 커넥션을 생성합니다.
    • server.tomcat.max-connections: 최대 커넥션 갯수
    • server.tomcat.accept-count: 연결 요청에 대한 최대 대기열 길이
  • 결론
    • Acceptor 는 커넥션 생성과 관련이 있습니다.
      -> 스레드풀의 스레드 갯수가 하나여도 커넥션 생성과는 관련이 없습니다.
  • 해당 프로퍼티가 사용되는 라인
// server.tomcat.max-connections
public class Acceptor<U> implements Runnable {

    public void run() {
        ...
        // 커넥션 갯수 카운트 업
        // 최대 커넥션 수(max-connections) 를 넘는 경우 대기
        endpoint.countUpOrAwaitConnection();

        if (endpoint.isPaused()) {
            continue;
        }

        U socket = null;
        try {
            // 커넥션 생성
            socket = endpoint.serverSocketAccept();
        } catch (Exception ioe) {
            // 커넥션 생성 실패시, 카운트 다운
            endpoint.countDownConnection();
            if (endpoint.isRunning()) {
                errorDelay = handleExceptionWithDelay(errorDelay);
                throw ioe;
            } else {
                break;
            }
        }
    }
}

// server.tomcat.accept-count
public class NioEndpoint extends AbstractJsseEndpoint<NioChannel,SocketChannel> {

    protected void initServerSocket() throws Exception {

        ...
        else if (getUnixDomainSocketPath() != null) {
            SocketAddress sa = JreCompat.getInstance().getUnixDomainSocketAddress(getUnixDomainSocketPath());
            serverSock = JreCompat.getInstance().openUnixDomainServerSocketChannel();

            // 소켓을 로컬 주소에 바인드
            // 최대 대기열 길이를 두번째 인자로 넘김
            serverSock.bind(sa, getAcceptCount());
            ...
        }
    }
}

IF: 가상스레드라면?

  • Virtual Thread 는 JDK 21 (Preview 는 JDK 19) 에 새로 추가된 경량 스레드로
    실제 작업이 일어나는 동안에만 기존의 Platform Thread 에 할당되어 실행됩니다.
  • Platform Thread 와 Virtual Thread 의 메모리와 응답 시간을 비교합니다.

사전 작업

  1. JDK 21 설치 OpenJDK JDK 21 Early-Access Builds
  2. Spring Boot 버전 2.6.15 로 버전업
    • 기존 2.6.6 의 경우, JDK 21 을 지원하지 않습니다.
    • Servlet: 4.0.1
    • Tomcat: 9.0.75
    • Spring WebMvc: 5.3.27
  3. 아래 가이드에 따라 Virtual Thread Pool 을 설정합니다.
  4. 스레드 갯수와 힙 메모리를 설정합니다.
    • 스레드 갯수: 50
    • 힙 메모리: 64MB
  5. Gatling 과 같은 성능 테스트 툴을 사용하여 초당 500개 요청에 대해 테스트합니다.

메모리 및 응답 시간 비교

  • 원래 기대하는 결과는 응답 시간은 Virtual Thread 를 사용하는 경우, 응답 시간은 비슷하나 메모리 사용량은 적은것이었습니다만.. (블로그: Virtual Thread 은 가벼운가?)
  • 실제 결과는 Virtual Thread 의 경우, OutOfMemoryError 가 발생하며 커넥션 생성에 실패했습니다.
    • Full GC 횟수가 계속 올라감에도 Old 영역의 메모리가 낮아지지 않는것으로 보아 GC 가 안되는 객체가 있을수 있다고 의심했으나
      초당 400개 요청에 대해서는 정상 처리되어서 아마도 가이드 에 나오는 한계로 인해 OOM 이 발생한게 아닐까 추측합니다.


  • 반면 Platform Thread 의 경우, 4% 의 실패율을 보였습니다.

결론

  • 스레드 수가 적어도 실제 클라이언트와의 커넥션은 더 생성할수 있습니다.
  • 추후 Tomcat 혹은 Spring Web Mvc 에서 Java Virtual Thread 도입을 기대해봅니다.

Reference

profile
매일매일 성장하고 싶은 백엔드 개발자입니다.

0개의 댓글