#chiroito ’s blog

Java を中心とした趣味の技術について

Infinispan Hot RodのDistributed-cacheで自作のエンティティを使う

インメモリデータグリッドを使用するにあたり分散キャッシュを使って自作のデータ型となるエンティティを使うことは避けられないでしょう。今回はHot Rodを使ったDistributed-cacheでどうやって自作のエンティティを使うかを紹介します。

オブジェクトを読み書きするためにはオブジェクトのシリアライズ/デシリアライズが必要になります。 Infinispan を分散キャッシュとして使うにはネットワーク通信が必要ですので、シリアライズ/デシリアライズが必要となります。サーバ内ではオブジェクトはシリアライズされた状態で格納されています。そのため、サーバ側でデシリアライズする必要が無ければクライアント側だけにシリアライズ/デシリアライズの情報やクラスがあれば動きます。シリアライズするにはマーシャラが必要ですが、Infinispan では自作のエンティティに設定をするだけでマーシャラを自動生成してくれます。

全体の流れ

自作のエンティティを使うには以下の手順が必要です。

  1. 依存関係の追加
  2. 自作のエンティティを作成
  3. コンテキストイニシャライザの設定
  4. ソースコードを自動生成
  5. Hot Rodクライアントの設定
  6. 実行

依存関係の追加

Infinispanでシリアライズするにはprotostream-processorが必要です。依存関係を追加するためpom.xmlに以下の設定を追加します。

pom.xml

        <dependency>
            <groupId>org.infinispan.protostream</groupId>
            <artifactId>protostream-processor</artifactId>
            <version>4.3.2.Final</version>
        </dependency>

自作のエンティティを作成

依存関係を追加することで、APIが使えるようになりました。このAPIを使用して自作のエンティティとそれをシリアライズする設定を記述します。この設定を書くだけでシリアライズ/デシリアライズが自動的に行われます。

自作のエンティティには次の2つの設定が必要です。

  • シリアライズするフィールドを指定
  • オブジェクトを作るファクトリメソッドを指定

シリアライズするフィールドを指定するには@ProtoFieldアノテーションを使用し、シリアライズする順番と必須フィールドかどうかの指定が必ず必要となり、必要に応じてデフォルト値を指定します。フィールドにプリミティブ型を使用する場合はデフォルト値の指定は必須です。 シリアライズする順番はnumber属性、必須フィールドかどうかはrequired属性、デフォルト値はdefaultValue属性を使用します。

オブジェクトを作るファクトリメソッドを指定するには@ProtoFactoryアノテーションでstaticメソッドを指定します。 static メソッドではないメソッドに@ProtoFactoryアノテーションを指定するとビルド時に以下の様なエラーが発生します。

Error:(79,5) java: org.infinispan.protostream.annotations.ProtoSchemaBuilderException: @ProtoFactory annotated method must be static: public chiroito.sample.CustomEntity CustomEntity.create(java.lang.String,int)

自作のエンティティとなるCustomEntity.javaは以下のとおりです。

import org.infinispan.protostream.annotations.ProtoFactory;
import org.infinispan.protostream.annotations.ProtoField;

public class CustomEntity {

    @ProtoField(number = 1, required = true)
    String name;

    @ProtoField(number = 2, required = true, defaultValue = "0")
    int num;

    @ProtoFactory
    public static CustomEntity create(String name, int num) {
        return new CustomEntity(name, num);
    }

    public CustomEntity(String name, int num) {
        this.name = name;
        this.num = num;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getNum() {
        return num;
    }

    public void setNum(int num) {
        this.num = num;
    }

    @Override
    public String toString() {
        return "CustomEntity{" +
                "name='" + name + '\'' +
                ", num=" + num +
                '}';
    }
}

コンテキストイニシャライザの設定

ソースコードを自動生成するための設定のためだけのインターフェースです。マーシャラを作成して欲しいクラス、スキーマの情報などを指定します。@AutoProtoSchemaBuilderアノテーションとSerializationContextInitializerインターフェースの組み合わせで設定します。SerializationContextInitializerインターフェースのサブインターフェースを作り、@AutoProtoSchemaBuilderアノテーションのincludeClasses属性でマーシャラを作成して欲しいクラスを指定するだけと言う理解で良いでしょう。

詳しくはこちらをご覧ください。

protostream/AutoProtoSchemaBuilder.java at master · infinispan/protostream · GitHub

import org.infinispan.protostream.SerializationContextInitializer;
import org.infinispan.protostream.annotations.AutoProtoSchemaBuilder;

@AutoProtoSchemaBuilder(
        includeClasses = {
                CustomEntity.class
        },
        schemaFileName = "library.proto",
        schemaFilePath = "proto/",
        schemaPackageName = "customEntity")
interface CustomInitializer extends SerializationContextInitializer {
}

ソースコードを自動生成

ここまでそろった後にビルドをすることで、いくつかのソースコードが自動生成されます。ビルドはmvn packageします。自動生成されたコードはtarget/generated-sources/annotations以下に格納され、コンパイルされてtarget/classes.classファイルが生成されます。また、この.classファイルはjarファイルにも含まれます。作成されない場合はビルド時に他のプロセッサが邪魔をしていないか確認してください。

今回の例では以下の 5 つのファイルが生成されました。

  • target/generated-sources/annotations/chiroito/sample/CustomEntity$___Marshaller_6fdc67bee880914a83a620b354601cbfaee4ffec71b6318bc7625017a75101f1.java
  • target/generated-sources/annotations/chiroito/sample/CustomInitializerImpl.java
  • target/classes/chiroito/sample/CustomEntity$___Marshaller_6fdc67bee880914a83a620b354601cbfaee4ffec71b6318bc7625017a75101f1.class
  • target/classes/chiroito/sample/CustomInitializerImpl.class
  • target/classes/proto/library.proto

Hot Rodクライアントの設定

最後に設定ファイルに自動生成されたクラスを指定して終わりです。infinispan.client.hotrod.context-initializersに自動生成されたクラス名を指定します。

infinispan.client.hotrod.server_list=192.168.1.1:11222
infinispan.client.hotrod.context-initializers=chiroito.sample.CustomInitializerImpl

もしくはコードで自動生成されたクラスのインスタンスを与えます。ConfigurationBuilderクラスのaddContextInitializerメソッドで指定します。

ConfigurationBuilder cb = new ConfigurationBuilder();
cb.addContextInitializer(new CustomInitializerImpl());

RemoteCacheManager manager = new RemoteCacheManager(cb.build());

実行

それでは実行してみましょう。今回は自作のエンティティをputしてからgetしてみます。 今回はクライアントでのみシリアライズ/デシリアライズするため、サーバにjarファイルを送ったり設定を書いたりは不要です。

public static void main(String[] args) throws Exception{
    RemoteCacheManager manager = new RemoteCacheManager();

    RemoteCache<String, CustomEntity> c = manager.getCache("mycache");
    c.put("key1", new CustomEntity("りんご", 3));
    System.out.println(c.get("key1"));
    manager.close();
}

以下の様にエンティティの中身が出力されれば成功です。

3 23, 2020 9:00:23 午後 org.infinispan.client.hotrod.RemoteCacheManager actualStart
INFO: ISPN004021: Infinispan version: Infinispan 'Turia' 10.1.3.Final
3 23, 2020 9:00:23 午後 org.infinispan.client.hotrod.impl.protocol.Codec20 readNewTopologyAndHash
INFO: ISPN004006: Server sent new topology view (id=1, age=0) containing 1 addresses: [192.168.1.1:11222]
CustomEntity{name='りんご', num=3}

Infinispan の Hot Rod で設定ファイルとコードによる設定を組み合わせる

Infinispanを触ってて、設定ファイルで設定した内容に加え、コードによる設定を足したいなと思うことがあったので書いてみました。 以下の通りにするとHot Rodの設定ファイルであるresources/hotrod-client.propertiesを読み込みつつ、コードによる設定もできます。

ConfigurationBuilder cb = new ConfigurationBuilder();
// 設定ファイルの読み込み
ClassLoader cl = Thread.currentThread().getContextClassLoader();
cb.classLoader(cl);
InputStream stream = FileLookupFactory.newInstance().lookupFile("hotrod-client.properties", cl);
Properties properties = new Properties();
properties.load(stream);
cb.withProperties(properties);
// コードによる設定
cb.addContextInitializer(new CustomInitializerImpl());

RemoteCacheManager manager = new RemoteCacheManager(cb.build());

参考にしたソースはorg.infinispan.client.hotrod.RemoteCacheManagerクラスのRemoteCacheManager(boolean start)Properties loadFromStream(InputStream stream)です。

   public RemoteCacheManager(boolean start) {
      ConfigurationBuilder builder = new ConfigurationBuilder();
      ClassLoader cl = Thread.currentThread().getContextClassLoader();
      builder.classLoader(cl);
      InputStream stream = FileLookupFactory.newInstance().lookupFile(HOTROD_CLIENT_PROPERTIES, cl);
      if (stream == null) {
         HOTROD.couldNotFindPropertiesFile(HOTROD_CLIENT_PROPERTIES);
      } else {
         try {
            builder.withProperties(loadFromStream(stream));
         } finally {
            Util.close(stream);
         }
      }
      this.configuration = builder.build();
      this.counterManager = new RemoteCounterManager();
      this.syncTransactionTable = new SyncModeTransactionTable(configuration.transaction().timeout());
      this.xaTransactionTable = new XaModeTransactionTable(configuration.transaction().timeout());
      registerMBean();
      if (start) actualStart();
   }
   private Properties loadFromStream(InputStream stream) {
      Properties properties = new Properties();
      try {
         properties.load(stream);
      } catch (IOException e) {
         throw new HotRodClientException("Issues configuring from client hotrod-client.properties", e);
      }
      return properties;
   }

Infinispan のクライアントサーバモードでget/putの性能測定

Infinispan をクライアントサーバモードで動かした時の、実行回数をスレッド数を変えて性能測定してみました。

環境

クライアントとサーバのマシンを1台ずつ用意しています。サーバマシンにはInfinispanを2プロセス起動しています。 使用したInfinispanは10.1.3.Finalです。

クライアント

Lenovo X1 Xtremeで、物理4コアのi5-9400H を搭載しています。クロックは2.5GHz-4.3GHzです。

サーバ

自作のPCで、物理8コアのi9-9900Kを搭載してます。クロックは3.6GHz-5.0GHzです。

※本来は複数のサーバマシン間で複製をとるため複製においてもネットワーク通信が発生します。しかし、今回は資材の都合上同じマシン上で複製処理が行われているためネットワークを介していません。そのため、put処理では実際のベンチマークとは異なる結果になると思います。

ベンチマーク

今回試した処理は get と put です。

アプリケーションとキャッシュが同一のJVM内に存在するライブラリモードではGetが5000万回、Putが600万回ほど実行できました。クライアントサーバモードではアプリケーションとキャッシュの間にネットワーク通信が発生します。そのため、レイテンシが発生して性能面ではライブラリモードに比べて低下します。結果としては32スレッドが最大のスループットとなり、Get はおよそ66,000回、Putはおよそ62,000回実行しました。

今回の検証ではクライアント側は32スレッドでCPU4コアをすべて使い果たしました。この時、サーバ側は8コアすべてを使ったときに100%とすると、Getでは15%、Putでは90%程度の使用率でした。

f:id:chiroito:20200318170829p:plain

ソースコード

ベンチマークに使用した JMH のコードは以下です。

import org.infinispan.client.hotrod.RemoteCache;
import org.infinispan.client.hotrod.RemoteCacheManager;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.io.IOException;
import java.util.stream.IntStream;

@State(Scope.Thread)
public class HotRodCacheAccessBenchmark {

    private RemoteCache<String, String> c;
    private RemoteCacheManager manager;
    private String key = "key"+(System.nanoTime() % 128);

    @Setup(Level.Trial)
    public void setup() {
        manager = new RemoteCacheManager();
        c = manager.getCache("mycache");
        IntStream.range(0,128).forEach(i->c.put(key+i, "value"));
    }

    @TearDown
    public void tearDown() throws IOException {
        manager.close();
    }

    public static void main(String[] args) {
        IntStream.of(1, 2, 4, 8, 16, 32, 64).forEach(i -> run(i));
    }

    private static void run(int threadCount) {
        try {
            Options opt = new OptionsBuilder()
                    // 実行対象のベンチマークが定義された"クラス名.メソッド名"を正規表現で指定
                    .include(HotRodCacheAccessBenchmark.class.getSimpleName())
                    .forks(1)
                    .warmupIterations(5)
                    .measurementIterations(5)
                    .threads(threadCount)
                    .mode(Mode.Throughput)
                    .build();
            new Runner(opt).run();
        } catch (RunnerException e) {
            e.printStackTrace();
        }
    }

    @Benchmark
    public void get(){
        c.get(key);
    }

    @Benchmark
    public void put(){
        c.put(key, "value");
    }
}