devoong2
devoong2
devoong2
전체 방문자
오늘
어제

인기 글

최근 글

  • Category (35)
    • Java (4)
    • Spring (13)
    • JPA (4)
    • DesignPattern (1)
    • 동시성 (Concurrency) (4)
    • 회고 (1)
    • Redis (1)
    • Network (3)
    • Kafka (2)
    • Spring Batch (2)

최근 댓글

반응형
hELLO · Designed By 정상우.
devoong2
Spring

[Tomcat] Tomcat Thread Pool 설정 정리 및 테스트

[Tomcat] Tomcat Thread Pool 설정 정리 및 테스트
Spring

[Tomcat] Tomcat Thread Pool 설정 정리 및 테스트

2024. 8. 5. 15:54
반응형

예제 및 테스트 코드는 github 에서 확인 가능합니다.

Apache Tomcat 설정에 대해 알아보자

안녕하세요 이번에는 스프링에서 쓰이는 Apache Tomcat 의 설정 옵션들
그리고 Thread 설정에 따라 어떻게 동작하는지에 대해 한번 알아보려합니다.

Tomcat 의 설정 옵션

application.yml

server:
  tomcat:
    max-connections: 8192
    accept-count: 100
    threads:
      max: 200
      min-spare: 10

다음은 스프링 부트에서 톰캣을 설정에 대한 옵션입니다.
각 옵션별로 하나씩 알아보겠습니다.

threads.max

생성할수있는 최대 thread 의 갯수이며 실제 Active User 수를 뜻합니다.
즉, 순간 처리가능한 트랜잭션의 수를 의미합니다.

threads.min-spare

항상 활성화 되어있는(idle) 스레드 갯수입니다.

accept-count

maxConnections 이상의 요청이 들어왔을때 대기하는 대기열(Queue)의 길이입니다.
큐가 꽉 찼을 때 수신된 모든 요청은 거부됩니다.

해당 설정은 OS 에서 관리하는 설정이며 무시될 수도 있습니다.

max-connections

서버가 수락하고 처리할 수 있는 최대 연결수를 의미합니다. 이 숫자에 도달하면 서버는 연결을 추가로 수락하지만 처리는 하지 않습니다.

처리 중인 연결 개수가 maxConnections 아래로 떨어지면 다시 새 연결을 수락하고 처리하기 시작합니다.
(주의할 점은 이 수는 실제 연결된 Connection 수가 아닌 Socket File Descriptor 의 수 입니다.)

 

maxConnections 에 연결이 가득 차더라도 운영체제는 acceptCount 만큼 추가 연결을 허용합니다.

Socket 은 Connection 이 끝난 후 바로 반환되지 않고 Connection 종료 후 FIN 신호, TIME_WAIT 시간을 거쳐서
Socket 의 Connection 을 종료합니다.

 

그리고 Http 의 KeepAlive 옵션을 사용하게되면 요청을 처리하지 않는 Connection 수도 유지되기에, 요청 처리 수보다
실제 연결되어있는 Connection 수가 높을 수 있습니다.

이러한 이유때문에 maxConnections 은 넉넉하게 설정하는것이 좋습니다.

 

NIO/NIO2의 경우에는 10000개, APR/native의 경우에는 8192개가 기본값입니다.

 

정리하자면 Tomcat 의 스레드 풀 매커니즘은 다음과 같습니다.

 

1) Tomcat 서버가 처음 실행될때 스레드 풀에 min-spare 만큼의 유휴 스레드를 생성합니다.
2) 이후 요청이 유휴 스레드보다 많은 요청이 들어오게 되면 maxConnections 이 증가하고 maxConnections 이 가득차면 
요청을 처리할 maxThreads 수가 증가합니다. (최대 maxThreads 만큼 증가)
3) 요청이 많아 maxConnections 가 가득차고 이후 추가 요청이 들어온다면 acceptCount 만큼 추가로 연결을 허용합니다.

 

Http Connector

Tomcat 서버는 HttpConnector 를 통해 요청을 수신하고 처리합니다.
그리고 Tomcat 버전별로 사용하는 HttpConnector 가 상이한데요 아래와 같습니다.

BIO(Blocking I/O) Connector

요청이 들어와 TCP 커넥션이 생성되면 하나의 Thread 가 할당됩니다.
그리고 요청에 대해 응답하기까지 하나의 Thread 에서 처리되며 응답 이후 소켓 연결이 닫히고 나서야 Thread 는 Pool 에 반환됩니다.

즉, 커넥션과 Thread 가 1:1 로 매핑됩니다.


이는 Thread 가 Idle 상태로 있는 시간이 길어져서 낭비되는 시간이 많을 수 있습니다.

만약 요청이 들어왔을때 할당할 수 있는 Idle Thread 가 없다면 요청은 Block 됩니다.

NIO(Non Blocking I/O) Connector

BIO 와 달리 소켓 연결을 담당하는 Poller 라는 Thread 가 존재합니다.
요청이 들어오면 Poller 는 Socket 들을 캐시로 들고있다가 처리가 가능한 순간에만 Thread 를 할당합니다.

그리고 NIO Connector 는 Thread 는 응답을 보내면 바로 Pool 로 반환됩니다.
그렇기에 BIO Connector 에 비해 Thread 가 Idle 상태로 있는 시간이 짧기에 효율적입니다.

 

Tomcat 설정옵션에 따른 테스트

SpringBoot version: 3.3.2
Tomcat version: 10.1.26

Tomcat 은 10.1.26 버전이기에 NIOConnector 를 사용합니다.
(BIO Connector 는 Tomcat 9 버전부터 Deprecated 되었습니다)

모든 테스트는 10개의 스레드를 동시에 요청하는것으로 진행하겠습니다.

 

ThreadPoolController.java

@Slf4j
@RestController
@RequestMapping(value = "/")
public class ThreadPoolController {

  @GetMapping
  public ResponseEntity<String> api() {
    try {
      Thread.sleep(5000);
      log.info("http call");
    } catch (InterruptedException e) {
      throw new RuntimeException(e);
    }

    return new ResponseEntity<>("OK", HttpStatus.OK);
  }
}

요청이 시간의 흐름에 따라 어떻게 처리되는지 확인하기 위해 컨트롤러에서 5초간 sleep 하도록 하겠습니다.

 

ThreadPoolTest.java

public class ThreadPoolTest {

  private static final HttpClient httpClient = HttpClient.newBuilder()
    .version(HttpClient.Version.HTTP_1_1)
    .build();

  @Test
  void test() {
    final HttpRequest request = HttpRequest.newBuilder()
      .uri(URI.create("http://127.0.0.1:8080"))
      .build();

    Supplier<HttpResponse<String>> task = () -> {
      try {
        return httpClient.send(request, HttpResponse.BodyHandlers.ofString());
      } catch (Exception e) {
        throw new RuntimeException(e);
      }
    };

    List<CompletableFuture<HttpResponse<String>>> futures = new ArrayList<>();
    for (int i = 0; i < 10; i++) {
      CompletableFuture<HttpResponse<String>> future = CompletableFuture.supplyAsync(task);
      futures.add(future);
    }

    CompletableFuture<Void> combinedFuture = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));
    combinedFuture.join();
  }

}

 

1. maxConnections 과 aceeptCount, maxThreads 가 모두 충분한 경우

server:
  tomcat:
    max-connections: 10
    accept-count: 10
    threads:
      max: 10

10개 요청 모두 한번에 잘 처리되었습니다.

 

2. acceptCount 는 부족하고 maxConnections 와 maxThreads 가 충분한 경우

server:
  tomcat:
    max-connections: 10
    accept-count: 2
    threads:
      max: 10

maxConnections 의 수가 10개 요청은 한번에 처리 가능한 수여서 모두 한번에 처리된것을 확인할 수 있습니다.

 

3. maxConnections 는 부족하고 aceeptCount 와 maxThreads 가 충분한 경우

server:
  tomcat:
    max-connections: 2
    accept-count: 10
    threads:
      max: 10

65초 간격으로 두개의 요청이 들어온것을 확인할 수 있습니다.

10개의 요청이 오면 우선 maxConnections 에 두개의 요청이 Queue 로 들어가고 나머지 요청은
acceptCount 에서 대기하게 됩니다.

 

그리고 두개가 처리 완료되었다면 acceptCount 에서 maxConnections 로 두개의 요청이 다시 이동해야 합니다.
그렇다면 5초간격으로 수행되어야 할것같은데 그렇지 않았습니다.

왜 이렇게 동작한걸까요??

 

maxConnections 은 요청의 Connection 이 끝나더라도 Socket 이 바로 종료되지 않는다고 위에서 이야기 했습니다.

Socket 은 Connection 연결해제 후 종료되기까지 FIN 신호 및 TIME_WAIT 시간을 거쳐서 종료되는데요.
이때 TIME_WAIT 이 과정이 60초가 소요된 것입니다.

 

그리고 컨트롤러에서 걸어놓은 sleep 시간은 5초입니다. 그래서 maxConnections 에 대해 Socket 이 종료되고
다음 요청에 대한 로그가 찍히기 까지 65초의 간격을 두고 요청을 처리하게 된겁니다.

 

WireShark 로 패킷 확인해보기

이 과정을 좀 더 자세히 보기 위해 WireShark 를 이용해 패킷이 어떻게 요청되고 반환되는지 확인해보겠습니다.

52546 port 를 기준으로 따라보겠습니다.

 

1) TCP 연결 요청

52546 Port 가 8080 Port 에게 TCP 요청을 하기 위해 3-Way-Handshake 를 진행하는것을 확인할 수 있습니다.
이때 시간은 4.43초 입니다.

 

2) HTTP 요청

TCP 연결에 성공했으니 이후 HTTP 요청을 진행합니다. 이후 서버는 요청을 잘 받았다는 신호로 ACK 를 응답합니다.
이때 시간은 4.44초 입니다.

 

3) HTTP 응답 반환 및 TCP 연결 해제 요청

컨트롤러에서 5초간 Sleep 후에 HTTP 응답을 반환합니다.
이때 시간은 요청 후 약 5초가 지난 9.46초 입니다.

 

그리고 마지막 패킷 로그를 보시면 응답을 받자마자 TCP 연결 해제를 위해 서버에게 ACK 신호를 전달합니다.

이 과정이 4-Way-Handshake 의 시작입니다.

 

4) TCP 연결 해제

TCP 연결 해제를 위해 4-Way-Handshake 를 진행합니다.
연결해제 요청을 9.46초에 보냈지만 FIN, ACK 응답이 60초 뒤인 69초에 온것을 볼 수 있습니다.

 

이때 서버는 자신의 통신이 끝날때까지 기다리는 TIME_WAIT 상태를 거치게 되는데 이 시간이 60초가 걸려서 그렇습니다.

그리고 클라이언트는 FIN,ACK 응답을 받고 확인했다는 ACK 를 서버에게 보냅니다.


마지막으로 서버는 ACK 신호를 받고 Socket을 종료합니다.

 

4. maxConnections, acceptCount 모두 부족하고 maxThreads 는 충분한 경우

server:
  tomcat:
    max-connections: 5
    accept-count: 2
    threads:
      max: 10

처음 maxConnections 만큼 5개의 요청을 처리하고 이후 acceptCount 만큼의 2개의 요청을 처리한것을 확인할 수 있습니다.

총 요청은 10개이지만 수신할 수 있는 큐의 사이즈는 총 7개로 제한되어서 7개의 요청만 처리되었습니다.

 

나머지 요청에 대해서는 IOException 이 발생했습니다.

 

5. maxConnections, acceptCount 는 충분하지만 maxThreads 가 부족한 경우

server:
  tomcat:
    max-connections: 10
    accept-count: 10
    threads:
      max: 2

maxThreads 만큼 5초 간격으로 10개의 요청을 모두 처리한것을 확인할 수 있습니다.

 


Tomcat 스레드 갯수를 어떻게 설정해야할까?

어플리케이션의 성격과 요구사항 그리고 처리량에 따라 다르겠지만 다음 부분을 고민해보면 좋을것 같습니다.

CPU Bound, I/O Bound

어플리케이션의 성격이 주로 어떤 작업을 하는지에 대해 고민해볼 수 있습니다.

CPU Bound

  • CPU 연산에 의존하는 작업 (CPU Burst)
  • 머신 러닝 작업, 동영상 편집 작업 ..

I/O Bound

  • I/O 작업을 요청하고 결과를 기다리는 작업 (I/O Burst)
  • DB 접근, 네트워크 요청, 파일 입출력 ..

일반적으로 CPU Bound 의 경우 대기시간이 거의 없기때문에 많인 스레드 수가 필요하지 않습니다.
그렇기에 코어수 만큼의 스레드 수가 있어도 충분합니다.

 

하지만 I/O Bound 의 경우 대기시간이 많기에 대기시간을 효율적으로 처리할만큼의 스레드 갯수가 있는게
작업을 더 효율적으로 처리할 수 있습니다.

 

Brian Goetz 의 Java Concurrency in Practice 라는 책에서 다음과 같은 공식을 권장합니다.

여기서 대기시간은 스레드가 대기하는 시간과 I/O 작업시 대기하는 시간을 모두 포함합니다.
서비스 시간은 대기시간을 제외한 실제 작업이 수행되는 시간을 뜻합니다.

TPS 와 성능 테스트

스레드 개수에 정답은 없으므로 가장 중요한것은 어플리케이션의 성격과 현재 서비스의 상황에 맞게
적정 스레드 개수를 찾는것이 중요하지 않을까 싶습니다.

 

평소엔 잠잠하다가 특정 시간에만 TPS 가 급증하는 서비스도 있고, 전체적으로 적당량의 수치를 유지하는 서비스도 존재합니다.

서비스 성격에 알맞는 TPS 와 AutoScaling 을 고려한 성능 테스트를 진행하며 서비스에 맞는 적정 스레드 개수를
찾아내는것이 가장 좋은 방법이라 생각합니다.

 

감사합니다.

 


reference

  • https://tomcat.apache.org/tomcat-8.0-doc/config/http.html#Connector_Comparison
  • https://hudi.blog/tomcat-tuning-exercise/
  • https://velog.io/@jihoson94/BIO-NIO-Connector-in-Tomcat
  • https://bcho.tistory.com/788
  • https://code-lab1.tistory.com/269
반응형

'Spring' 카테고리의 다른 글

[Spring Boot] Hikari CP 의 옵션과 설정방법  (0) 2025.01.05
테스트 성능 및 정합성 개선하기(with ContextCaching, TestContainer)  (0) 2024.11.11
[Spring] @Async 사용 방법 및 TaskExecutor, ThreadPool  (0) 2024.07.07
[Spring] Spring Event 를 이용한 비동기 이벤트 처리  (0) 2024.06.24
[Spring] ContextCaching 으로 Test 성능 개선하기 (@MockBean, @SpyBean)  (0) 2024.06.02
  • Tomcat 의 설정 옵션
  • threads.max
  • threads.min-spare
  • accept-count
  • max-connections
  • Http Connector
  • BIO(Blocking I/O) Connector
  • NIO(Non Blocking I/O) Connector
  • Tomcat 설정옵션에 따른 테스트
  •  
  • 1. maxConnections 과 aceeptCount, maxThreads 가 모두 충분한 경우
  •  
  • 2. acceptCount 는 부족하고 maxConnections 와 maxThreads 가 충분한 경우
  •  
  • 3. maxConnections 는 부족하고 aceeptCount 와 maxThreads 가 충분한 경우
  • 4. maxConnections, acceptCount 모두 부족하고 maxThreads 는 충분한 경우
  •  
  • 5. maxConnections, acceptCount 는 충분하지만 maxThreads 가 부족한 경우
  • Tomcat 스레드 갯수를 어떻게 설정해야할까?
  • CPU Bound, I/O Bound
  • TPS 와 성능 테스트
'Spring' 카테고리의 다른 글
  • [Spring Boot] Hikari CP 의 옵션과 설정방법
  • 테스트 성능 및 정합성 개선하기(with ContextCaching, TestContainer)
  • [Spring] @Async 사용 방법 및 TaskExecutor, ThreadPool
  • [Spring] Spring Event 를 이용한 비동기 이벤트 처리
devoong2
devoong2
github 주소: https://github.com/limwoobin

티스토리툴바

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.