Tomcat은 was로서 내부에 Web 서버와 Web 컨테이너(서블릿 컨테이너)로 구성되어 있다.
이전에는 정적인 페이지만을 주었으므로, 웹 서버만으로 좋다.
그러나 동적 페이지를 요청하기 시작해 CGI(Common gateway Interface)가 나왔다.
그러나 CGI는 요청마다 프로세스를 생성하여 처리해 주며, 요청이 많아지므로 메모리 용량에 한계가 있다.
따라서 자바에서는 서블릿을 통해 해결했다.
서블릿은 프로세스가 아닌 스레드를 만들고 처리합니다.
또한 Java로 구성되어 있기 때문에 GC에 의한 메모리 누수에 대해 걱정할 필요가 없었습니다.
이러한 서블릿을 관리하는 것이 서블릿 컨테이너이며, 톰캣의 was가 이 서블릿 컨테이너로 구성되어 있다.
서블릿은 init→service→destroy의 라이프사이클을 가지고 있습니다.
Tomcat 내부에서 Catalina는 Tomcat 엔진에서 요청에 대한 커넥터를 처리하는 파이프라인입니다.
예를 들어 80 포트에서 코요테가 요청을 받으면 카탈리나에 연결하여 카탈리나가 요청 응답을 처리하고 결과를 반환합니다.
웹 서버와 was 격리를 권장하는 이유
was만으로 처리하면 부하가 크다.
웹 서버에서 정적 리소스를 토출하고 was에서 동적 리소스를 제공하도록 분리합니다.
보안적으로도 was는 비즈니스 로직이 존재한다.
따라서 내부 네트워크를 사용하고 웹 서버를 외부 네트워크에 두고 분리합니다.
Tomcat에서는 일반적으로 HTTP 요청을 처리하기 위해 네트워크 I/O 방식으로 BIO 커넥터(Blocking I/O) 또는 NIO 커넥터(비블록 I/O) 방법을 선택할 수 있습니다.
커넥터
소켓을 접속해, 데이터 패킷을 취득해 ServletRequest 오브젝트를 생성해 Servlet Container에 건네주는 역할.
- 먼저 port listen에서 Socket Connection을 가져옵니다.
- Socket Connection에서 데이터 패킷을 가져옵니다.
- 데이터 패킷을 파싱하여 ServletRequest Object를 생성합니다.
- 얻은 ServletRequest Object를 적절한 Servlet Container에 보냅니다.
BIO 커넥터
BIO(Blocking I/O)는 입출력 동작을 할 때,
- 스레드 풀에서 하나의 스레드가 반환되고 소켓 연결을 받습니다.
- 요청을 처리하고 요청에 응답한 후
- 소켓 연결이 끝나면 스레드 수영장으로 돌아갑니다.
스레드는 작업이 완료될 때까지 대기그리고 지금까지 다른 작업을 처리하지 않는 방식이다.
따라서 하나의 스레드가 하나의 클라이언트에서만 요청을 처리할 수 있고 여러 클라이언트가 요청을 보내면 대기열에 넣어 하나씩 순차적으로 처리합니다.
이 방법은 간단하지만 여러 클라이언트 요청을 동시에 처리할 수 없기 때문에 성능이 저하될 수 있습니다.
NIO 커넥터
nio connector는 내부적으로 Java NIO를 사용합니다.
따라서 관련 키워드를 맛볼 수 있습니다.
Java NIO
Java 1.4 버전 이후에 등장한 NIO(New Input Output)를 사용한다.
채널
- Java io stream의 대안으로 볼 수 있습니다.
파일에서 읽고 쓰는 역할로서 File Channel, Socket Channel 등 여러가지 존재한다. - stream는 양방향이 되지 않고, 입력, 출력 stream를 만들어야 했다.
그러나 채널은 양방향이 가능하다. - 동기화와 비동기 모두 가능합니다.
- 항상 Buffer와 함께 사용됩니다.
Buffer
- 서버가 클라이언트와 데이터를 교환할 때는 채널을 통해 버퍼(ByteBuffer)를 사용하여 읽고 씁니다.
- 자바의 기존 io는 buffer가 없었고, byte마다 처리해 디스크나 네트워크 액세스 오버헤드로 성능이 좋지 않았다.
셀렉터
- Java NIO에는 여러 채널에서 이벤트(연결 생성, 데이터 도착 등)를 모니터링할 수 있는 서렉터가 포함되어 있으므로 하나의 스레드에서 여러 채널을 모니터링할 수 있습니다.
비블로킹(non-blocking) I/O
- Java NIO에서는 비블로킹 I/O를 사용할 수 있습니다.
도로 있다.
예를 들어 스레드 채널로 버퍼에서 데이터를 읽도록 요청하면 채널이 버퍼에 데이터를 입력하는 동안 스레드는 다른 작업을 수행할 수 있습니다.
그런 다음 채널이 버퍼에 데이터를 포함한 후 스레드는 해당 버퍼를 사용하여 처리를 계속할 수 있습니다.
반대로 데이터를 채널로 전송해도 비 블로킹으로 처리 할 수 있습니다.
Selector가 지원하는 메소드
- select(): 등록된 채널에서 I/O 이벤트가 발생한 채널 수를 반환합니다.
- select(long timeout): timeout 시간 동안 등록된 채널에서 I/O 이벤트가 발생한 채널 수를 반환합니다.
- selectedKeys(): 이벤트가 발생한 채널의 Set 를 돌려준다
- selectNow(): 등록된 채널에서 I/O 이벤트가 발생한 채널 수를 반환합니다.
이 메소드는 select() 와는 달리, 블록 되지 않습니다.
Tomcat에서 NIO 커넥터 작동 프로세스
Tomcat 8.X부터 기본적으로 NIO로 동작한다.
NIO Connector에서는 BIO와 달리 연결이 발생했을 때 새로운 Thread를 할당하지 않고(Connection: Thread≠1:1), Poller라는 개념의 Thread에 Connection(Channel)을 건네준다.
Poller는 Socket을 캐시에 넣고 있지만, 그 Socket에서 data에 대한 처리가 가능한 순간에만 스레드를 할당하는 방식이다.
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와 동일) 작업자 스레드 내에서 소켓에서 가져옵니다.
Http 요청 처리를 종료하고 HttpServletRequest Object로 변환한 후, 적절한 Servelt에 Reqeust Object를 전달하여 서블릿 작업이 완료된 후 가지고 있던 Socat을 통해 클라이언트에 응답을 반환합니다.
된다.
즉, 선택기를 사용하여 데이터 처리가 가능한 경우에만 스레드를 사용하므로 유휴 상태에 낭비되는 스레드가 줄어듭니다.
또 HTTP connectors(based on APR or NIO/NIO2)를 사용하면 비동기를 사용하기 때문에 대량의 정적 파일 send시에도 효율적으로 처리할 수 있다.
정리
NIO 커넥터 작동 순서
- Acceptor 가 소켓으로부터의 요구를 받습니다.
- 소켓에서 객체를 가져오고 PollerEvent 객체로 변환합니다.
- PollerEvent Queue에 넣습니다.
- 폴러 스레드의 선택기 객체를 사용하여 여러 채널을 관리합니다.
- 상태를 감시해 데이터를 읽어낼 수 있는 소켓을 취득해, 작업자 thread를 취득하면, 그 소켓을 thread에 접속합니다.
- 작업자 스레드에서 작업을 처리하면 해당 소켓에 응답을 전달하면서 종료됩니다.