2022년 7월 16일 토요일

Spring Webflux: EventLoop vs Thread Per Request Model

 Spring Webflux: EventLoop vs Thread Per Request Model - DZone Java 의 번역글 


이 기사에서는 Spring Webflux를 살펴보고 반응형 프로그래밍을 사용하여 애플리케이션 성능을 높이는 방법을 이해합니다.

스프링 웹플럭스 소개

Spring Webflux는 Spring 5의 일부로 도입되었으며 이를 통해 Reactive Programming을 지원하기 시작했습니다. 비동기 프로그래밍 모델을 사용합니다. 반응형 프로그래밍을 사용하면 일정 기간 동안 높은 요청 로드를 지원하도록 애플리케이션을 고도로 확장할 수 있습니다.

Spring MVC와 비교하여 해결한 문제

Spring MVC는  각 요청이 스레드에 매핑되고 요청 소켓에 응답을 다시 가져오는 책임이 있는 동기 프로그래밍 모델을 사용합니다. 응용 프로그램이 데이터베이스에서 데이터 가져오기, 다른 응용 프로그램에서 응답 가져오기, 파일 읽기/쓰기 등과 같은 일부 네트워크 호출을 수행하는 경우 요청 스레드는 원하는 응답을 얻기 위해 기다려야 합니다. 

여기에서 요청 스레드가 차단되고 이 기간 동안 CPU 사용률이 없습니다. 이것이 이 모델이 요청 처리를 위해 큰 스레드 풀을 사용하는 이유입니다. 이것은 낮은 요청 비율의 응용 프로그램에 적합할 수 있지만 높은 요청 비율의 응용 프로그램의 경우 궁극적으로 응용 프로그램이 느려지거나 응답하지 않을 수 있습니다. 이것은 확실히 오늘날의 시장에서 비즈니스에 영향을 미칩니다.

서블릿 컨테이너가 있는 Spring MVC

이러한 응용 프로그램의 경우 반응 프로그래밍을 효과적으로 사용할 수 있습니다. 여기서 요청 스레드는 필요한 입력 데이터를 네트워크로 보내고 응답을 기다리지 않습니다. 오히려 이 차단 작업이 완료되면 실행되는 콜백 함수를 할당합니다. 이런 식으로 요청 스레드는 다른 요청을 처리하는 데 사용할 수 있습니다. 

적절하게 사용하면 단일 스레드를 차단할 수 없으며 스레드가 CPU를 효율적으로 사용할 수 있습니다. 여기서 적절한 방식은 이 모델의 경우 모든 스레드가 반응적으로 동작해야 함을 의미합니다. 따라서 데이터베이스 드라이버, 서비스 간 통신, 웹 서버 등도 반응 스레드를 사용해야 합니다. 

임베디드 서버

Spring Webflux는 기본적으로 Netty를 임베디드 서버로 가지고 있습니다. 그 외에도 Tomcat, Jetty, Undertow 및 기타 Servlet 3.1+ 컨테이너에서도 지원됩니다. 여기서 주목해야 할 점은 Netty와 Undertow는 비서블릿 런타임이고 Tomcat과 Jetty는 잘 알려진 서블릿 컨테이너라는 것입니다.

Spring Webflux가 도입되기 전에 Spring MVC는 요청당 스레드 모델을 사용하는 기본 임베디드 서버로 Tomcat을 사용했습니다 이제 Webflux에서는 Event Loop Model을 사용하는 Netty가 기본값으로 선호되었습니다 Webflux는 Tomcat에서도 지원되지만 Servlet 3.1 이상 API를 구현하는 버전에서만 가능합니다. 

참고 : Servlet 3은 비동기 프로그래밍을 위한 기능을 도입하고 Servlet 3.1은 모든 비동기를 허용하는 비동기 I/O를 추가로 도입했습니다. 

이벤트 루프

EventLoop은 NIO (Non-Blocking I/O Thread )로, 지속적으로 실행되고 다양한 소켓 채널에서 새 요청을 받습니다. EventLoop가 여러 개인 경우 각 EventLoop는 소켓 채널 그룹에 할당되고 모든 EventLoop는 EventLoopGroup.

다음 다이어그램은 EventLoops의 작동 방식을 보여줍니다. 

이벤트 루프 작동

여기에서 EventLoop은 서버 측에서의 사용법을 설명하기 위해 표시됩니다. 그러나 클라이언트 측에 있을 때 동일한 방식으로 작동하며 I/O 요청과 같은 다른 서버에 요청을 보냅니다.

  1. 모든 요청은 로 알려진 채널과 연결된 고유 소켓에서 수신됩니다 SocketChannel
  2. 의 범위와 연결된 단일 EventLoop 스레드가 항상 있습니다 SocketChannels따라서 해당 Sockets/SocketChannels에 대한 모든 요청은 동일한 EventLoop로 전달됩니다.
  3. EventLoop에 대한 요청은 채널 파이프라인을 통과하며, 여기에서 다수의 인바운드 채널 핸들러 WebFilters가 필요한 처리를 위해 구성됩니다.
  4. 그 후 EventLoop는 애플리케이션별 코드를 실행합니다. 
  5. 완료되면 EventLoop는 구성된 처리를 위해 여러 아웃바운드 채널 처리기를 다시 거칩니다. 
  6. 결국 EventLoop는 동일한 SocketChannel/Socket에 대한 응답을 반환했습니다.
  7. 루프에서 1단계에서 6단계를 반복합니다.

이것은 단일 스레드 사용으로 인한 리소스 사용을 확실히 최소화하는 간단한 사용 사례이지만 실제 문제를 해결하지는 못합니다. 

다음과 같은 이유로 애플리케이션이 EventLoop를 오랫동안 차단하면 어떻게 됩니까?

  • 높은 CPU 집약적 작업.
  • 읽기/쓰기 등과 같은 데이터베이스 작업
  • 파일 읽기/쓰기.
  • 네트워크를 통해 다른 응용 프로그램에 대한 모든 호출. 

이러한 경우 EventLoop은  4단계 에서 차단되며  애플리케이션이 곧 느려지거나 응답하지 않을 수 있는 동일한 상황에 처할 수 있습니다. 추가 EventLoop를 만드는 것은 솔루션이 아닙니다. 이미 언급했듯이 소켓 범위는 단일 EventLoop에 바인딩됩니다. 따라서 EventLoop의 블록은 이미 바인딩된 소켓에 대해 다른 EventLoop가 대신 작동하도록 허용하지 않습니다.  

참고 : EventLoops는 계속 실행되어야 하고 단일 CPU는 여러 EventLoops를 동시에 실행할 수 없기 때문에 애플리케이션이 다중 CPU 플랫폼에서 실행 중인 경우에만 다중 EventLoops를 생성해야 합니다. 기본적으로 애플리케이션은 CPU 코어 수만큼 EventLoops로 시작합니다.

이제 NIO EventLoop의 진정한 힘은 그 이면에 있는 뛰어난 단순성과 함께 그림으로 나타납니다. 사용자 애플리케이션은 요청을 다른 스레드에 위임하고 콜백을 통해 비동기적으로 결과를 반환하여 새로운 요청 처리를 위해 EventLoop의 차단을 해제해야 합니다. 따라서 EventLoop의 업데이트된 작업 단계는 다음과 같습니다.

  1. 위와 같이 1~3단계를 진행합니다.
  2. EventLoop는 요청을 새 작업자 스레드에 위임합니다.
    1. 작업자 스레드는 이러한 긴 작업을 수행합니다.
    2. 완료 후 작업에 대한 응답을 작성하고 ScheduledTaskQueue.
  3. 작업 대기열의 EventLoop 폴링 작업ScheduledTaskQueue
    1. 있다면 태스크의 Runnable run방법으로 5~6단계를 수행합니다.
    2. 그렇지 않으면 SocketChannel에서 새 요청을 계속 폴링합니다.

Webclient를 사용한 완전 반응형 접근 방식

이점 :

  1. 경량 요청 처리 스레드.
  2. 하드웨어 리소스의 최적 활용.
  3. 단일 EventLoop는 HTTP 클라이언트와 요청 처리 간에 공유될 수 있습니다.
  4. 단일 스레드는 여러 소켓(즉, 다른 클라이언트)에 대한 요청을 처리할 수 있습니다.
  5. 이 모델은 무한 스트림 응답 의 경우 배압  처리를 지원합니다 .

요청당 스레드 모델(  서블릿 3.1+ 구현 )

요청당 스레드 모델은 동기식 서블릿 프로그래밍이 도입된 이후로 실제로 사용되었습니다. 이 모델은 들어오는 요청을 처리하기 위해 서블릿 컨테이너에 의해 채택되었습니다. 요청 처리가 동기식이므로 이 모델은 요청을 처리하기 위해 많은 스레드가 필요합니다. 따라서 리소스 활용도가 높아집니다. 이러한 스레드를 차단할 수 있는 네트워크 I/O는 고려하지 않았습니다. 따라서 확장성을 위해 일반적으로 리소스를 확장해야 합니다 이것은 시스템 비용을 증가시킵니다. 

반응형으로 이동하여 Spring Webflux를 사용하면 Servlet 3.1+ API를 구현하는 경우 Servlet 컨테이너도 사용할 수 있는 옵션이 제공됩니다. Tomcat은 가장 일반적으로 사용되는 Servlet Container이며 반응형 프로그래밍도 지원합니다.

Tomcat에서 Spring Webflux를 선택하면 응용 프로그램은 스레드 풀(예: 10)에서 특정 수의 요청 처리 스레드로 시작합니다. 소켓의 요청은 이 풀의 스레드에 할당됩니다. 여기서 주의할 점은 소켓이 스레드에 영구적으로 바인딩되지 않는다는 것입니다.

요청 스레드가 I/O 호출에 대해 차단되면 스레드 풀의 다른 스레드에서 요청을 계속 처리할 수 있습니다. 그러나 그러한 요청이 많을수록 모든 요청 스레드를 차단할 수 있습니다. 지금까지는 동기 처리에서 사용하던 것과 동일합니다. 그러나 반응적 접근 방식에서는 EventLoop에서 설명한 것처럼 사용자가 요청을 다른 작업자 스레드 풀에 위임할 수 있습니다. 그렇게 하면 요청 처리 스레드를 사용할 수 있게 되고 애플리케이션이 응답을 유지합니다. 

Webclient를 사용한 요청 모델당 반응 스레드

다음은 이 모델에서 요청을 처리하는 동안 실행되는 단계입니다.

  1. 모든 요청은 SocketChannel이라는 채널과 연결된 고유 소켓에서 수신됩니다. 
  2. 요청은 스레드 풀에서 스레드에 할당됩니다.
  3. 스레드에 대한 요청은 필요한 사전 처리를 위해 특정 처리기(예: 필터, 서블릿)를 거칩니다.
  4. 요청 스레드는 컨트롤러에서 차단 코드를 실행하는 동안 작업자 스레드 또는 반응 웹 클라이언트에 요청을 위임할 수 있습니다.
  5. 작업이 완료되면 작업자 스레드 또는 웹 클라이언트(EventLoop)가 해당 소켓에 응답을 되돌려 줄 책임이 있습니다.

다시 말하지만, 반응형 클라이언트(예: Reactive Webclient) 및 Reactive DB 드라이버를 선택하여 응용 프로그램을 완전히 반응형으로 만들면 최소 스레드가 시스템에 효과적인 확장성을 제공할 수 있습니다. 

장점 :

  1. Servlet API의 사용을 지원하고 허용합니다.
  2. 요청 스레드가 차단되면 EventLoop에서와 같이 소켓 범위가 아닌 단일 클라이언트 소켓만 차단합니다.
  3. 하드웨어 자원의 최적 활용

성능 비교

이 비교를 위해 Spring Webflux를 사용하여 100ms의 의도적인 지연을 사용하여 샘플 Reactive Spring Boot 애플리케이션을 생성했습니다. 요청 처리 스레드를 차단 해제하기 위해 요청이 다른 작업자 스레드에 위임되었습니다. 

이 비교에는 다음 구성이 사용되었습니다.

  • 속성을 사용하여 Netty 성능을 위해 구성된 단일 EventLoopreactor.netty.ioWorkerCount=1
  • 속성을 사용하여 Tomcat 성능에 대해 구성된 단일 요청 처리 스레드server.tomcat.max-threads=1
  • (1GB RAM, 1CPU) 구성의 AWS EC2 인스턴스 . t2.micro
  • 100명의 사용자와 10분 동안 1초의 램프업이 실행되는 Jmeter 테스트 스크립트 .

CPU 사용률 비교

최대 CPU 사용률

요청 모델당 Tomcat 스레드: 37.6%

네티 이벤트 루프: 34.6% 

결과  : Tomcat은CPU 사용률이 8.67052% 증가한 것으로 나타났습니다.

성과 결과

Tomcat 90 백분위수 응답 시간: 114ms

Tomcat의 응답 시간

 

Netty EventLoop 90 백분위수 응답 시간: 109ms

Netty EventLoop 응답 시간

댓글 없음:

댓글 쓰기