1. Servlet 이란?
- Servlet이란?
- 자바 기반의
javax.servlet.Servlet
인터페이스로, 사용자의 요청에 따라 동적인 데이터를 제공한다.
- 각 요청마다 프로세스가 아닌 쓰레드로 처리한다.
- JVM에 의해 관리되기 때문에 Garbage Collection을 활용하여 Memory Leak에 대한 걱정을 덜 수 있다.
- Web Container란?
- Servlet은 Web Container 안에 Load 되어있다.
- Web Container는 Servlet의 생명주기를 관리한다. (Load -> Init -> Service -> Destroy)
- Web Container는 요청 온 URL을 올바른 서블릿에 맵핑해준다.
- Web Container에서 Requset -> Response 과정
- HTTP 요청이 온다.
- Web Container는 요청 URL을 처리하기 위해서 서블릿이 필요한지 확인한다. 서블릿이 필요 없다면 (ex 정적 데이터) 해당 데이터를 바로 respnose한다.
- Web Container가 'HttpServletRequest'와 'HttpServletResponse'라는 객체를 생성한다.
- Web Container가 요청 URL에 맵핑되는 Servlet을 찾고, 요청을 처리하기 위한 쓰레드를 생성(Thread Pool을 사용한다면 할당)한 후, 앞서 만든 request 와 response 객체를 Servlet Thread에 전달한다.
- Web Container는 Servlet의 service() 메서드를 호출하고, 메서드 수행이 완료되면 컨테이너는 HTTP 응답을 반환한다. 이후 desctroy()를 수행하며 쓰레드를 반환한다
2. Tomcat
Apache Tomcat은 Servlet Container를 포함하고 있는 대표적인 오픈소스 웹 서버이다.
[1] Thread를 얼마나 잡아야하지?
- 서버의 처리량이 좋다는건, 결국 동시에 얼마나 많은 요청을 처리하냐에 달려 있다. 톰캣은 요청을 처리할 때 Thread를 활용한다. 그렇다면 Thread가 많을수록 많은 요청을 처리할 수 있을까?
- 톰캣의 MaxThread 수는 200개가 기본 값이다. 1개의 요청 당 1개의 쓰레드를 할당한다고 가정할 때, 201개의 요청부터는 대기할텐데 무한정 스레드를 늘리면 안되나? 결론은 아니다. 스레드를 무한정 늘리면 스레드에게 할당할 메모리가 작아지고, CPU Core 갯수에 따라 Context Switching이 빈번해질 수 있다.
- 스레드마다 각각의 'Stack' 영역을 가진다. 따라서 스레드를 무한정 늘리면 각각의 할당받는 메모리가 적을 수 밖에 없다.
- Context Switching 작업은 JVM/OS 커널에 무거운 작업이다. 작업을 진행하다 메모리에 올리고 새롭게 작업을 대체해야하기 때문이다. 참고로 CPU Core가 8개면 최대 8개의 스레드만 동시에 처리할 수 있다.
- 요청 Thread 뿐만 아니라 DB Connection Pool, 외부 API 호출(Ex RestTemplate) 등도 고려해야한다. 만약 DB Connection Pool에 가용할 스레드가 없다면 요청 온 Thread는 DB Connection Pool의 스레드를 사용할 수 있을때까지 대기하게 된다. 또한 동기방식으로 외부 서비스를 Call할 때, API의 응답속도도 느리고 timeout이 길다면 외부API의 응답이 올 때까지 스레드를 점유하게 된다. 결국 외부서비스를 호출할 때 사용하는 스레드 풀과 timeout 도 고려해야한다.
결국, 처리량을 높이기 위해선 Thread가 많이 확보 되있어야 하며, Thread를 늘리는데에 Memory와 CPU Core를 고려해야한다. 뿐만 아니라 외부 서비스를 활용하는데의 Thread Pool, timeout도 고려하자.
[2] Thread를 효율적으로 사용하기 위한 노력
- 앞서 외부서비스를 call할 때 동기방식으로 처리하고, timeout이 길다면 가용할 스레드가 적어지는 문제가 있다. 다만 이러한 상황은 I/O Bound 인 상황으로 응답을 받고 처리하는 경우다.
- 스레드가 Request를 계속 점유하는게 아닌, Event 기반으로 스레드당 여러 요청을 처리하는 Reactive Programming(코틀린에서는 코루틴)라는 개념이 등장했다. 코루틴으로 비동기 처리가 용이해졌으며, 쓰레드를 효율적으로 사용하게 됐다. 쓰레드를 가성비 있게 사용하다보니, 많은 요청을 처리 할 수 있다.
- Coroutine 정리 포스트
[3] 그렇다면 Thread 갯수를 얼마나 잡아야할까? by tomcat
- 적정 스레드 갯수를 아래의 공식을 참고할 수 있다. 적절한 쓰레드 갯수는 성능 테스트를 통해 확인하는게 좋다. 메모리는 효율적으로 사용하는지, Context Switching이 빈번한지 등을 모니터링을 통해 확인한다.
- 처리량을 계산할 때 MaxTreads, MaxConnections를 고려해야한다.
- 우리의 Application이 CPU Bound인지, I/O Bound인지도 고려해야 한다.
- 예를 들어 외부 네트워크 호출 또는 I/O 작업으로 인해 상당한 대기 시간이 있는 시나리오에서는 coroutine을 활용하거나, 메모리를 적게 가져가고 스레드 수를 늘리는 것이 유리할 수 있다.(CPU 작업도 거의 없어 Context Switching 작업도 적게 일어난다.) 반대로 CPU, 메모리등의 리소스 제한이 있거나 애플리케이션이 일련의 순차적 계산 실행을 수행할 때는 경합이 발생하여 스레드 수를 늘리는 것이 불리할 수 있다.
- MaxConnections : 서버가 수락하고 처리할 수 있는 최대 연결수다. Tomcat 7에서는 BIO가 기본이지만, 8.5 이상 버전에서는 NIO를 사용한다. Thread가 200개라면 7버전에서는 200개의 Connection이 사용되지만, 8.5이상 버전에서는 여러개의 Connection을 가질 수 있다.
- BIO : Blocking I/O (Thread당 하나의 Connection)
- NIO : Non-Blocking I/O (Thread당 여러 개의 Connection)
- 일반적으로는 Thread의 기본값은 200이고 maxConnections는 8192이기 때문에 Thread에서 병목이 발생할 가능성이 크다.
- 서버 어플리케이션의 품질은 동시에 처리할 수 있는 요청 개수와 관련있다. 잘못된 설정으로 생겨날 수 있는 시나리오는 2가지이다.
- 요청 수에 비해 스레드를 너무 많이 설정 : 놀고 있는 스레드가 많아져 메모리,cpu 자원 비효율 증대
- 요청 수에 비해 스레드를 너무 적게 설정 : 동시 처리 요청수가 줄어든다. 평균응답시간, TPS 감소(대기)한다.
Tomcat Thread, Connection 관련 설정 값
- server.tomcat.threads.max : Thread Pool에서 사용할 최대 스레드 개수, 기본값은 200
- server.tomcat.threads.min-spare : Thread Pool에서 최소한으로 유지할 Thread 개수, 기본값은 10
- server.tomcat.max-connections : 동시에 처리할 수 있는 최대 Connection 의 개수, 기본값은 8192다. 사실상 서버의 실질적인 동시 요청처리개수라고 생각할 수 있다.
- server.tomcat.accept-count : max-connections 이상의 요청이 들어왔을 때 사용하는 요청 대기열 Queue 의 사이즈 기본값은 100이다.
3. Tomcat에서의 처리량 정리
위에서 언급한 내용들을 정리한다.
- Tomcat은 Servlet Container를 사용하며, Servlet은 기본적으로 Thread로 요청을 처리한다. 결국 얼마나 많은 요청을 처리할 수 있는지는 Thread 갯수와 관련있다.
- Thread 갯수를 설정할 때는 Cpu Core(Context Switching 때문)와 메모리가(스레드마다 스택이라는 공간이 각각 가지게된다) 중요한 지표다.
- 또한 외부서비스를 Call할 때의 Thread Pool 영역과, timeout도 함께 고려가 필요하다.
- 서비스가 CPU Bound 작업인지, I/O Bound인지에 따라서도 스레드 갯수가 달라질 수 있으며, 성능 향상을 위해 비동기 방식의 코루틴도 고려한다. (코루틴의 장점 중 하나는 이벤트 방식으로 돌아가면서 I/O 작업시 스레드를 효율적으로 사용한다.)
4. 실제 서버에서의 처리량
- 지금까지는 톰캣에서 얼마나 많은 요청을 처리하냐? 에대한 답으로 Thread 갯수와 관련이있다. 그리고 Thread 갯수를 설정하는데 CPU Core, 메모리가 중요하다. 그리고 I/O 작업이 많을 때 쓰레드를 가성비 있게 쓰기 위해 코루틴이라는 것도 잠시 소개했다.
- 서버 위에서 톰캣이 돌아가는데, 서버는 어떻게 설정하면 많은 요청을 처리할 수 있을까?
- 이전에는 서버 하나에 톰캣을 여러개 두고(톰캣을 여러개 두는 이유는 JVM 방식에서 Stop The World 때문이라고 본인은 생각한다.), Load Balancer 위에서 job을 분배했다. 최근에는 쿠버네티스를 활용해서 필요할 때 서버를 늘리고(결국 요청을 처리할 톰캣을 늘린것과 비슷하다.) 요청량이 줄어들면 서버도 제거하는 방식이 도입됐다. 관련 설명은 아래 링크로 대체한다.
- 쿠버네티스 관련 링크
5. 기타
[Q] Nginx, Proxy Server는 어떻게 많은 요청을 처리할 수 있지?
- 공통으로 CPU작업보다는 I/O 작업 즉, 외부에 call하고 응답을 받고 처리하는 형태라고 가정한다.
- 쓰레드를 활용
- CPU 작업이 많지 않기 때문에 Context Switching이 빈번하지 않고, 메모리 영역도 작게 가져가도 괜찮다고 생각한다. 따라서 Memory, CPU Resource를 작게한 여러개의 쓰레드를 생성한다. 이로써 많은 request를 커버할 수 있다.
- 이벤트 방식 활용
- CPU 작업이 많지 않기 때문에, 공통 로직은 빠르게 처리되고 외부서비스에 call하고 쓰레드를 반환한다. 이후에 요청이 완료됐을 경우에 Event를 받아서 응답한다. 이로써 많은 request를 커버할 수 있다.