※本記事は、Aleks Seovićによる”Hello, Coherence Community Edition: Creating cloud native stateful applications that scale, Part 1“を翻訳したものです。


オープンソースのOracle Coherence Community Editionを使い、現在構築しているステートレス・アプリケーションと同程度にスケーリングが容易なステートフル・アプリケーションを作成する

著者:Aleks Seović
2020年8月14日

 

Oracle Coherenceが誕生してほぼ20年が経過しました。最初は分散キャッシュ製品として登場し、その後インメモリ・データ・グリッドへと進化を遂げました。Java EEアプリケーションのパフォーマンスとスケーラビリティの改善に不可欠なツールとして、大規模プロジェクトで広く使われています。

このツールは、複数のJVMやマシン、場合によってはデータセンターに分散され、スケーラビリティ、並列性、耐障害性を兼ね備えたjava.util.Map実装と考えてください。もちろん、これは簡略化しすぎた表現ではありますが、今のところは十分です。

Oracle Coherenceは、その歴史の大部分において、かなり高い値札が付いた商用製品でした。そのため、ターゲットは主に法人ユーザーに限定され、小規模プロジェクトやオープンソース・プロジェクトなど、この製品の機能がどうしても必要というわけではない多くのアプリケーションでは検討対象にならなかったのです。

それがすっかり変わったのが、オラクルがこの製品のオープンソース版であるCoherence Community Edition(CE)をリリースした2020年6月25日です。

最新のクラウド・ネイティブなアプリケーションやサービスを構築する場合、Coherence CEが最適な選択肢であることが多いでしょう。このプラットフォームの概要や、次のアプリケーションでの使用を検討すべき理由については、同僚のHarvey Rajaとともに最近執筆した「Coherenceの親切な紹介」(英語)にまとまっているため、その内容を繰り返すことはしません。

本記事では、Coherence CEを使ってスケーラブルなステートフル・アプリケーションを構築する方法に重点を置いて説明します。

 

サンプルTo Do Listサービス・アプリケーションの拡張

本記事では、Coherence CE Webサイトでクイック・スタート・サンプルとして使われているTo Do Listサービス・アプリケーション(図1)を拡張します。これには、2つの理由があります。

  • 誰もがわかる、とてもシンプルなドメイン・モデルが使用されているため、本記事ではCoherence CEの使用方法に集中できる
  • シンプルであるにもかかわらず、基本的な読取りや書込みから、クエリーや集計、そしてインプレース処理やイベントなど、Coherence CEが持つ多くの機能のデモを行うものになっている

Screenshot of the To Do List application

シンプルで図1:To Do Listアプリケーションのスクリーンショット 

とはいえここでは、クイック・スタート・サンプルよりもはるかに興味深いアプリケーションにしたいと思います。もともと実装されているHelidon RESTサービスに加えて、このプロジェクトでは以下を行います。

  • REST APIにServer-Sent Events(SSE)サポートを追加し、Coherence CEイベントをRESTクライアントにブロードキャストする
  • Helidon REST APIを使用するReactベースのWebフロントエンドを実装し、Helidon Web Server経由でも提供する
  • Coherence CEのgRPCサーバーを構成し、ネイティブJavaクライアントを使用してgRPCでCoherence CEと通信するJavaFXフロントエンドを実装する
  • 以上のすべてのコンポーネントを連携して動作させる方法を示す
  • Coherence Operatorを使用してアプリケーションをKubernetesクラスタにデプロイする

このアプリケーションの実装にはHelidon、JavaFX、Reactを使いますが、特に重点を置いて説明するのはCoherence CEの使用方法とAPIです。その他は補助的な説明になりますが、完全なアプリケーションのソース・コードをGitHubで公開しています。ぜひご覧ください。

なお、ここで断っておきますが、筆者はJavaFXやReactの専門家ではないため、UI関連のコードには最適でないものが含まれているかもしれません。

先ほどのリストを見ればわかるように、1本の記事に収めるには内容が多すぎます。そこで、今回はアプリケーションにREST APIバックエンドを実装します。フロントエンドの実装と、デプロイや監視などの運用関連の内容については、本シリーズの今後の記事で取り上げます。

 

REST APIの実装

最初の手順はMavenプロジェクトの作成です。このときに、今回必要となる、HelidonおよびCoherence CEへの依存性すべてをプロジェクトに追加します。Helidon MPチュートリアルの手順に従ってプロジェクトを作成してから、そこにいくつかの依存性を追加します。

POMファイルは次のようになります。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://maven.apache.org/POM/4.0.0"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <parent>
    <groupId>io.helidon.applications</groupId>
    <artifactId>helidon-mp</artifactId>
    <version>2.0.0</version>
    <relativePath/>
  </parent>

  <groupId>com.oracle.coherence.examples</groupId>
  <artifactId>todo-list-server</artifactId>
  <version>1.0.0-SNAPSHOT</version>

  <properties>
    <coherence.groupId>com.oracle.coherence.ce</coherence.groupId>
    <coherence.version>20.06</coherence.version>
  </properties>

  <dependencies>
     <!-- Helidonへの依存性 -->
    <dependency>
      <groupId>io.helidon.microprofile.bundles</groupId>
      <artifactId>helidon-microprofile</artifactId>
    </dependency>
    <dependency>
      <groupId>org.glassfish.jersey.media</groupId>
      <artifactId>jersey-media-json-binding</artifactId>
    </dependency>
    <dependency>
      <groupId>org.glassfish.jersey.media</groupId>
      <artifactId>jersey-media-sse</artifactId>
    </dependency>

    <!-- Coherenceへの依存性 -->
    <dependency>
      <groupId>${coherence.groupId}</groupId>
      <artifactId>coherence-cdi-server</artifactId>
      <version>${coherence.version}</version>
    </dependency>
    <dependency>
      <groupId>${coherence.groupId}</groupId>
      <artifactId>coherence-mp-config</artifactId>
      <version>${coherence.version}</version>
    </dependency>
    <dependency>
      <groupId>${coherence.groupId}</groupId>
      <artifactId>coherence-mp-metrics</artifactId>
      <version>${coherence.version}</version>
    </dependency>

  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.jboss.jandex</groupId>
        <artifactId>jandex-maven-plugin</artifactId>
        <executions>
          <execution>
            <id>make-index</id>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>

</project>

Helidon MicroProfile Bundleの他に、JSON-BシリアライズとSSEに使用するEclipse Jerseyサポートを追加しました。さらに、このコードではJandexプラグインも構成しています。このプラグインは、ビルド時にアプリケーションのクラスにインデックスを付けて、Contexts and Dependency Injection(CDI)の起動を高速化するためのものです。

Coherence CEに関しては、次に示す3つのモジュールを追加しています。

  • Coherence CDI Server:アプリケーション・プロセス内でCoherenceサーバーを起動するCDI拡張が提供されます。このモジュールにより、Coherence CEマップをHelidonサービスに注入できます。また、標準のCDIオブザーバを使って処理できるように、Coherence CEイベントがCDIイベントにマッピングされます。
  • Coherence MicroProfile Config:Helidon MP Config実装を使ってCoherence CEを構成します。
  • Coherence MicroProfile Metrics:Coherence CEメトリックをHelidon Metric Registryにパブリッシュし、Prometheusなどのモニタリング・ツールからアクセスできるようにします。

ここでは、Mavenプロパティを使って、Coherence CEのバージョンだけでなく、Coherence CEへの依存性のMavenグループIDも指定しました。オープンソース版(Coherence CE)と商用版(Oracle Coherence Enterprise EditionまたはOracle Coherence Grid Edition)のグループIDは異なることから、このようにすることで2つのバージョンを簡単に切り替えることができます。そのため、この方法が推奨されています。アーティファクト名、パッケージ名、クラス名など、その他のコードはすべてまったく同じで構いません。

Mavenプロジェクトの作成に加えて、いくつかの構成ファイルを作成する必要があります。まず、次に示すMETA-INF/beans.xmlファイルを作成し、アプリケーションを適切なCDI Beanアーカイブにします。

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://xmlns.jcp.org/xml/ns/javaee"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
                           http://xmlns.jcp.org/xml/ns/javaee/beans_2_0.xsd"
        version="2.0"
        bean-discovery-mode="annotated">

HelidonとCoherence CEからのログ出力を確認するために、Java Loggingの構成も必要です。Java Loggingを構成するためには、次に示すlogging.propertiesファイルをsrc/main/resourcesディレクトリに追加します。

handlers=io.helidon.common.HelidonConsoleHandler

# グローバル・デフォルト・ロギング・レベル。固有のハンドラとロガーで上書き可能
.level=CONFIG

# フォーマッタの構成
java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n

 

ドメイン・モデルの実装

To Do ListアプリケーションのREST APIを実装するためには、アプリケーションで2つのことが必要です。1つは、To Doリストに含まれる1つのタスクを表すデータ・モデルです。もう1つは、必要なRESTエンドポイントを提供するJAX-RSリソースです。

前者は、いくつかの属性を持つ単純なPlain Old Java Object(POJO)です。属性の内容は一見すればすぐにわかります。

public class Task
        implements Serializable
    {
    private String id;
    private long createdAt;
    private String description;
    private Boolean completed;

     /**
     * Taskインスタンスの作成
     *
     * @param description  タスクの説明
     */
    public Task(String description)
        {
        this.id = UUID.randomUUID().toString().substring(0, 6);
        this.createdAt = System.currentTimeMillis();
        this.description = description;
        this.completed = false;
        }

     // 簡潔さを優先し、アクセッサは省略
    }

このPOJOは、JavaのシリアライズとJSON-Bでシリアライズすることができる点に注意してください。Coherence CEではストレージ・フォーマットとしてJavaのシリアライズを使い、RESTとgRPC APIではJSON-Bを使います。Coherence CEでは、ストレージとクライアント・サーバー間通信の両方でプラガブルなシリアライザをサポートしています。ストレージとクライアント・サーバー間通信の両方にJSONを使うこともできました。また、転送フォーマットとしてJSONを、ストレージ・フォーマットとしてCoherence Portable Object Format(POF)を使うこともできました。

ここでの目的は、Coherence CEでは必要に応じてオブジェクトが自動変換され、クライアント・サーバー間通信とストレージで異なるシリアライズ・フォーマットを使えると示すことです。しかし、ここでCoherence POFに触れるのは少し早すぎます。この説明は、改めて行います。

CRUDエンドポイントの実装

データ・モデルが完成したため、REST APIの実装に着手します。

@Path("/api/tasks")
@ApplicationScoped
public class ToDoResource
    {
    @Inject
    private NamedMap<String, Task> tasks;

    // 未定義
    }

上記のコードを見ればわかりますが、このJAX-RSリソースでは/api/tasks HTTPエンドポイントへのリクエストを扱っています。具体的には、Coherence CEのCDIサポートを使って、Coherence CEのNamedMapをこのタスク用のデータ・ストアに注入しています。

Coherence CEのNamedMapは、多くの開発者にとっておなじみのインタフェースであるjava.util.Mapの拡張です。これには、良い面も悪い面もあります。良い面は、おなじみのものを使ってさまざまな処理を行う方法を開発者がすでに知っていることです。悪い面は、もっと適していてすぐに使える方法が他にあるにもかかわらず、そうでない方法を開発者が採用する可能性があることです。

もう少し詳しく見てみます。Coherence CEのNamedMapは、JavaのMapとは異なり、分散データ構造になっています。たとえばJavaのHashMapのように、データがローカルのバケットに格納されるのではありません。データが格納されるのは、パーティションの中です。パーティションは多数のJVMやマシン、場合によってはデータセンターに分散させることができます。つまり、ローカル・マップを扱う場合に当たり前のように行う操作の中には、NamedMapを扱う場合に行うと効率がやや低下するものもあります。

例として、反復処理を考えてみます。HashMapのエントリに対して反復処理を行うというのは、当たり前のことです。しかし、NamedMapに対する反復処理は、非常に高価な操作になる場合があります。数ギガバイト(場合によっては数テラバイト)のデータがネットワークを移動し、そのデシリアライズ、処理、ガベージ・コレクションなどが行われる可能性があります。

ありがたいことに、ほとんどのことは期待どおりに動作します。Coherence CEチームは、そうなるようにさまざまな対応を行っています。たとえば、Mapのメソッドの多くは、分散Map実装に適した方法であるCoherence CEプリミティブ(アグリケータ、エントリ・プロセッサなど)を使うように再実装されています。同様に、Stream APIもCoherence CEのアグリケータを使って完全に再実装され、多数のマシンでストリーム操作を効果的に並列処理できるようになっています。分散ラムダも再実装され、分散環境で安全に使用できる形で、サポートされている任意のシリアライズ・フォーマットを使ってシリアライズできるようになっています。クラスタ・メンバーによって異なるバージョンのラムダが存在していても構いません。

重要なことは、Coherence CEの開発チームがあらゆる最適化を行ってはいるものの、扱っているのは分散データ構造であり、ほとんどの呼出しがネットワーク通信につながることを覚えておかなければならない点です。したがって、その影響を最低限に抑えることができる、Coherence CEの機能を使う必要があります。

それでは、必要なRESTエンドポイントの実装に入ります。

まず、コードには基本的な作成、読取り、更新、削除(CRUD)機能が必要です。つまり、個々のタスクを追加、更新、削除する機能と、すべてのタスクのリストを取得する機能です。まずは、最後に挙げた機能から始めます。HTTP GETリクエストでJAX-RSリソースにアクセスすると、アプリケーションではすべてのタスクのリストをそこから返します。

@GET
@Produces(APPLICATION_JSON)
public Collection<Task> getTasks(@QueryParam("completed") Boolean completed)
    {
    Filter<Task> filter = completed == null
                          ? Filters.always()
                          : Filters.equal(Task::isCompleted, completed);

    return tasks.values(filter, Comparator.comparingLong(Task::getCreatedAt));
    }

このコードは、先ほど触れた最適化の一例を示しています。すべてのタスクをフェッチするのではなく、オーバーロードされたMap.values()メソッドを使っています。このメソッドでは、複数のクラスタ・メンバーに対してクエリーを並列実行し、指定された完了ステータスを持つタスクのサブセットのみを返しています。また、タスクの作成日時に基づいて結果をソートしているため、クライアントに返される結果は一貫性のある順序になります。

クエリー対象となる完了ステータスをクライアントが指定していない場合は、メソッドにAlwaysFilterインスタンスを渡し、すべてのタスクを返しています。

メソッドの中には、プレーンなMapをデータ・ストアとして使う場合と変わらないものもあります。

@POST
@Consumes(APPLICATION_JSON)
public void createTask(Task task)
    {
    task = new Task(task.getDescription());
    tasks.put(task.getId(), task);
    }

@DELETE
@Path("{id}")
public void deleteTask(@PathParam("id") String id)
    {
    tasks.remove(id);

この2つのRESTエンドポイントでクライアントはそれぞれ、Map.putメソッドを使ってタスクを作成し、Map.removeメソッドを使ってタスクを削除することができます。これらもリモート呼出しではありますが、その点に関してできることは何もありません。

注意すべき点は、アプリケーションではcreateTaskメソッドで新しいTaskを作成していることです。この目的は、新しいタスクの初期化方法をサーバー実装内の1か所で管理することにあります。これにより、クライアントは説明のみを含む新しいタスクの最低限のJSON表現を送ることができ、IDや作成日時、完了ステータスがどのように初期化されるかを心配する必要はなくなります。

次は、もう少しおもしろい機能である、完了したタスクの一括削除です。

@DELETE
public void deleteCompletedTasks()
    {
    tasks.invokeAll(Filters.equal(Task::isCompleted, true),
                    Processors.remove(Filters.always()));
    }

多くの開発者はMapのinvokeAllメソッドを見たことがないため、追加の説明が必要です。

NamedMap.invokeとNamedMap.invokeAllは、Coherence CEを使ううえで特に重要な分散コンピューティング・プリミティブであるエントリ・プロセッサをサポートするメソッドです。

エントリ・プロセッサを理解する

エントリ・プロセッサによって、関数をクラスタに送信し、そこでデータを処理することができます。これにより、ほとんどの場合において、データをネットワーク越しに移動する必要は完全になくなります。言い換えるなら、データを関数の側に移動させるのではなく、関数がデータの側に移動するのです。

先ほどのコードでは、ConditionalRemoveProcessorを送信しました。このエントリ・プロセッサは、NamedMapに含まれるすべての完了済みタスクに対して実行し、(引数としてAlwaysFilterを渡すことで)実行対象のすべてのエントリを削除するように構成しました。なお、別のアプローチとしては、引数としてエントリ・プロセッサのみを渡してすべてのエントリを対象とする方法、実行対象となる一連のキーを指定して一連のエントリを対象とする方法、あるいはinvokeAllではなくinvokeを呼び出して単一のエントリを対象とする方法もありました。

エントリ・プロセッサには、ストアド・プロシージャとの間にある程度の類似性があります。エントリ・プロセッサは、データをネットワーク越しに移動することなく、大量のデータセットを効率的に並列処理します(オプションで返すことができる処理結果は除きます)。しかし、エントリ・プロセッサとストアド・プロシージャには2つの重要な違いがあります。

  • エントリ・プロセッサは、SQLではなくJavaで書かれています。Coherence CEにとってのJavaはデータベースにとってのSQLのようなものであるためです。これにより、(少なくともJava開発者にとって)エントリ・プロセッサはストアド・プロシージャよりもはるかに書きやすく使いやすくなっています。
  • エントリ・プロセッサは、クライアントとサーバーの両方で利用できる既存のJavaクラス実装を使って事前定義することができます。または、Javaのラムダ式を使ってエントリ・プロセッサを動的に作成することもできます。この場合、ラムダ式(バイトコードなどのすべて)がクライアントからサーバーに送信され、クラスタ内の同じラムダ式の複数のバージョンをサポートするために、バージョン付きクラスとして登録されます。先ほどの例ではCoherence CE自体の既存クラスを使っていますが、ラムダ式を使った例を後ほど紹介します。

エントリ・プロセッサの重要性と強力さは、どれほど強調してもしきれるものではありません。理論的には、エントリ・プロセッサを使ってNamedMap APIのその他すべてのメソッドを実装することもできます。現に筆者は、Java 8で追加されたデフォルトのMapメソッドの多くをエントリ・プロセッサを使って実装しています。

それでは、次のRESTエンドポイントの説明に移ります。このエンドポイントは、既存のタスクを更新するものです。

@PUT
@Path("{id}")
@Consumes(APPLICATION_JSON)
public Task updateTask(@PathParam("id") String id, Task task)
    {
    String  description = task.getDescription();
    Boolean completed   = task.isCompleted();

    return tasks.compute(id, (k, v) ->
            {
            Objects.requireNonNull(v);

            if (description != null)
                {
                v.setDescription(description);
                }
            if (completed != null)
                {
                v.setCompleted(completed);
                }
            return v;
            });
    }

一見しただけでは、このコードはエントリ・プロセッサの呼出しには見えないかもしれません。しかし、少し深く調べれば、NamedMap.computeが実際には姿を変えたエントリ・プロセッサであることがわかるでしょう。

default V compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) 
    {
    return invoke(key, CacheProcessors.compute(remappingFunction));
    }

上記のコードで興味深いのは、ローカルのMapを使う場合と実装が変わらない点です。Map.computeを呼び出し、RESTペイロードを使って説明や完了ステータスを更新するだけでよいのです。異なる点は、コードが実行される場所です。この例でのエントリ・プロセッサは必ず、指定されたIDのタスクを格納しているクラスタ・メンバーで実行されるようになっています。

ところで、エントリ・プロセッサは厳密に一度だけ実行されることが保証されています。この点は、プライマリ・メンバーやバックアップ・メンバーが動作していない場合も変わりません(ただし、プライマリ・メンバーとすべてのバックアップが同時に停止している場合は除きます)。分散システムでこれを保証するのは、大変に難しいことです。この保証は、Coherence CEと、一部競合他社の製品との間における大きな差別化要因になっています。他社製品では、同様の機能があっても、Coherence CEと同じ保証はない場合があります。

ここまでで、REST APIのCRUDサポートに関する話題はほとんど終わりました。唯一残されているタスクはSSEサポートの実装です。これにより、既存タスクのいずれかが変更されたときや、タスクが追加または削除されたときに、クライアントに通知できます。

Server-Sent Eventsの実装

Coherenceでは、高機能で強力なイベント・モデルが提供されます。アプリケーションでは、サーバーサイド・イベントとクライアントサイド・イベントを監視できます。サーバーサイド・イベントは、イベントを発行したエントリを格納するメンバーで発生します。クライアントサイド・イベントによって、クラスタで発生するイベントを監視できます。

これに必要なのが、SSEエンドポイント(/api/tasks/events)の提供です。REST APIクライアントがこのエンドポイントを使用して変更イベントのストリームを受け取る登録を行うことにより、Coherence CEのNamedMapに格納されたタスク・セットが変更されるたびに、ローカルの状態を更新することができます。登録後は、すべてのクライアントが同じイベント・ストリームを受信する必要があります。アプリケーションでは、JAX-RSのSSEブロードキャスト・サポートを利用してこれを実現します。

@Context
private Sse sse;
private SseBroadcaster broadcaster;

@PostConstruct
void createBroadcaster()
    {
    this.broadcaster = sse.newBroadcaster();
    }

@GET
@Path("events")
@Produces(MediaType.SERVER_SENT_EVENTS)
public void registerEventListener(@Context SseEventSink eventSink)
    {
    broadcaster.register(eventSink);
    }

このコードがJAX-RSリソースに追加されれば、クライアントがSSEブロードキャスタに登録できるようになります。ただし、アプリケーションではまだ実際には何のイベントも送信していません(イベントの監視は可能です)。

Coherence CEのイベントはCoherence CEのMapEvent型で表されます。しかし、SSEブロードキャスタに対しては、イベント名と関連データをペイロードとしたOutboundSseEventインスタンスでパブリッシュすることが求められます。ありがたいことに、その解決法は簡単です。Coherence CEのイベントを監視し、それをSSEイベントに変換してパブリッシュするためには、次のようにします。

void broadcastEvents(@Observes @MapName("tasks") MapEvent<String, Task> event)
    {
    switch (event.getId())
        {
        case MapEvent.ENTRY_INSERTED:
            broadcaster.broadcast(createEvent("insert", event.getNewValue()));
            break;
        case MapEvent.ENTRY_UPDATED:
            broadcaster.broadcast(createEvent("update", event.getNewValue()));
            break;
        case MapEvent.ENTRY_DELETED:
            broadcaster.broadcast(createEvent("delete", event.getOldValue()));
            break;
        }
    }

private OutboundSseEvent createEvent(String name, Task task)
    {
    return sse.newEventBuilder()
                .name(name)
                .data(Task.class, task)
                .mediaType(APPLICATION_JSON_TYPE)
                .build();
    }

ここでは、クライアントサイドCDIオブザーバを使って、タスク・マップのすべてのマップ・イベントをリスニングしています。それぞれのイベントには、イベント・タイプ、エントリのキー、エントリの新旧の値(該当する場合)を含むペイロードが設定されています。

新旧の値の両方が設定されるのは、更新イベントのみです。挿入イベントには新しい値のみ、削除イベントには古い値のみが設定されます。createEventメソッドに適切なイベント名とペイロードを渡してMapEventをSSEイベントに変換し、接続されているすべてのクライアントにそのSSEイベントをブロードキャストしています。

以上でREST APIの実装が完了しました。

必要なCRUDエンドポイントをすべて実装し、その過程でさまざまなCoherence CE機能について説明しました。さらに、Coherence CEイベントをSSEイベントに変換し、登録済みのクライアントにそのSSEイベントをブロードキャストすることにより、SSEサポートも実装しました。

次は、うまく動作するかどうかを確かめてみます。

 

サーバーの実行

サーバーを実行する一番簡単な方法は、任意のIDEからそのまま実行するというものです。ここでは、IntelliJ IDEAを使います(図2参照)。

Server run configuration in IntelliJ IDEA

図2:IntelliJ IDEAでのサーバー実行構成

実行構成を定義して「Run」ボタンをクリックすると、Helidonによって、REST APIを提供するHelidon独自のWebサーバーと、データの格納に使われるCoherence CEクラスタ・メンバーの両方が起動します。

これはどういうことでしょうか。間違いではありません。Coherence CEは、起動する必要があるサーバーではありません。この点は、リレーショナル・データベース(Oracle Database、MySQLなど)や、NoSQLのキーと値のデータ・ストア(Mongo、Redisなど)など、皆さんがすでによく知っているかもしれないほとんどのデータ・ストアとは異なります。Coherence CEは、任意のJavaアプリケーションに容易に埋め込めるJavaライブラリなのです(GraalVMを使ってNode.jsアプリケーションに埋め込むこともできます)。

これは、起動する必要があるものは何もないという意味ではありません。Coherence CE(ライブラリ)では、クラスタの作成やデータの管理を行う場合に起動する必要がある一連のサービス(Cluster、PartitionedServiceなど)が提供されています。ただし、実際のところこれらのサービスは、com.tangosol.util.Serviceインタフェースを実装した単なるJavaクラスであるため、アプリケーションからプログラムで簡単に起動できます。

ここで行っているのは、まさにそのことです。

Helidonのio.helidon.microprofile.cdi.Mainクラスでは、ロギングの構成とCDIコンテナの起動のみを行います。CDIコンテナでは利用可能なすべてのCDI拡張機能を探します。すると、その他の拡張機能とともに、Helidon Web ServerとCoherence CEの拡張機能も見つかります。この2つの拡張機能では、CDIコンテナでその使用準備ができたときに発行される@Initializedイベントを監視しています。このイベントを受け取ると、Helidon Web ServerとCoherence CEサーバーがそれぞれ起動します。

このアーキテクチャには、2つの大きなメリットがあります。

  • アプリケーション・サーバーとデータ・ストアを効果的に組み合わせて、1つのステートフル・アプリケーション・サーバーにすることにより、複雑さが低下します。これにより、プロビジョニング、スケーリング、監視、管理の対象となる要素が減るため、アーキテクチャがはるかにシンプルになります。この点は、シンプルなアプリケーションやサービスで特に効果的となる傾向があります。HelidonとCoherence CEの組み合わせをステートフル・マイクロサービス・プラットフォームと表現するのは、この点を意図してのことです。
  • このアーキテクチャによって、Coherence CE内で実行するコード(エントリ・プロセッサなど)のデバッグが簡単になります。現在では、完全にドキュメント化された実際のCoherence CEのソース・コードがGitHubMaven Centralで公開されているため、なおさらです。

ところで、アプリケーションは必ず先ほどのようにして実行しなければならないというわけではありません。もちろん、Coherence CEのロールを使ってアプリケーション・サーバーとデータ・ストアを分割してから、それぞれを個別に管理することもできます。しかし、これは純粋にデプロイ時の決断であり、開発時の決断ではありません。アプリケーションを開発し、IDEからシングル・プロセスとして実行してテストやデバッグを行い、1つのDockerイメージにパッケージングすることもできます。Kubernetesにアプリケーションをデプロイする方法を変えれば、後の段階で別のアプリ・サーバーやストレージ階層を実行することもできます。

1つのCoherence CEクラスタや複数のクラスタでさまざまなマイクロサービスを実行することも、すべてのクラスタ・メンバーですべてのサービスを実行して疑似モノリスのようにすることさえもできます。可能性は無限大で、最適なオプションは個々のユースケースによって異なります。

ただし、データ・ストアをアプリに埋め込むだけで一般的な開発者ワークフローがどれほどシンプルになるかは、言葉ではとても言い表せません。試してみればわかるでしょう。

To Do Listサーバーを実際に実行する手順に戻ります。先ほど「Run」ボタンをクリックした場合は、最後の方に次のようなログ出力があるはずです。

2020.07.30 05:55:53 INFO io.helidon.microprofile.server.ServerCdiExtension Thread[main,5,main]:Server started on http://localhost:7001 (and all other host addresses) in 12215 milliseconds (since JVM startup).

この出力は、Helidon Web Serverが正常に起動したことと、ポート7001でアクセスできるようになっていることを示しています。ログの最後より少し前に、次の出力もあるはずです。

2020.07.30 05:55:53 CONFIG org.glassfish.jersey.server.ApplicationHandler Thread[main,5,main]: Jersey application initialized.
Root Resource Classes:
  com.oracle.coherence.examples.todo.server.ToDoResource

この出力は、HelidonがJAX-RSリソースを見つけてデプロイしたことを表しています。

さらに上にスクロールすることができ、次の内容が見つかるはずです。

2020.07.30 05:55:52 INFO coherence Thread[Logger@9258732 20.06,3,main]: (thread=DefaultCacheServer, member=1, up=10.634): 
Services
  (
   // 簡潔さを優先して省略
  )
Started DefaultCacheServer...

この出力は、Coherence CEサーバーが正常に起動したことと、アプリケーションで実際にCoherence CEにデータを格納できるようになっていることを示しています。

HelidonとCoherence CEからは他にも多くの情報がログに出力されているため、確認してみてください。ただし、ログで特に重要なのはこの3つの部分です。これらが出力されていれば、REST APIの使用準備が整っています。

 

REST APIのテスト

JUnitとREST Assuredを使って、REST APIの自動テストを実装することもできました。しかしここではひとまず、すべてが予想どおり動作するかどうかを確認するため、curlツールを使うことにします。

アプリケーションにはまだ何もデータが入っていないため、POSTエンドポイントを使っていくつかのタスクを作成します。

$ curl -i -X POST -H "Content-Type: application/json" \
       -d '{"description": "Learn Coherence"}' http://localhost:7001/api/tasks
HTTP/1.1 204 No Content
$ curl -i -X POST -H "Content-Type: application/json" \
       -d '{"description": "Write an article"}' http://localhost:7001/api/tasks
HTTP/1.1 204 No Content

HTTPレスポンス・コードを見る限り、うまく動作しているようです。それを確認するために、次のコマンドを実行します。

$ curl -i -X GET -H "Accept: application/json" http://localhost:7001/api/tasks
HTTP/1.1 200 OK
Content-Type: application/json

[{"completed":false,"createdAt":1596105616507,"description":"Learn Coherence","id":"9c6d9a"}
,{"completed":false,"createdAt":1596105656378,"description":"Write an article","id":"a3f764"}]

出力は問題ないようです。タスクを完了できるでしょうか。

$ curl -i -X PUT -H "Content-Type: application/json" \
       -d '{"completed": true}' http://localhost:7001/api/tasks/a3f764
HTTP/1.1 200 OK
Content-Type: application/json

{"completed":true,"createdAt":1596105656378,"description":"Write an article","id":"a3f764"}

うまくいきました。出力からも、問題なく動作したことがわかります。

 

まとめ

本記事では、Coherence CEとHelidonを使ってTo Do Listアプリケーションを作成しました。このアプリケーションは、問題なく動作しているように見えます。完了したタスクを削除する機能や、IDを指定してタスクを削除する機能は、読者の演習として残しておきます。しかし、考え方はおわかりでしょう。ここまでで開発したREST APIを使って、Coherence CEのタスクを格納、更新、削除、取得することができます。

サーバーが起動して実行されている限り、すべて問題なく動作します。しかし、サーバーをシャットダウンすれば、データはすべて失われてしまうことにお気づきでしょう。

まだデータをディスクに永続化するようにCoherence CEを構成していないため、これは当然のことです。したがって今のところ、データはメモリにのみ格納されているため、クラスタが再起動したら失われてしまいます。現在、クラスタのメンバーは1つだけです(そういう意味では、まだ「クラスタ」とは呼べないかもしれません)。

この点を修正する方法の1つは、起動するメンバーを増やすことです。こうすることにより、自動的に高可用性モードが有効になり、タスクのバックアップが作成されます。しかし残念ながら、現時点でこれを行うことはできません。メンバーを追加しようとした場合、TCPバインド例外が発生して起動に失敗します。これは、Helidonが同じポート(7001)でWebサーバーを起動しようとするからです。この問題は、本シリーズの最後の記事でアプリケーションをKubernetesにデプロイすれば解決します。

ディスク永続化を有効にすることでも、再起動によるデータ喪失を防げます。これは、コマンドラインで-Dcoherence.distributed.peristence.mode=activeシステム・プロパティを指定すれば実現できます。しかし、今はこの点について考慮しません。永続化を無効にした方が、サーバーの起動は高速になります。また、通常は白紙の状態からテストを始める方がシンプルです。

次に行うのは、今後はもうcurlを使ってタスクを管理しなくてもよいように、アプリケーションに対してきちんとしたフロントエンドを作ることです。本シリーズの次の記事では、その点に着手したいと思います。


Aleks Seović

Aleks Seović(@aseovic):オラクルのアーキテクト。主要なインメモリ・データ・グリッド製品であるOracle Coherenceに携わるとともに、Helidonマイクロサービス・フレームワークにも貢献。最近では、HelidonのgRPCフレームワークや、CoherenceのCDIおよびEclipse MicroProfileサポートの設計と実装を主導した。現在は、Coherenceネイティブ・クライアント、GraphQLサポート、Spring統合の実装を率いている。2016年にオラクルに入社する前は、専門のコンサルタント会社を率いて、世界中の顧客がCoherenceを用いたミッション・クリティカルなアプリケーションを実装する作業をサポートしていた。『Oracle Coherence 3.5』(Packt Publishing、2010年)の著者であるほか、業界のカンファレンス、Javaや.NETユーザー・グループのイベント、Coherence SIGで頻繁に講演を行い、Coherenceの普及に努めている。