- はじめに
- Micronaut のスレッドモデル
- Quarkus のスレッドモデル
- Quarkus Reactive Routes
- Quarkus Reactive Routes のパフォーマンス
- まとめ
はじめに
先日、Quarkus のブログに以下の記事がポストされました。
Micronaut 陣営が行ったマイクロベンチマークにて、Micronaut の方が Quarkus より高パフォーマンスであるという結果に異を唱えるものです。
要約すると、Micronaut ではリクエストがIOスレッド上で処理されるのに対して、Quarkus では JAX-RS (Quarkus RESTEasy)を使った場合に、リクエストは Vert.x の IOスレッドで受け付けた後、ワーカースレッドプールから取得した別のスレッドに処理がディスパッチされるため、このオーバーヘッドを考慮した同等の条件で比較すべきだ ということになります。
本稿は、上記の内容についての補足説明です。
Micronaut のスレッドモデル
Micronaut ではメソッドの戻り値が非リアクティブ型(つまり String 型など)だった場合、(Micronaut において) I/Oスレッドプールと呼ばれるプールにあるスレッドで処理が行われます(Micronaut 1.X)。
メソッドの戻り値がリアクティブ型(つまりObservable
や CompletableFuture
)だった場合、イベントループスレッドと呼ばれるスレッドで処理が行われます。
この イベントループスレッドは、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
を引数に取り、リクエストとレスポンスにアクセスできます。RoutingExchange
は RoutingContext
のラッパであり便利なメソッドを提供します。
@Rute
アノテーションの type
にtype = 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 のブログでは明示していませんが、まぁ、これでしょう)。