[Tomcat]NIO Connector를 중심으로
톰캣은 was로서 내부에 웹 서버와 웹 컨테이너(서블릿 컨테이너)로 이루어져 있다.
예전에는 정적인 페이지만 줬었기 때문에 웹서버만 있으면 됐다. 하지만 동적인 페이지를 요구하기 시작했고 CGI(Common gateway Interface)가 나왔다. 하지만 CGI는 요청마다 프로세스를 생성해서 처리해 줬고 요청이 많아지니 메모리 용량에 한계가 있다. 따라서 자바에서는 서블릿을 통해서 해결했다.
서블릿은 프로세스가 아닌 스레드를 생성해서 처리한다. 또한 자바로 이루어져 있어서 GC로 인해 메모리 누수를 걱정하지 않아도 되었다. 이런 서블릿을 관리하는 게 서블릿 컨테이너고 톰캣의 was가 이 서블릿 컨테이너로 이루어져 있다.
서블릿은 init → service → destroy의 생명주기를 갖는다.
톰캣 내부에서 카탈리나는 톰캣 엔진으로 요청에 대한 커넥터들을 처리하는 파이프라인이다. 예를 들어, 80 포트로 코요테가 요청을 받으면 카탈리나에게 연결해서 카탈리나가 요청 응답을 처리하고 결과값을 반환해 준다.
웹서버와 was 분리를 추천하는 이유
was혼자서만 처리하면 부하가 크다. 웹서버에서 정적 자원을 뱉어주고 was에서 동적인 자원들을 주도록 분리한다. 보안적으로도 was는 비즈니스로직이 존재한다. 따라서 내부망을 사용하고 웹서버를 외부망으로 두어 분리한다.
Tomcat에서는 일반적으로 HTTP 요청에 대한 처리를 위해 네트워크 I/O 방식으로 BIO Connector(Blocking I/O) 또는 NIO Connector(Non-blocking I/O) 방식을 선택할 수 있다.
Connector
소켓을 연결하고 데이터 패킷을 얻어 ServletRequest 객체를 생성하여 Servlet Container에게 전달해 주는 역할.
- 우선, port listen을 통해 Socket Connection을 얻게 됩니다.
- Socket Connection으로부터 데이터 패킷을 획득.
- 데이터 패킷을 파싱 해서 ServletRequest Object를 생성합니다.
- 얻어진 ServletRequest Object를 알맞은 Servlet Container에게 보냅니다.
BIO Connector
BIO(Blocking I/O)는 입출력 작업을 수행할 때,
- thread pool에서 한 thread가 반환되어 소켓 연결을 받고
- 요청을 처리하고 요청에 대해 응답한 후
- 소켓 연결이 종료되면 thread pool에 다시 돌아온다.
thread는 해당 작업이 완료될 때까지 대기하며, 그동안 다른 작업을 처리하지 않는 방식이다. 따라서 한 스레드가 하나의 클라이언트의 요청만 처리할 수 있고, 다수의 클라이언트가 요청을 보내면, 대기열에 대기하다가 하나씩 순차적으로 처리한다.
이 방식은 단순하지만, 다수의 클라이언트 요청을 동시에 처리할 수 없기 때문에, 성능이 저하될 수 있다.
NIO Connector
nio connector는 내부적으로 Java NIO를 사용한다. 따라서 관련 키워드를 맛보고 간다.
Java NIO
Java 1.4 버전 이후 등장한 NIO(New Input Output)를 사용한다.
Channel
- Java io의 stream의 대체라고 볼 수 있다. 파일에서 읽고 쓰는역할로 File Channel, Socket Channel 등 다양하게 존재한다.
- stream은 양방향이 안 돼서 입력, 출력 stream을 만들어야 했다. 하지만 channel은 양방향이 가능하다.
- 동기, 비동기 모두 가능하다.
- 항상 Buffer와 함께 사용된다.
Buffer
- 서버에서 클라이언트와 데이터를 주고받을 때 채널을 통해서 버퍼(ByteBuffer)를 이용해 읽고 쓴다.
- java의 기존의 io는 buffer가 없어서 byte마다 처리해서 디스크나 네트워크 접근 오버헤드로 성능이 좋지 않았다.
Selector
- Java NIO에는 여러 개의 채널에서 이벤트(예: 연결 생성, 데이터 도착 등)를 모니터링할 수 있는 설렉터가 포함돼 있기 때문에 하나의 스레드로 여러 채널을 모니터링할 수 있다.
논블로킹(non-blocking) I/O
- Java NIO에서는 논블로킹 I/O를 사용할 수도 있다. 예를 들어, 스레드가 channel 한테 버퍼에서 데이터를 읽어달라고 요청하면, 채널이 버퍼에 데이터를 채워 넣는 동안 해당 스레드는 다른 작업을 수행할 수 있다. 이후 채널이 버퍼에 데이터를 채워 넣고 나면 스레드는 해당 버퍼를 이용해 계속 처리를 진행할 수 있다. 반대로 데이터를 채널로 보내는 경우에도 논블로킹으로 처리할 수 있습니다.
Selector가 지원하는 메서드
- select(): 등록된 채널 중에서 I/O 이벤트가 발생한 채널의 수를 반환
- select(long timeout): timeout 시간 동안 등록된 채널 중에서 I/O 이벤트가 발생한 채널의 수를 반환
- selectedKeys(): 이벤트가 발생한 채널의 Set을 반환
- selectNow(): 등록된 채널 중에서 I/O 이벤트가 발생한 채널의 수를 반환. 이 메서드는 select()와 달리 블로킹되지 않는다.
Tomcat에서 NIO Connector 동작 과정
Tomcat 8.X부터 기본으로 NIO로 동작한다.
NIO Connector에서는 BIO와 달리, 연결이 발생할 때 바로 새로운 Thread를 할당하지 않고(Connection : Thread ≠ 1:1) Poller라는 개념의 Thread에게 Connection(Channel)을 넘겨준다.
Poller는 Socket들을 캐시로 들고 있다가 해당 Socket에서 data에 대한 처리가 가능한 순간에만 thread를 할당하는 방식이다.
Acceptor는 Socket Connection을 accept 한다. 소켓에서 Socket Channel 객체를 얻어서 NioSocketWrapper -> PollerEvent
객체로 변환한다. 그리고 이 객체를 PollerEvent Queue에 넣게 된다.
Acceptor는 event Queue의 producer, Poller thread는 event Queue의 consumer입니다.
하나의 Poller 스레드 속 Selector를 사용하여 하나의 스레드로 여러 채널을 처리한다. Selector에 PollerEvent에서 받은 Channel을 등록한다. select() 동작으로 데이터를 읽을 수 있는 소켓을 얻고, Worker Thread Pool에서 이용할 수 있는 Woker Thread를 얻어서 해당 소켓을 worker thread에게 넘기게 된다.
마무리로(이후부터 BIO와 동일) Worker Thread 내에서 소켓에서 얻은 Http 요청을 처리하는 작업을 끝내고 HttpServletRequest Object로 변환 후, 알맞은 Servelt에게 Reqeust Object를 전달해서 servlet 작업이 완료한 후 가지고 있던 소캣을 통해 클라이언트에게 응답을 돌려주게 된다.
즉, Selector를 사용해서 data 처리가 가능할 때만 Thread를 사용하기 때문에 idle 상태로 낭비되는 Thread가 줄어들게 된다. 또한 HTTP connectors(based on APR or NIO/NIO2)를 사용하면 비동기를 사용하므로 대량의 정적 파일을 send할때도 효율적으로 처리할 수 있다.
정리
NIO Connector 동작 순서
- Acceptor가 소켓의 요청을 받는다.
- 소켓에서 객체를 얻어 PollerEvent 객체로 변환해 준다.
- PollerEvent Queue에 넣는다.
- Poller thread속 Selector Object를 이용하여 여러 채널을 관리한다.
- 상태를 모니터링하다가 데이터를 읽을 수 있는 소켓을 얻고, worker thread를 얻으면 해당 소켓을 thread에 연결해 준다.
- worker thread에서 작업을 처리하면 해당 소켓으로 응답을 건네주면서 끝.