Thread per request VS EventLoop Model in Spring

jiho·2021년 6월 3일
10

Spring

목록 보기
4/13
post-thumbnail

Tomcat은 NIO Connector를 적용해서 Client(Browser)와 Servlet Container 사이의 Blocking을 Non-Blocking으로 바꿈으로써 socket stream에서 데이터를 읽을 수 있을 때 사용할 수 있어 Thread들의 idle 시간을 줄여 동시에 더 많은 요청을 처리할 수 있었습니다.

BIO 와 NIO Connector 비교 정리

하지만 여전히 Servlet Container에서 요청을 Thread에 할당해서 Servlet에서 처리할 시점에서는 thread-per-request model을 여전히 따르고 있기 때문에 Blocking API를 호출할 경우 Thread는 idle 상태가 될 것 입니다.

위와 같은 문제를 안고 갈 경우, 리소스가 부족한 서버(thread를 많이 만들 수 없는)에서 처리할 수 있는 동시 처리량은 매우 제한적일 것 입니다.

흔히 사용하는 Spring Web MVC는 DispathServlet형태의 Servlet으로 처리되니 톰캣에 의존적입니다. 즉, Servlet API와 Servlet Container을 위해 만들어진 Spring 프레임워크 속에 있는 웹 프레임워크입니다.

그래서 Spring Web MVC도 thread-per-request model의 문제점을 그대로 안고 갑니다. 이러한 문제를 개선하기 위해 Servlet 3.1 부터는 확장성 있는 어플리케이션을 작성하기 위해 Non-blocking I/O API들이 추가했습니다. 하지만 이벤트 리스너를 스트림에 추가해서 작업이 끝났을 때 콜백을 호출하는 방식을 사용해서 콜백 지옥의 문제도 있고 Blocking API들도 많기 때문에 완전한 NonBlocking Application 을 만들기는 어렵습니다. (Spring WebFlux를 개발된 동기이기도 합니다.)

예상했듯이 이러한 문제를 극복하기 위해서는 NodeJS와 같은 Asyncronous Non-Blocking 개발 모델이 필요해 보입니다. 이에 해당하는 것이 Spring WebFlux 입니다.

Thread per reqeust모델의 한계

전형적인 웹 어플리케이션(Spring MVC)은 다소 복잡하고 특정한 상호작용에서 일반적으로 Blocking 방식으로 처리됩니다. 예를들어, 데이터를 수정하거나 가져오는 데이터베이스 호출하는 경우가 이에 해당합니다.

예를 들어, 웹 서버를 향한 두 개의 사용자 요청은 다른 스레드에 의해 처리될 수 있습니다. 멀티 코어 플랫폼에서 응답 시간의 관점에서는 확실히 이득이 있습니다. 이런 식으로 동시성을 처리하는 방식이 Servlet 기반의 전통적인 Spring MVC가 사용하는 thread-per-request 모델으로 알려져있습니다.

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

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

Thread pool속에 더많은 thread를 추가해서 더 많은 요청을 처리하는 방법도 있지만 Core 수 이상의 많은 Thread가 사용되면 Context Switching에 의한 비용이 문제가 되게됩니다. 그래서 이렇게 thread 개수를 늘리는 최적화는 확장성에 좋지않습니다.

결론적으로, 상대적으로 적은 수의 스레드를 가지고 급증하는 요청을 다룰 수 있는 방식이 필요합니다. 이것이 Reactive Programming이 필요한 이유입니다.

Reactive Programming 에서 동시성(Concurrency)

여기서 Reactive Programming이란 변화에 반응하는 것을 중심에 두고만든 프로그래밍 모델을 말합니다. 논블록킹은 작업을 기다리기보단 완료되거나 데이터를 사용할 수 있게 되면 반응하게 됩니다.

Reactive Programming은 우리가 데이터 흐름의 관점으로 프로그램을 설계하도록 도와줍니다. 이러한 방식은 Non-Blocking 환경에서 더 나은 thread 사용률을 가지고 높은 동시성을 성취하는 것을 가능하게 해줍니다.

Reactive Programming은 Thread Per Reqeust 처리와 매우 다른 접근을 가지고 있습니다. 가장 기반이 되는 차이점은 비동기성입니다.

다른 말로하면 프로그램의 흐름이 일련의 동기적인 작업들에서 이벤트의 비동기적인 스트림으로 변화된 것입니다.

예를 들어 Reactive Model을 적용할 경우, 데이터베이스를 읽는 호출은 데이터를 fetch할 동안 API를 호출한 현재 Thread가 block되지 않습니다. 해당 호출은 subscribe할 수 있는publisher를 즉각적으로 반환합니다. Subscriber는 해당 작업(데이터 읽기)이 끝난 후 이벤트를 처리할 수 있고 심지어 또 다른 event를 생성할 수도 있다.

여기서 이야기하는 Publisher와 Subscriber에 대한 처리는 Non-blocking Backpressure를 사용해서 처리하는 Reactive Streams 스펙을 살펴보면 도움이 됩니다.

Reactive Streams에 대한 글

reactive programming은 이벤트가 생성되고 사용되는 thread가 어디인지를 강조하지 않습니다. 오히려 강조할 점은 프로그램을 구성하는 비동기 이벤트 스트림입니다.

여기서 publisher and subscriber가 같은 thread에 있을 필요도 없습니다. 이러한 방식은 thread 사용율을 더 개선할 수 있고 더 높은 동시성을 얻을 수 있습니다.

Event Loop

동시성에 대한 reactive 접근을 묘사하는 몇 가지 프로그래밍 모델이 있습니다.

서버를 위한 reactive asynchronous programming model 중 하나는 Event Loop Model 입니다.

위 그림은 reactive asynchronous programming의 추상적인 이벤트 루프의 디자인입니다.

  • 이벤트 루프는 하나의 싱글(혹은 여러 개)에서 계속 동작합니다.
  • 이벤트 루프는 이벤트 큐로부터 이벤트들을 순차적으로 처리하고 알맞은 콜백 함수를 실행합니다.
  • 외부 서비스 호출이나 데이터베이스 호출과 같은 동작의 완료에 의해 트리거가 될 수 있습니다.
  • 이벤트 루프는 IO작업의 완료에 대한 콜백을 발생하고 그 결과를 원래 호출자에게 결과를 보낼 수 있습니다.

이벤트 루프는 Node.js,Netty, and Nginx를 포함한 많은 플랫폼에서 구현되었습니다. 그러한 구현체들은 전통적인 플렛폼(Apacahe HTTP Server, Tomcat, JBoss)들에 비해 더 나은 확장성을 제공해줍니다.

Spring WebFlux를 사용한 Reactive Programming

여기까지 살펴봤다면 충분한 리액티브 프로그래밍과 관련된 동시성 모델에 대한 인사이트는 얻었을 것 입니다.

WebFlux는 Spring version 5.0부터 추가된 Spring reactive stack web framework입니다.

스프링의 전통적인 웹스택을 어떻게 보완해주는지 이해하기 위해 Spring WebFlux의 서버사이드 스택을 살펴보겠습니다.

위 다이어그램에서 볼 수 있는 것 처럼 Spring WebFlux는 전토적인 웹 프레임워크(Spring MVC)와 나란히 놓여 있으며 대체되는 것이 아닙니다.

여기서 알아야할 몇가지 중요한점은

  • Spring WebFlux 는 전통적인 annotation기반의 프로그래밍 모델을 함수형 라우팅을 가지고 확장했습니다.
  • Reactive Streams API와 HTTP runtimes을 나란히 적용했습니다. 런타임을 상호 운용할 수 있게 만들기위해서

it adapts the underlying HTTP runtimes to the Reactive Streams API making the runtimes interoperable => 다시 확인해서 자세히 이해하기.

  • Servler 3.1+ Container를 포함한 다양한 reactive runtime들을 지원할 수 있습니다. (Tomcat, Reactor, Netty, or UnderTow)

스프링 부트는 default로 Netty를 사용합니다.

  • 마지막으로, 함수형이며 fluent API들을 제공하는 HTTP 요청들을 위한 reactive, Non-blocking client인 WebClient를 포함하고 있습니다.

Threading Model in Netty

스레드의 수는 선택한 실제 Reactive Stream API 런타임에 따라 달라집니다.

Reactor Netty 에서 thread

Reactor Netty는 Spring Boot WebFlux starter 속에 embeded된 default 서버입니다. Netty에 의해 default로 생성되는 스레드를 보겠습니다. 만약 우리가 SpringBoot starter를 사용해서 Spring WebFlux application를 시작한다면 우리는 아래와 같은 default thread들을 볼 수 있습니다.

Server를 위한 일반 스레드 이외에 Netty는 Request 처리를 위한 몇가지 thread를 생성합니다. 일반적으로 이용할 수 있는 CPU Core 수보다 많지 않습니다. 위 결과는 4 core를 사용하는 머신일 경우의 결과입니다.

Netty는 반응성 비동기의 형태로 확장성 있는 동시성을 제공하기 위해 Event Loop 모델을 사용합니다. Netty는 아래와 같은 확장성을 위해 Java NIO를 이용해서 event loop를 구현합니다.

EventLoopGroup은 하나 이상의 Event Loop(지속적으로 동작하는)를 관리합니다. 이용가능한 core수 보다 더많은 Event Loop 를 만드는 것은 추천되지않습니다. (context switching 을 최소한으로 하기 위해)

이 EventLoopGroup은 새롭게 추가된 각각의 Channel을 Event Loop에 할당합니다. Channel의 라이프 사이클 동안 모든 동작은 하나의 thread에서 처리되게 됩니다.

Apache tomcat

Sping WebFlux는 물론 Apache Tomcat같은 전통적인 Servlet Container에서도 지원됩니다.

이 경우 WebFlux는 Non-blocking I/O를 가진 Servelt 3.1 API에 의존합니다. 저수준의 Adapter 뒤에서 Servlet API를 사용하면서 Servlet API는 직접적으로는 사용할 수 없습니다.

Tomcat에서 실행되는 WebFlux application 에는 어떤 Thread들이 있는지 살펴보겠습니다.

Netty 기반의 WebFlux와는 달리 많은 thread들이 존재합니다. Tomcat 5에서는 NIO기반의 Connector를 사용해서 Acceptor thread들을 통해 연결을 처리하고 각 연결을 Poller thread들에 의해서 데이터를 이용할 수 있는지 체크하면 Workerthread에 의해 각 요청을 매핑해서 처리합니다. 즉, 많은 thread가 사용됩니다.

둘 중 어떤 Stack을 사용해야할까?

https://www.infoq.com/news/2017/12/servlet-reactive-stack/

Spring Framework에서 Spring WebFlux의 등장이 servlet기반의 spring-mvc가 앞으로 deprecated될 것이라는 것을 의미하지는 않습니다. spring-mvc의 대안으로 제시될 뿐입니다.

모든 application에 webflux의 후보가 되는 것은 아닙니다.

주된 차이는 계속 말해왔듯이 spring mvc는 thread pool 기반이고 반면에 spring-webflux는 event-loop 메커니즘 기반입니다.

만약 현재 어플리케이션이 spring-mvc에서 돌아가고 특별한 문제가 없다면 spring-mvc에 머무는 것을 추천드립니다. 특히, blocking 되는 라이브러리의 의존성이 있다면 spring-mvc에 머무는 게 더 나을 것 같습니다.

확장성과 높은 효율성이 주된 장점임을 기억하면 spring-webflux는 손쉽게 스케일 업하거나 다운할 필요가 있는 usecase에 적절합니다.

기억해야할 것은 spring-webflux가 반드시 더 빠르다는 것이 아니라 하드웨어 리소스 사용에 대한 확장성과 효율성관련 이슈을 다루기가 더 쉽다는 것입니다.

정리

내용을 요약하자면 Servlet Container에서 Thread per reqeust model방식으로 요청을 처리할 경우, thread pool속 thread에 의존해서 동시에 처리할 수 있는 요청 수가 제한적입니다. 좀 더 확장을 위해 thread pool속 thread 수를 늘리는 방식은 버그를 발생시키며 context switching의 오버헤드를 증가시키는 꼴이 됩니다.

반면에 reactive programming 방식을 사용하는 Spring WebFlux는 동시 처리를 하는 데 있어서 thread 수에 의존하지않고 특정한 수의 thread(이벤트 루프를 돌리는)를 이용하며 Async Non-blocking 방식을 통해 thread 사용률(실제 CPU 사용량에서 있어서 큰 차이)을 최대한으로 활용합니다. 이러한 방식은 Core수를 늘려주면 쉽게 확장됩니다.

추가로 함수형 프로그래밍이라는 구조적인 장점을 제공해줄 수 있어서 구조적인 소프트웨어를 설계할 수 있습니다.

주의할 점

  • 코어 수 이상으로 thread를 활용하면 context thread 오버헤드가 발생한다.
  • WebFlux의 어느 곳이라도 Blocking IO 방식을 사용할 경우, Blocking 방식이 되어버려 Non-blocking방식이 깨지게 됩니다. MVC보다 못한 성능이 나올 수 있습니다.

Reference

https://www.reactive-streams.org/
https://www.reactivemanifesto.org/ko
https://engineering.linecorp.com/ko/blog/reactive-streams-with-armeria-1/
https://howtodoinjava.com/spring-webflux/spring-webflux-tutorial/
https://www.slideshare.net/kslisenko/networking-in-java-with-nio-and-netty-76583794
https://dev.to/kamilahsantos/spring-webflux-reactive-java-applications-part-2-5b0l
https://projectreactor.io/docs/core/release/api/reactor/core/publisher/Flux.html
https://medium.com/javarevisited/basic-introduction-to-spring-webflux-eb155f501b17
https://dzone.com/articles/understanding-spring-reactiveclient-to-server-comm
https://dzone.com/articles/understanding-spring-reactive-servlet-async
https://www.baeldung.com/spring-webflux-concurrency
https://m.blog.naver.com/joebak/222008524688
https://dzone.com/articles/spring-webflux-eventloop-vs-thread-per-request-mod

profile
Scratch, Under the hood, Initial version analysis

0개의 댓글