BIO, NIO Connector Architecture in Tomcat

jiho·2021년 5월 21일
16

Tomcat

목록 보기
3/3

이전 Velog - Servlet과 Servlet Container에서 Servlet Container에 대해 소개했었습니다.

이어서 이번에는

위 다이어그램에서 Connector를 자세히 알아보겠습니다.

Tomcat 의 핵심요소로 저번에 소개한 Servlet Container 인 Catalina와 이번에 소개할 Connector Framework인 Coyote가 있습니다.

이번에는 Servlet Container가 외부와의 소통을 가능하게하는 Connector에 대해 알아보겠습니다.

Connector들의 공통적인 역할

Connector의 역할을 간단한 다이어그램으로 그려봤습니다.

  1. 우선, port listen을 통해 Socket Connection을 얻게 됩니다.
  2. Socket Connection으로부터 데이터 패킷을 획득.
  3. 데이터 패킷을 파싱해서 ServletRequest Object를 생성합니다.
  4. 얻어진 ServletRequest Object를 알맞는 Servlet Container에게 보냅니다.

모든 Connector는 network port를 Listen해서 connection을 얻은 후 데이터를 받아서 Servlet이 처리할 수있는 형태(servlet reqeust)로 바꿔주는 역할을 함을 알 수 있습니다.

Connectors in Apache Tomcat

Apache Tomcat의 Connector를 비교하는 표를 가져왔습니다. 링크

위 표를 이해하기 위해서는 NIO와 BIO이 무엇인지 아는 것이 중요합니다.

그리고 APR Connector은 성능 개선을 위한 Native 언어로 작성된 Connector에 해당합니다. 이번 글에서는 따로 설명하지 않겠습니다.

그리고 버젼 별로 Connector의 변경사항들입니다.

  • Tomcat 4.1 had just the java blocking IO(BIO) connector
  • Tomcat 5.5 added the Apache Portable Runtime(APR) connector
  • Tomcat 6.0 added the java new IO(NIO) connector
  • Tomcat 8.0 added the Java new IO 2(NIO2) connector
  • Tomcat 9.0 dropped the BIO connector
  • Tomcat 9.0 added the ability to use OpenSSL for TLS with NIO and NIO2

톰켓의 Connector와 관련된 역사를 살펴보면 BIO기반의 Connector는 사라지고 NIO, NIO2 기반의 connector가 추가되는 역사를 살펴볼 수 있습니다.

BIO Connector 와 NIO Connector

BIO Connector는 Socket Connection을 처리할 때 Java의 기본적인 IO 기술을 사용합니다. thread pool에 의해 관리되는 thread는 소켓 연결을 받고 요청을 처리하고 요청에 대해 응답한 후 소켓 연결이 종료되면 pool에 다시 돌아오게 됩니다. 즉, conneciton이 닫힐 때까지 하나의 thread는 특정 connection에 계속 할당되어 있을 것입니다.

이러한 방식으로 Thread 를 할당하여 사용할 경우, 동시에 사용되는 thread 수가 동시 접속할 수 있는 사용자의 수가 될 것입니다. 그리고 이러한 방식을 채택해서 사용할 경우 thread들이 충분히 사용되지 않고 idle(아무것도하지않는) 상태로 낭비되는 시간이 많이 발생합니다. 이러한 문제점을 해결하고 리소스(thread)를 효율적으로 사용하기 위해 NIO Connector가 등장했습니다.

NIO가 어떻게 Thread의 Idle 상태 유지시간을 줄여가는지 주목하며 정리해보겠습니다.

BIO Connector (Blocking IO)

BIO Connector 는 위 그림처럼 크게 3가지 요소로 이루어집니다.

  • HTTP11Protocol
  • CoyoteAdapter
  • Mapper

실행 흐름에 따라 각 요소를 살펴보겠습니다.

Http11Protocol

Tomcat 9부터 해당 BIO Connector는 Deprecated 되었지만 NIO Connector와 어떤 부분들이 변화되었는지 알기 위해 살펴보겠습니다.

Http11Protocol은 HTTP 프로토콜을 지원하며 JIoEndpoint object 와 Http11ConnectionHandler objcet를 포함하고있습니다. 그리고 Http11ConnectionHandler object는 Http11Processor Object Pool를 가지고 있습니다.

그리고 Http11Processor object는 http 요청을 처리하기 위해 CoyoteAdapter를 호출합니다.

JioEnpoint 2가지 Acceptor thread 와 Worker thread를 가지고 있습니다.

Acceptor는 소켓을 획득하고 worker thread pool 에서 socket을 처리하기 위한 idle상태인 worker thread를 찾습니다. 만약 Worker thread pool에 idle thread가 없다면, 요청을 처리할 thread가 없기 때문에 Acceptor는 block됩니다. 이 부분은 NIO와 비교할 때, 주목해야할 부분입니다.

worker thread가 socket을 받은 후, Http11Processor object pool에서 Http11Processor를 얻고 요청을 처리하게됩니다.

Mapper

Class Full path는 org.apache.tomcat.util.http.mapper.Mapper 이며, Mapper는 주로 HTTP Request를 그에 상응하는 Servlet(요청을 처리해줄)에 바인딩하기 위해 사용됩니다.

CoyoteAdapter

class full path는org.apache.catalina.connector.CoyoteAdapter이며, 이 object는 HTTP 요청을 HttpServletRequest Object로 변환하는 역할을 하며, 추가로 적절한 Container에 바인딩 시키는 역할을 합니다.

그리고 CoyoteAdapter는 세션 관리에도 관여하게 됩니다. 요청의 JSESSIONID값에 따라 서버 내의 session pool속에 있는 상응하는 session oject를 찾아서 HttpServletReqeust Object 속에 바인딩 시켜줍니다.

이러한 모든 일들이 CoyoteAdapter의해 이루어집니다.

중요한 점은 Connector 내부에서 각 요청에 상응하는 Thread를 Woker Thread Pool에서 꺼내서 1:1 로 매핑해준다는 점입니다.

NIO Connector (Non-Blocking IO)

Tomcat5 이후의 버젼부터 NIO Connector를 지원하기 시작했습니다.

NIO Connector에서는 BIO 와 달리 새로운 연결이 발생할 때, 바로 새로운 Thread를 할당하지않고(Connection이 Thread와 1대1 매핑관계가 아님) Poller라는 개념의 Thread에게 Connection(Channel)을 넘겨줍니다. Poller는 Socket들을 캐시로 들고 있다가 해당 Socket에서 data에 대한 처리가 가능한 순간에만 thread를 할당하는 방식을 사용해서 thread이 idle 상태로 낭비되는 시간을 줄여줍니다.

NIO Connector의 주요 요소는 아래와 같습니다. BIO와 유사하지만 Connection을 처리할 때 Java Nio를 활용한다는 점에서 차이를 보입니다.

  • Http11NioProtocol
  • Mapper
  • CoyoteAdapter

Http11NioProtocolNioEndpoint object와 Http11Connectionhandler object 를 가지고 있으며, NioEndpoint는 Http11NioProtocol에서 socket들을 얻고 처리하기위한 주요 모듈입니다.
Http11Connection Handler는 Connection Processor입니다.

NioEndpoint는

  • Socket request listener thread 인 Acceptor
  • Socket NIO thread인 Poller
  • Request processing thread 인 Worker Thread

로 구현되어있습니다.

NioEndpoint는 BIO connector 의 JioEndPoint에 비해 복잡합니다. 내부 동작을 간단히 살펴보겠습니다.

NioEndpoint's internal Processing

  • Acceptor

Acceptor

https://github.com/apache/tomcat/blob/510f0b6a0e8e20d1db173e95a64afd207b44b517/java/org/apache/tomcat/util/net/Acceptor.java#L69

Acceptor는 이름 그대로 Socket Connection을 accept합니다. NIO connector이지만 소켓을 받는 것은 여전히 전통적인 serverSocket.accept() 방식을 사용하고 있습니다. 그리고 Socket Channel object를 얻어서 톰캣의 class org.apache.tomcat.util.net.NioChannel object로 캡슐화하게됩니다. 그리고 추가로 NioChannel object를 PollerEvent라는 object로 한번 더 캡슐화해서 PollerEvent를 event queue에 넣게 됩니다. 여기서 나타는 방식은 전형적인 Producer-Consumer model입니다. AcceptorPoller thread들은 queue를 통해 소통합니다. 즉,Acceptor는 Event Queue의 producer이고 Poller는 Event Queue의 consumer가 되는 것 입니다.

public class Acceptor<U> implements Runnable {
 @Override
    public void run() {
    
    	while(!stopCalled) {
        	...
            
			U socket = null;
            try {
                // Accept the next incoming connection from the server
                // socket
                socket = endpoint.serverSocketAccept();
            } catch (Exception ioe) {
                ...
            }
            // Successful accept, reset the error delay
            errorDelay = 0;

            // Configure the socket
            if (!stopCalled && !endpoint.isPaused()) {
                if (!endpoint.setSocketOptions(socket)) {
                    endpoint.closeSocket(socket);
                }
            } else {
                endpoint.destroySocket(socket);
            }
		}
    }
}

실제 package org.apache.tomcat.util.net.Acceptor 내의 코드는 위와 같이 하나의 thread를 통해서 socket을 처리하게 됩니다. (Tomcat에서는 acceptor thread를 늘릴 수도 있습니다.)

socket = endpoint.serverSocketAccept(); 를 통해 Generic Type의 소켓을 받고 endpoint.setSocketOption(socket)을 사용해서 해당 소켓을 처리하게 됩니다.

public class NioEndpoint extends AbstractJsseEndpoint<NioChannel,SocketChannel> {
    @Override
    protected boolean setSocketOptions(SocketChannel socket) {
      NioSocketWrapper socketWrapper = null;
      
      ...
      
      NioSocketWrapper newWrapper = new NioSocketWrapper(channel, this);
      channel.reset(socket, newWrapper);
      connections.put(socket, newWrapper);
      socketWrapper = newWrapper;
      
      ....
      
      poller.register(socketWrapper);
}

/**
* Poller class.
*/
public class Poller implements Runnable {
  /**
   * Registers a newly created socket with the poller.
   *
   * @param socketWrapper The socket wrapper
   */
  public void register(final NioSocketWrapper socketWrapper) {
    PollerEvent event = null;
    if (eventCache != null) {
        event = eventCache.pop();
    }
    if (event == null) {
        event = new PollerEvent(socketWrapper, OP_REGISTER);
    } else {
        event.reset(socketWrapper, OP_REGISTER);
    }
    addEvent(event);
  }
}

받았던 소켓을 NioEndpoint.setSocketOptionsPoller.register을 연쇄적으로 호출을 통해 잘 감싼 후 PollerEventQueue에 Push하게 됩니다.

  • Poller

Poller Code

https://github.com/apache/tomcat/blob/510f0b6a0e/java/org/apache/tomcat/util/net/NioEndpoint.java#L571

NIO Connector는 Selector 기반으로 되어있습니다.(즉, 하나의 thread로 여러 가지) 그래서 Poller thread 속에는 Selector Object가 있습니다. 하나의 Connector에 하나 이상의 Selector가 있을 수도 있습니다.

우선 Poller Thread 속에서 유지되는 Selector를 Main Selector라고 합니다.

Poller EventQueue

private final SynchronizedQueue<PollerEvent> events =
                new SynchronizedQueue<>();

Poller는 NIO 구현에 있어서 주요한 Thread입니다. 우선, 첫번째로 event queue의 consumer로서 Event queue로부터 PollerEvent를 받습니다. 그리고 Main Selector에 PollerEvent 속 Channel을 등록합니다. 그리고 Main Selector는 select 동작을 수행하고 데이터를 읽을 수 있는 소켓을 얻고 Worker Thread Pool에서 이용할 수 있는 Woker Thread를 얻어서 해당 소켓을 worker thread에게 넘기게 됩니다. 이러한 처리 방식은 전형적인 NIO를 활용한 구현입니다.

 /**
 * The background thread that adds sockets to the Poller, checks the
 * poller for triggered events and hands the associated socket off to an
 * appropriate processor as events occur.
 */
    @Override
    public void run() 
       
  • Worker

https://github.com/apache/tomcat/blob/8301307ce76540e92c6068d8277038d133da862c/java/org/apache/tomcat/util/net/AbstractEndpoint.java#L1120

Worker Thread가 Poller에 의해 소켓을 넘겨받은 후, socket을 SocketProcessor Object로 캡슐화합니다.
Http11ConnectionHandler로 부터 Http11NioProcessor object를 받게되고 Http11NioProcessor내에서 CoyoteAdapter를 호출합니다. (이제부터는 BIO Connector와 동일합니다.) Worker Thread 내에서 소켓에서 얻은 Http 요청을 처리하는 작업을 끝내고 HttpServletReqeust Object로 변환 후, 알맞는 Servelt에게 Reqeust Object를 전달해서 servlet 작업이 완료한 후 가지고 있던 소캣을 통해 클라이언트에게 응답을 돌려주게 됩니다.

NIO Connector의 동작 방식을 정리하자면 하나의 Thread(Poller)에서 Java NIO Selector를 사용하여 각 Channel를 처리하기 때문에 데이터를 이용하지 않을 때 thread idle 상태를 줄이고 데이터를 이용할 수 있을 때만 Thread를 사용하기 때문에 더 많은 Thread를 즉, 더 많은 동시 사용자의 요청을 처리해 줄 수 있습니다.

많은 요청이 올 때 Tomcat의 처리방식

지금까지 기본적인 Connector의 내부 처리 방식을 살펴봤습니다. 이번에는 너무 많은 요청이 올 경우 어떻게 될지 정리해보겠습니다.

각 요청이 들어올 때 그 요청을 처리하는 동안 하나의 Worker Thread가 필요합니다. 만약 현재 이용할 수 있는 스레드들보다 더 많은 요청이 동시에 올 경우, 최대 maxThreads 속성 값까지 추가 thread가 생성합니다. 여전히 더 많은 요청이 올 경우, Connector의 Server Socket 내부에 최대acceptCount속성 값까지 쌓이게 됩니다. 이 이상 더 많은 요청들을 받게되면 요청을 처리할 자원이 있을 때까지 "Connection Refused" 에러를 발생하게됩니다.

요약

NIO 기반의 Connector는 BIO Connector에 비해 더 적은 Thread를 사용합니다.

  • Java Nio Selector를 사용해서 data 처리가 가능할 때만 Thread를 사용하기 때문에 idle 상태로 낭비되는 Thread가 줄어들게 됩니다.

NIO Endpoint를 직접 코드를 살펴보면서 어떤 방식으로 돌아가는지 확인했습니다. 이정도까지 알아야하나 하고 의문을 가질 수도 있었지만 이 내용을 알고 Tomcat Connector의 속성들을 사용한다면 더욱 효과적으로 사용할 수 있을 것 입니다.

BIO connector removed

The Java blocking IO implementation (BIO) for both HTTP and AJP has been removed. Users are recommended to switch to the Java non-blocking IO implementation (NIO). As of Tomcat 8.5.17, if a BIO Connector is explicitly configured, rather than failing to start the Connector, Tomcat will automatically switch the Connector to use the NIO implementation and log a warning. Apache Tomcat Link

사실 Tomcat 9.0 부터는 BIO 관련 Connector 자체가 사라진 것을 보아 BIO Connector에서 얻을 이점은 크게 없는 듯 합니다.

Reference

http://tomcat.apache.org/tomcat-7.0-doc/config/http.html
https://stackoverflow.com/questions/25356703/tomcat-connector-architecture-thread-pools-and-async-servlets
https://www.mulesoft.com/tcat/tomcat-connectors
https://dzone.com/articles/understanding-tomcat-nio
https://www.programmersought.com/article/1699692284/
https://www.youtube.com/watch?v=LBSWixIwMmU
https://www.programmersought.com/article/48543375033/
https://www.datadoghq.com/blog/tomcat-architecture-and-performance/#catalina-server-service-and-connectors
https://www.quora.com/Why-is-NIO-much-more-efficient-than-BIO-even-though-NIO-doesnt-save-any-CPU-circles
https://www.programmersought.com/article/20561051255/
https://www.slideshare.net/Paganel/tomcatx-performancetuning

profile
Scratch, Under the hood, Initial version analysis

7개의 댓글

comment-user-thumbnail
2021년 6월 7일

좋은 글 정말 잘 읽었습니다. 정리가 잘되어서 쉽게 이해할 수 있었습니다.

1개의 답글
comment-user-thumbnail
2021년 9월 12일

상세한 설명 감사해요!! 속이 뻥 뚫리는 기분입니다

1개의 답글
comment-user-thumbnail
2022년 12월 27일

안녕하세요 글이 너무 좋습니다.
톰캣에 대해 공부를 좀 하고 있는데요
소켓을 간단하게 나마 구현을 해보면서 정리를 하긴 했는데
중간즈음 NIO Connector에 대한 설명에서
"해당 소켓에서 데이터에 대해 처리가 가능한 순간"이라는게 무슨 의미인지요?
개발 공부를 시작한지 얼마 안되서 이해가 안되네요...

1개의 답글