Tomcat의 핵심 요소로는 저번에 Servlet Container 인 Catalina와 Connector Framework인 Coyote가 있다.
이중, Servlet Container가 외부와의 소통을 가능하게 하는 Connector에 대해서 알아보자.
Connector의 역할
- 우선, port listen을 통해 Socket Connection을 얻는다.
- Socket Connection으로부터 데이터 패킷을 획득.
- 데이터 패킷을 파싱해서 ServletRequest Object를 생성한다.
- 얻어진 ServletRequest Object를 알맞은 Servlet Container에게 보낸다.
즉, Connector의 역할은 network port를 listen 하여 connection을 얻은 후, 데이터를 파싱하여 알맞은 servlet request로 바꿔주는 것이다.
Tomcat에서의 Connector
위에서 설명한 Connector는 Blocking IO방법인 BIO Connector와 NonBlocking IO방법인 NIO Connector가 있다.
BIO Connector는 요청시 하나의 thread가 할당되어 요청을 처리한다. 하지만 connection이 닫힐 때까지 하나의 thread는 특정 connection에 할당되어 있기 때문에 idle(아무것도 하지 않는) 상태로 낭비되는 시간이 많아져 효율적으로 자원을 사용하지 못하게 된다.
이를 해결하기 위해 NIO Connector가 등장하게 되었다.
BIO Connector
참고로 Tomcat 9부터는 Deprecated 된 기술이다. BIO Connector는 접속자 한 명당 하나의 스레드를 생성하는 구조로, 커넥션이 닫힐 때까지 하나의 스레드는 특정 커넥션에 할당되어 있다.
크게 3가지 요소로 이루어진다.
- HTTP11Protocol
- acceptor 스레드와 worker 스레드를 가지고 있다.
- Acceptor는 소켓을 획득하고 worker thread pool 에서 socket을 처리하기 위한 idle상태인 worker thread를 찾는다. 만약 Worker thread pool에 idle thread가 없다면, 요청을 처리할 thread가 없기 때문에 Acceptor는 block된다.
- 즉, 동기 처리가 된다는 점이 NIO와의 가장 큰 차이점이다.
- worker thread가 socket을 받은 후, Http11Processor object pool에서 Http11Processor를 얻고 요청을 처리한다.
- CoyoteAdapter
- HTTP요청을 httpServlet Request Object로 반환하는 역할.
- 적절한 Container에 바인딩
- 세션 관리의 역할.
- Mapper
- HTTP Request에 상응하는 Servlet에 바인딩하기 위해 사용
요청 처리 흐름
- 클라이언트 요청 시 Acceptor가 port listener를 통해 소켓을 얻는다.
- 해당 소켓을 worker thread poold에서 idle한 worker thread를 할당받는다.
- worker thread는 Http11ConnectionHandler에서 Http11Processor object을 받는다.
- Http11Processor object에서 CoyoteAdapter를 통해 http요청을 HttpServletRequest로 변환한다.
- 요청에 맞는 Servlet 호출.
NIO Connector
Tomcat 5부터 지원하고 있다. BIO의 다이어그램과는 다른 점은 Poller라는 개념이 등장한 것이다.
BIO에서는 Connection과 Thread가 1:1 관계였기 때문에 IO 작업과 같이 대기가 긴 작업에서도 해당 처리가 끝날 때까지 idle인 thread가 낭비되었다. 하지만, Poller는 Socket들을 캐시로 들고 있다가 해당 Socket에서 data에 대한 처리가 가능한 순간에만 thread를 할당하는 방식을 사용해서 thread이 idle 상태로 낭비되는 시간을 줄일 수 있다.
주된 세 가지 요소
- Http11NioProtocol
- Mapper
- CoyoteAdapter
NIO Endpoint의 내부 동작
기본적으로 Producer-Consumer mode을 사용하며, Acceptor와 Poller thread들은 queue를 통해 소통한다. 즉,Acceptor는 Event Queue의 producer이고 Poller는 Event Queue의 consumer가 된다고 보면 된다.
Acceptor
⇒ event queue의 producer
Acceptor는 이름 그대로 Socket Connection을 accept한다.
NIO connector이지만 소켓을 받는 것은 여전히 전통적인 serverSocket.accept() 방식을 사용하고 있다.
Poller
⇒ event queue의 consumer로서 Event queue로부터 PollerEvent를 받는다.
Poller는 NIO 구현에 있어서 주요한 Thread이다. (당연히 poller도 thread이다)
NIO Connector는 Selector 기반으로 되어있다.(즉, 하나의 thread로 여러 가지) 그래서 Poller thread 속에는 Selector Object가 있다. 하나의 Connector에 하나 이상의 Selector가 있을 수도 있다.
- Selector에 PollerEvent 속 Channel을 등록한다.
- Poller의 selector는 select동작을 수행하여 Worker Thread Pool에서 이용할 수 있는 Woker Thread를 얻어서 해당 소켓을 worker thread에게 넘긴다.
이러한 처리 방식은 전형적인 NIO를 활용한 구현으로, BIO Connector와 달리 Selector에 등록해두었다가 데이터 처리가 가능한 소켓을 별도로 worker 스레드에 할당시켜 줌으로써 좀 더 효율적으로 스레드를 사용할 수 있게 된다.
Worker
⇒ Poller에 의해 넘겨받은 소켓을 프로세서 오브젝트로 캡슐화
Poller에 의해 소켓을 넘겨받은 후, Worker Thread 내에서 소켓에서 얻은 Http 요청을 처리하는 작업을 끝내고 HttpServletReqeust Object로 변환 후, 알맞은 Servelt에게 Reqeust Object를 전달해서 servlet 작업이 완료한 후 가지고 있던 소캣을 통해 클라이언트에게 응답을 돌려주게 된다. 사실상 Poller로 받는 것을 제외하고는 BIO와 동일하다.
많은 요청이 들어올 때 Tomcat의 처리 방식
만약 현재 이용할 수 있는 스레드들보다 더 많은 요청이 동시에 올 경우, 최대 maxThreads 속성 값까지 추가 thread가 생성된다. 여전히 더 많은 요청이 올 경우, Connector의 Server Socket 내부에 최대 acceptCount속성 값까지 쌓이게 된다. 이 이상 더 많은 요청들을 받게 되면 요청을 처리할 자원이 있을 때까지 "Connection Refused" 에러를 발생한다.
요약
NIO 기반의 Connector는 BIO Connector에 비해 더 적은 Thread를 사용한다.
- Java Nio Selector를 사용해서 data 처리가 가능할 때만 Thread를 사용하기 때문에 idle 상태로 낭비되는 Thread가 줄어든다.
참고자료
https://velog.io/@jihoson94/BIO-NIO-Connector-in-Tomcat
https://velog.io/@hyunjong96/Spring-NIO-Connector-BIO-Connector