Quarkus のスレッドモデルと Quarkus Reactive Routes

f:id:Naotsugu:20191119224111p:plain


はじめに

先日、Quarkus のブログに以下の記事がポストされました。

quarkus.io

Micronaut 陣営が行ったマイクロベンチマークにて、Micronaut の方が Quarkus より高パフォーマンスであるという結果に異を唱えるものです。

要約すると、Micronaut ではリクエストがIOスレッド上で処理されるのに対して、Quarkus では JAX-RS (Quarkus RESTEasy)を使った場合に、リクエストは Vert.x の IOスレッドで受け付けた後、ワーカースレッドプールから取得した別のスレッドに処理がディスパッチされるため、このオーバーヘッドを考慮した同等の条件で比較すべきだ ということになります。

本稿は、上記の内容についての補足説明です。


Micronaut のスレッドモデル

Micronaut ではメソッドの戻り値が非リアクティブ型(つまり String 型など)だった場合、(Micronaut において) I/Oスレッドプールと呼ばれるプールにあるスレッドで処理が行われます(Micronaut 1.X)。

メソッドの戻り値がリアクティブ型(つまりObservableCompletableFuture)だった場合、イベントループスレッドと呼ばれるスレッドで処理が行われます。 この イベントループスレッドは、Netty の NioEventLoopGroup で扱われる EventLoop におけるワーカースレッドそのものです(デフォルトではCPU数の2倍のワーカースレッドが利用可能)。

Netty の EventLoop では、Java の NIO を使うことで 1つのスレッドで複数のリクエスト(Socket) を扱えるため並行実行性を高めることができますが、IO処理の待機などでスレッドがブロックした場合には、他のリクエスト処理もブロックしてしまうため注意が必要となります。

@NonBlocking としてメソッドがブロックしないことを明示することでイベントループスレッドで処理させたり、@Blocking としてメソッドがブロックすることを明示することで I/Oスレッドで処理させたりを制御することもできます。

イベントループスレッドはブロックしたくないため、ブロックする場合は、 I/O スレッドプールで処理させるか、イベントループスレッドからブロック処理を別スレッドスケジュールする必要があります。I/Oスレッドはブロックが全体に影響しないように比較的大きなスレッドがプールされています。

なお、メソッドの戻り値が非リアクティブ型の場合に I/Oスレッドプールが使われる挙動は、Add a configuration option to disable automatic thread pool selection · Issue #2215 · micronaut-projects/micronaut-core · GitHubにもあるように Micronaut 2.0 で変更が予定されています。 この変更により、戻り値の定義によらず、デフォルトでイベントループスレッドが使われるようになります。

そして、ベンチマークでは Micronaut 2.0 M2 において以下のようなコードが使われているため、イベントループスレッドでのパフォーマンスが計測されています。

@Get(value = "/hello/{name}", produces = MediaType.TEXT_PLAIN)
String hello(@NotBlank String name) {
    return messageService.sayHello(name);
}


Quarkus のスレッドモデル

Quarkus では、全てのリクエストは Vert.x が受け付けます。Vert.x は内部で Netty を利用しており、1スレッドで複数のリクエストを扱えるイベントループがここでも利用されています。 Quarkus ではこの部分のイベントループスレッドを I/Oスレッド と呼んでいます。

そして、リクエストが JAX-RS にルーティングされる際に(ブロック可能な)ワーカースレッドにディスパッチされ、アプリケーションの処理はこのワーカースレッドで実行されます。

Quarkus がこのような実行形式を取っているのは、現在は命令的な実行モデルが広く利用されているためユーザーアプリケーションが JAX-RS (Quarkus RESTEasy) を使用する場合にはデフォルトでワーカースレッドにおいてワークロードを実行するようになっています。

ベンチマークでは Quarkus 1.3.1 において以下のようなコードが使われています。

@GET
@Path("/hello/{name}")
@Produces(MediaType.TEXT_PLAIN)
public String hello(@NotBlank @PathParam("name") String name) {
    return messageService.sayHello(name);
}

そのため、リクエストはI/Oスレッドを経て、ワーカースレッドにディスパッチされて実行されるため、特に何の処理も行わないメソッド呼び出しにおいては、スレッドのディスパッチが結果に大きく影響することとなります。

しかし、Quarkus においても Quarkus Reactive Routes を使うことで、ワーカースレッドを使わずに、I/O スレッドで直接処理することができます。


Quarkus Reactive Routes

Quarkus でイベントループ上で処理を行うには @Rute アノテーションを使います。

import io.quarkus.vertx.web.Route;
import io.quarkus.vertx.web.RoutingExchange;
import io.vertx.core.http.HttpMethod;
import io.vertx.ext.web.RoutingContext;

import javax.enterprise.context.ApplicationScoped;

@ApplicationScoped 
public class MyDeclarativeRoutes {

    @Route(methods = HttpMethod.GET) 
    void hello(RoutingContext rc) { 
        rc.response().end("hello");
    }

    @Route(path = "/greetings", methods = HttpMethod.GET)
    void greetings(RoutingExchange ex) { 
        ex.ok("hello " + ex.getParam("name").orElse("world"));
    }
}

@Rute アノテーションを付与すると、リアクティブ・ルートとして扱われ、処理はイベントループ上で扱われます(すなわちメソッドではブロックするコードを書いてはいけません)。

リアクティブ・ルートのメソッドは RoutingContext を引数に取り、リクエストとレスポンスにアクセスできます。RoutingExchangeRoutingContext のラッパであり便利なメソッドを提供します。

@Rute アノテーションの typetype = Route.HandlerType.BLOCKING と指定すれば、このメソッドはJAX-RS を使ったリクエストと同様に、ワーカー・スレッドで呼び出されます。

@Rute の指定は @RouteBase を使ってクラスレベルでパス指定することもできます。

@RouteBase(path = "simple", produces = "text/plain") 
public class SimpleRoutes {

    @Route(path = "ping")
    void ping(RoutingContext rc) {
        rc.response().end("pong");
    }
}

この場合、ping メソッドは /simple/ping というパスになります。

@Rute アノテーションではなく、初期化時に Router に直接登録することもできます。

public void init(@Observes Router router) {
    router.get("/my-route").handler(rc -> rc.response().end("Hello from my route"));
}


@RouteFilter を使えば、イベントループスレッド(I/Oスレッド) 上で処理を挟むことができます。

import io.vertx.ext.web.RoutingContext;

public class MyFilters {

    @RouteFilter(100) 
    void myFilter(RoutingContext rc) {
       rc.response().putHeader("X-Header", "intercepting the request");
       rc.next(); 
    }
}

I/Oスレッド で処理されるため、後続のワーカースレットで呼び出される JAX-RS や Servlet などでも有効になります。


Quarkus Reactive Routes のパフォーマンス

さて、Quarkus のブログに以下の記事に戻ると、以下の JAX-RS と、

@Path("/")
public class MessageResource {

    private MessageService messageService;

    public MessageResource(MessageService messageService) {
        this.messageService = messageService;
    }

    @GET
    @Path("/hello/{name}")
    @Produces(MediaType.TEXT_PLAIN)
    public String hello(@NotBlank @PathParam("name") String name) {
        return messageService.sayHello(name);
    }
}

以下の Reactive Routes によるパフォーマンスを計測しています。

@Singleton
public class VertxRoute {

    @Inject Validator validator;

    final MessageService messageService;

    public VertxRoute(MessageService messageService) {
        this.messageService = messageService;
    }

    @Route(path = "/hello/:name", methods = HttpMethod.GET)
    void greetings(RoutingExchange ex) {
        Set<ConstraintViolation<RequestObj>> violations = validator.validate(new RequestWrapper(ex.getParam("name").get()));
        if(violations.size() == 0) {
            ex.ok(messageService.sayHello(ex.getParam("name").orElse("world")));
        } else {
            StringBuilder vaidationError = new StringBuilder();
            violations.stream().forEach(violation -> vaidationError.append(violation.getMessage()));
            ex.response().setStatusCode(400).end(vaidationError.toString());
        }
    }

    private class RequestWrapper {
        @NotBlank
        public String name;

        public RequestWrapper(String name) {
            this.name = name;
        }
    }
}

Reactive Routes 版では、メソッドパラメータの @NotBlank による BeanValidation が利用できないため、Validator で直接実施しています。


結果は、Reactive Routes 版が、スループットが2.6倍、メモリ使用量は30%削減という結果だったということです(詳細は Quarkus のブログをご覧ください)。


まとめ

Micronaut と Quarkus のスレッドモデルの違いについて説明しながら各フレームワークにおけるマイクロベンチマーク結果について紹介しました。

これらのベンチマークはビジネスワークの無いものであり、この結果にどれだけの意味があるのかについては何とも言えないところではあります。

両陣営とも、Netty による 非同期イベント駆動アプローチによるイベントループを活用していますが、ビジネスワークはイベントループ外のスレッドを組み合わせて対処している点は興味深いところですね。


Object Computing 社による Micronaut のマイクロベンチマークは以下の記事で見ることができます(Quarkus のブログでは明示していませんが、まぁ、これでしょう)。

objectcomputing.com