Helidon Níma — Helidon on Virtual Threads

The original articles were written by Tomáš Langer (Consulting Member of Technical Staff at Oracle).
https://medium.com/helidon/helidon-n%C3%ADma-helidon-on-virtual-threads-130bb2ea2088

Helidon 4.0.0-ALPHA1 は、仮想スレッドベースのwebサーバを提供する、真新しい Helidon Níma と共にリリースされました。このリリースは、最新の Java 技術に興味のある方のための早期アクセスリリースですが、まだ実運用には適していません。

実運用に耐えうるHelidonを試すには、最新リリースであるHelidon 3をチェックしてください。

Helidon 3.0 is released
https://medium.com/helidon/helidon-3-0-is-released-1bd2df1f999b
https://logico-jp.io/2022/07/28/helidon-3-0-is-released/

Nímaの詳細については、JavaOneのセッション「Helidon, present and (faster) future – LRN1405」でご紹介しますので、ぜひご覧ください。

Helidon, present and (faster) future — LRN1405
https://reg.rf.oracle.com/flow/oracle/cloudworld/session-catalog/page/catalog?search=LRN1405

What is Níma?

Nímaは、仮想スレッド(Java Virtual Threads、Project Loom由来)用に設計されたサーバーのJava 19(原文時点では早期アクセス)ベースの実装です。

アルファ版では、以下のプロトコルを実装しています。

  • HTTP/1.1 with pipelining — serverおよびclient
  • HTTP/2 server (prototype, known issues)
  • gRPC server (prototype, known issues)
  • WebSocket server (prototype)

Threads

この実装では仮想スレッドを使用し、ブロッキングスレッドモデルを維持しながら、すばらしく低オーバーヘッドで高並列サーバを提供するよう設計・実装されています。これにより、コードを記述する上で、リアクティブプログラミングでよく遭遇するような問題に煩わされることがなくなります。例えば以下のような問題点です。

ソケットリスナー (Socket listeners)

  • ソケットリスナーはプラットフォームスレッド(オープンしたサーバーソケットに1つずつ、非常に少数のスレッドが存在)。

HTTP/1.1

  • 接続を処理するための1個の仮想スレッド(ルーティングを含む)
  • その接続上での書き込みのための1個の仮想スレッド (書き込みが接続ハンドラースレッドで行われるように無効化可能)
  • 単一の接続に対するすべてのリクエストは、接続ハンドラが処理

HTTP/2.2

  • 接続を処理するための1個の仮想スレッド
  • その接続上での書き込みのための 1 つの仮想スレッド (書き込みが接続ハンドラースレッドで行われるように無効化可能)
  • HTTP/2ストリームごとに1個の仮想スレッド (ルーティングを含む)

仮想スレッドエクゼキュータサービスは、unboundedエクゼキュータを使用します。

Protocols

アルファ版では、以下のプロトコルを実装しています。

  • 拡張可能なアップグレード機構を持つHTTP/1.1
  • HTTP/1.1 WebSocket アップグレード実装
  • HTTP/1.1 から HTTP/2 プレーンテキスト (h2c) へのアップグレードの実装
  • 拡張可能な「サブプロトコル」機構を持つHTTP/2
  • HTTP/2 gRPCサブプロトコルの実装
  • 他のTCPプロトコル(非HTTPを含む)のための拡張性
  • 任意のプロトコルのサーバーサイドTLSサポート
  • 任意のプロトコルの相互TLSサポート
  • 拡張可能なアプリケーション層プロトコルネゴシエーション(ALPN)、HTTP/2 (h2)で利用可能

Routing

同じ Web サーバを使い複数のプロトコルにルーティングできます(例えば、1 つのポートで HTTP/1.1, HTTP/2, WebSocket および gRPCを受け付け可能)。HTTP/1.1 ルーティングはデフォルトで実装されています(他のプロトコルはそこからアップグレードされます)。他のプロトコルは別モジュール化されていて追加可能です。

  • HTTPルーティングはバージョン非依存
    • HTTP/1.1とHTTP/2の両方で同じルートを使用可能
  • プロトコル固有のルーティングをサポート
    • HTTP/2 のみを提供するルーティングが可能
  • gRPC ルーティング
    • unary、サーバーストリーミング、クライアントストリーミング、双方向
  • WebSocketルーティング
    • 現在、サブプロトコルは未実装

Features

以下の機能を実装しており、試すことができます。

  • トレーシングのサポート
    • JaegerやZipkinなど、既存のHelidonトレース実装の利用
  • 静的コンテンツのサポート
    • クラスパスやファイルシステムから取得可能
  • CORSのサポート
  • アクセスログのサポート
  • Observabilityエンドポイント
    • health、アプリケーション情報、config
  • フォールトトレランス
    • バルクヘッド、サーキットブレーカー、リトライ、タイムアウト機能
  • HTTP/1.1クライアント
  • テストのサポート

Getting Started

以下の前提条件があります。

Nímaを実行するには

1. 以下のリポジトリをクローンする

helidon-nima-example
https://github.com/tomas-langer/helidon-nima-example

git clone https://github.com/tomas-langer/helidon-nima-example
cd helidon-nima-example

2. リポジトリのルートでMavenを実行する

mvn clean package

3. Nímaサービスを実行する(仮想スレッドはJava 19でプレビュー機能のため、previewを有効にする必要がある)

java --enable-preview -jar nima/target/example-nima-blocking.jar

4. エンドポイントの一つを試す

  • シングルレスポンス
curl -i http://localhost:8080/one
  • 複数のエンドポイントの順次実行
curl -i http://localhost:8080/sequence
curl -i http://localhost:8080/sequence?count=5
  • 複数のエンドポイントの並列実行
curl -i http://localhost:8080/parallel
curl -i http://localhost:8080/parallel?count=5

この例のソースは、モジュールnima内のチェックアウトしたプロジェクトあります。

  • NimaMain — ルーティング構成ならびにサーバー起動のmainクラス
  • BlockingService — 上記で使用したエンドポイントを持つHTTPサービス
  • resources/application.yaml — サーバーの構成

Blocking vs. Reactive

同じタスクについて、Níma(ブロッキング型)とHelidon SE(リアクティブ型)の実装を比較してみましょう。

それぞれのフレームワークの基本ルールを確立する必要があります。

Reactiveリクエストのスレッドをブロックできません。これは、HelidonのMulti and SingleのようなリアクティブストリームAPIでサポートされています。
Blockingレスポンスを非同期で終了させてはいけません(例えば、レスポンスはリクエストが発行されたのと同じスレッドから送信されなければなりません)。

注意:どちらの場合でも、スレッドを「妨害」してはいけません。妨害とは、スレッドを長期間、フルに使用することです。リアクティブフレームワークでは、これはイベントループのスレッドの一つを消費し、効果的にサーバーを停止させることになります。ブロッキング(Níma)では、これはpinned thread(固定されたスレッド)の問題を引き起こすかもしれません。どちらの場合も、プラットフォームスレッドを使用して、専用の実行サービスに重い負荷をオフロードすることで解決できます。

Simple Case

この例では、別のサービスを呼び出して、その応答をクライアントに書き込むことにします。例外が発生した場合は、内部サーバーエラーを返します。

ブロッキング:

private void one(ServerRequest req, ServerResponse res) {
String response = callRemote(client());
res.send(response);
}

リアクティブ:

private void one(ServerRequest req, ServerResponse res) {
Single<String> response = client.get()
.request(String.class);
response.forSingle(res::send)
.exceptionally(res::send);
}

ブロッキングのコードでは、レスポンスを取得して送信するだけでよいことがわかります。何かあれば、デフォルトの例外ハンドラが例外を正しく処理するか、内部サーバーエラーを送信します。

一方、リアクティブの場合は、例外が発生したときに、ストリームを操作して例外処理を行わなければなりません。そうしないと例外が失われてしまうからです。

ブロッキングコードの利点は以下の通りです。

  • わかりやすい例外処理
  • 意味のあるスタックトレース(スレッドダンプを含む)
  • デバッグが容易
  • scaffoldingが少ない

このことは、もう少し複雑なユースケースでより明確になります。

Complex Case

この例では、別のサービスを並列呼び出しして、その結果をひとつのレスポンスにまとめます。

クエリパラメータから並列リクエストの数を取得することを想定しています(変数 “count” に格納します)。また、InterruptedExceptionExecutionExceptionの処理はここでは示していませんが、必ず行う必要があります(次のAlphaリリースでは、ハンドラによってチェックされた例外を投げることができるようになる予定です)。

ブロッキング:

List<String> responses = new LinkedList<>();
// list of tasks to be executed in parallel
List<Callable<String>> callables = new LinkedList<>();
for (int i = 0; i < count; i++) {
callables.add(() -> client.get().request(String.class));
}
// execute all tasks (blocking operation)
for (var future : EXECUTOR.invokeAll(callables)) {
responses.add(future.get());
}
// send it
res.send("Combined results: " + responses);
view raw nima.java hosted with ❤ by GitHub

リアクティブ:

// create a dummy stream from numbers 0 to count
Multi.range(0, count)
// for each number, call the task on a different thread
.flatMap(i -> Single.create(CompletableFuture.supplyAsync(() ->
client().get().request(String.class), EXECUTOR))
// flat map from Single<Single<String>> to Single<String>
.flatMap(Function.identity()))
.collectList()
.map(it -> "Combined results: " + it)
.onError(res::send)
.forSingle(res::send);
view raw reactive.java hosted with ❤ by GitHub

リアクティブなコードで同じことを実現していますが、コードの可読性はかなり低下するとともに、正しく書くのが難しくなっています。

Performance

最も重視したのはパフォーマンスです。以下に、純粋なNetty(バージョン4.1.36.Final – 追加機能なし、HTTPのみ)でのノンブロッキング実装と比較した、現在の数字(ALPHA-1リリース)を示します。

これらはループバックインタフェースを用いて単一のマシンで行われた非常に単純なベンチマークです(例えば、CPUを共有しているため、クライアントとサーバプロセスの干渉があることが知られています)。それでも、ノンブロッキングの実装と比較できるかどうかを確認するために、性能の素早い比較が可能です。どのようなパフォーマンステストでもそうですが、結果は多くの要因(特にLinux環境でのパフォーマンスの最適化がどのように行われているか)によって異なります。

テスト機の環境

Intel Core i9–9900K @ 3.6 GHz x 16
32 GiB memory
Ubuntu Linux

ツール

HTTP/1.1 ベンチマークはwrkを使って実施

HTTP/2 ベンチマークはh2loadを使って実施

注意:Nímaをデザイン通り使いました。つまり、フル機能のルーティング、フルブロッキングとシンプルに利用できるルーティングを使用しました。Nettyのテストでは、単一タイプのリクエストを処理するために設計された「最小」の例です。Dropwizardは制御の反転と実行時依存性注入を備えたフレームワークです。

結果

Performance results (requests/second)
Performance results (requests/second), HTTP/1.1 with pipelining

注意:この数字から分かることは(そしてNímaの目標も)、シンプルで使いやすいプログラミングモデルを維持しながら、最小限のNettyサーバーに匹敵する性能を実現できることです。

これらのテストを再現するには

HTTP/1.1 ではwrkを利用

HTTP/2ではh2loadを利用

h2load -n 50000000 -t 5 -c 5 -m 100 http://localhost:8081/plaintext
  • Netty: (前述の)カスタマイズ済みTechEmpowerベンチマークをHTTP/2で利用

What’s Next

Helidonについて詳細を知りたい方、特にHelidon Nimaを知りたい方は、以下のカンファレンスに参加してください。

回答入手、貢献、情報収集のために

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト /  変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト /  変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト /  変更 )

%s と連携中