ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Tomcat Thread의 수는 얼마나 늘리는게 좋을까?
    성능테스트 2023. 8. 9. 00:01
    728x90

    개요

    "Tomcat의 Thread의 수는 얼마나 늘리는 게 좋을까요?"라는 궁금증을 가지고 포스팅을 시작합니다.

     

    다음은 Apache Tomcat의 공식문서에 대한 maxThreads의 설명입니다.

    The maximum number of request processing threads to be created by this Connector, which therefore determines the maximum number of simultaneous requests that can be handled. If not specified, this attribute is set to 200. If an executor is associated with this connector, this attribute is ignored as the connector will execute tasks using the executor rather than an internal thread pool. Note that if an executor is configured any value set for this attribute will be recorded correctly but it will be reported (e.g. via JMX) as -1 to make clear that it is not used.

    간단하게 해석해 보면 Connector에 의해 생성되는 최대 요청 처리 스레드 수는 200개가 기본값입니다.

     

     

    Tomcat의 Thread 수 기본설정은 왜 200개 인가?

    SpringBoot에서는 최대 요청 스레드의 수가 200개로 설정된 Tomcat을 활용합니다.

    "그렇게 되면 201개의 요청부터는 대기하게 될 텐데 왜 1000개, 10000개가 아닌 200개 스레드만 사용할까요?"

     

    각 스레드에는 스택(stack)이 있고 스택은 RAM을 소비합니다. 또한 콘텍스트 스위칭의 작업은 JVM/OS 커널에 무거운 작업입니다.

    그렇게 때문에 많은 스레드는 많은 메모리를 사용하게 됩니다.

     

    "그러면 RAM의 메모리를 늘리면 안 될까요?"

     

    하지만 RAM을 늘리더라도 다른 스레드 수를 처리하는 데는 제한이 있습니다.

    이는 CPU의 Core와 연관이 있습니다.

    예를 들어 CPU의 Core가 8개라면 최대 8개의 스레드만 동시에 처리할 수 있습니다.

     

    Thread는 많을수록 좋은가?

    스레드는 과연 많을수록 좋을까요?

    처리할 수 있는 개수가 늘어나니 좋을 것 같지만 아래와 같은 단점들이 존재합니다.

     

    1. 스레드를 생성하는데 비용이 든다.
    2. 스레드 간의 콘텍스트 스위칭(Context Switching)이 발생하여 오버헤드가 발생한다.

     

    CPU에서 실행되던 스레드(t1)는 짧은 시간 동작하고 다른 스레드(t2)로 바뀌게 됩니다.

    현재까지 진행한 기존 스레드(t1)의 정보를 저장하고 이전 스레드(t2)의 정보를 다시 가져와 작업을 진행해야 합니다.

    이런 작업을 Context Switching이라 하며이 과정 중에 오버헤드가 발생하게 됩니다.

     

    이에 따라 적절한 수를 측정하는 것이 중요합니다.

     

    이뿐만 아니라 어디에서 병목이 발생할지 모르기 때문에 Tomcat의 Thread수도 고려해야 하고, DB Connection Pool Thread도 고려해야 합니다. (심지어 OkHttp 라이브러리 등에서 Connection Pool도 고려해야 할 수 있습니다.)

     

    더 나아가서 외부 시스템을 호출하는 경우에는 해당 외부 시스템이 감당할 수 있는 적절한 TPS만큼한 호출해줘야 할 수도 있습니다.

     

    따라서 스레드는 많을수록 좋지 않을 수 있습니다.

     

    Thread와 Virtual Thread 그리고 Coroutine

    위에서 소개한 것처럼 Thread를 생성하는데 비용이 들기 때문에 이에 대한 대안책으로 JDK 21에서는 가상스레드(Vitual Thread)를 도입하기도 하였고 Kotlin에서는 경량스레드가 부르는 Coroutine을 도입하기도 하였습니다.

     

    또한 스레드마다 하나의 요청을 처리하지 않고 event 기반으로 스레드당 여러 요청을 처리하는 Reactive Programming이라는 개념이 등장하기도 하였습니다.

     

    Spring Boot Tomcat ThreadPool 설정 방법

    server:
      tomcat:
        threads:
          max: 200 # 생성할 수 있는 thread의 총 개수
          min-spare: 10 # 항상 활성화 되어있는(idle) thread의 개수
        accept-count: 100 # 작업 큐의 사이즈

    Spring Boot에서는 위와 같은 yml설정을 토대로 Thread의 설정들을 사용할 수 있습니다.

     

    maxConnections와 maxThreads의 차이

    다음은 maxConnections에 대한 설명입니다.

    The maximum number of connections that the server will accept and process at any given time. When this number has been reached, the server will accept, but not process, one further connection. This additional connection be blocked until the number of connections being processed falls below maxConnections at which point the server will start accepting and processing new connections again. Note that once the limit has been reached, the operating system may still accept connections based on the acceptCount setting. The default value varies by connector type. For NIO and NIO2 the default is 10000. For APR/native, the default is 8192.
    For NIO/NIO2 only, setting the value to -1, will disable the maxConnections feature and connections will not be counted.

    간단하게 해석해 보자면 서버가 수락하고 처리할 수 있는 최대 연결수이며 NIO/NIO2의 경우에는 10000개, APR/native의 경우에는 8192라고 합니다.

     

    maxConnections와 maxThreads는 어떤 차이가 있을까요?

    Thread와 Connection에 대한 관계를 먼저 이해해야 합니다.

     

    Tomcat은 2가지 모드로 작동할 수 있습니다.

    • BIO - blocking I/O (thread 당 하나의 connection)
    • NIO - non-blocking I/O (threads당 여러 개의 connections)

     

    Tomcat 7에서는 BIO가 기본이었지만 8.5 이상 버전에서는 BIO를 사용하지 않습니다.

    여기서 Connector는 클라이언트의 요청을 받아 Socket Connection을 수행하며 이를 토대로 Servlet이 처리할 수 있는 HttpServlertRequest를 생성합니다.

     

    따라서 thread가 200개라면 thread는 각각 여러 개의  connection을 가질 수 있습니다.

     

    여기서 저는 Spring Boot 3.x.x를 사용하고 있기 때문에 tomcat은 10 버전이상입니다.

    10.1.8 버전

    위의 예시처럼 gradle 의존성을 통해 어떤 버전의 tomcat인지 확인해 볼 수 있습니다.

     

    그러면 서버는 maxThreads가 1개여도 2개 이상의 요청을 처리할 수 있을까?

    server:
      tomcat:
        threads:
          max: 1 # 생성할 수 있는 thread의 총 개수

    Tomcat의 Thread가 1개만 생성하도록 설정해 보았습니다.

     

    @Bean
    public TomcatWebServerFactoryCustomizer tomcatWebServerFactoryCustomizer(Environment environment,
        ServerProperties serverProperties) {
            return new TomcatWebServerFactoryCustomizer(environment, serverProperties);
    }

    TomcatWebserverFactoryCustomizer에서 디버깅포인트를 찍어보면 max-threads가 잘 설정된 것을 확인할 수 있습니다.

     

    2로 표기된것은 max: 2로 설정 값을 놓고 테스트했을때 스크린샷을 남겨서 그렇습니다..

     

    Controller 구성 및 호출 테스트

    @RestController
    class HttpMaxConnectionTestController(
    ) {
        @GetMapping("/long-time")
        fun `5초 걸리는 메서드입니다`(): String{
            sleep(5000)
            return "sorry, i'm late"
        }
    }

    간단하게 RestController를 구성하여 Thread를 5초 동안 sleep 하고 문자열을 반환하는 HTTP Endpoint를 하나 구성하였습니다.

     

    호출 테스트 결과

    tomcat의 thread가 1개일 때

    Thread가 1개이기 때문에 동시 요청을 Thread 1개가 1개의 요청을 처리합니다.

    request 1~8으로 증가함에 따라 5초 간격으로 수행시간이 증가하는 모습을 볼 수 있습니다.

    (5초.. 9초.. 14초 ~ 증가함)

     

     

    Thread의 수가 2개일 때

    tomcat의 thread가 2개일 때

    Tomcat의 Thread의 개수 설정을 2개로 늘려보았습니다.

    이제 2개의 스레드가 각각 1건씩 처리하기 때문에 request1, request2이 5초 만에 수행되었고, request3, request4는 10초 만에 수행된 모습을 확인할 수 있습니다.

     

     

    thread의 수가 8개일 때

    8개의 스레드가 각각 하나의 task를 동시에 처리되기 때문에 8개의 요청에 대해 5초 만에 모든 일이 끝나게 됩니다.

     

    그러면 maxConnections 수가 1이라면 어떻게 될까?

    server:
      tomcat:
        threads:
          max: 20 # 생성할 수 있는 thread의 총 개수
        max-connections: 1

    이번에는 스레드의 개수는 20개로 설정하였으며, max-connections은 1개로 설정하였습니다.

     

    스레드는 20개로 넉넉하게 주었지만 http 연결을 처리하는 Connector의 수가 1개이기 때문에 동시 요청을 한건씩 처리합니다.

    request 2~8으로 증가함에 따라 수행시간이 5초씩 증가하는 모습을 볼 수 있습니다.

    테스트를 통하여 Thread, maxConnections 중 더 작은 값으로 병목이 발생하는 것을 이해할 수 있습니다.

    일반적으로는 Thread의 기본값은 200이고 maxConnections는 8192이기 때문에 Thread에서 병목이 발생할 가능성이 큽니다.

     

    그래서 Tomcat Thread 수는 얼마나 늘리는 게 좋은데?

    Tomcat의 Thread를 설정하는 것에 대한 정답은 없으며 애플리케이션의 특정 요구 사항 및 특성에 따라 다릅니다.

    저는 개인적으로 3가지 방법을 제안해보려 합니다.

     

    첫 번째로 스레드 개수에 대한 공식을 통해 기본적인 설정을 해볼 수 있습니다.

     

     

    두 번째로는 DB나 외부 의존성이 감당할 수 있는 목표하는 적절한 TPS를 두고 성능테스트를 통해 적절한 값을 찾아나갈 수 있습니다.

     

    세 번째로는 우리의 Application이 CPU bound인지 I/O bound인지 고려해서 적절한 스레드를 산정할 수 있습니다.

     

    예를 들어 위와 같은 케이스로 외부 네트워크 호출 또는 I/O 작업으로 인해 상당한 대기 시간이 있는 시나리오에서는 200개의 스레드 보다 스레드 수를 늘리는 것이 유리할 수 있습니다.

     

    반대로 CPU, 메모리등의 리소스 제한이 있거나 애플리케이션이 일련의 순차적 계산 실행을 수행할 때는 경합이 발생하여 스레드 수를 늘리는 것이 불리할 수 있습니다.

     

     

     

    참고자료

    https://tomcat.apache.org/tomcat-8.5-doc/config/http.html

    https://stackoverflow.com/questions/24678661/tomcat-maxthreads-vs-maxconnections

    https://code-lab1.tistory.com/269

    https://alpitanand20.medium.com/tomcat-why-just-200-default-threads-febd2411b904

     

    댓글

Designed by Tistory.