본문 바로가기
Spring

[Spring] Tomcat VS Netty Connector - NIO(non-blocking I/O) or BIO(Blocking I/O)

by 곰민 2023. 3. 26.
728x90

Tomcat과 Netty의 차이점을 클라이언트의 요청과 응답을 처리하는 Server Connect의 측면에서 우선 적으로 살펴보도록 하겠습니다.
BIO (Blocking I/O) 및 NIO (Non-blocking I/O) 커넥터의 작동 원리에 대해서 알아보겠습니다.

 

 

Connector


Spring에서 커넥터는 클라이언트 요청을 수신 및 처리하고, 추가 처리를 위해 애플리케이션으로 전달하고, 응답을 클라이언트로 다시 보내는 역할을 하는 필수 구성 요소입니다.


커넥터의 주요 역할

1. 연결을 설정하고 관리하여 클라이언트와 서버 간의 통신을 활성화합니다.
2. 클라이언트 요청을 애플리케이션이 처리할 수 있는 형식으로 변환하고, 응답의 경우 그 반대로 변환합니다.
3. 동시 클라이언트 요청을 효율적으로 처리하기 위해 스레딩 및 리소스 할당을 관리합니다.

Tomcat에서의 BIO & NIO Connector


 

출처:Apache Tomcat 8 Configuration Reference (8.0.53) - The HTTP Connector

BIO(Blocking I/O) Connector와 Client 요청 Flow

 

1. 클라이언트가 HTTP 요청을 보냅니다.

 

2. Tomcat의 커넥터는 클라이언트로부터 요청을 수신하고 이를 Http11 Protocol 인스턴스로 전달합니다.
이 인스턴스는 HTTP/1.1 요청을 처리하는 데 사용됩니다.

 

3. Http11Protocol 인스턴스는 JIoEndpoint 인스턴스를 생성하고 요청을 전달합니다.
JIoEndpoint는 들어오는 연결을 수신 대기하는 Acceptor 스레드를 생성합니다.

 

4. 연결이 수락되면 Acceptor는 이를 Worker 스레드로 전달합니다.
Worker 스레드는 연결에 대한 Http11ConnectionHandler 인스턴스를 생성합니다.

 

5. Http11ConnectionHandler 인스턴스는 요청을 처리하기 위해 Http11Processor 인스턴스를 생성합니다.
Http11Processor 인스턴스는 클라이언트로부터 요청을 읽습니다.

동시에, Tomcat은 요청 URI에 따라 적절한 컨텍스트 및 서블릿을 결정하기 위해 내부 매핑 데이터 구조인 Mapper를 사용합니다. Mapper는 Host, Context, Wrapper 및 서블릿 인스턴스에 대한 정보를 포함하고 있습니다.

 

6. Http11Processor 인스턴스는 Mapper를 사용하여 요청을 처리할 적절한 서블릿을 결정합니다.
그런 다음 요청을 CoyoteAdapter를 통해 서블릿으로 전달합니다.

CoyoteAdapter는 Servlet API와 Tomcat의 내부 요청 및 응답 객체 사이의 인터페이스 역할을 합니다.
요청 객체를 HttpServletRequest로 변환하고, 서블릿의 응답 객체를 HttpServletResponse로 변환합니다.

 

7. 서블릿은 inputStream을 사용하여 클라이언트에서 요청 데이터를 읽습니다.
데이터가 사용 가능할 때까지 스레드를 Blocking 합니다.

 

8. 서블릿은 요청 처리를 수행합니다.
이 과정에서 서블릿 호출, 응답 생성, 데이터베이스 쿼리 등이 이루어질 수 있습니다.

 

9. 요청 처리가 완료되면, 서블릿은 응답 데이터를 반환합니다.
이 응답은 다시 CoyoteAdapter를 거쳐 Http11 Processor 인스턴스로 전달됩니다.

 

10. Http11Processor 인스턴스는 output streams을 사용하여 응답 데이터를 클라이언트에게 전송합니다.
이후 연결이 종료되거나 재사용됩니다.

 

HTTP11 Protocol은 Tomcat9부터 Deprecated 되었습니다.

 

Tomcat 5 이후 버전부터 Nio Connector를 지원하기 시작했습니다.

 

NIO(Non-Blocking I/O) Connector

 

대부분의 로직은 비슷하지만 nio를 활용하는 부분에서 차이점이 있습니다.

 

NIO의 핵심 개념 중 하나는 셀렉터(Selector)입니다.
셀렉터는 I/O 이벤트가 발생한 채널을 찾아 이벤트를 처리하는 역할을 합니다.
셀렉터는 다중 채널을 모니터링할 수 있으므로, 하나의 스레드가 여러 개의 채널을 처리하는 것이 가능합니다.

 

1. 클라이언트가 HTTP 요청을 보냅니다.

2. Http11 NioProtocol을 사용하여 구성된 NIO 커넥터가 요청을 수신합니다.
이 프로토콜 구현은 비동기 I/O를 통해 HTTP/1.1을 지원하며, NioEndpoint, Acceptor 및 Poller를 초기화합니다.

 

3. 연결을 받으면 클라이언트를 위한 새 소켓 채널을 만들고 Acceptor에 전달합니다.

4. Acceptor는 새로운 클라이언트 연결을 수신하는 전용 스레드입니다.
Acceptor 스레드는 서버 소켓에 대한 연결 요청을 수락하는 역할을 하며, 기본적으로 단일 Acceptor 스레드가 사용됩니다.
새 연결을 감지하면 Poller에 소켓 채널을 등록합니다.

 

5. Poller는 I/O 이벤트를 위해 등록된 소켓 채널을 모니터링합니다.

 

NioSelectorPool에서 Poller와 Workers는 Selector 인스턴스를 공유합니다.
Poller는 Selector를 사용하여 등록된 소켓 채널을 모니터링하고, I/O 이벤트가 발생하면 해당 이벤트를 처리하기 위해 Worker로 이벤트를 전달합니다.


이를 위해 Poller는 이벤트 큐를 유지하고, Selector를 사용하여 이벤트 큐에 이벤트가 있는지 주기적으로 확인합니다.
이벤트가 발생하면 Poller는 해당 이벤트를 이벤트 큐에 추가하고, 다음 작업으로 이동합니다.

Worker는 이벤트 큐에서 이벤트를 가져와 해당 이벤트를 처리합니다.
이를 위해 Worker는 Selector를 사용하여 이벤트 큐에서 이벤트가 있는지 주기적으로 확인합니다.
이벤트가 발생하면 Worker는 해당 이벤트를 처리합니다.
이벤트 처리가 완료되면 Worker는 다음 이벤트를 처리하기 위해 다시 이벤트 큐에서 이벤트를 가져옵니다.

 

NioSelectorPool은 Poller와 Workers에서 사용하는 Selector 인스턴스를 관리합니다.
또한 NioBlockingSelector와 BlockPoller를 만들고 관리할 수도 있습니다.

6. Worker는 Poller에 의해 전달된 I/O 이벤트를 처리하는 스레드입니다.
클라이언트에서 데이터를 읽고 비동기 I/O 작업을 사용하여 Http11 ConnectionHandler로 전달합니다.
요청이 들어올 때 그 요청을 처리하는 동안 하나의 Worker Thread가 필요합니다. 만약 현재 이용할 수 있는 스레드들보다 더 많은 요청이 동시에 올 경우, 최대 maxThreads 속성 값까지 추가 thread가 생성합니다.

 

7. Http11 ConnectionHandler는 HTTP 연결을 관리하고 HTTP 요청 및 응답을 처리합니다.
HttpNioProcessor를 생성하고 초기화합니다.

 

위 두 다이어그램은 BIO, NIO Connector Architecture in Tomcat (velog.io) 글을 많이 참조했습니다.

 

Netty Basic Core Concept


들어가기 전 간단한 Netty 관련 개념들을 잡고 가겠습니다.

 

Channel

채널은 Java NIO의 base입니다.
읽기 및 쓰기와 같은 IO 작업이 가능한 연결을 나타냅니다.

Future

Netty에서 Channel의 모든 IO 작업은 논블록킹(non-blocking) 방식으로 처리됩니다.

이것은 모든 작업이 호출 후 즉시 반환된다는 것을 의미합니다.
표준 Java 라이브러리에는 Future 인터페이스가 있지만 Netty 목적에는 편하게 사용되기 힘듭니다.
Future를 사용하면 작업 완료 여부를 확인하거나 작업이 완료될 때까지 현재 스레드를 차단할 수 있습니다.

그래서 Netty는 자체 ChannelFuture 인터페이스를 갖고 있습니다.
ChannelFuture에 콜백을 전달하여 작업 완료 후 호출되도록 할 수 있습니다.

 

Event & Event Handler

Netty는 이벤트 기반 애플리케이션 패러다임을 사용하므로 데이터 처리 파이프 라인은 핸들러를 통해 이벤트 체인으로 흐릅니다.
이벤트와 핸들러는 인바운드 및 아웃바운드 데이터 흐름과 관련될 수 있습니다.

인바운드 이벤트는 다음과 같을 수 있습니다.

1. 채널 활성화 및 비활성화 읽기 작업 이벤트
2. 예외 이벤트
3. 사용자 이벤트

아웃바운드 이벤트는 더 간단하며 일반적으로 연결을 열고 닫고 데이터를 쓰고 플러시와 관련이 있습니다.

Netty 애플리케이션은 몇 가지 네트워킹 및 애플리케이션 logic 이벤트 및 그들의 핸들러로 구성됩니다.

 

Encoder & Decoder

 

인코더와 디코더 네트워크 프로토콜을 다루기 위해서 데이터 직렬화 및 역직렬화를 수행합니다.

Netty에서의 BIO와 NIO Connector


이벤트 루프와 project reactor에 관한 글은 아래 글을 참조해 주시면 감사합니다.
[Spring] Project Reactor EventLoop와 Flux와 Mono. (tistory.com)

 

[Spring] Project Reactor EventLoop와 Flux와 Mono.

Project Reactor는 JVM(Java Virtual Machine)에서 비동기 및 반응형 애플리케이션을 구축하기 위해 Reactive Streams를 활용하는 라이브러리입니다. 주요 publisher인 Mono와 Flux와 함께 다양한 연산자들을 제공하

colevelup.tistory.com

 

BIO(Blocking I/O) Connector

 

Netty는 주로 비차단 I/O를 사용하는 비동기 이벤트 중심 프레임워크입니다. 

내장된 BIO 커넥터가 없습니다. 

 

NIO Connector

 

출처:Spring Webflux: EventLoop vs Thread Per Request Model - DZone

 

1. 클라이언트가 서버에 연결 요청을 보냅니다.

2. 서버에서는 Netty의 부트스트랩 클래스 (ServerBootstrap 또는 Bootstrap)를 사용하여 NIO 이벤트 루프 그룹(EventLoopGroup)을 초기화하고, 이벤트 처리를 위한 채널 파이프라인을 설정합니다.

 

출처:https://www.baeldung.com/spring-webflux-concurrency

 

이벤트 루프 그룹은 I/O 이벤트 처리를 위한 스레드 그룹입니다.
보통 두 개의 이벤트 루프 그룹이 사용되는데, 하나는 소켓 연결 요청을 받아들이는 부모 그룹 (일반적으로 'boss' 그룹이라 불림)이고, 다른 하나는 I/O 작업을 처리하는 자식 그룹 (일반적으로 'worker' 그룹이라 불림)입니다.

3. 부모 이벤트 루프 그룹은 클라이언트의 연결 요청을 수락하고, 연결이 이루어지면 자식 이벤트 루프 그룹으로 연결을 전달합니다.

자식 이벤트 루프 그룹은 채널 파이프라인(ChannelPipeline)을 사용하여 I/O 이벤트를 처리합니다.
채널 파이프라인은 각기 다른 기능을 수행하는 채널 핸들러(ChannelHandler)들의 연결입니다.
핸들러들은 입출력 데이터를 읽거나 쓰는 동작과 같은 I/O 이벤트를 처리하며, 적절한 응답을 생성합니다.

4. 채널 파이프라인의 각 핸들러는 위에서 말한 인바운드 아웃바운드 이벤트를 처리합니다.

 

5. 연결된 클라이언트로부터 데이터를 수신하면, 자식 이벤트 루프 그룹의 채널 파이프라인은 등록된 핸들러들을 통해 데이터를 처리하고 응답을 생성합니다.

 

6. 데이터 처리 및 응답 생성이 완료되면, 채널 파이프라인의 마지막 핸들러는 최종 결과를 소켓 버퍼로 전달합니다.

이후 Netty는 소켓 버퍼에 있는 데이터를 클라이언트에게 전송합니다.

비동기 I/O 작업을 위해 자식 이벤트 루프 그룹은 Java NIO의 Selector를 사용하여 여러 채널을 관리하고 처리할 수 있습니다.

 

채널 핸들러들은 일반적으로 상태를 유지하지 않으며, 이로 인해 다른 클라이언트 연결에 대한 처리가 동시에 이루어질 수 있습니다.

클라이언트와 서버 간의 통신이 종료되면, Netty는 채널 파이프라인과 관련된 자원을 해제하고, 연결을 닫습니다. 이 과정에서 채널 핸들러의 생명주기 메서드를 호출하여 핸들러에게 종료 이벤트를 알립니다.

애플리케이션이 종료되면, 부모 및 자식 이벤트 루프 그룹을 종료하고, 관련된 자원을 해제합니다.

 

 

Thread Per Request의 한계와 Reactive Programming으로 가는 이유


전통적인 웹 애플리케이션은 다양한 복잡한 상호작용을 포함합니다.
이러한 상호작용 중 많은 것들이 데이터베이스 호출과 같은 블로킹 작업이며, 데이터를 가져오거나 업데이트하는 데 사용됩니다.
그러나 다른 작업들은 독립적으로 수행될 수 있으며, 가능한 경우 병렬로 처리될 수 있습니다.
이를 처리하기 위한 전통적인 동시성 모델은 Thread Per Request 모델입니다.

예를 들어, 웹 서버로의 두 개의 사용자 요청은 서로 다른 스레드에서 처리될 수 있습니다.
멀티 코어 플랫폼에서는 전반적인 응답 시간 측면에서 명백한 이점이 있습니다.
그러나 이러한 동시성 모델은 웹 애플리케이션의 확장성과 처리량에 한계가 있습니다.

Tomcat은 NIO 커넥터를 적용하여 클라이언트(브라우저)와 Servlet Container 사이의 blocking을 Non-blocking으로 변경함으로써 스레드들의 유휴 시간이 줄어들어 동시에 더 많은 요청을 처리할 수 있게 되었습니다.
그러나 Servlet Container에서 요청을 스레드에 할당하여 Servlet에서 처리할 시점에서는 여전히 thread-per-request 모델을 따르고 있습니다.


따라서 블로킹 API를 호출할 경우 스레드는 유휴 상태가 됩니다.

여러 요청이 들어오지만 사용 가능한 스레드 수만큼만 요청 처리기에 전달되고, 블로킹 I/O 처리를 할 때 해당 스레드는 아무것도 하지 않고 대기 상태가 됩니다.

 

출처:https://www.baeldung.com/spring-webflux-concurrency

위 다이어그램은 하나의 thread가 한 번에 하나의 요청을 처리하는 것을 나타내는 것을 나타냅니다.

여러 요청이 들어오지만 사용가능한 Thread의 수만큼만 Request handler에게 전달되고 
Blocking IO처리를 할 때 해당 Thread는 아무것도 하지 않고 대기상태가 됩니다.

Thread pool속에 더 많은 thread를 추가해서 문제를 해결할 수도 있지만.

Thread pool을 더 크게 설정하여 처리할 수 있는 요청의 수를 늘리는 것은 일반적으로 비용 효율적인 방법은 아닙니다.
이유는 CPU 코어 수 이상의 Thread가 실행 중이면, Context Switching 비용 때문에 CPU가 많은 비용을 들이게 되기 때문입니다.

 

Thread context switching은 CPU가 작업을 변경할 때 발생하는 오버헤드입니다.
각 스레드는 자신의 스택과 레지스터 상태와 함께 실행 상태를 유지합니다.
따라서 CPU는 다른 스레드를 실행하기 전에 현재 스레드의 상태를 저장하고 다음 스레드의 상태를 로드해야 합니다.
이러한 작업은 시간이 많이 소요되므로, 많은 수의 스레드가 실행 중일 때는 Context Switching 오버헤드가 크게 증가합니다.
따라서, 스레드 풀을 더 크게 설정하여 처리할 수 있는 요청의 수를 늘리는 것은 일반적으로 좋은 방법이 아닙니다.


Spring MVC는 이러한 Thread Per Request 모델을 사용하여 웹 애플리케이션을 구축합니다.
그러나 웹 애플리케이션의 요청이 증가함에 따라 이러한 모델의 한계에 도달할 수 있습니다.
특히, 스레드 풀에 사용할 수 있는 스레드가 부족할 때 병목 현상이 발생하고, 웹 애플리케이션의 성능과 처리량이 저하됩니다.

 

결과적으로, 상대적으로 적은 수의 스레드로 더 많은 요청을 처리할 수 있는 동시성 모델이 필요합니다.
이것이 리액티브 프로그래밍을 채택하는 주요 동기 중 하나입니다.

Spring WebFlux는 이러한 문제를 해결하기 위해 탄생하였습니다.

1. 전통적인 요청 당 스레드 모델의 한계를 극복하기 위해.
2. 상대적으로 적은 수의 스레드로 더 많은 요청을 처리할 수 있는 동시성 모델을 제공하기 위해.
3. 리액티브 프로그래밍 패러다임을 적용하여 애플리케이션의 흐름을 더 유연하고 확장 가능하게 만들기 위해.
4. 백 프레셔 제어와 고성능 데이터 스트리밍을 지원하기 위해.

다음 블로그 포스팅은 Spring WebFlux에 대해서 진행하도록 하겠습니다.

 

참조


What is fundamental difference between NIO and BIO in Tomcat? - Stack Overflow

request-process.png (2901×1431) (apache.org)

Http11Protocol (Apache Tomcat 8.5.87 API Documentation)

BIO, NIO Connector Architecture in Tomcat (velog.io)

Introduction to Netty | Baeldung

HTTP Server with Netty | Baeldung

728x90

댓글