Upload
others
View
1
Download
0
Embed Size (px)
Citation preview
ORACLE.COM/JAVAMAGAZINE ////////////////////// JANUARY/FEBRUARY 2018
21
//reactive programming /
Eclipse Vert.xは、JVM上にリアクティブな分散システムを実装するためのツールキットで、最初からリアクティブ
な設計と非同期を念頭に置いて設計されています。さらに、Vert.xは自由さが重要な目的です。システムの構築
方法を示してくれるわけではなく、決めるのは開発者自身だということです。また、広範なエコシステムが存在する
ため、レスポンシブでインタラクティブな分散アプリケーションを構築するために必要なものは何でも得ることが
できます。本記事では、Vert.xで非同期実行モデルとリアクティブな実装を組み合わせることにより、不確かで常に
進化し続ける開発ニーズに対応できるアプリケーションを構築する方法について説明します。
リアクティブとは?まずは、基本から始めます。「リアクティブ」とは、実際どのような意味なのでしょうか。オックスフォード英語辞典
の「リアクティブ」の定義には、「showing a response to a stimulus」(刺激に対して反応を見せること)とありま
す。そこから考えると、リアクティブなソフトウェアとは、「刺激に対して反応するソフトウェア」と定義できます。しか
し、この定義に従うと、ソフトウェアはコンピュータの時代の初期から、リアクティブであり続けていることになりま
す。ソフトウェアは、入力、クリック、コマンドなどのユーザーの要求に反応するように設計されているためです。
ただし、分散システムの台頭とともに、ピアや失敗したイベントから送られるメッセージに反応するアプリケー
ションが生まれ始めました。最近、リアクティブが見直されてきている主な理由は、安定した分散システムを構築す
るのが難しいことにあります。分散システムは難しく、容量の問題、ネットワーク障害、ハードウェアの問題、バグな
ど、さまざまな理由で失敗します。開発者は、苦しみながらこのことを学んできました。それを受けて、数年前、リア
クティブ宣言によって、以下の特徴を持つ分散システムがリアクティブ・システムであると定義されました。■■ メッセージ駆動(Message-driven):メッセージを非同期に渡すことによって通信する。■■ 弾力性(Elastic):ワークロードが変化してもレスポンシブであり続ける。■■ 耐障害性(Resilient):問題が起きてもレスポンシブであり続ける。
JULIEN PONGE氏の写真:MATT BOSTOCK/GETTY IMAGES
Eclipse Vert.xとRxJavaで リアクティブ・プログラミング有名なリアクティブ・ライブラリの1つを使ってレスポンシブでスケーラブルな アプリを構築する
CLEMENT ESCOFFIER
JULIEN PONGE
ORACLE.COM/JAVAMAGAZINE ////////////////////// JANUARY/FEBRUARY 2018
22
//reactive programming /
■■ 即応性(Responsive):タイムリーに応答する。
このようなアーキテクチャ・スタイルによって、分散システムの中核に非同期という考え方が吹き込まれ、分散シス
テムを構築する新たな手法が促進されます。リアクティブ・システムは「正しく作られた分散システム」と表現され
ていますが、そのようなシステムを構築するのが難しい場合もあります。開発者の立場から考えれば、非同期とい
う獣を手なずけるのはとても難しいものです。さらに、従来のスレッド・モデル(1つのリクエストあたり1つのスレッ
ド)は、メモリやCPUを必要以上に消費する傾向にあるため、このアプローチで非同期コードを処理するのはとて
も非効率です。
そして、非同期アプリケーションの開発を簡単にするために、アクター、ファイバー、コルーチン、そしてリアクテ
ィブ・プログラミングなどのいくつかの開発モデルが登場しました。本記事では、最後に挙げたリアクティブ・プログ
ラミングに着目します。
リアクティブ・プログラミング(そして、その主な派生物であるリアクティブ拡張、別名RX)は、データ・ストリー
ムの操作に主眼を置いた非同期プログラミング・パラダイムです。リアクティブ・プログラミングでは、非同期でイベ
ントドリブンなアプリケーションを作るAPIが提供されます。リアクティブ・プログラミングを使うときは、データが
流れているストリームを扱うことになります。つまり、ストリームを監視し、新しいデータが利用できるようになった
とき、それに反応します。
しかし、データのストリームには、本質的な欠陥があります。届くメッセージが多すぎて、処理が間に合わなか
った場合、どうなるでしょうか。ソースとハンドラの間にバッファを置くこともできますが、この方法で対応できる超
過量はわずかです。届くデータを捨てることも解決策の1つですが、データ破棄が常に許容されるとは限りません。
最終的には、メッセージが届くペースを調整する方法が必要になります。その点について提案しているのが、リアク
ティブ・ストリーム仕様です。この仕様では、非同期でノンブロッキングなバックプレッシャー(逆流)プロトコルが
定義されています。この制御フローでは、コンシューマがプロデューサに現在の容量を通知します。したがって、プロ
デューサがストリームに過剰な量のデータを送らなくなるため、システムは焦げつくことなく、ストリームの容量に
合わせて自動的に調整されるようになります。
リアクティブ・システムが重要な理由ここ数年の間に、さまざまな場所でリアクティブ・プログラミングを見かけるようになったのはなぜでしょうか。とて
も長い間にわたり、ほとんどのアプリケーションは同期実行モデルを使って開発されています。そして、ほとんどの
APIはそのアプローチに従うように設計されています。
しかし、コンピュータ・システムや分散システムは非同期です。同期処理は、わかりやすいように単純化された
ORACLE.COM/JAVAMAGAZINE ////////////////////// JANUARY/FEBRUARY 2018
23
//reactive programming /
ものです。システムの非同期的な性質は、久しく無
視され続けてきましたが、今こそそれをばん回する
時です。最新のアプリケーションの多くは、リモー
ト呼出しやファイル・システムへのアクセスなどの
I/O操作に依存しています。しかし、アプリケーショ
ン・コードは同期的な性質を持つため、これらのI/
O操作はブロッキングI/Oとなるように設計されて
います。そこで、アプリケーションはレスポンスを待
ってから実行を続けます。アプリケーションで同時
実行性を実現するためには、マルチスレッドを利用
して、使うスレッドの数を増やします。しかし、スレッドは高い代償を伴います。まず、コードにおいて、その状態へ
の同時アクセスからコード自体を保護する必要があります。さらに、スレッドはメモリの観点から見ても代償を伴
います。また、これは見逃されがちですが、スレッドの切り替えにはCPUサイクルが必要であるため、CPUタイムの
観点から見てもスレッドは高い代償を伴います。
そのため、より効率的なモデルが必要になります。非同期実行モデルにより、タスクベースでの同時実行性の
実現が促進されます。この方式では、タスクが作業を進められない場合(たとえば、ノンブロッキングI/Oを使って
リモート・サービスを呼び出し、結果が利用できるようになった際に通知を受ける場合)、スレッドを開放します。
こうすると、同じスレッドを別のタスクに切り替えることができます。その結果、1つのスレッドで複数のタスクを交
互に処理できるようになります。
従来型の開発や実行のパラダイムで、この新しいモデルを使うことはできません。しかし、アプリケーション
が広く分散して相互に接続されているクラウドやコンテナの世界では、アプリケーションは増加し続けるトラフィッ
クに対応しなければなりません。リアクティブ・システムが確約する内容は、そのような世界にはまさにうってつけ
です。とはいえ、リアクティブ・システムの実装には、2つの転換が必要になります。1つは、非同期実行モデルを使
用するという、実行方式の転換です。もう1つは、非同期のAPIやアプリケーションを書くという、開発方式の転換で
す。Eclipse Vert.xが役立つのはこの点です。この2つの要素を組み合わせるVert.xは、皆さんの強力な武器になりま
す。本記事の以降の部分では、その詳細について説明します。
RxJava:Javaのためのリアクティブ・プログラミング・ツールボックス
リアクティブ・システムの実装には、2つの転換が必要になります。1つは、非同期実行モデルを使用するという、実行方式の転換です。もう1つは、非同期のAPIやアプリケーションを書くという、開発方式の転換です。
ORACLE.COM/JAVAMAGAZINE ////////////////////// JANUARY/FEBRUARY 2018
24
//reactive programming /
それでは、リアクティブ・プログラミングに注目してみます。これは、非同期コードを書くための開発モデルです。リア
クティブ・プログラミングを使うとき、コードではデータのストリームを処理しています。データは、パブリッシャによ
って生成されます。そのデータはパブリッシャからコンシューマへと流れ、コンシューマによって処理されます。コン
シューマはデータ・ストリームを監視しており、新しいアイテムが利用できるようになったとき、ストリームが完了し
たとき、エラーがキャッチされたときに通知を受けます。コンシューマが過負荷になるのを避けるため、ストリーム
に流れるデータ量を制御するバックプレッシャー・プロトコルが必要になりますが、このプロトコルは通常、リアクテ
ィブ・フレームワークでは透過的に扱われます。
リアクティブ・プログラミング・パラダイムには、いくつかの実装があります。RxJavaは、リアクティブ拡張(RX)
をJavaプログラミング言語で単純に実装したものです。RxJavaは、よく使われているリアクティブ・プログラミング用
ライブラリであり、ネットワーク化されたデータ処理のアプリケーション、JavaFXによるグラフィカル・ユーザー・イ
ンタフェース、Androidアプリの開発に使用できます。RxJavaは、Javaのリアクティブ・ライブラリ用の基本ツールキ
ットです。表1のように、データ・ストリームの種類に応じたデータ・パブリッシャを記述する5つのデータ型が提供
されています。
これらの型はデータ・パブリッシャを表し、データ・ストリームを監視して処理するコンシューマのもとにデー
タを運びます。型は、ストリームを流れるアイテムの数に応じて異なります。有限または無限にアイテムが続くスト
リームでは、Observable型およびFlowable型が使われます。
ObservableとFlowableの違いは、Flowableがバックプレッシャーを扱う(すなわち、リアクティブ・ストリー
ム・プロトコルを実装している)のに対し、Observableは扱わない点にあります。バックプレッシャーをサポートする
ソースから流れてくる大規模なデータ・ストリーム(たとえば、TCPコネクション)にはFlowableの方が適しています
が、バックプレッシャを適用できない、いわゆる「ホット」な監視可能対象(たとえば、GUIイベントやその他のユー
表1:RxJavaのリアクティブ・パブリッシャ型
ユースケース ストリーム内に想定されるアイテムの数
RXJAVAの型
通知、データ・フロー 0~N Observable、Flowable
結果を生成する可能性がある非同期操作
1~10~1
SingleMaybe
結果を生成しない非同期操作
0 Completable
ORACLE.COM/JAVAMAGAZINE ////////////////////// JANUARY/FEBRUARY 2018
25
//reactive programming /
ザー・アクション)を扱う場合は、Observableの方が適しています。
すべてのストリームがバックプレッシャーをサポートしているわけではない点を認識しておくことは重要です。実
際、現実世界から取得したデータを運ぶほとんどのストリームは、バックプレッシャーに対応していません。このよ
うな場合に対処するため、リアクティブ・プログラミング・ライブラリでは、バッファを使う、許容できるデータ損失を
定義するなどの戦略が用意されています。
RxJavaを使ってみる:それでは、実際にコードを見ながら、リアクティブについて理解を深めていきます。
プロジェクトの完全なソース・コードは、オンラインで公開されています。このプロジェクトをクローンまたはダウン
ロードし、rxjava-samplesサブプロジェクトの内容を確認してください。RxJava 2.xと、logback-classicロギング・ラ
イブラリを使用しています。この例が、RxJavaを使ったスレッド制御を理解する助けになることは、後ほどわかって
いただけると思います。
前のセクションでは、RxJavaで使うことができるさまざまなリアクティブ型について、簡単に紹介しました。次
のクラスでは、それらの型のインスタンスを作成し、基本的な操作を行っています。
package samples;
import io.reactivex.Completable;import io.reactivex.Flowable;import io.reactivex.Maybe;import io.reactivex.Single;import io.reactivex.functions.Consumer;import org.slf4j.Logger;import org.slf4j.LoggerFactory;
public class RxHello {
private static final Logger logger = LoggerFactory.getLogger(RxHello.class);
public static void main(String[] args) {
ORACLE.COM/JAVAMAGAZINE ////////////////////// JANUARY/FEBRUARY 2018
26
//reactive programming /
Single.just(1) .map(i -> i * 10) .map(Object::toString) .subscribe((Consumer<String>) logger::info);
Maybe.just("Something") .subscribe(logger::info);
Maybe.never() .subscribe(o -> logger.info("Something is here..."));
Completable.complete() .subscribe(() -> logger.info("Completed"));
Flowable.just("foo", "bar", "baz") .filter(s -> s.startsWith("b")) .map(String::toUpperCase) .subscribe(logger::info); }}
この例を実行すると、次のように出力されます。
11:24:28.638 [main] INFO samples.RxHello - 1011:24:28.661 [main] INFO samples.RxHello - Something11:24:28.672 [main] INFO samples.RxHello - Completed11:24:28.716 [main] INFO samples.RxHello - BAR11:24:28.716 [main] INFO samples.RxHello - BAZ
重要な点は、Javaコレクション・ストリームでは、終了イベントが発生するまで、何の処理も行われないことで
す。RxJavaでは、サブスクリプションが終了イベントになっています。この例では、1つのパラメータを渡して
ORACLE.COM/JAVAMAGAZINE ////////////////////// JANUARY/FEBRUARY 2018
27
//reactive programming /
subscribe()を使用しています。このパラメータはラムダ式で、イベントを受け取るたびに呼び出されます。次に示す
のは、他の形式のsubscribeです。コンシューマが受け取りたいイベントによっては、こちらを指定することもできま
す。■■ 引数なし:処理のトリガーのみを行う■■ 2つの引数:イベントとエラーを処理する ■■ 3つの引数:イベントとエラーを処理し、処理が完了した際に通知する
パブリッシャの作成とエラーからのリカバリ:Observableなどのデータ・ストリームの作成が、先ほ
どの例のようにjust()ファクトリ・メソッドの呼出しに限られていたのであれば、もちろん、RxJavaの用途は非常に
限定的なものとなるでしょう。パブリッシャのすべての型は、新しいサブスクライバを扱うためのコードを定義する
create()メソッドをサポートしています。
List<String> data = Arrays.asList("foo", "bar", "baz");Random random = new Random();
Observable<String> source = Observable.create(subscriber -> { for (String s : data) { if (random.nextInt(6) == 0) { subscriber.onError( new RuntimeException("Bad luck for you...")); } subscriber.onNext(s); } subscriber.onComplete();});
上記の例では、Stringの値を持つObservable(言い換えれば、String値のストリーム)を作成しています。値は、
事前定義されたリストから取り出しています。さらに、ランダムに失敗させる仕組みも追加しています。次の3つのメ
ソッドを使用して、サブスクライバに通知を行うことができます。
ORACLE.COM/JAVAMAGAZINE ////////////////////// JANUARY/FEBRUARY 2018
28
//reactive programming /
■■ onNextは、サブスクライバに新しい値を送るときに使います。サブスクライバに届くまでに、いくつかの中間オペ
レータを経由する可能性もあります。■■ onCompleteは、それ以上送る値がないことを示すときに使います。■■ onErrorは、エラーが発生し、それ以上送る値がないことを示すときに使います。エラーの値として、任意の
Throwableを使うことができます。
カスタムのパブリッシャは、create()以外の方法でも定義できますが、他の方法については、本記事では割愛
します。
エラーが発生する確率が十分になるように、このObservableを10回テストしてみます。
for (int i = 0; i < 10; i++) { logger.info("======================================="); source.subscribe( next -> logger.info("Next: {}", next), error -> logger.error("Whoops"), () -> logger.info("Done"));}
実行トレースから、正常に完了したものとともに、エラーが起きているものがあることがわかります。
11:51:47.469 [main] INFO samples.RxCreateObservable - =======================================11:51:47.469 [main] INFO samples.RxCreateObservable - Next: foo11:51:47.469 [main] INFO samples.RxCreateObservable - Next: bar11:51:47.469 [main] INFO samples.RxCreateObservable - Next: baz11:51:47.469 [main] INFO samples.RxCreateObservable - Done11:51:47.469 [main] INFO samples.RxCreateObservable - =======================================11:51:47.469 [main] INFO samples.RxCreateObservable - Next: foo11:51:47.469 [main] INFO samples.RxCreateObservable - Next: bar11:51:47.469 [main] ERROR samples.RxCreateObservable - Whoops
ORACLE.COM/JAVAMAGAZINE ////////////////////// JANUARY/FEBRUARY 2018
29
//reactive programming /
11:51:47.469 [main] INFO samples.RxCreateObservable - =======================================11:51:47.469 [main] INFO samples.RxCreateObservable - Next: foo11:51:47.469 [main] ERROR samples.RxCreateObservable - Whoops
RxJavaでは、エラーからリカバリするさまざまな方法がサポートされています。たとえば、別のストリームへの切り
替えや、デフォルト値の提供などです。別の選択肢が、retry()です。
source .retry(5) .subscribe(next -> logger.info("Next: {}", next), error -> logger.error("Whoops"), () -> logger.info("Done"));
上記の例では、エラーが起きた場合、新しいサブスクリプションを5回までリトライすることを指定しています。な
お、リトライは別のスレッドで実行される可能性があることに注意してください。ここでは、エラーがランダムに発
生するため、厳密な出力トレースは実行するたびに異なります。次の出力は、リトライの例です。
11:51:47.472 [main] INFO samples.RxCreateObservable - Next: foo11:51:47.472 [main] INFO samples.RxCreateObservable - Next: bar11:51:47.472 [main] INFO samples.RxCreateObservable - Next: foo11:51:47.472 [main] INFO samples.RxCreateObservable - Next: bar11:51:47.472 [main] INFO samples.RxCreateObservable - Next: baz11:51:47.472 [main] INFO samples.RxCreateObservable - Done
RxJavaとスレッド:ここまでは、マルチスレッドについてさほど意識してきませんでした。別の例を実行して
みます。
Flowable.range(1, 5) .map(i -> i * 10) .map(i -> {
ORACLE.COM/JAVAMAGAZINE ////////////////////// JANUARY/FEBRUARY 2018
30
//reactive programming /
logger.info("map({})", i); return i.toString(); }) .subscribe(logger::info);
Thread.sleep(1000);
ログから、すべての処理がmainスレッドで行われていることがわかります。
12:01:01.097 [main] INFO samples.RxThreading - map(10)12:01:01.100 [main] INFO samples.RxThreading - 1012:01:01.100 [main] INFO samples.RxThreading - map(20)12:01:01.100 [main] INFO samples.RxThreading - 2012:01:01.100 [main] INFO samples.RxThreading - map(30)12:01:01.100 [main] INFO samples.RxThreading - 3012:01:01.100 [main] INFO samples.RxThreading - map(40)12:01:01.100 [main] INFO samples.RxThreading - 4012:01:01.100 [main] INFO samples.RxThreading - map(50)12:01:01.100 [main] INFO samples.RxThreading - 50
実際、オペレータの処理とサブスクライバへの通知は、いずれもmainスレッドで行われています。デフォルトで
は、パブリッシャ(とそれに適用されるオペレータのチェーン)は、subscribeメソッドが呼び出されたスレッドで
処理とコンシューマへの通知を行います。RxJavaでは、専用のスレッドやエグゼキュータに処理をオフロードする
Schedulerが提供されています。Schedulerは、サブスクライバへの通知を行います。その際、subscribeの呼出し
に使用されたスレッド以外でサブスクライバが実行されていたとしても、正しいスレッドに通知されます。
io.reactivex.schedulers.Schedulersクラスでは、いくつかのスケジューラが提供されています。特に興味深
いのは、以下のスケジューラです。■■ computation()は、ブロッキングI/O操作を伴わない、CPU負荷が高い処理に使います。■■ io()は、すべてのブロッキングI/O操作に使います。
ORACLE.COM/JAVAMAGAZINE ////////////////////// JANUARY/FEBRUARY 2018
31
//reactive programming /
■■ single()は、共有スレッドで順番どおりに実行する操作に使います。■■ from(executor)は、すべてのスケジューリングされた処理をカスタムのエグゼキュータにオフロードします。
それでは、先ほどの例に戻ります。サブスクリプションと監視のスケジューリング方法を指定できます。
Flowable.range(1, 5) .map(i -> i * 10) .map(i -> { logger.info("map({})", i); return i.toString(); }) .observeOn(Schedulers.single()) .subscribeOn(Schedulers.computation()) .subscribe(logger::info);
Thread.sleep(1000);logger.info("===================================");
subscribeOnメソッドは、サブスクリプションと、オペレータの処理のスケジューリングを指定し、observeOnメソ
ッドはイベント監視のスケジューリングを指定しています。この例では、map操作は計算用のスレッド・プールで呼
び出され、subscribeコールバック(logger::info)は別のスレッド(このスレッドは変化しません)で呼び出されま
す。例を実行すると、次の実行トレースが出力されます。いくつかの異なるスレッドが関連していることがはっきり
とわかります。
12:01:03.127 [RxComputationThreadPool-1] INFO samples.RxThreading - map(10)12:01:03.128 [RxComputationThreadPool-1] INFO samples.RxThreading - map(20)12:01:03.128 [RxSingleScheduler-1] INFO samples.RxThreading - 1012:01:03.128 [RxComputationThreadPool-1] INFO samples.RxThreading - map(30)
ORACLE.COM/JAVAMAGAZINE ////////////////////// JANUARY/FEBRUARY 2018
32
//reactive programming /
12:01:03.128 [RxSingleScheduler-1] INFO samples.RxThreading - 2012:01:03.128 [RxComputationThreadPool-1] INFO samples.RxThreading - map(40)12:01:03.128 [RxSingleScheduler-1] INFO samples.RxThreading - 3012:01:03.128 [RxSingleScheduler-1] INFO samples.RxThreading - 4012:01:03.128 [RxComputationThreadPool-1] INFO samples.RxThreading - map(50)12:01:03.128 [RxSingleScheduler-1] INFO samples.RxThreading - 5012:01:04.127 [main] INFO samples.RxThreading===================================
監視可能対象の結合:RxJavaは、ストリームを結合するさまざまな方法を提供します。その例を、merge操作とzip操作を使って説明します。ストリームをマージすると、さまざまなソースの要素が混合された1つのストリー
ムを提供できます。次の例をご覧ください。
package samples;
import io.reactivex.Flowable;import io.reactivex.schedulers.Schedulers;import org.slf4j.Logger;import org.slf4j.LoggerFactory;
import java.util.UUID;import java.util.concurrent.TimeUnit;
public class RxMerge {
ORACLE.COM/JAVAMAGAZINE ////////////////////// JANUARY/FEBRUARY 2018
33
//reactive programming /
private static final Logger logger = LoggerFactory.getLogger(RxMerge.class);
public static void main(String[] args) throws InterruptedException {
Flowable<String> intervals = Flowable .interval(100, TimeUnit.MILLISECONDS, Schedulers.computation()) .limit(10) .map(tick -> "Tick #" + tick) .subscribeOn(Schedulers.computation());
Flowable<String> strings = Flowable.just( "abc", "def", "ghi", "jkl") .subscribeOn(Schedulers.computation());
Flowable<Object> uuids = Flowable .generate(emitter -> emitter.onNext(UUID.randomUUID())) .limit(10) .subscribeOn(Schedulers.computation());
Flowable.merge(strings, intervals, uuids) .subscribe(obj -> logger.info("Received: {}", obj));
Thread.sleep(3000); }}
この例を実行すると、ところどころに異なるソースの要素が挟み込まれたトレースが出力されます。別の選択肢とし
ORACLE.COM/JAVAMAGAZINE ////////////////////// JANUARY/FEBRUARY 2018
34
//reactive programming /
て便利なのがzip()であり、さまざまなソースの要素が取得されて組み合わせられます。
Flowable.zip(intervals, uuids, strings, (i, u, s) -> String.format("%s {%s} -> %s", i, u, s)) .subscribe(obj -> logger.info("Received: {}", obj));
これを実行すると、次のようなトレースが出力されます。
14:32:40.127 [RxComputationThreadPool-7] INFO samples.RxMerge - Received:Tick #0 {67e7cde0-3f29-49cb-b569-e01474676d98} -> abc14:32:40.224 [RxComputationThreadPool-7] INFO samples.RxMerge - Received:Tick #1 {a0a0cc83-4bed-4793-9ee0-11baa7707610} -> def14:32:40.324 [RxComputationThreadPool-7] INFO samples.RxMerge - Received:Tick #2 {7b7d81b6-cc39-4ec0-a174-fbd61b1d5c71} -> ghi14:32:40.424 [RxComputationThreadPool-7] INFO samples.RxMerge - Received:Tick #3 {ae88eb02-52a5-4af7-b9cf-54b29b9cdb85} -> jkl
現実世界の使用例では、他の要素(サービスなど)からデータを収集し、受信した内容に基づいて結果を生成する
場合、zip()を使うと便利です。
リアクティブ・プログラミングによるリアクティブ・システムの実装リアクティブ・プログラミングを使うと、非同期でイベントドリブンなアプリケーションを構成できますが、全体のゴ
ールを見失ってしまってはいけません。クラウドやコンテナの世界でレスポンシブな分散システムをうまく構築する
ためには、非同期実行モデルの採用が欠かせません。リアクティブ・プログラミングによって非同期開発モデルに
は対処できますが、それでもまだタスクベースの同時実行モデルとノンブロッキングI/Oが必要になります。Eclipse
ORACLE.COM/JAVAMAGAZINE ////////////////////// JANUARY/FEBRUARY 2018
35
//reactive programming /
Vert.xは、この2つの欠けたピースを、RxJavaにとって便利なAPIとともに提供してくれます。
Vert.xの実行モデルは、イベント・ループという考え方に基づいています。イベント・ループとは、キューからイベ
ントを取り出すスレッドで、それぞれのイベントについて、そのイベントを処理対象とするハンドラを探して呼び出し
ます。ハンドラは、パラメータとしてイベントを受け取るメソッドです。このモデルを使うと、シングルスレッドのコー
ドでも、同時に実行される複雑に絡み合った多数のタスクを処理できます。しかし、このアプローチにはいくつか
の欠点もあります。実行されるハンドラは、イベント・ループをブロックしてはいけません。イベント・ループがブロッ
クされると、システムはレスポンシブではなくなり、キュー内の未処理イベントの数が増加します。
うれしいことに、Vert.xには大きなエコシステムがあり、そこでは、ほとんどすべてのことが非同期かつノンブ
ロッキングな方法で実装されています。たとえば、Vert.xでは、最新のWebアプリケーションの構築、データベースへ
のアクセス、レガシー・システムとの通信に使えるビルディング・ブロックが提供されています。それでは、いくつか
の例を見てみます。Vert.xの「hello world」アプリケーション(コードは、オンラインで公開されています)は次のよ
うになります。
package samples;
import io.vertx.core.Vertx;
public class HttpApplication {
public static void main(String[] args) { // 1 - Vert.xインスタンスの作成 Vertx vertx = Vertx.vertx();
// 2 - HTTPサーバーの作成 vertx.createHttpServer() // 3 - リクエストを処理するリクエスト・ハンドラをアタッチ .requestHandler(req -> req.response() .end("Hello, request handled from " + Thread.currentThread().getName())) // 4 - ポート8080でサーバーを起動 .listen(8080);
ORACLE.COM/JAVAMAGAZINE ////////////////////// JANUARY/FEBRUARY 2018
36
//reactive programming /
}}
HTTPリクエスト(イベント)が届くたびに、リクエスト・ハンドラが呼び出されます。ハンドラは常に同じスレッド、す
なわちイベント・ループ・スレッドから呼び出される点に注意してください。次に、リクエスト・ハンドラで別のサービ
スを(HTTPを使って)呼び出す場合、次の例のようなコードを使用します。
package samples;
import io.vertx.core.Vertx;import io.vertx.ext.web.client.WebClient;
public class TwitterFeedApplication {
public static void main(String[] args) { Vertx vertx = Vertx.vertx(); // 1 - Webクライアントの作成 WebClient client = WebClient.create(vertx); vertx.createHttpServer() .requestHandler(req -> { // 2 - リクエスト・ハンドラでTwitterのフィードを取得 client .getAbs("https://twitter.com/vertx_project") .send(res -> { // 3 - 結果に基づいてレスポンスに書込み if (res.failed()) { req.response().end("Cannot access " + "the twitter feed: " + res.cause().getMessage()); } else { req.response().end(res.result()
ORACLE.COM/JAVAMAGAZINE ////////////////////// JANUARY/FEBRUARY 2018
37
//reactive programming /
.bodyAsString()); } }); }) .listen(8080); }}
この例は、Vert.xのノンブロッキングI/Oを使っているため、コード全体がVert.xイベント・ループで(シングルスレッ
ド式に)実行されます。これが同時リクエストの処理を妨げることはありません。それどころか、シングルスレッド
ですべてのリクエストを処理できます。ただし、ある問題にすぐ気づくはずです。コールバックがネストされている
ため、コードがわかりにくくなることです。そんなときに力を発揮するのがRxJavaです。先ほどのコードは、次のよう
に書き換えることができます。
package samples;
import io.vertx.reactivex.core.Vertx;import io.vertx.reactivex.core.http.HttpServer;import io.vertx.reactivex.ext.web.client.HttpResponse;import io.vertx.reactivex.ext.web.client.WebClient;
public class RXTwitterFeedApplication {
public static void main(String[] args) { Vertx vertx = Vertx.vertx(); WebClient client = WebClient.create(vertx); HttpServer server = vertx.createHttpServer(); server // 1 - 一連のリクエストをストリームに変換 .requestStream().toFlowable() // 2 - 各リクエストについてTwitter APIを呼出し
ORACLE.COM/JAVAMAGAZINE ////////////////////// JANUARY/FEBRUARY 2018
38
//reactive programming /
.flatMapCompletable(req -> client.getAbs("https://twitter.com/vertx_project") .rxSend() // 3 - ボディを文字列として抽出 .map(HttpResponse::bodyAsString) // 4 - 失敗した場合 .onErrorReturn(t -> "Cannot access the twitter " + "feed: " + t.getMessage()) // 5 - レスポンスの書込み .doOnSuccess(res -> req.response().end(res)) // 6 - 結果をCompletableに変換 .toCompletable() ) // 7 - 忘れずにリアクティブ型をサブスクライブする // そうしないと、何も起こらない .subscribe();
server.listen(8080); }}
RxJavaのリアクティブ型周辺のコードの構造を変えることで、RxJavaオペレータのメリットを活用できるようになり
ます。
リアクティブなエッジ・サービスの実装別の例を見てみます。簡単ですが効果的なものです。3つの入札サービスの中から、ある時点での最適な提案を選
択するエッジ・サービスを考えます。各サービスは、単純なHTTP/JSONエンドポイントを提供します。当然ですが、
現実世界の使用例では、こういったサービスは一時的に利用できなくなることや、応答時間に大きなばらつきが生
じることがあります。
そこで、以下の開発を行い、そのようなシステムをシミュレートします。■■ 人為的な遅延とランダムなエラーが発生する入札サービス
ORACLE.COM/JAVAMAGAZINE ////////////////////// JANUARY/FEBRUARY 2018
39
//reactive programming /
■■ HTTPでサービスに問合せを行うエッジ・サービス
それでは、RxJavaを使って、リクエスト・ストリームの結合や失敗時の処理を行いつつ、最適な提案をある時間内で
確実に返す方法について説明します。プロトタイプであるため、すべてのverticle(後述)を同じアプリケーションに
デプロイしますが、それで汎用性が失われることはありません。完全なコードは、vertx-samplesサブプロジェクト
にあります。
mainスレッドを使ってアプリケーションを起動するのではなく、verticleを使用します。verticleとは、Vert.xが
デプロイして実行するひとまとまりのコードのことで、通常はJavaクラスです。verticleはシンプルかつスケーラブル
で、アクターと同じようなデプロイメントと同時実行モデルを使っています。verticleにより、コードを一連の疎結合
されたコンポーネントとしてまとめることができます。デフォルトでは、verticleはイベント・ループによって実行さ
れ、さまざまな種類のイベント(HTTPリクエスト、TCPフレーム、メッセージなど)を監視します。アプリケーション
は、起動時にVert.xに対して一連のverticleをデプロイするよう指示します。
入札サービスverticle:このverticleは、HTTPポートを設定できるように設計されています。以下をご覧くだ
さい。
public class BiddingServiceVerticle extends AbstractVerticle {
private final Logger logger = LoggerFactory.getLogger(BiddingServiceVerticle.class);
@Override public void start(Future<Void> verticleStartFuture) throws Exception { Random random = new Random(); String myId = UUID.randomUUID().toString(); int portNumber = config().getInteger("port", 3000);
// (...) }}
ORACLE.COM/JAVAMAGAZINE ////////////////////// JANUARY/FEBRUARY 2018
40
//reactive programming /
config()メソッドを使うと、verticleの設定にアクセスできます。getIntegerなどのアクセッサ・メソッドでは、2番目
の引数としてデフォルト値を渡すことができます。そのため、この例でのデフォルトHTTPポートは3000となります。
サービスのレスポンスには、エンドポイントを識別するランダムなUUIDを含めています。UUIDは、乱数ジェネレー
タを使って生成します。
次のステップでは、Vert.xのWebルーターを使って、/offerというパスに対するHTTP GETリクエストを受け取り
ます。
Router router = Router.router(vertx);router.get("/offer").handler(context -> { String clientIdHeader = context.request() .getHeader("Client-Request-Id"); String clientId = (clientIdHeader != null) ? clientIdHeader :"N/A"; int myBid = 10 + random.nextInt(20); JsonObject payload = new JsonObject() .put("origin", myId) .put("bid", myBid); if (clientIdHeader != null) { payload.put("clientRequestId", clientId); } long artificialDelay = random.nextInt(1000); vertx.setTimer(artificialDelay, id -> { if (random.nextInt(20) == 1) { context.response() .setStatusCode(500) .end(); logger.error("{} injects an error (client-id={}, " + "artificialDelay={})", myId, myBid, clientId, artificialDelay); } else { context.response()
ORACLE.COM/JAVAMAGAZINE ////////////////////// JANUARY/FEBRUARY 2018
41
//reactive programming /
.putHeader("Content-Type", "application/json") .end(payload.encode()); logger.info("{} offers {} (client-id={}, " + "artificialDelay={})", myId, myBid, clientId, artificialDelay); } });});
失敗をシミュレートするため、5%の確率で失敗するようにしていることに注意してください(失敗の場合、サービ
スはHTTP 500レスポンスを返します)。また、最終的なHTTPレスポンスは、タイマーを使ってランダムに0から1,000
ミリ秒遅延させています。
最後に、通常どおりHTTPサーバーを起動します。
vertx.createHttpServer() .requestHandler(router::accept) .listen(portNumber, ar -> { if (ar.succeeded()) { logger.info("Bidding service listening on HTTP " + "port {}", portNumber); verticleStartFuture.complete(); } else { logger.error("Bidding service failed to start", ar.cause()); verticleStartFuture.fail(ar.cause()); } });
最適な提案を選択するエッジ・サービス:このサービスは、Vert.xが提供するRxJava APIを使って実
装します。まず、初期設定とverticleクラスのstartメソッドを示します。
ORACLE.COM/JAVAMAGAZINE ////////////////////// JANUARY/FEBRUARY 2018
42
//reactive programming /
public class BestOfferServiceVerticle extends AbstractVerticle {
private static final JsonArray DEFAULT_TARGETS = new JsonArray() .add(new JsonObject() .put("host", "localhost") .put("port", 3000) .put("path", "/offer")) .add(new JsonObject() .put("host", "localhost") .put("port", 3001) .put("path", "/offer")) .add(new JsonObject() .put("host", "localhost") .put("port", 3002) .put("path", "/offer")); private final Logger logger = LoggerFactory .getLogger(BestOfferServiceVerticle.class); private List<JsonObject> targets; private WebClient webClient;
@Override public void start(Future<Void> startFuture) throws Exception { webClient = WebClient.create(vertx);
targets = config().getJsonArray("targets", DEFAULT_TARGETS) .stream() .map(JsonObject.class::cast) .collect(Collectors.toList());
ORACLE.COM/JAVAMAGAZINE ////////////////////// JANUARY/FEBRUARY 2018
43
//reactive programming /
vertx.createHttpServer() .requestHandler(this::findBestOffer) .rxListen(8080) .subscribe((server, error) -> { if (error != null) { logger.error("Could not start the best offer " + "service", error); startFuture.fail(error); } else { logger.info("The best offer service is running " + "on port 8080"); startFuture.complete(); } }); }
このコードには、いくつかの興味深い点があります。■■ Vert.xが提供するRxJava APIにアクセスするために、io.vertx.reactivex.core.AbstractVerticleクラスをインポー
トして拡張しています。■■ 対象のサービス(デフォルトでは、localhostのポート3000、3001、3002上のもの)を指定できま
す。host、port、pathの各キーを含むJSONオブジェクトのJSON配列を渡すと、そのような設定が可能です。■■ RxJavaオブジェクトを返す、Vert.x APIの亜種には、接頭辞「rx」が付いています。この例の
rxListenは、Single<HttpServer>を返します。実際には、サブスクライブを行うまでサーバーは起動しません。
いよいよ、findBestOfferメソッドの実装に取りかかります。このメソッドは、まず各サービスに対してHTTPリクエス
トを発行し、レスポンスとしてSingle<JsonObject>のリストを受け取り、それをリデュースして1つの最適なレスポ
ンスを作成した後、最終的にHTTPのレスポンスを終了します。
private final AtomicLong requestIds = new AtomicLong();private static final JsonObject EMPTY_RESPONSE = new JsonObject() .put("empty", true)
ORACLE.COM/JAVAMAGAZINE ////////////////////// JANUARY/FEBRUARY 2018
44
//reactive programming /
.put("bid", Integer.MAX_VALUE);
private void findBestOffer(HttpServerRequest request) { String requestId = String.valueOf(requestIds.getAndIncrement());
List<Single<JsonObject>> responses = targets.stream() .map(t -> webClient .get(t.getInteger("port"), t.getString("host"), t.getString("path")) .putHeader("Client-Request-Id", String.valueOf(requestId)) .as(BodyCodec.jsonObject()) .rxSend() .retry(1) .timeout(500, TimeUnit.MILLISECONDS, RxHelper.scheduler(vertx)) .map(HttpResponse::body) .map(body -> { logger.info("#{} received offer {}", requestId, body.encodePrettily()); return body; }) .onErrorReturnItem(EMPTY_RESPONSE)) .collect(Collectors.toList());
Single.merge(responses) .reduce((acc, next) -> { if (next.containsKey("bid") && isHigher(acc, next)) { return next; }
ORACLE.COM/JAVAMAGAZINE ////////////////////// JANUARY/FEBRUARY 2018
45
//reactive programming /
return acc; }) .flatMapSingle(best -> { if (!best.containsKey("empty")) { return Single.just(best); } else { return Single.error(new Exception("No offer " + "could be found for requestId=" + requestId)); } }) .subscribe(best -> { logger.info("#{} best offer: {}", requestId, best.encodePrettily()); request.response() .putHeader("Content-Type", "application/json") .end(best.encode()); }, error -> { logger.error("#{} ends in error", requestId, error); request.response() .setStatusCode(502) .end(); });}
各HTTPリクエストについて、以下の点に注目してみるとおもしろいでしょう。■■ レスポンスは、as()メソッドを使ってJsonObjectに変換しています。■■ サービスがエラーを返した場合、1回リトライを試みます。■■ 500ミリ秒が経過すると処理はタイムアウトし、空のレスポンスが返されます。こうすることによって、すべてのレ
スポンスやエラーが返されるまで待機しないようにしています。
なお、スケジューラを受け取るすべてのRxJavaの操作で、RxHelper::schedulerを使って、すべてのイベントがVert.x
ORACLE.COM/JAVAMAGAZINE ////////////////////// JANUARY/FEBRUARY 2018
46
//reactive programming /
イベント・ループで処理され続けるようにすることができます。
全体の処理は、map、flatMap、reduceなどの関数型プログラミングのイディオムと、デフォルト値によるエラ
ー処理だけで構成されています。500ミリ秒以内に入札できるサービスがない場合、何の提案も行われません。そ
の場合、HTTP 502エラーとなります。それ以外の場合には、受信したレスポンスの中から、最適な提案が選ばれま
す。
verticleのデプロイと、サービスとの通信:メインverticleのコードは次のようになります。
public class MainVerticle extends AbstractVerticle {
@Override public void start() { vertx.deployVerticle(new BiddingServiceVerticle());
vertx.deployVerticle(new BiddingServiceVerticle(), new DeploymentOptions().setConfig( new JsonObject().put("port", 3001)));
vertx.deployVerticle(new BiddingServiceVerticle(), new DeploymentOptions().setConfig( new JsonObject().put("port", 3002)));
vertx.deployVerticle("samples.BestOfferServiceVerticle", new DeploymentOptions().setInstances(2)); }}
3つのサービスをシミュレートするため、入札サービスを別々のポートに3回デプロイします。その際に、各サービス
がリスニングするHTTPポートをJSONの設定で渡します。また、届くトラフィックを1つでなく2つのCPUコアで処理
できるようにするため、2つのインスタンスのエッジ・サービスverticleをデプロイします。この2つのインスタンスは
同じHTTPポートをリスニングしますが、競合することはない点に注意してください。これは、Vert.xがラウンドロビ
ン法でトラフィックを分散させているためです。
これで、HTTPieコマンドライン・ツールを使うなどして、HTTPサービスと通信できるようになります。ポート
ORACLE.COM/JAVAMAGAZINE ////////////////////// JANUARY/FEBRUARY 2018
47
//reactive programming /
3000のサービスと通信してみます。
$ http GET localhost:3000/offer 'Client-Request-Id:1234' --verboseGET /offer HTTP/1.1Accept: */*Accept-Encoding: gzip, deflateClient-Request-Id:1234Connection: keep-aliveHost: localhost:3000User-Agent:HTTPie/0.9.9
HTTP/1.1 200 OKContent-Length:83Content-Type: application/json
{ "bid":21, "clientRequestId":"1234", "origin": "fe299565-34be-4a7b-ac09-d88fcc1e42e2"}
ログから、人為的な遅延とエラーの両方が起きていることがわかります。
[INFO] 16:08:03.443 [vert.x-eventloop-thread-1] ERROR samples.BiddingServiceVerticle - 6358300b-3f2d-40be-93db-789f0f1cde17 injects an error (client-id=1234, artificialDelay=N/A)
[INFO] 16:11:10.644 [vert.x-eventloop-thread-1] INFO samples.BiddingServiceVerticle - 6358300b-3f2d-40be-93db-789f0f1cde17 offers 10 (client-id=1234, artificialDelay=934)
ORACLE.COM/JAVAMAGAZINE ////////////////////// JANUARY/FEBRUARY 2018
48
//reactive programming /
同じように、エッジ・サービスでさまざまなことを試し、レスポンスの監視、ログのチェックを行って、どのようにレス
ポンスが組み立てられているかを確認することができます。ときどき、次のようなエラーが発生することがありま
す。
$ http GET localhost:8080 'Client-Request-Id:1234' HTTP/1.1 502 Bad GatewayContent-Length:0
これは、すべてのレスポンスで、届くまでの時間が500ミリ秒を超え、一部のサービスでエラーが発生したためで
す。
[INFO] 16:12:51.869 [vert.x-eventloop-thread-2] INFO samples.BiddingServiceVerticle - d803c4dd-1e9e-4f76-9029-770366e82615 offers 16 (client-id=0, artificialDelay=656)[INFO] 16:12:51.935 [vert.x-eventloop-thread-1] INFO samples.BiddingServiceVerticle - 6358300b-3f2d-40be-93db-789f0f1cde17 offers 17 (client-id=0, artificialDelay=724)[INFO] 16:12:52.006 [vert.x-eventloop-thread-3] INFO samples.BiddingServiceVerticle - 966e8334-4543-463e-8348-c6ead441c7da offers 14 (client-id=0, artificialDelay=792)1つまたは2つのレスポンスのみが考慮される場合もあります。
この例で重要なのは、Vert.xとRxJavaを組み合わせて、宣言的かつ関数的なモデルを実現できることです。こ
のモデルでは、非同期イベントだけで駆動しつつ、柔軟な数のネットワーク・リクエストの実行や処理を記述するこ
とができます。
まとめ
ORACLE.COM/JAVAMAGAZINE ////////////////////// JANUARY/FEBRUARY 2018
49
//reactive programming /
Eclipse Vert.xではリアクティブ・プログラミングおよび非同期実行モデルと組み合わせてリアクティブ・システムを
構築できます。本記事では、その方法について見てきました。リアクティブ・プログラミングでは、データ・ストリーム
の操作や結合によって、非同期でイベントドリブンなアプリケーションを構成できます。RxJavaなどの最新のリアク
ティブ・プログラミング・ライブラリは、バックプレッシャーを扱うリアクティブなストリームを実装します。しかし、リ
アクティブなアプローチは、リアクティブ・プログラミングのみに限られているわけではありません。レスポンシブで
安定し、インタラクティブなよりよいシステムを構築するという点は、見失わないでください。Vert.xが推進する実
行モデルやノンブロッキングI/O機能を使うことは、真のリアクティブに向かう道を歩いているということです。
本記事は、その表面を軽くなぞったにすぎません。Vert.xは、皆さんが望んでいる魅力的でスケーラブルな
21世紀のアプリケーションを作成する強い力と際立つ俊敏性を与えてくれます。単純なネットワーク・ユーティリテ
ィから、高度な最新のWebアプリケーション、HTTP/RESTマイクロサービス、大量のイベント処理、そして本格的な
バックエンド・メッセージバス・アプリケーションに至るまで、Vert.xは大いに活躍してくれます。</article>
Clement Escoffier(@clementplop):Red Hatのプリンシパル・ソフトウェア・エンジニアで、Vert.xのコア開発者として活躍中。OSGi、モバイル・アプリ開発、継続的デリバリ、DevOpsなどの多くの領域やテクノロジーに触れるプロジェクトや製品に関わるとともに、Apache Felix、iPOJO、Wisdom Framework、Eclipse Vert.xなど、多くのオープンソース・プロジェクトに積極的に貢献している。
Julien Ponge(@jponge):INSA Lyon准教授兼CITI-INRIA研究所研究員で、現Eclipse Vert.xチーム・メンバー。長い間オープンソースの開発に携わり、IzPackやGoloプログラミング言語を作成してきた。現在は休暇でINSAを離れ、Red Hatへの派遣コンサルタントとしてVert.xプロジェクトに関わっている。