WebFlux로 Asynchronous & Non-blocking I/O 전환하여 API 성능 튜닝하기


목적

  • WebFlux로 Asynchronous & Non-blocking I/O 전환하여 API 성능 튜닝

배경

  • APM을 통해 특정 API는 평균 성능 보다 수 십배 느려, 성능 편차가 크다는 걸 알게 되었습니다.
  • 성능 저하의 원인은 I/O의 순차 처리와 Blocking으로, 병목 현상을 유발하고 있었습니다.
  • 따라서, 병목을 해소하여 API의 성능 및 처리율을 향상 시키고자 합니다.

응답시간을 나타내는 녹색점이 위 아래로 넓게 퍼져있어 성능 편차가 큰 것을 알 수 있다.

Pinpoint 성능 편차

목표

  • 순차적으로 처리하던 다수의 I/O 작업을 동시에 처리하도록 개선하여, API 성능 향상
  • Blocking I/O를 Non-Blocking 하도록 개선하여, API 성능 향상
  • 컴퓨팅 자원(Thread)을 효율적으로 활용하여, 급증하는 트래픽을 안정적으로 처리

필수 지식

  • Blocking and Non-blocking
  • Synchronous and Asynchronous
  • Reactive API를 사용하면 무엇이 좋은가 ?
  • Reactive API는 언제 써야 하는가 ?
  • Reactive API는 왜 써야 하는가 ?
  • Reactor (Mono & Flux)

도구

  • spring-boot-starter-webflux
  • reactor-cache
  • reactor-extra
  • reactor-test

작업

작업은 전환, 조립, 튜닝 3단계로 나누어 진행.

전환 작업

  • 외부 API 호출 모듈 전체를 비동기/논블로킹을 지원하는 WebClient 기반으로 전환

RestTemplate & WebClient Blocking -> WebClient Non-blocking

  • AS-IS

    RestTemplate Blocking I/O WebFlux Blocking I/O

  • TO-BE

    WebFlux Non-Blocking I/O

조립 작업

  • 외부 API, DB 조회, Redis 등 I/O 작업의 순서가 중요하지 않다면 동시에 처리하도록 개선.

이때, 순차 처리하는 I/O를 동시에 처리하도록 Mono.zip을 활용하여 조립.

  • 대상
    • API 호출
    • DB 조회
    • Redis 조회
  • 작업 예시

    Mono.zip Function Usage

  • 유의 사항
    • WebFlux는 Netty 기반으로 동작하기에 적은 수의 Thread를 효율적으로 처리해야 한다.
    • 따라서, Thread가 Blocking 되면 안된다.
    • 만약, Blocking I/O를 어쩔 수 없이 함께 사용해야 할 경우 반드시 Elastic Thread 혹은 Custom Thread를 할당하여 처리해야 한다. Elastic Scheduler

튜닝 작업

  • 배경
    • Spring Boot 2.1.6 기준 Spring의 @Cacheable에서 Reactor Cache 미지원
  • 작업
    • Reactor Cache 개발
  • 요구사항
    • Spring에서 Reactive Cache를 지원할 경우를 고려해서 추가 및 제거가 용이해야 한다.
    • Spring @Cacheable 인터페이스와 구현이 변경되도 영향을 받지 않게 개발해야 한다.
  • 요구사항 구체화
    • AOP & Annotation 기반으로 개발
    • Spring @Cacheable 관련 코드를 재활용 하지 않는다.
  • Github

결과

비동기 논블로킹 개선 작업으로 병목 현상이 해소되어 성능이 대폭 향상되었음.

Total_Newrelic

평균 응답 속도보다 느린 API를 개선하여 Latency가 튀는 현상 즉, 성능 편차를 줄였다.

Total_Pinpoint

마치며

  • 예상보다 성능 향상 효과가 컸다.
  • 비동기 논블로킹 개선 작업은 외부 I/O 응답이 느리면 느릴 수록, 동시에 호출하는게 많으면 많을 수록 효과가 크다.
    • 순차 처리하던 것을 동시에 처리하도록 변경하니 당연하다.
  • 아직 JDBC가 비동기 논블로킹 스펙을 지원하지 않아, R2DBC를 기다리는 중이다.
    • DB까지 Full 비동기 논블로킹을 지원하게 되면, 1 Request = 1 Thread의 서블릿 기반의 개념을 벗어날 수 있지 않을까 생각한다.

pkgonan

서버 개발자 Github Linkedin Facebook

Java 및 Spring을 활용한 서버 개발과 성능 튜닝에 관심이 있습니다. 객체지향 및 테스트 코드 작성을 중요하게 생각하며, 변화에 강한 코드를 작성하고자 노력하고 있습니다.