티스토리 뷰

학습 배경

우아한테크코스 6기 레벨4 미션 중, 톰캣의 커넥션과 스레드 풀 설정을 수정했을 때 동시에 여러 요청을 보낼 경우 톰캣이 어떻게 동작하는지 궁금했다. 톰캣 설정을 케이스별로 수정해보며 차이를 비교해보자. 코드는 우아한테크코스 tomcat 미션의 학습 테스트에서 확인할 수 있다. 이 글을 쓰면서 우아한테크코스 선배인 hudi님의 글에 많은 도움을 받았다. hudi 블로그의 링크에 들어가서 전체 구조 다이어그램을 보면 이해하기 쉽다. 

 

초기 설정

클라이언트는 10개의 요청을 동시에 보낸다. 처음 서버 설정 정보는 다음과 같다.

server:
  tomcat:
    connection-timeout: 60s # tomcat default
    accept-count: 10
    max-connections: 10
    threads:
      min-spare: 10
      max: 10

 

클라이언트 설정 정보는 다음과 같다. 

public class TestHttpUtils {

    private static final HttpClient httpClient = HttpClient.newBuilder()
            .version(HttpClient.Version.HTTP_1_1)
            .connectTimeout(Duration.ofSeconds(1))
            .build();

    public static HttpResponse<String> send(final String path) {
        final var request = HttpRequest.newBuilder()
                .uri(URI.create("http://localhost:8080" + path))
                .timeout(Duration.ofSeconds(1))
                .build();

        try {
            return httpClient.send(request, HttpResponse.BodyHandlers.ofString());
        } catch (IOException | InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

 

이를 정리하면 다음과 같다. 앞으로 케이스마다 다음 설정 정보를 명시한다. 

클라이언트 서버
connection time-out 1s connection time-out 60s
request time-out 1s 스레드수 10개
    최대 커넥션 수 10개
    요청 대기 큐 크기 10개
    서버 요청 처리 시간 500ms

 

Case1: 모든 설정이 충분한 경우

클라이언트 서버
connection time-out 1s connection time-out 60s
request time-out 1s 스레드수 10개
    최대 커넥션 수 10개
    요청 대기 큐 크기 10개
    서버 요청 처리 시간 500ms

 

서버에 요청이 들어온 시점에 로그를 남기는데, 10개의 요청을 빠르게 처리한 것을 확인할 수 있다. 10개의 요청이 들어온 데에는 시간 차이가 거의 없다. 

서버 로그

 

Case2: max-connection을 줄인 경우

클라이언트 서버
connection time-out 1s connection time-out 60s
request time-out 1s 스레드수 10개
    최대 커넥션 수 5개
    요청 대기 큐 크기 10개
    서버 요청 처리 시간 500ms

 

요청 시간을 확인하면, 처음 5개의 요청을 처리하고 나서 나머지 5개의 요청을 받았다. 서버 스레드는 10개의 요청을 동시 처리할 능력이 있지만, 맺을 수 있는 커넥션 수가 5개로 제한됐기에 때문이다. 나머지 요청은 대기 큐에 있다가 처리됐다. 

서버 로그

 

이번에는 클라이언트 로그를 보자. 10개의 요청 중 5개는 request timeout을 받았는데, 이것은 request-timeout 안에 응답을 받지 못했기 때문이다. 커넥션을 맺고 큐에서 기다렸지만 request-timeout인 1초 이상 응답을 받지 못해 6~10번째 요청은 타임아웃이 발생했다. 

클라이언트 로그

 

Case3: max-connection과 accept-count를 줄인 경우

클라이언트 서버
connection- time-out 1s connection time-out 60s
request time-out 1s 스레드수 10개
    최대 커넥션 수 5개
    요청 대기 큐 크기 1개
    서버 요청 처리 시간 500ms

 

처음 5개 요청은 바로 커넥션을 맺고 스레드를 할당받는다. 6번째 요청은 커넥션을 맺지 못하고 큐에 쌓인다. 7~10번째 요청은 큐에 들어가지 못하고 커넥션을 맺지 못한다. 앞선 5개의 요청이 처리된 후에 큐에 있던 6번째 요청이 들어온다. 

5번째 요청과 6번째 요청 사이에 요청 시간 차이는 1.5s가 발생하는데, 이것은 서버 요청 처리 시간 + 클라이언트의 request time-out 이다. 

서버 로그

 

클라이언트는 5개의 요청이 실패한 것을 볼 수 있다. 처음 5개 요청은 정상 처리됐지만, 6번째 요청은 request time-out을, 나머지 요청은 connection time-out이 발생했다. 6번째 요청은 대기 큐에서 기다리다 커넥션을 맺고 처리되던 중 request time-out이 발생했고, 나머지 요청은 대기 큐에 들어가지 못한 채 클라이언트의 connection time-out이 발생했다. 

클라이언트 로그

 

Case4: threads.max를 줄인 경우

클라이언트 서버
connection time-out 1s connection time-out 60s
request time-out 1s 스레드수 5개
    최대 커넥션 수 10개
    요청 대기 큐 크기 10개
    서버 요청 처리 시간 500ms

 

10개 요청은 바로 커넥션을 맺었지만 스레드가 5개 밖에 없어서, 5개가 먼저 처리된다. 

서버 로그

 

클라이언트는 1개의 request time-out을 받게 되는데, 마지막 요청이 아쉽게도 시간 안에 처리되지 못했다. 

클라이언트 로그

 

Case4-1: 클라이언트의 read-timeout을 늘린 경우

case4에서 클라이언트의 read-timeout을 1초에서 2초로 늘리면 어떻게 될까? 마지막 요청에 대한 응답까지 받을 수 있다. 

클라이언트 로그

 

Case5: 복잡한 케이스

복잡한 상황을 테스트 해보자. 

클라이언트 서버
connection time-out 1s connection time-out 60s
request time-out 1s 스레드수 6개
    최대 커넥션 수 5개
    요청 대기 큐 크기 3개
    서버 요청 처리 시간 500ms

 

서버가 맺을 수 있는 동시 커넥션 수는 5개지만 스레드는 6개이다. 처음 5개의 요청은 커넥션을 맺고 스레드를 할당 받는다. 6~8번 요청은 큐에 쌓인다. 9, 10번 요청은 큐에 쌓이지 못하고 connection time-out이 발생한다. 

서버 로그

 

클라이언트 로그를 보면, 3개의 요청은 커넥션 맺었지만 read-timeout이 발생했다. 나머지 2개의 요청은 connection-timeout이 발생했다. 정상적인 응답을 받은 요청은 5개이다.

클라이언트 로그

 

Case5-1: 스레드 수보다 최대 커넥션 수가 많은 경우

클라이언트 서버
connection time-out 1s connection time-out 60s
request time-out 1s 스레드수 5개
    최대 커넥션 수 6개
    요청 대기 큐 크기 3개
    서버 요청 처리 시간 500ms

 

Case5와 다른 점은 스레드 수와 최대 커넥션 수가 바뀌었다는 점이다. 이것으로 확인하고 싶은 부분은, 최대 커넥션 한도까지 맺지 않았지만 스레드가 남지 않은 경우에 요청이 커넥션을 맺는지 또는 요청 대기 큐로 가는지 여부이다. 이를 눈으로 보기 쉽게 서버 요청 처리 시간과 request time-out을 길게 늘렸다. 

 

이 경우에 처음 5개 요청은 커넥션을 맺고 스레드를 할당 받는다. 6번째 요청은 하나 남은 커넥션을 맺는다. 7~9번째 요청은 대기 큐에서 기다리고, 10번째 요청은 connection time-out을 받는다. 서버 로그를 확인하면 6번째 요청은 500ms 뒤에 서버에 도달하는데, 이것은 처음 5개의 요청을 처리하는 데에 500ms가 걸리기 때문이다. 처음 5개 요청을 처리한 뒤에 500ms만에 6번째 요청이 처리되는 것으로 보아, 처리된 요청은 스레드는 놓은채 request time-out이 끝날 동안 커넥션만 잡고 있다. 7~9번째 요청은 처음 5번 요청 후에 약 1.5초 뒤에 실행되는 것을 확인할 수 있다. 서버 요청 처리 시간과 request time-out을 더한 값이다. 

서버 로그
클라이언트 로그

 

Case6: 클라이언트 read time-out 늘린 경우

클라이언트 서버
connection time-out 1s connection time-out 60s
request time-out 1s 스레드수 10개
    최대 커넥션 수 5개
    요청 대기 큐 크기 10개
    서버 요청 처리 시간 500ms

 

위 설정에서 클라이언트의 request time-out만 1s에서 5s로 변경해보자. 처음 5번 요청과 나머지 요청 시간 차이가 1.5s에서 5.5s로 증가했다. 서버의 커넥션은 요청을 처리한 후에도 클라이언트의 request time-out이 끝날 때까지 기다린다. 

 

클라이언트 request time-out: 1s
클라이언트 request time-out: 5s

 

Case7: Case5를 해결해보자

Case5에서 5개의 요청만 정상 처리되고 6~8번째 요청은 request time-out, 9~10번째 요청은 connection time-out이 발생했다. 6~8번째 요청도 정상 처리되게 하려면 어떻게 해야 할까? Case5에서 사용한 설정은 다음과 같다. 

 

클라이언트 서버
connection time-out 1s connection time-out 60s
request time-out 1s 스레드수 6개
    최대 커넥션 수 5개
    요청 대기 큐 크기 3개
    서버 요청 처리 시간 500ms

 

클라이언트 로그

 

어떻게 하면 6~8번째 요청에 대한 응답을 받을 수 있을까? 고민해 보자! 

 

Case8: 서버가 connection time-out을 걸어야 하는 이유

현재 버전에서 내장 톰캣의 connection-timeout 디폴트 값은 60s다. Case6에서 서버의 커넥션은 클라이언트의 read time-out이 종료될 때까지 커넥션을 놓지 않았다. 요청은 모두 처리했지만 클라이언트로 인해 서버 커넥션이 대기하는 있는 상황이다. 서버에 connection time-out을 걸어서 10초가 지나면 서버가 커넥션을 닫도록 해보자. 

 

클라이언트 서버
connection time-out 1s connection time-out 10s
request time-out 60s 스레드수 10개
    최대 커넥션 수 5개
    요청 대기 큐 크기 10개
    서버 요청 처리 시간 500ms

 

처음 5번 요청과 6~8번째 요청 사이에 요청 사이의 시간은 10.5초 차이가 발생한다. 0.5초는 서버 요청 처리 시간이고, 10초는 서버의 커넥션 time-out이다. 서버의 커넥션 time-out을 걸지 않으면 서버는 꼼짝 없이 60초 기다렸어야 한다. 

서버 로그

 

클라이언트의 request time-out인 60초 전에 서버와 커넥션이 닫혔지만, 모든 요청은 정상 처리된 것을 확인할 수 있다. 

클라이언트 로그

 

Case9: 더 복잡한 경우

더 복잡한 경우를 보자. 아래 설정대로 클라이언트가 10개의 요청을 보내면 어떻게 될까? 

클라이언트 서버
connection time-out 5s connection time-out 3s
request time-out 10s 스레드수 6개
    최대 커넥션 수 2개
    요청 대기 큐 크기 3개
    서버 요청 처리 시간 500ms

 

0s: 1, 2번 요청은 커넥션을 맺고 스레드를 할당받아 처리된다. 3~5번 요청은 대기 큐에서 기다린다.

0.5s: 1, 2번 요청을 모두 처리하고 클라이언트의 request time-out을 기다린다. 커넥션을 놓지 않기 때문에 대기 큐의 요청은 계속 기다린다.

3.5s: 1, 2번 요청에 서버의 커넥션 time-out이 발생한다. 클라이언트는 응답을 받고 서버는 커넥션을 놓는다. 대기 큐에 있던 3, 4번 요청이 커넥션을 맺고 스레드를 할당 받아서 처리되기 시작한다. 대기 큐에는 5번만 남게 되고 2개의 요청을 더 받을 수 있게 된다. 6, 7번 요청이 대기 큐에 담기게 된다. 이때까지 6~10번 요청은 클라이언트 커넥션 time-out 전이라 기다리고 있었다. 

4s: 3, 4번 요청이 처리되고 클라이언트의 request time-out을 기다린다.

5s: 8~10번 요청은 5초 동안 커넥션이 맺어지길 기다렸지만, 끝내 대기 큐에 들어가지 못하고 time-out이 발생한다.

7s: 서버의 커넥션 time-out으로 3, 4번 요청의 커넥션이 종료되고 5, 6번이 처리되기 시작한다. 대기 큐에는 7번 요청만 남게 되고 더이상 기다리는 요청이 없으므로 대기 큐에 새로운 요청이 추가되지 않는다. 

10s: 대기 큐에 있던 7번 요청이 클라이언트의 request time-out 발생으로 종료된다. 5, 6 요청도 time-out이 발생하지만 이미 처리된 이후다. 

서버 로그

 

클라이언트의 결과를 보면 1~6번 요청은 정상 처리되고 7번은 request time-out, 나머지는 connection time-out이 발생한 것을 확인할 수 있다. 

클라이언트 로그

 

 

출처

링크
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday