CLOVER🍀

That was when it all began.

Payara(Hazelcast)のLite Membersについて

この記事は、「Payara Advent Calendar 2016 - Qiita」の17日目の記事となります。

昨日は、@khasunumaさんの「Payara がサポートする 2 つのクラスタリング - notepad」でした。
明日も…@khasunumaさんのご担当となります…(すごい)。

この記事では、Lite Membersをテーマに扱おうと思います。

Lite Membersとは?

Lite Membersとは、Payara 4.1.1.162で追加されたクラスタリング関係の新機能です。

What's New in Payara Server 162?

Lite Membersとはなにか?ですが、Lite Members自体はHazelcast 3.6の新機能です。

Release Notes 3.6

Enabling Lite Members

Hazelcast 3.0のリリース前に1度ドロップされた機能が、3.6で追加されたようです。通常、Hazelcastクラスタ
参加したNodeは、データが分配され各Nodeでデータは持ちます。しかし、Lite Memberとして設定されたNodeに
ついては、Hazelcastのクラスタには参加するもののデータを持たなくなります。ちょっと特殊なNodeです。

このようにデータを持たないままHazelcastクラスタへアクセスする場合は、Hazelcastの
クライアントライブラリを使用して(いわゆるClient/Server Mode)クラスタ外から
アクセスするのが通例なのですが、Lite Memberの場合はクラスタに参加しつつもデータを持たないNodeになります。

Payaraでは、Hazelcastの利用モードはEmbedded Mode固定であり、Client/Server Modeでは
利用できません。

Lite MemberとClient/Server Modeとの違いは、こちら。
Difference between Lite Member and Smart Client?

Payaraに組み込まれたHazelcastを使用している限りは、Client/Server Modeに対するLite Memberの
利点は気にしなくてもよいでしょう。

今回は、Payaraにフォーカスして書いていきます。使用するPayaraのバージョンは、4.1.1.164です。
※ちなみに、サンプルで使うのはPayara Microとします

PayaraにおけるLite Members

Payaraでの関連記事は、こちらです。

Payara Server Lite Nodes

Flexible Clustering with Payara Server

Payara Server Lite Nodesを見ると、コンセプトとしては

  • ネイティブなNodeより良いパフォーマンス
  • イベントに対するListenerの登録が可能
  • Partition(Hazelcastにおけるデータの管理単位)は持たないが、非Lite Memberへのアクセスは可能
  • Near Cacheが利用可能

のようです。パフォーマンスについては、Lite Members単体の説明としてはちょっと謎ですが…。

最後にオマケ的に入れますが、Hazelcast固有の機能を直接使い倒すわけでもなければ、最後のNear Cacheについては忘れていいと思います。
Listenerもそうですが、特にNear Cacheについては。

Lite Memberとしてクラスタに参加することでなにが変わるかですが、Lite MemberについてはNodeの追加や削除の影響
クラスタで保持するデータのリバランスの発生やリカバリ)がなくなります。

通常のHazelcast Clusterのイメージは次のような感じです。各Nodeでデータ(Primary/Backup)を保持し、リクエストされたNodeに
データがなければ他のNodeに取得しにいきます。

Nodeを追加、削除を行うと、クラスタの自動リバランスが動作します。Nodeが追加されればクラスタ全体で
使用可能なメモリ量は増えますし、削除されればデータが一部なくなるため、バックアップから復元しようとします。

クラスタ参加NodeをLite Memberとすることで、クラスタで管理するデータには依存せず、純粋に計算能力を
保持しただけのNodeを追加/削除することができます。

Lite Member「のみ」のクラスタを構成することはできませんが(必ず非Lite MemberなNodeが必要)、仮にLite Memberが
すべてダウンしても、データは一切欠損しませんしね。
※デフォルトではデータのバックアップ数は1のため、2つの非Lite MemberなPayara Nodeが同時にダウンするとデータが一部失われる可能性があります

クラスタ上の分散データは非Lite Memberの持つメモリの総量で管理し、多くの計算能力が必要なケースは
Lite Memberを使ってもよいのでは、というのがFlexible Clustering with Payara Serverで想定している
シナリオのようです。

とまあ、そんな感じなのがLite Memberなのですが、データを持たない分だけHazelcastクラスタ上にあるデータが
必要な時には、ネットワーク呼び出しが発生するのでその点は意識しておく必要があります。

ここまででのLite Memberを含めたクラスタのイメージは次のような感じでしょう。

Payara上で動作するアプリケーション的に、Hazelcastの分散データ構造を使用するのは以下のようなケースだと思います。
※他にもあるかもですが、現在自分が把握している範囲で…

  • Web Session(Distributed Mapによるセッションレプリケーション
  • JCache(Distributed Cacheによる分散キャッシュ)
  • EJB
  • Clustered CDI Event Bus(TopicによるNode間のCDIイベントの伝播)
  • jBatchのJob/CheckpointのStore

直接Hazelcastを使うのでもなければ、まずはセッションとJCacheが該当するでしょう。

今回は、この2つを使ってLite Memberがどのようなものかを見ていきたいと思います。

準備

それでは、ここからはプログラムを書いていきます。実行は、Payara Microで行います。

まずは、Payara MicroへのMaven依存関係を追加。

        <dependency>
            <groupId>fish.payara.extras</groupId>
            <artifactId>payara-micro</artifactId>
            <version>4.1.1.164</version>
            <scope>provided</scope>
        </dependency>

また、WARファイルの名前は「app.war」となるように今回は設定しています。

    <build>
        <finalName>app</finalName>
    </build>

サンプルコード

サンプルは、JAX-RSでのHttpSessionの操作とJCacheのCDI Interceptorで行います。

JAX-RSの有効化。
src/main/java/payaraadventcalendar/JaxrsActivator.java

package payaraadventcalendar;

import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;

@ApplicationPath("rest")
public class JaxrsActivator extends Application {
}

ログインではありませんが、指定された名前のユーザーを登録された時刻でセッションに保持するJAX-RSリソース。
src/main/java/payaraadventcalendar/UserResource.java

package payaraadventcalendar;

import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.UUID;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;

@Path("user")
public class UserResource {
    @GET
    @Path("register")
    public Response register(@QueryParam("firstName") String firstName,
                             @QueryParam("lastName") String lastName,
                             @Context HttpServletRequest request,
                             @Context UriInfo uriInfo) {
        User user = new User();
        user.setId(UUID.randomUUID().toString());
        user.setFirstName(firstName);
        user.setLastName(lastName);
        user.setLoginTime(LocalDateTime.now());

        request.getSession().setAttribute("user", user);

        return Response.created(uriInfo.getRequestUri()).build();
    }

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public User get(@Context HttpServletRequest request) {
        return (User) request.getSession().getAttribute("user");
    }

    public static class User implements Serializable {
        private static final long serialVersionUID = 1L;

        private String id;

        private String firstName;

        private String lastName;

        private LocalDateTime loginTime;

        // getter、setter省略
    }
}

JCacheを使ったJAX-RSリソースクラス。こちらは、CDI管理Beanとしています。
src/main/java/payaraadventcalendar/CalcResource.java

package payaraadventcalendar;

import java.util.concurrent.TimeUnit;
import javax.cache.annotation.CacheDefaults;
import javax.cache.annotation.CacheResult;
import javax.enterprise.context.ApplicationScoped;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.QueryParam;

@Path("calc")
@ApplicationScoped
@CacheDefaults(cacheName = "calcCache")
public class CalcResource {
    @GET
    @Path("add")
    @CacheResult
    public int add(@QueryParam("a") int a, @QueryParam("b") int b) throws InterruptedException {
        TimeUnit.SECONDS.sleep(5L);

        return a + b;
    }
}

セッションレプリケーションが使えるように、web.xmlにdistributableを設定しておきましょう。
src/main/webapp/WEB-INF/web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app 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/web-app_3_1.xsd"
         version="3.1">
    <distributable/>
</web-app>

で、パッケージング。

$ mvn package

Payara Microを起動してクラスタを構成する

では、クラスタを構成してみましょう。今回は、Lite Memberを2 Node、非Lite Memberを2 Node起動させます。

Lite Memberは、「--lite」オプションを付けて起動するのですが、この時に先にLite Memberを起動してしまうとエラーになります。

$ java -jar payara-micro-4.1.1.164.jar --deploy target/app.war --lite

こんなのとか

No member group is available to assign partition ownership...

こんなエラーメッセージを見ることになるでしょう。

  Exception while dispatching an event
com.hazelcast.partition.NoDataMemberInClusterException: Partitions can't be assigned since all nodes in the cluster are lite members

なので、先に非Lite Memberから起動します。今回は、非Lite Memberを2 Node、Lite Memberを2 Nodeの構成としましょう。

JAX-RSリソースへのアクセスは、Lite Memberに対して行うことにします。なので、非Lite Memberには8080ポートは空けてもらいます。

## 非Lite Member 1
$ java -jar payara-micro-4.1.1.164.jar --deploy target/app.war --port 18080
## 非Lite Member 2
$ java -jar payara-micro-4.1.1.164.jar --deploy target/app.war --port 19080

## Lite Member 1
$ java -jar payara-micro-4.1.1.164.jar --deploy target/app.war --lite --port 8080
## Lite Member 2
$ java -jar payara-micro-4.1.1.164.jar --deploy target/app.war --lite --port 9080

クラスタができました、と。

Members [4] {
	Member [172.17.0.1]:5900 - a99907a6-a7ff-4151-956a-410bf093db38
	Member [172.17.0.1]:5901 - b9dd75c3-29a7-4b59-84cf-26a7a0b9c5e4
	Member [172.17.0.1]:5902 - f383ec99-f98b-41d4-93e4-04cd388a925e this lite
	Member [172.17.0.1]:5903 - 6cfd1213-5edf-46fa-82b1-783aa37ca171 lite
}

「lite」の表記が出ているのが、Lite Memberです。

蛇足

今回は全NodeにWARファイルをデプロイしていますが、実は非Lite Memberについてはアプリケーションとしての
実アクセスがなければWARファイルのデプロイは不要だったりします。
Hazelcastの設定などをしていれば話は
別ですが、単にデータの配置先として使用している限りはWARはなくても大丈夫です。というか、Payaraでなくて
通常のHazelcast Nodeでもいい気が…。

この場合は、非Lite Memberは単純にアプリケーション外部の分散KVSとして扱われることになりますね。

動作確認

では、ここでデータを放り込んでみましょう。

「app/user/register」で、ちょっと50人くらいセッションに放り込んでみます。
スクリプトの中身は割愛(ソースコードGitHubに置いていますので、興味があればそちらへ)。

$ groovy gen-users.groovy

# 次みたいなのが、50個流れます
$ curl -i 'http://localhost:8080/app/rest/user/register?firstName=%E9%BA%97%E5%A5%88&lastName=%E8%A5%BF%E6%B2%A2'

テストデータは、ここで作りました。
テストデータ・ジェネレータ

また、Cacheにも数個登録してみましょう。

$ curl -i 'http://localhost:8080/app/rest/calc/add?a=3&b=5'
$ curl -i 'http://localhost:8080/app/rest/calc/add?a=8&b=9'
$ curl -i 'http://localhost:8080/app/rest/calc/add?a=1&b=5'
$ curl -i 'http://localhost:8080/app/rest/calc/add?a=21&b=3'
$ curl -i 'http://localhost:8080/app/rest/calc/add?a=12&b=25'

あまり、内容に意味はありません…。

ここで、JMXで統計情報を見てみます。Payara Microでは、デフォルトでHazelcastのJMXについての設定が有効化されています。
https://github.com/payara/Payara/blob/payara-server-4.1.1.164/nucleus/payara-modules/hazelcast-bootstrap/src/main/java/fish/payara/nucleus/hazelcast/HazelcastCore.java#L194

まずはLite Member。HazelcastのDistributed Map(IMap)を見てみましょう。ここで表示しているのはMap名が「/app」で
セッションの内容が格納されているDistributed Mapになります。コンテキストパスがMapの名前になるようです。
https://github.com/payara/Payara/blob/payara-server-4.1.1.164/appserver/web/web-ha/src/main/java/org/glassfish/web/ha/strategy/builder/ReplicatedWebMethodSessionStrategyBuilder.java#L123
https://github.com/payara/Payara/blob/payara-server-4.1.1.164/appserver/web/web-ha/src/main/java/org/glassfish/web/ha/session/management/ReplicationWebEventPersistentManager.java#L266
https://github.com/payara/Payara/blob/payara-server-4.1.1.164/appserver/ha/ha-hazelcast-store/src/main/java/fish/payara/ha/hazelcast/store/HazelcastBackingStoreFactory.java#L55

4 Node全部載せるのも冗長なので、それぞれ1 Nodeずつにします

で、注目するのは「localOwnerEntryCount」と「localBackupEntryCount」が0になっていることです。

「localOwnerEntryCount」がそのNodeのCollectionでPrimaryとして保持しているデータの数、「localBackupEntryCount」がバックアップとして保持しているデータの数となります。
つまり、データを持っていません。

続いて、非Lite Member。こちらについては、「localOwnerEntryCount」と「localBackupEntryCount」がそれぞれ
25になっています。もう半分は、別の非Lite Memberが持っていることになります。
※いつもこのようにキレイに分かれるとは限りません

一応ですが、セッション数です。今回属性はひとつしか登録していませんが、その数ではありません。

Cacheについては、この設定だけでは見れなかったです…。

このように、Lite Memberはデータを持たないため、たとえこの2つのLite Memberを再起動しようがデータは
なくなりません。

興味のある方は、クラスタを構成してデータを入れた後にLite Memberを全部落としたりしてみるとよいでしょう。

Near Cacheについて

Near Cacheについてですが、今回のようにJava EEの標準APIを使っている限りではあまり関係がありません。

そもそもNear Cacheとはなにか?という話ですが、自Nodeにデータがない場合には他Nodeにネットワーク越しにデータを
取りにいくわけですが、この結果をローカルのNodeにキャッシュしておく機能です。メモリ消費量が上がり、他Nodeのデータのコピーを
持つため一貫性が弱まるというトレードオフはありますが、性能を向上できる可能性があります。

Creating Near Cache for Map
JCache Near Cache

とはいえ、今回のようにPayara+Hazelcastでは

  • Web Session … Near Cacheを設定しても、リクエストの最後にセッションを更新するためNear Cacheが破棄される
  • Cache … Near CacheのサポートがClient/Server Modeのみのため、Embedded Modeを利用しているPayaraではそもそも使用できない

となります。なので、あんまり関係ない、と…。

ただ、Web Sessionについてはローカルにキャッシュされたセッションが使われるので、セッションパーシステンスしていればNear Cacheは
なくてもいい気がします。
https://github.com/payara/Payara/blob/payara-server-4.1.1.164/appserver/web/web-ha/src/main/java/org/glassfish/web/ha/session/management/ReplicationManagerBase.java#L138

それでもNear Cacheを使う場合は、Hazelcast固有のAPIを使うケースになるでしょう。

まとめ

Payaraの…といってもHazelcastの機能そのものですが、Lite Membersについてご紹介しました。

いかがでしょうか?使うかどうかはケースバイケースですが、知っていればクラスタの構成パターンについて取れる
バリエーションが増えると思います。クラスタ構成時の参考になれば、幸いです。

今回作成したコードは、こちらに置いています。
https://github.com/kazuhira-r/payara-advent-calendar/tree/master/2016

さらに蛇足ですが、実はHazelcast単独でのLite Membersについては前に取り上げていたりします。興味があれば、こちらもどうぞ。
HazelcastのLite Membersを試す - CLOVER