イベントループとワーカー
モジュールがデプロイされて起動したVerticleインスタンスには、イベントループと呼ばれるスレッドプールから1つのスレッドを割り当てられます。以降、そのVerticleにおける処理は、そのシングルスレッドで処理が行われます。同じモジュール内でもVerticleごとにスレッドが割り当てられます。スレッドプールの数よりもVerticleが多ければ、一つのスレッドに複数のVerticleが割り当てられます。
Vert.xのイベントループのスレッド数はサーバーのCPUコア数と同じ数が自動的に起動されます(起動時オプション[-Dvertx.pool.eventloop.size]で変更も可能です)。
一つのVerticleの処理はシングルスレッドで順番に行われ、非同期でかなり効率はいいのですが、処理件数が多すぎる場合には限界がきてしまいます。
その場合には対象のモジュールやVerticleのインスタンス数を増やすことでスループットを上げることが可能です。
インスタンス数を2にして起動したVerticleはクラスインスタンスが2つ生成され、それぞれがイベントループからスレッドを別に割り当てられて、イベントは各インスタンスで順次割り当てられます。インスタンス数によって増えたVerticleは同じクラスなので、同じモジュールクラスローダーとなります。
アーキテクチャとしてやるべきではありませんが、この場合はVerticle間でstaticな値も共有可能です。ただしマルチスレッドで共有されることになるため、スレッドセーフなアクセスができるようにしなければいけません。逆にstaticでないMapなどのメンバーは別々のインスタンスになるため当然共有していません。この場合はシングルスレッドによるアクセスになるため、スレッドセーフを意識する必要がありません。
インスタンス数を増やす実行方法としては、vertx起動時のパラメータでインスタンス数を指定する方法(例: -instances 2)とdeployModule、deployVerticleメソッド実行時に指定する方法があります。
// Verticleを2インスタンス起動
container.deployVerticle("verticleClassName", config, 2);
// モジュールを4インスタンス起動
container.deployModule("moduleName", config, 4);
インスタンス数を増やしてスループットを上げたとしても、JDBCトランザクションなどの時間のかかる同期処理やCPU負荷の高い処理を実行してしまうと、後続の処理までが遅延してしまうことになります。
そういった処理をイベントループで行うことは厳禁となるため、ワーカーと呼ばれるバックグラウンドプールを利用します。バックグラウンドプールは20スレッドのスレッドプールです(起動時オプション[-Dvertx.pool.worker.size]で変更可能です)。
モジュールをワーカーとして起動するためには、mod.jsonで"worker":true と設定する必要があります。ただし、"worker":trueだけではバックグランドプールの中のシングルスレッドで稼働するだけになります。複数スレッドで動かすためには、非ワーカーの場合と同様にインスタンス数を増やす方法と、" multi-threaded":trueと設定し、マルチスレッドで動かすことで可能となります。マルチスレッドで動かす場合は、インスタンスが1つのまま複数スレッドで稼働するため、スレッドセーフを意識したコーディングが必要になります。
モジュール内でVerticleをデプロイする場合は、明示的にdeployWorkerVerticle()メソッドを使用する必要があります。
container.deployWorkerVerticle("className", config); // instance=1,multithreaded=false
container.deployWorkerVerticle("className", config, 1/*instances*/);// multithreaded=false
container.deployWorkerVerticle("className", config, 1/*instances*/, true/*multithreaded*/);
メッセージパッシング ~ イベントバス
ここまでのように、Verticleはそれぞれクラスローダーが独立しており、相互のコミュニケーションが通常のJavaアプリケーションのようなメソッド呼び出しではできません。また、シングルスレッドであるため、外部リソースとの同期IO処理も持たせることもできません。
そのために用いるのがイベントバスを使用したメッセージパッシングになります。
各モジュールはイベントバスにユニークなアドレスを指定してハンドラーを登録しておき、そのアドレスに対して非同期に起こるイベント(メッセージ)を受け取る形式で処理を行います。
ここでテンプレートで動かしたソースを見てみます。
1 public class PingVerticle extends Verticle {
2 public void start() {
3 final Logger logger = container.logger();
4 vertx.eventBus().registerHandler("ping-address",
5 new Handler<Message<String>>() {
6 @Override
7 public void handle(Message<String> message) {
8 message.reply("pong!");
9 logger.info("Sent back pong");
10 }
11 });
12 logger.info("PingVerticle started");
13 }
14 }
1 var vertx = require('vertx');
2 var eventBus = require('vertx/event_bus');
3 var container = require('vertx/container');
4 var console = require('vertx/console');
5 console.log('start');
6 container.deployModule('com.mycompany~my-module~1.0.0-final');
7 vertx.setTimer(5000, function(time) {
8 eventBus.send('ping-address', 'ping', function(reply) {
9 console.log(reply);
10 container.exit();
11 });
12 });
JavaScriptで書かれていますが、ping.jsが最初に起動されているVerticleになります。
ping.jsとPingVerticle.javaは別スレッドで動作するため、並列した形で動きを記述してみます(分かりやすくするためにソース名の横に仮のスレッド名を記述しています)。
| イベント | ping.js(event-loop-1) | PingVerticle.java(event-loop-2) |
|---|---|---|
| 起動 |
・モジュールロード(6行目) ・5秒タイマーを登録して終了(7行目) |
・ロードされ、start()メソッドの起動 ・イベントバスに'ping-address'をアドレスとして処理ハンドラーを登録(4行目) |
| 5秒後 | ・イベントバスの'ping-address'宛にメッセージを送信し、同時にreply受信用のハンドラーを登録(8行目) | |
| 'ping | ・イベントバスからメッセージを受信 | |
| -address' | ・ハンドラーで応答を送信(8行目) | |
| reply |
・replyメッセージをハンドラーで受信 ・コンソールに表示(9行目) ・プロセスを終了(10行目) |
クラスローダーもスレッドも異なるモジュール間でメッセージのやり取りができます。
イベントバスを経由したメッセージパッシングにより、スレッドを占有して処理する時間が短くなり、シングルスレッドでも大量に処理できる流れがイメージできたでしょうか?
まとめ
Vert.xの動作は、既存のJavaアプリケーションを作っていた方にとってはイメージしづらいアーキテクチャです。とりあえず動くものを作る手順はいろいろな方がブログなどで公開されていますが、実際に業務で使えるものとなるとより深い知識が必要となります。
今回は、Vert.xのアーキテクチャを理解する助けとなるよう少し深めに説明してみました。次回は、この知識を前提にシンプルなHTTPサーバーモジュールを作成し、メッセージパッシングでJDBCモジュールと連携して動くアプリケーションを作成してみたいと思います。

