Webflux + Non-blocking call 테스트

김콜라
6 min readNov 21, 2022

--

이전 포스팅
Webflux + JPA(Blocking call) 비동기 처리 테스트

우선 이전 포스팅의 결론을 다시 생각해보겠습니다.

이전 포스팅에서는 Webflux 에서 JPA 의 Blocking Call 을 비동기 처리하여 워커 스레드가 Blocking 되지 않도록 만들었습니다.

이렇게 구현해도 정상적으로 동작하는 것처럼 보이지만, 실제로는 다음과 같은 문제가 있습니다.

  1. 비동기 처리를 위해 별도의 스레드를 활용합니다.
  2. 별도의 스레드는 JPA 응답을 기다리기 위해 Block 됩니다.

JPA(JDBC) 가 Blocking 으로 동작하기 때문에, JPA 로직이 실행되는 스레드는 응답을 받을때까지 블로킹 됩니다.

즉, 워커 스레드를 Blocking 하지 않아 Webflux 에서 할당한 스레드만큼만 사용하여 모든 요청을 처리하는 것처럼 보이지만,
실제로는 Thread Per Request 모델과 마찬가지로 비동기처리를 위한 스레드 풀의 크기를 넘는 동시요청이 들어오면 처리량이 낮아지게 됩니다.

이를 완벽히 해결하기 위해선, 아래 그림처럼 I/O 작업 자체가 Non-blocking 으로 동작해야합니다.

출처: https://dzone.com/articles/spring-webflux-eventloop-vs-thread-per-request-mod

예를 들어, Spring 4.0 에서 나온 AsyncRestTemplate 은 기본적으로 앞서 저희가 JPA 를 처리한 방식과 비슷하게 스레드 하나를 할당하여 비동기로 처리하는데, 효율적인 방법은 아니라고 생각합니다.
근데, AsyncRestTemplate 의 AsyncClientHttpRequestFactory 설정을 Non-blocking I/O 를 지원하는 Netty 기반으로 변경하면 마법처럼 모든 요청을 별도의 스레드 할당없이 처리할 수 있습니다.

네티는 네트워크 애플리케이션을 쉽고 빠르게 개발할 수 있도록 추상화된Non-blocking IO client/server 프레임워크입니다.
(https://netty.io/)

R2DBC

앞서 작성한 AsyncRestTemplate 는 WebClient 라는 좋은 대안이 존재합니다. 마찬가지로, Blocking 으로 동작하는 JDBC 도 R2DBC 라는 대안이 있습니다.

R2DBC 는 SQL 데이터베이스를 위한 Reactive API 스펙입니다. SQL DB 의 Non-blocking call 을 가능하게 하기 위해 각 벤더들이 Non-blocking 을 지원하도록 구현하여 driver 를 만듭니다.

각 driver 가 Non-blocking I/O 를 지원하는 방식은 다르겠지만, 제가 살펴본 jasync-sql, r2dbc-mysql 는 Netty 기반이란 것을 확인 했습니다.

driver 목록은 https://r2dbc.io/drivers/ 에서 확인할 수 있습니다.

이제부턴 실제로 Non-blocking call 에 대해 Webflux 의 스레드가 어떻게 생성되는지 확인해보겠습니다.

테스트 환경은 다음과 같습니다.

  • Kotlin + Webflux + R2DBC
  • SQL 요청은 SLEEP(1) 로 1초동안 처리되도록 함

DB 연결 없이 테스트

아래는 요청을 처리하는데 1초 정도 걸리는 API 에 요청 150개를 보낸 결과입니다. (R2DBC 연결없이 단순 연산에 1초 정도 소요됩니다.)

webflux thread 10 개

이전에 확인했듯이, 10개의 워커 스레드로 모든 요청을 처리해주며 동시성을 제공해줍니다.

아래처럼 워커 스레드 수를 조절할 수 있습니다.

// 코어수와 관계없이 워커 스레드 수를 명시할 수 있습니다.
System.setProperty("reactor.netty.ioWorkerCount", "5")

r2dbc-mysql 연결 후 테스트

이번에는 r2dbc-mysql 로 요청을 보내는 API 를 사용해봅시다. 여기서는 아래 라이브러리를 사용합니다.

아래는 R2DBC 연결 후, 요청을 처리하는데 1초 정도 걸리는 API 에 요청 150개를 보낸 결과입니다.

Webflux 에서 reactor-http thread 10 개와 r2dbc-mysql 에서 생성된 reactor-tcp thread 10개가 생성됨.
1초 요청 150개 처리에 약 1.2초 정도 소요.

Webflux, r2dbc-mysql 모두 reactor-netty 기반으로 동작하여, 기존 reactor-http thread 10 개 이외에 r2dbc-mysql 에서 생성된 reactor-tcp thread 10개를 추가로 확인할 수 있습니다.

이전 포스팅에서는 동기작업을 비동기처리 하기 위해 요청만큼의 추가 스레드를 이용했지만, 모든 I/O 처리를 Non-blocking 으로 하면 추가 스레드를 사용하지 않아도 많은 요청을 처리할 수 있게됩니다.

마무리

JPA (JDBC) 가 아닌 R2DBC 를 이용하면 Netty 가 기본적으로 생성하는 워커 스레드 이외의 추가 스레드 없이 이전과 동일한 결과를 얻을 수 있다는 것을 확인했습니다.

결국 가장 핵심은 Non-blocking I/O 를 지원하는 Database / Http Client 라이브러리를 사용하여 Netty 가 생성하는 기본 워커 스레드를 Blocking 하지 않아야 된다는 것입니다.

만약, 이러한 I/O 작업 이외에 굉장히 무거운 연산을 코드레벨에서 하면 여전히 워커 스레드는 Blocking 되겠죵?

다음에는 Netty 에 대해서 더 알아보고 싶네요.

Reference

https://github.com/mirromutth/r2dbc-mysql

https://docs.spring.io/spring-framework/docs/current/reference/html/web-reactive.html

https://github.com/jasync-sql/jasync-sql

--

--