Java

Web applications and Project Loom (번역)

AlwaysPr 2023. 3. 12. 21:52

Web applications and Project Loom를 번역한 글입니다.


소개

Project Loom은 "간편한 사용성(easy-to-use), 높은 처리량(high-throughput), 가벼운 동시성(lightweight concurrency)"을 JRE에 제공하는 것을 목표로 합니다. Project Loom이 도입한 기능 중 하나는 Virtual thread입니다. 이 블로그 게시물에서는 Apache Tomcat에서 배포한 몇 가지 간단한 웹 애플리케이션을 사용하여 Virtual thread가 웹 애플리케이션에 어떤 의미를 주는지 살펴보겠습니다.

높은 처리량 / 경량화

첫 번째 실험은 Tomcat 표준 Thread pool 사용에 따른 overhaed와 Virtual thread(Loom) 사용에 따른 overhead를 비교합니다.  테스트에 사용된 환경은 이 게시물의 마지막에 자세히 설명되어 있습니다.  다양한 크기의 응답과 동시성 요청에 대한 RPS(초당 평균 요청) 성능을 실험했습니다. 결과는 다음 그래프와 같습니다.

요청을 처리하기 위해 새로운 Virtual thread를 생성하는 overhead가 Thread pool에서 Platform thread를 가져오는 overhead보다 낮다는 결과를 보여줍니다.

Thread pool 테스트에서 예상치 못한 결과는 응답 body의 크기가 작고 동시 사용자가 2명일 때가 한 명의 동시 사용자보다 RPS 수가 더 낮다는 점입니다. 확인 결과, Executor에게 전달되는 task과 task의 run() 메서드를 호출하는 Executor 사이에 추가 지연이 발생하는 것으로 확인되었습니다. 동시 사용자가 4명일 때는 이 차이가 줄어들었고, 8명일 때는 거의 사라졌습니다.

사용 가능한 프로세서 코어수보다 더 많은 동시 작업이 있을 때 높은 수준의 동시성에서 Vircual thread Executor의 성능이 다시 향상되는 것으로 나타났습니다. 이는 더 작은 응답 body를 사용한 테스트에서 더욱 두드러졌습니다.
 

간편한 사용성

두 번째 실험에서는 표준 Thread pool과 함께 Servlet 비동기 I/O를 사용하여 얻은 성능과 Virtual Thread 기반 Executor와 함께 간단한 블로킹 I/O를 사용하여 얻은 성능을 비교했습니다. 여기서 Vircual Thread의 잠재적 이점은 단순성(simplicity)입니다. 블로킹 읽기 / 쓰기는 특히 오류 처리를 고려할 때 같은 조건의 Servlet 비동기 읽기 / 쓰기보다 훨씬 더 간단합니다.

Servlet 비동기 I/O는 응답에 지연이 있는 외부 서비스에 접근하는 데 자주 사용됩니다. 테스트 웹 애플리케이션은 Service 클래스에서 이를 시뮬레이션했습니다. Virtual Thread 기반 Executor와 함께 사용된 Servlet은 블로킹 스타일로 서비스에 액세스 한 반면, 표준 Thread pool과 함께 사용된 Servlet은 Servlet 비동기 API를 사용하여 서비스에 액세스 했습니다. 네트워크 I/O는 포함되지 않았지만 결과에 영향을 미치지는 않았을 것입니다.

초기 테스트에서는 당연히 블로킹 방식과 비동기 방식 간에 측정 가능한 차이가 없었는데, 5초 지연이 대부분을 시간을 차지했기 때문입니다. 지연의 영향이 없는 차이를 살펴보기 위해 지연을 0으로 줄이고 처리량 테스트와 유사한 테스트 세트를 실행했습니다. 결과는 다음 그래프에 나와 있습니다.

다시 우리는 Virtual Thread가 일반적으로 성능이 더 뛰어나다는 것을 알 수 있습니다. 그 차이는 동시성이 낮을 때와 테스트에 사용 가능한 프로세서 코어 수를 초과하는 동시성이 요구될 때 가장 두드러집니다.

분석

Virtual Thread 기반 Executor와 Tomcat 표준 Thread pool의 차이는 위의 그래프에서 보이는 것만큼 뚜렷하지 않습니다. 이 테스트는 각 접근 방식과 관련된 overhead를 측정하기 위해 설계되었으며, 실제 애플리케이션을 대표하지 않습니다. 실제 애플리케이션에서 테스트에 나타난 차이는 요청을 완료하는 데 걸리는 시간과 비교할 때 무시해도 될 정도입니다.

Tomcat 표준 Thread pool과 Virtual Thread 기반 Executor 간 성능 차이의 주요 원인은 Thread pool 사용 시 Queue에서 task를 추가하고 제거할 때 발생하는 경합입니다. 그리고 이는 Tomcat의 구현을 최적화함으로써 Thread pool queue의 경합을 줄여 처리량을 개선할 수 있어보입니다.

상대적 성능에 영향을 미치는 두 번째 요소는 Context switching입니다. Virtual thread의 Context switching 비용이 표준 Thread pool의 Thread보다 저렴합니다. 그래서 사용 가능한 프로세서 코어 수 이상이 요구되는 동시성이 두 번째 실험에서 나타난 성능 차이를 설명할 수 있는 원인일 수 있습니다.
 

결론

Virtual thread 기반 Executor를 사용하는 것은 Tomcat 표준 Thread pool에 대한 실행 가능한 대안입니다. Virtual thread로 전환하면 컨테이너 overhead 측면에서 얻을 수 있는 이점은 미미합니다.

Servlet 비동기 API, 리액티브 프로그래밍, 기타 비동기 API로 전환하지 않은 웹 애플리케이션인 클래식 Spring MVC와 같은 블로킹 기반의 웹 어플리케이션은 Virtual Thread 기반 Exctuor로 전환하면 확장성(scalability)이 약간 개선될 수 있습니다. 웹 애플리케이션에 따라 웹 애플리케이션 코드의 큰 변경 없이 이러한 개선 사항을 달성할 수 있습니다.

Servlet 비동기 API, 리액티브 프로그래밍, 기타 비동기 API를 사용하도록 전환한 웹 애플리케이션은 Virtual Thread 기반 Executor로 전환해도 유의미한 차이(긍정적이든 부정적이든)를 얻지 못할 가능성이 높습니다.

장기적으로 볼 때 Virtual Thread의 가장 큰 장점은 애플리케이션 코드의 간소화입니다. 현재 Servlet 비동기 API, 리액티브 프로그래밍, 기타 비동기 API를 사용해야 하는 일부 사용 사례는 블로킹 I/O와 Virtual Thread를 사용하여 충족할 수 있을 것입니다. 여기서 주의할 점은 애플리케이션이 다른 외부 서비스를 여러 번 호출해야 하는 경우가 많다는 것입니다. 이 작업은 병렬로 수행하는 것이 가장 효율적이며 Project Reactor와 같은 프레임워크가 이를 최고 수준으로 지원하지만, 이에 상응하는 JRE의 솔루션(구조적 동시성)은 아직 incubator 단계에 있으며 여러 future를 조정하는 것을 목표로 할 뿐 가장 편리한 방식으로 서로를 기준으로 선언하거나 구성하는 것은 아닙니다.

마지막으로, Project Loom은 아직 preview 모드에 있습니다. 프로덕션 환경에서 Virtual Thread 사용을 고려하기에는 아직 이르지만, 지금이 바로 Project Loom과 Virtual Thread 사용 계획을 세워 JRE에서 Virtual Thread를 일반적으로 사용할 수 있게 될 때를 대비해야 합니다.

테스트 환경

테스트 환경은 다음과 같이 구성되었습니다.

테스트는 Intel i7-6950X processor, 32GB RAM 최신버전의 Ubuntu 22.04.1 LTS 머신에서 수행되었습니다.

테스트 간의 차이점을 극대화하고, 일반적인 overhead를 최소화하기 위해 기본 설정에서 다음의 구성들을 변경했습니다.

  • loopback 인터페이스를 사용하며, 하나의 머신에서 테스트를 실행하여 네트워크 overhead를 최소화합니다.
  • 요청량이 많을 때 상당한 Disk I/O의 원인이 되는 액세스 로그를 비활성화합니다.
  • maxKeepAliveRequests -1로 설정하여 TCP 연결 설정 / 해지에 소요되는 시간을 줄입니다.

또한 테스트 웹 애플리케이션은 일반적인 overhead를 최소화하고, 테스트 간의 차이점을 강조하도록 설계되었습니다.

사용된 server.xml 파일은 다음과 같습니다.

<?xml version="1.0" encoding="UTF-8"?>
<Server port="8005" shutdown="SHUTDOWN">
  <Listener className="org.apache.catalina.startup.VersionLoggerListener" />

  <Service name="Catalina">

    <Executor
        className="org.apache.catalina.core.LoomExecutor"
        name="loomExecutor"
        />

    <Connector 
        protocol="org.apache.coyote.http11.Http11NioProtocol"
        port="8080"
        maxKeepAliveRequests="-1"
        />

    <Connector
        executor="loomExecutor"
        protocol="org.apache.coyote.http11.Http11NioProtocol"
        port="8081"
        maxKeepAliveRequests="-1"
        />

    <Engine name="Catalina" defaultHost="localhost">
      <Host name="localhost"  appBase="webapps"
            unpackWARs="true" autoDeploy="true">
      </Host>
    </Engine>
  </Service>
</Server>


사용된 setenv.sh 파일은 다음과 같습니다.

JAVA_OPTS=--enable-preview