Connected Posts
2021.07.16 - [3. 기술 공부/Java (Spring, Spring Boot)] - [Java Servlet] 2. Servlet과 Servlet Container
2021.07.16 - [3. 기술 공부/Java (Spring, Spring Boot)] - [Java Servlet] 3. Servlet 3.0, 3.1 그리고 Spring MVC2021.07.16 - [3. 기술 공부/Java (Spring, Spring Boot)] - [Java Servlet] 3. Servlet 3.0, 3.1 그리고 Spring MVC
Goal
[Java Servlet] 시리즈 포스트에서는 Java Servlet에 대한 간단한 개념과, Java Servlet 역사에 큰 breakpoint가 있었던 Servlet 3.0(Async Servlet), 3.1(Non-blocking I/O)에 대해 정리해보려고 한다. 이번 포스팅에서는 어떤 breakpoint가 있었는지 살펴보고, 실제로 벤치마크 테스트를 한 결과를 보겠다.
1. Servlet 3.0 Async Servlet
지난 포스팅에서 살펴보았던 것처럼, Client <-> Servlet Container 단의 Asyn/Non-blocking 적용으로 뒷단 Servlet API에서도 Async/Non-blocking 지원이 필요해졌다. Async servlet은 Servlet 3.0부터 추가되었는데, 애플리케이션에 들어오는 request을 비동기적인 방식으로 처리할 수 있도록 지원하게 되었다.
- Async 방식
- Request Thread는 Processing Thread에게 할 일을 위임하고 반환되어 재사용
- Request Thread의 고갈을 막을 수 있지만, Active Thread의 개수가 줄어드는 것은 아님
Async Servlet을 사용하면 이제 서블릿 컨테이너의request thread는 들어오는 request를 받아 request의 processing을 담당할 다른 background thread에게 넘기게 된다. (당연히 이 background thread도 풀로 관리가 되어야함) 그리고 request thread는 client에게 응답을 보낸다. 그리고 할일을 다한 request thread는 background thread에게 request를 넘기자마자 thread pool에 회수되어 재사용될 수 있게 된다.
그러나, 이 접근 방식 자체는 request thread pool 고갈 문제를 해결할 수 있지만, 시스템 리소스 소비 문제를 해결할 수는 없다. 왜냐하면 다른 background thread가 request를 처리하기 위해 생성되고, 리퀘스트를 프로세싱하는 작업 자체가 non-blocking으로 작동하지 않으면 동시에 활성화되어있는 active thread의 개수는 줄어들지 않아 리소스 사용량이 개선되지 않기 때문이다.
그러므로 Async servlet만으로는 기존 문제를 완전히 해결할 수 없다는 한계가 있으며, 또다른 blocking을 막기 위해 프로세싱하는 작업 자체를 non-blocking 방식으로 동작하는 작업만 해야할 것이다.
또 다른 한계는 request를 처리하는 부분에 async가 적용되었을 뿐, request와 response를 서빙하는 I/O 는 Traditional I/O만을 허용하여 blocking이 된다는 점이다. 이 I/O 단에서 다수의 thread가 블락되게 되면 thread starvation을 유발하고 성능에 영향을 줄 수 있을 것이다.
2. Servlet 3.1 Non-blocking I/O
- Non-blocking I/O 추가
- Servlet 3.0에서 tradition I/O만 허용하였던 문제점을 개선
- Write/Read Listener를 추가하여 write/read에 대해 non-blocking으로 동작
그래서 Java EE 7에 포함되어 배포된 Servlet 3.1에는 non-blocking IO가 포함되었다.
만약 우리가 client에게 사이즈가 큰 JSON file을 돌려주어야한다고 가정했을 때, `OutputStream`은 NIO buffer에 우선 write를 하고, selector/channel 매커니즘을 사용하여 그 buffer가 client에 의해 비워져야할 것이다. 만약 클라이언트가 열악한 네트워크 상황에 있다면, `InputStream`과 `OutputStream`은 blocking이기 때문에 `out.write()`는 buffer가 다 비워질 때까지 기다려야할 것이다.
이러한 문제를 Servlet 3.1에서는 `Write/Read Listener`를 소개하여 해결하게 되었다.
`WriteListener`는 Servlet container에 의해 호출되는 `onWritePossible()` 메소드를 가진 인터페이스이다. Output 스트림을 관리하는 `ServletOutputStream.isReady()` 메소드가 NIO channel buffer에 write할 수 있는지 여부를 체크하는데 사용된다. false를 리턴하는 경우, Servlet container에 대한 `onWritePossible()` 메소드를 호출할 것을 예약하고, 언젠가 다른 thread에 의해 `onWritePossible()`이 호출된다. 이런 방식을 사용하게 되면 `out.write()`는 느린 client가 channel buffer를 비울 때까지 block될 일이 없게 된다.
3. Asynchronous Non-blocking I/O Serlvet
이로써 서블릿을 사용하는 애플리케이션에 모든 과정에 Asynchronous non-blocking이 적용이 되었다. 필자는 Servlet을 기반으로 동작하는 Spring MVC 애플리케이션을 Async/Non-block으로 작동하게 하고 싶었다. 그러면 이제 Servlet 버전을 올리고, Spring MVC에서 지원하는데로 반환 타입만 Future 타입으로 바꿔주면 될까 싶었는데 또다른 한계가 있었다.
서블릿에 Request, response를 다루는 HttpServletRequest, HttpServletResponse객체는 내부적으로 InputSream과 OutputStream을 사용하고 있다는 것이다. 자바의 Stream은 기본적으로 블로킹 방식이기 때문에, 앞단의 모든 노력에도 기본적으로 완전한 논블로킹이 아닌 블로킹 I/O 방식이라는 한계를 가지게 된다.
따라서 우리가 Spring 5 MVC에서 non-blocking 방식으로 동작하는 코드를 작성할 수 있지만, 여전히 서블릿 컨테이너 3.1 API에는 동기적이거나 블로킹되는 메서드들이 남아있어 non-blocking으로 동작하게끔 하려는 애플리케이션에서 실수로 blocking API를 호출하는 일이 발생할 위험이 항상 도사리고 있다.
이러한 이유로 blocking API가 사용될 잠재적인 위험이 없는 스택의 필요성이 대두되었고, 그에 따라 Spring 5에서는 Async concurrency model을 위해 또다른 선택지로 Reactive 기반의 webflux를 제공하고 있다. Servlet기반의 Spring MVC와 대조적으로 Spring WebFlux는 Servlet API를 기반으로 구축되지 않았으며 설계 상 비동기이기 때문에 그러한 비동기 요청 처리 기능이 필요하지도 않다고 한다.
4. Servlet 3.1 + vs Reactive
그렇다면 무조건 Spring Webflux만이 답인가? 혼돈이 왔다. Servlet 3.1 이상에서 Async/Non-blocking을 적용하더라도 response/request를 write/read하는 부분에서 blocking이 된다는 한계가 있지만 그래도 앞단에서 Async/Non-blocking이 적용된 부분에 대해서는 Sync/blocking인 경우와 확실한 성능 차이를 기대해볼 수 있지 않을까하는 실낱같은 희망을 가지고 실제 테스트를 진행해보았다. 테스트 대상은 Spring MVC sync block, Spring MVC async non-block, Spring Webflux기반의 웹서버이며 테스트 시나리오는 아래와 같다.
Test Case 1 | - 스레드 풀 200개 + Remote Blocking API Call - tomcat, jetty 스레드 풀을 200개로 고정한다. - 스레드가 1초 동안 Sleep되는 Remote API “/sleep” 을 호출한다. - wrk를 통해 Request Context를 생성하여 부하를 준다. - 10 thread and 100 connection |
Test Case 2 | - 스레드 풀 1개 + Remote Blocking API Call - tomcat, jetty 스레드 풀을 1개로 고정한다. - 스레드가 1초 동안 Sleep되는 Remote API “/sleep” 을 호출한다. - wrk를 통해 Request Context를 생성하여 부하를 준다. - 10 thread and 100 connection |
Test Case 3 | - 스레드 풀 1개 + Remote Blocking API Call - tomcat, jetty 스레드 풀을 1개로 고정한다. - 스레드가 1초 동안 Sleep되는 Remote API “/sleep” 을 호출한다. - wrk를 통해 Request Context를 생성하여 부하를 준다. - 100 thread and 100 connection |
결과는 아래와 같다.
Case | 대상 | 스레드 수 | 요청 수 | 10초 동안 처리량 | Avg Latency | Timeout |
1 | Spring MVC sync block | 200 | 10 thread 100 connection |
135 | 1.02s | 120 |
Spring webflux | 200 | 133 | 1.25s | 118 | ||
Spring MVC Async non-block |
200 | 135 | 1.30s | 118 | ||
2 | Spring MVC sync block | 1 | 10 thread 100 connection |
9 | 1.02s | 0 |
Spring webflux | 1 | 135 | 1.22s | 120 | ||
Spring MVC Async non-block |
1 | 135 | 1,12s | 120 | ||
3 | Spring MVC sync block | 1 | 100 thread 100 connection |
9 | 1.02s | 0 |
Spring webflux | 1 | 142 | 1.03s | 127 | ||
Spring MVC Async non-block |
1 | 150 | 1.03s | 135 |
response/request 사이즈가 의미있게 크지 않아서인지 몰라도 Spring webflux와 Spring MVC async/non-block가 큰 성능 상의 차이점을 보이지는 않았다. 기존 Spring MVC sync/block의 경우 Spring MVC async/non-block만 적용해도 아주 큰 성능의 향상을 볼 수 있다는 점이 확인 되었다.
결론적으로 테스트 결과만 두고 보면 아직 Spring Webflux가 Spring MVC에 비해 월등히 우세하다?라고 보기에는 어렵다고 생각했다. 각 팀의 상황(기존에 Spring MVC를 바탕으로 개발했다거나하면.. Webflux 전환을 위한 리소스가 들테니..)에 맞추어 사용하면 되겠지만 프로젝트를 처음 세팅하는 상황에서 완전한 Async concurrency model을 적용하기 위해서는 Webflux를 기반으로 프로젝트를 시작하는 것이 (미래에 더 나은 지원이 될 것을 기대하며..) 낫지 않을까 싶다.
이번 포스팅을 통해 Java Servlet의 기본과 어떻게 Async concurrency model을 적용하기 위해 Servlet이 변화해왔는지 변화 과정을 알아보았다. 연결된 부분들을 폭넓게 조사하다보니 Async concurrency model로의 변화를 논하기 위해서는 Reactive에 대해 알아보아야할 필요가 있다는 생각이 들었다. 이후 주제에서 Reactive도 다룰 수 있기를 바라며 포스팅을 마치도록 하겠다.
'3. 기술 공부 > Java (Spring, Spring Boot)' 카테고리의 다른 글
[Spring/Spring boot] Property 파일 제대로 설정하기 (0) | 2021.08.06 |
---|---|
[Java Servlet] 2. Servlet과 Servlet Container (0) | 2021.07.16 |
[Java Servlet] 1. Sync? Async?, Blocking? Non-Blocking? (0) | 2021.07.16 |
[Effective Java 3E] 5. 제네릭 (Generic) (0) | 2021.03.03 |