リアクティブ入門
リアクティブ とは、堅牢で効率的、かつコンカレントなアプリケーションやシステムを構築するための一連の原則です。これらの原則により、従来のアプローチよりも多くの負荷を処理しながら、リソース(CPUやメモリ)をより効率的に使用し、また障害にも適切に対応することができます。
Quarkusは Reactive フレームワークです。当初から、 _Reactive_はQuarkusのアーキテクチャに欠かせない要素でした。多くのReactive機能が含まれており、幅広いエコシステムを提供しています。
このガイドは、 リアクティブ とは何か、Quarkusでどのようにリアクティブアーキテクチャを実現するかについての詳細な記事ではありません。これらのトピックについて詳しく知りたい場合は、Quarkusのリアクティブエコシステムの概要を説明した リアクティブアーキテクチャガイド を参照してください。
このガイドでは、Quarkusのリアクティブな機能をご紹介します。簡単なCRUDアプリケーションを実装する予定です。ただし、 Hibernate with Panacheガイド とは異なり、Quarkusのリアクティブ機能を使用します。
このガイドでは、次の情報を提供します。
-
RESTEasy Reactiveを使ったリアクティブCRUDアプリケーションのブートストラップ
-
Hibernate Reactive with Panacheによるリアクティブなデータベースとの連携
-
Quarkus REST(旧RESTEasy Reactive)を使用して、リアクティブ原則に基づいてHTTP APIを実装します。
-
アプリケーションのパッケージ化と実行
前提条件
このガイドを完成させるには、以下が必要です:
-
約15分
-
IDE
-
JDK 17+がインストールされ、
JAVA_HOME
が適切に設定されていること -
Apache Maven 3.9.9
-
使用したい場合は、 Quarkus CLI
-
ネイティブ実行可能ファイルをビルドしたい場合、MandrelまたはGraalVM(あるいはネイティブなコンテナビルドを使用する場合はDocker)をインストールし、 適切に設定していること
Mavenが期待するJavaバージョンを使用していることを確認します。複数のJDKがインストールされている場合、Mavenが期待されるものを使用していることを確認してください。 mvn --version. を実行することで、Mavenが使用するJDKを確認することができます。
|
命令型かリアクティブ型か: スレッドの問題だ
前述したように、このガイドでは、リアクティブなCRUDアプリケーションを実装します。しかし、伝統的なモデルや命令型のモデルと比べて、どのような違いや利点があるのか疑問に思うかもしれません。
この対比をよりよく理解するためには、リアクティブ型と命令型の実行モデルの違いを説明する必要があります。 リアクティブ は単なる実行モデルの違いではなく、このガイドを理解するためにはその区別が必要であることを理解しておく必要があります。
伝統的な命令型のアプローチでは、フレームワークはリクエストを処理するスレッドを割り当てます。つまり、リクエストの処理全体がこのワーカースレッド上で実行されるのです。このモデルは、スケールがあまりよくありません。実際、複数の同時リクエストを処理するためには、複数のスレッドが必要となり、アプリケーションの同時実行性はスレッドの数によって制限されます。さらに、これらのスレッドは、コードがリモートサービスとやり取りするとすぐにブロックされます。そのため、より多くのスレッドが必要となり、各スレッドはOSのスレッドにマッピングされているため、メモリやCPUのコストがかかり、リソースの非効率的な使用につながります。
一方、リアクティブモデルは、ノンブロッキングI/Oおよび、異なる実行モデルに依存しています。ノンブロッキングI/Oは、同時I/Oを効率的に処理する方法です。I/Oスレッドと呼ばれる最小限のスレッドで、多くの同時I/Oを処理することができます。このようなモデルでは、リクエスト処理をワーカースレッドに委ねるのではなく、I/Oスレッドを直接使用します。リクエストを処理するためにワーカースレッドを作成する必要がないため、メモリとCPUを節約できます。リクエストを処理するためにワーカスレッドを作成する必要がないため、メモリやCPUを節約できます。また、スレッド数の制約がなくなるため、同時実行性も向上します。最後に、スレッドの切り替え回数が減ることで、応答時間も改善されます。
シーケンシャルから継続タイルへ
つまり、リアクティブな実行モデルでは、I/Oスレッドを使ってリクエストを処理します。しかし、それだけではありません。I/Oスレッドは、複数の同時リクエストを処理することができます。どうやって?これが、リアクティブとインペラティブの最も重要な違いの一つであるトリックです。
リクエストの処理に、HTTP APIやデータベースなどのリモートサービスとのやりとりが必要な場合、レスポンスを待つ間に実行をブロックすることはありません。代わりに、I/O操作をスケジューリングし、リクエスト処理の残りのコードである継続を添付します。この継続は、コールバック(I/Oの結果とともに呼び出される関数)として渡すこともできますし、リアクティブプログラミングやコルーティンなどのより高度な構成を使用することもできます。継続をどのように表現するかに関わらず、重要な点はI/Oスレッドの解放であり、その結果、このスレッドを使って別のリクエストを処理することができるという事実です。スケジュールされたI/Oが完了すると、I/Oスレッドは継続を実行し、保留中のリクエストの処理は継続されます。
そのため、I/Oが実行をブロックするような命令型モデルとは異なり、リアクティブ型ではI/Oスレッドを解放し、I/Oが完了した時点で継続を呼び出す継続型のデザインに切り替わります。その結果、I/Oスレッドは複数の同時リクエストを処理できるようになり、アプリケーション全体の同時性が向上します。
しかし、これには問題があります。継続パッシングのコードを書くための方法が必要なのです。これにはいろいろな方法があります。Quarkusでは、以下を提案しています:
-
Mutiny - 直感的でイベント駆動型のリアクティブ・プログラミング・ライブラリ
-
Kotlin co-routines - 非同期のコードを逐次的に記述する方法
このガイドでは、Mutinyを使用します。Mutinyについて詳しく知りたい方は、 Mutinyのドキュメントをご覧ください。
JDKに近々導入されるProject Loomは、仮想スレッドベースのモデルを提案しています。Quarkusのアーキテクチャは、Loomがグローバルに利用可能になり次第、サポートする準備ができています。 |
リアクティブフルーツアプリケーションの起動
これを念頭に置いて、Quarkusを使用してCRUDアプリケーションを開発する方法を見てみましょう。このアプリケーションは、I / Oスレッドを使用してHTTPリクエストを処理し、データベースとやり取りし、結果を処理し、HTTP応答を記述します。言い換えれば、リアクティブCRUDアプリケーションです。
ステップバイステップの手順に従うことをお勧めしますが、最終的なソリューションは https://github.com/quarkusio/quarkus-quickstarts/tree/main/hibernate-reactive-panache-quickstart で見つけることができます。
まず、 code.quarkus.ioにアクセスし、以下のエクステンションを選択します:
-
Quarkus REST Jackson
-
Hibernate Reactive with Panache
-
リアクティブなPostgreSQLクライアント
最後のエクステンションは、PostgreSQL用のリアクティブデータベースドライバです。Hibernate Reactiveはこのドライバを使用して、呼び出し元のスレッドをブロックすることなくデータベースと対話します。
選択したら、"Generate your application "をクリックし、zipファイルをダウンロードして解凍し、お好みのIDEでコードを開きます。
Reactive Panache Entity
では、 Fruit
から始めましょう。以下の内容で src/main/java/org/acme/hibernate/orm/panache/Fruit.java
ファイルを作成します。
package org.acme.hibernate.orm.panache;
import jakarta.persistence.Cacheable;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import io.quarkus.hibernate.reactive.panache.PanacheEntity; (1)
@Entity
@Cacheable
public class Fruit extends PanacheEntity {
@Column(length = 40, unique = true)
public String name;
}
1 | PanacheEntity のリアクティブ版をインポートしてください。 |
このクラスは Fruits
を表します。1つのフィールド( name
)を持つ単純なエンティティです。なお、このクラスは PanacheEntity
のリアクティブ版である io.quarkus.hibernate.reactive.panache.PanacheEntity
を使用しています。つまり、舞台裏では、Hibernate は上述の実行モデルを使用しています。スレッドをブロックすることなく、データベースと対話します。さらに、このリアクティブな PanacheEntity
は、リアクティブな API を提案しています。このAPIを使用してRESTエンドポイントを実装します。
先に進む前に、 src/main/resources/application.properties
ファイルを開き、以下を追加します:
quarkus.datasource.db-kind=postgresql
quarkus.hibernate-orm.database.generation=drop-and-create
データベースにPostgreSQLを使用し、データベーススキーマの生成を処理するようにアプリケーションに指示します。
同じディレクトリに、 import.sql
ファイルを作成します。このファイルにはいくつかのフルーツが挿入されており、開発モードで空のデータベースから始めないようになっています。
INSERT INTO fruit(id, name) VALUES (1, 'Cherry');
INSERT INTO fruit(id, name) VALUES (2, 'Apple');
INSERT INTO fruit(id, name) VALUES (3, 'Banana');
ALTER SEQUENCE fruit_seq RESTART WITH 4;
ターミナルで、次のようにしてアプリケーションを開発モードで起動します: ./mvnw quarkus:dev
. Quarkusは自動的にデータベースインスタンスを起動し、アプリケーションの設定を行います。あとは、HTTPエンドポイントを実装するだけです。
リアクティブリソース
データベースとのやりとりはノンブロッキングで非同期なので、HTTPリソースを実装するために非同期の構造を使用する必要があります。Quarkusは、中心となるリアクティブプログラミングモデルとしてMutinyを使用しています。そのため、HTTPエンドポイントからMutinyタイプ( Uni
と Multi
)を返すことをサポートしています。また、Fruit Panacheのエンティティはこれらの型を使ったメソッドを公開していますので、私たちは 糊 を実装するだけで済みます。
src/main/java/org/acme/hibernate/orm/panache/FruitResource.java
ファイルを以下の内容で作成します。
package org.acme.hibernate.orm.panache;
import java.util.List;
import io.quarkus.panache.common.Sort;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.ws.rs.Path;
@Path("/fruits")
@ApplicationScoped
public class FruitResource {
}
まずは、 getAll
のメソッドから見てみましょう。 getAll
メソッドは、データベースに保存されているすべてのフルーツを返します。 FruitResource
の中に、以下のコードを追加します。
@GET
public Uni<List<Fruit>> get() {
return Fruit.listAll(Sort.by("name"));
}
http://localhost:8080/fruits を開いて、このメソッドを呼び出します。
[{"id":2,"name":"Apple"},{"id":3,"name":"Banana"},{"id":1,"name":"Cherry"},{"id":4,"name":"peach"}]
期待通りのJSON配列が得られます。 Quarkus RESTは特に指示がない限り、自動的にリストをJSON配列にマップします。
戻り値の型を見てください。これは List<Fruit>
の Uni
を返します。 Uni
は非同期型です。これはfutureのようなものです。これは後で値(アイテム)を得るためのプレースホルダーです。アイテムを受け取ったら(Mutinyではアイテムを _emit_すると言っています)、何らかの動作を付けることができます。Uniを取得し、そのUniがアイテムをemitしたら、残りの処理を実行する、という継続を表現しています。
リアクティブな開発者は、なぜフルーツのストリームを直接返すことができないのかと思うかもしれません。それは、データベースを扱うときにはよくない考えになりがちです。リレーショナル・データベースはストリーミングをうまく扱えません。これは、このユースケースのために設計されていないプロトコルの問題です。そのため、データベースから行をストリーミングするには、すべての行が消費されるまで、コネクション(場合によってはトランザクション)を開いておく必要があります。コンシューマーが遅い場合、データベースの黄金律である「コネクションを長く保持しない」を破ってしまいます。実際、コネクションの数はかなり少なく、コンシューマーがコネクションを長時間保持することは、アプリケーションの並行性を劇的に低下させます。ですから、可能な限り Uni<List<T>> を使用し、コンテンツをロードしてください。結果のセットが大きい場合は、ページネーションを実装してください。
|
getSingle
でAPIを継続してみましょう。
@GET
@Path("/{id}")
public Uni<Fruit> getSingle(Long id) {
return Fruit.findById(id);
}
この場合、 Fruit.findById
を使ってフルーツを取得します。これは、データベースが行を取得したときに完了する Uni
を返します。
create
メソッドでは、新しいフルーツをデータベースに追加することができます。
@POST
public Uni<RestResponse<Fruit>> create(Fruit fruit) {
return Panache.withTransaction(fruit::persist).replaceWith(RestResponse.status(CREATED, fruit));
}
コードはもう少し複雑です。データベースに書き込むにはトランザクションが必要なので、 Panache.withTransaction
を使って(非同期に)トランザクションを取得し、 persist
メソッドを呼び出します。 persist
メソッドは、フルーツのデータベースへの挿入結果を返す Uni
を返却します。挿入が完了すると(これが継続の役割を果たす)、 201 CREATED
HTTPレスポンスを作成します。
マシンに curlがあれば、エンドポイントを使って試すことができます。
> curl --header "Content-Type: application/json" \
--request POST \
--data '{"name":"peach"}' \
http://localhost:8080/fruits
同じ考え方で、他のCRUDメソッドも実装できます。
テストと実行
リアクティブなアプリケーションのテストは、非リアクティブなアプリケーションのテストと同様に、 HTTP エンドポイントを使用して、HTTP レスポンスを検証します。アプリケーションがリアクティブであることによる差異はありません。
FruitsEndpointTest.javaでは、Fruitアプリケーションのテストがどのように実装出来るかを見ることができます。
アプリケーションのパッケージ化と実行も同様に差異はありません。
以下のコマンドは通常通り使用できます。
quarkus build
./mvnw install
./gradlew build
また、ネイティブ実行可能ファイルのビルドも同様です。
quarkus build --native
./mvnw install -Dnative
./gradlew build -Dquarkus.native.enabled=true
また、アプリケーションをコンテナにパッケージすることもできます。
アプリケーションを実行するには、データベースを起動し、アプリケーションに設定を提供することを忘れないでください。
例えば、Dockerを使ってデータベースを動かすことができます。
docker run -it --rm=true \
--name postgres-quarkus -e POSTGRES_USER=quarkus \
-e POSTGRES_PASSWORD=quarkus -e POSTGRES_DB=fruits \
-p 5432:5432 postgres:14.1
そして、アプリケーションを起動します。
java \
-Dquarkus.datasource.reactive.url=postgresql://localhost/fruits \
-Dquarkus.datasource.username=quarkus \
-Dquarkus.datasource.password=quarkus \
-jar target/quarkus-app/quarkus-run.jar
あるいは、アプリケーションをネイティブ実行可能ファイルとしてパッケージ化する場合は、次を使用します。
./target/getting-started-with-reactive-runner \
-Dquarkus.datasource.reactive.url=postgresql://localhost/fruits \
-Dquarkus.datasource.username=quarkus \
-Dquarkus.datasource.password=quarkus
アプリケーションに渡されるパラメータは、データソースガイドに記載されています。アプリケーションを設定する方法は他にもあります。 設定ガイドを参照して、利用可能なオプションの概要を確認してください(env変数、.envファイルなど)。